From 9ad62a2ed64364d4c746420d70a9ebaa84c50eeb Mon Sep 17 00:00:00 2001 From: Jarvis Jr Hermes Date: Fri, 2 Oct 2026 10:33:55 +0000 Subject: [PATCH 1/2] feat: add section navigation and embedded Archify diagrams --- .gitattributes | 2 + .gitignore | 1 + README.md | 8 +- .../organization-structure.architecture.json | 94 + docs/contribution/contribute.md | 19 + docs/contribution/diagrams.md | 46 + docs/{ => core}/maintain.md | 0 docs/{ => core}/overview.md | 0 docs/{ => core}/reference.md | 0 docs/organization/structure.mdx | 19 + docs/strategy/direction.md | 11 + docusaurus.config.ts | 14 +- package.json | 11 +- pnpm-lock.yaml | 23 +- scripts/build-diagrams.mjs | 30 + scripts/portal.test.mjs | 38 + scripts/verify.mjs | 8 +- sidebars/contribution.ts | 2 + sidebars/organization.ts | 2 + sidebars/strategy.ts | 2 + src/components/ArchifyDiagram/index.tsx | 12 + src/css/custom.css | 5 + src/pages/index.tsx | 6 +- vendor/archify-checksums.json | 110 + vendor/archify-provenance.json | 12 + vendor/archify/LICENSE | 22 + vendor/archify/SKILL.md | 107 + vendor/archify/THIRD_PARTY_NOTICES.md | 69 + vendor/archify/assets/JetBrainsMono-OFL.txt | 93 + vendor/archify/assets/template.html | 13709 +++++++++++++++ vendor/archify/bin/archify.mjs | 6936 ++++++++ vendor/archify/bin/delivery-update.mjs | 91 + vendor/archify/bin/finalize.mjs | 895 + vendor/archify/bin/open-artifact.mjs | 146 + vendor/archify/bin/preview.mjs | 969 ++ vendor/archify/bin/recover-output.mjs | 41 + vendor/archify/bin/visual-check.mjs | 2667 +++ vendor/archify/brand-marks/README.md | 31 + vendor/archify/brand-marks/catalog.json | 131 + vendor/archify/delta/architecture-delta.mjs | 1313 ++ .../archify/examples/agent-run.lifecycle.json | 55 + .../examples/agent-tool-call.workflow.json | 74 + .../async-job-roundtrip.sequence.json | 56 + .../brand-aware-delivery.architecture.json | 34 + .../examples/cache-miss-request.sequence.json | 77 + .../checkout-platform.base.architecture.json | 32 + .../checkout-platform.head.architecture.json | 32 + .../examples/dataflow-product-analytics.html | 13820 +++++++++++++++ .../deployment-release.lifecycle.json | 48 + .../examples/event-stream.dataflow.json | 52 + .../examples/holiday-planning.workflow.json | 143 + .../examples/incident-response.workflow.json | 59 + .../archify/examples/lifecycle-agent-run.html | 13765 +++++++++++++++ vendor/archify/examples/locales/de.json | 425 + vendor/archify/examples/locales/es.json | 440 + vendor/archify/examples/locales/fr.json | 425 + .../archify/examples/locales/fr.partial.json | 22 + vendor/archify/examples/locales/it.json | 425 + vendor/archify/examples/locales/ja.json | 425 + vendor/archify/examples/locales/ko.json | 440 + vendor/archify/examples/locales/pt.json | 425 + vendor/archify/examples/locales/ru.json | 425 + .../examples/product-analytics.dataflow.json | 71 + .../production-deployment.architecture.json | 51 + .../examples/release-delivery.workflow.json | 57 + .../examples/sequence-cache-miss-request.html | 13835 ++++++++++++++++ vendor/archify/examples/web-app-rendered.html | 13808 +++++++++++++++ .../examples/web-app.architecture.json | 41 + .../workflow-agent-tool-call-rendered.html | 13826 +++++++++++++++ vendor/archify/migrations/workflow-v2.mjs | 280 + vendor/archify/package.json | 40 + vendor/archify/recipes/scenarios.mjs | 430 + .../references/architecture-layout-repair.md | 18 + .../archify/references/authoring-contract.md | 419 + .../archify/references/authoring-defaults.md | 41 + vendor/archify/references/brand-marks.md | 82 + .../archify/references/delivery-contract.md | 588 + .../references/repository-authoring.md | 105 + vendor/archify/references/update-awareness.md | 12 + vendor/archify/references/viewer-runtime.md | 35 + .../archify/renderers/architecture/grid.mjs | 62 + .../archify/renderers/architecture/labels.mjs | 215 + .../architecture/render-architecture.mjs | 1001 ++ .../renderers/architecture/routing.mjs | 1157 ++ vendor/archify/renderers/dataflow/README.md | 105 + .../renderers/dataflow/render-dataflow.mjs | 544 + vendor/archify/renderers/lifecycle/README.md | 171 + .../renderers/lifecycle/grid-routing.mjs | 429 + .../renderers/lifecycle/render-lifecycle.mjs | 938 ++ vendor/archify/renderers/sequence/README.md | 129 + .../renderers/sequence/render-sequence.mjs | 526 + .../renderers/shared/atomic-output.mjs | 1982 +++ .../archify/renderers/shared/brand-marks.mjs | 670 + vendor/archify/renderers/shared/cli.mjs | 451 + .../renderers/shared/desktop-readability.mjs | 132 + .../archify/renderers/shared/diagnostics.mjs | 173 + .../renderers/shared/engineering-profiles.mjs | 157 + .../shared/generated-brand-marks.mjs | 2003 +++ .../renderers/shared/generated-validators.mjs | 134 + vendor/archify/renderers/shared/geometry.mjs | 1941 +++ vendor/archify/renderers/shared/i18n.mjs | 603 + .../renderers/shared/layout-report.mjs | 42 + vendor/archify/renderers/shared/legend.mjs | 217 + .../archify/renderers/shared/output-path.mjs | 440 + .../renderers/shared/path-semantics.mjs | 1017 ++ .../renderers/shared/portable-path.mjs | 408 + .../renderers/shared/repository-evidence.mjs | 352 + .../renderers/shared/repository-location.mjs | 59 + .../renderers/shared/route-quality.mjs | 614 + .../archify/renderers/shared/sidecar-path.mjs | 38 + .../archify/renderers/shared/spatial-grid.mjs | 66 + vendor/archify/renderers/shared/text-fit.mjs | 105 + vendor/archify/renderers/shared/utils.mjs | 254 + vendor/archify/renderers/shared/validator.mjs | 86 + vendor/archify/renderers/workflow/README.md | 301 + .../renderers/workflow/render-workflow.mjs | 37 + .../renderers/workflow/workflow-compiler.mjs | 4913 ++++++ .../workflow/workflow-migration-geometry.mjs | 144 + vendor/archify/schemas/README.md | 256 + .../archify/schemas/architecture.schema.json | 152 + vendor/archify/schemas/common.schema.json | 181 + vendor/archify/schemas/dataflow.schema.json | 254 + vendor/archify/schemas/lifecycle.schema.json | 280 + vendor/archify/schemas/sequence.schema.json | 234 + vendor/archify/schemas/workflow.schema.json | 439 + .../archify/scripts/check-render-output.mjs | 1388 ++ vendor/archify/scripts/check-update.mjs | 1710 ++ .../archify/scripts/delivery-update-child.mjs | 23 + .../archify/scripts/generate-brand-marks.mjs | 141 + .../archify/scripts/generate-validators.mjs | 98 + vendor/archify/scripts/render-examples.mjs | 26 + vendor/archify/scripts/update-contract.mjs | 182 + vendor/archify/skill-release.json | 10 + 133 files changed, 131095 insertions(+), 28 deletions(-) create mode 100644 .gitattributes create mode 100644 diagrams/organization-structure.architecture.json create mode 100644 docs/contribution/contribute.md create mode 100644 docs/contribution/diagrams.md rename docs/{ => core}/maintain.md (100%) rename docs/{ => core}/overview.md (100%) rename docs/{ => core}/reference.md (100%) create mode 100644 docs/organization/structure.mdx create mode 100644 docs/strategy/direction.md create mode 100644 scripts/build-diagrams.mjs create mode 100644 scripts/portal.test.mjs create mode 100644 sidebars/contribution.ts create mode 100644 sidebars/organization.ts create mode 100644 sidebars/strategy.ts create mode 100644 src/components/ArchifyDiagram/index.tsx create mode 100644 vendor/archify-checksums.json create mode 100644 vendor/archify-provenance.json create mode 100644 vendor/archify/LICENSE create mode 100644 vendor/archify/SKILL.md create mode 100644 vendor/archify/THIRD_PARTY_NOTICES.md create mode 100644 vendor/archify/assets/JetBrainsMono-OFL.txt create mode 100644 vendor/archify/assets/template.html create mode 100755 vendor/archify/bin/archify.mjs create mode 100644 vendor/archify/bin/delivery-update.mjs create mode 100644 vendor/archify/bin/finalize.mjs create mode 100644 vendor/archify/bin/open-artifact.mjs create mode 100644 vendor/archify/bin/preview.mjs create mode 100644 vendor/archify/bin/recover-output.mjs create mode 100644 vendor/archify/bin/visual-check.mjs create mode 100644 vendor/archify/brand-marks/README.md create mode 100644 vendor/archify/brand-marks/catalog.json create mode 100644 vendor/archify/delta/architecture-delta.mjs create mode 100644 vendor/archify/examples/agent-run.lifecycle.json create mode 100644 vendor/archify/examples/agent-tool-call.workflow.json create mode 100644 vendor/archify/examples/async-job-roundtrip.sequence.json create mode 100644 vendor/archify/examples/brand-aware-delivery.architecture.json create mode 100644 vendor/archify/examples/cache-miss-request.sequence.json create mode 100644 vendor/archify/examples/checkout-platform.base.architecture.json create mode 100644 vendor/archify/examples/checkout-platform.head.architecture.json create mode 100644 vendor/archify/examples/dataflow-product-analytics.html create mode 100644 vendor/archify/examples/deployment-release.lifecycle.json create mode 100644 vendor/archify/examples/event-stream.dataflow.json create mode 100644 vendor/archify/examples/holiday-planning.workflow.json create mode 100644 vendor/archify/examples/incident-response.workflow.json create mode 100644 vendor/archify/examples/lifecycle-agent-run.html create mode 100644 vendor/archify/examples/locales/de.json create mode 100644 vendor/archify/examples/locales/es.json create mode 100644 vendor/archify/examples/locales/fr.json create mode 100644 vendor/archify/examples/locales/fr.partial.json create mode 100644 vendor/archify/examples/locales/it.json create mode 100644 vendor/archify/examples/locales/ja.json create mode 100644 vendor/archify/examples/locales/ko.json create mode 100644 vendor/archify/examples/locales/pt.json create mode 100644 vendor/archify/examples/locales/ru.json create mode 100644 vendor/archify/examples/product-analytics.dataflow.json create mode 100644 vendor/archify/examples/production-deployment.architecture.json create mode 100644 vendor/archify/examples/release-delivery.workflow.json create mode 100644 vendor/archify/examples/sequence-cache-miss-request.html create mode 100644 vendor/archify/examples/web-app-rendered.html create mode 100644 vendor/archify/examples/web-app.architecture.json create mode 100644 vendor/archify/examples/workflow-agent-tool-call-rendered.html create mode 100644 vendor/archify/migrations/workflow-v2.mjs create mode 100644 vendor/archify/package.json create mode 100644 vendor/archify/recipes/scenarios.mjs create mode 100644 vendor/archify/references/architecture-layout-repair.md create mode 100644 vendor/archify/references/authoring-contract.md create mode 100644 vendor/archify/references/authoring-defaults.md create mode 100644 vendor/archify/references/brand-marks.md create mode 100644 vendor/archify/references/delivery-contract.md create mode 100644 vendor/archify/references/repository-authoring.md create mode 100644 vendor/archify/references/update-awareness.md create mode 100644 vendor/archify/references/viewer-runtime.md create mode 100644 vendor/archify/renderers/architecture/grid.mjs create mode 100644 vendor/archify/renderers/architecture/labels.mjs create mode 100644 vendor/archify/renderers/architecture/render-architecture.mjs create mode 100644 vendor/archify/renderers/architecture/routing.mjs create mode 100644 vendor/archify/renderers/dataflow/README.md create mode 100644 vendor/archify/renderers/dataflow/render-dataflow.mjs create mode 100644 vendor/archify/renderers/lifecycle/README.md create mode 100644 vendor/archify/renderers/lifecycle/grid-routing.mjs create mode 100644 vendor/archify/renderers/lifecycle/render-lifecycle.mjs create mode 100644 vendor/archify/renderers/sequence/README.md create mode 100644 vendor/archify/renderers/sequence/render-sequence.mjs create mode 100644 vendor/archify/renderers/shared/atomic-output.mjs create mode 100644 vendor/archify/renderers/shared/brand-marks.mjs create mode 100644 vendor/archify/renderers/shared/cli.mjs create mode 100644 vendor/archify/renderers/shared/desktop-readability.mjs create mode 100644 vendor/archify/renderers/shared/diagnostics.mjs create mode 100644 vendor/archify/renderers/shared/engineering-profiles.mjs create mode 100644 vendor/archify/renderers/shared/generated-brand-marks.mjs create mode 100644 vendor/archify/renderers/shared/generated-validators.mjs create mode 100644 vendor/archify/renderers/shared/geometry.mjs create mode 100644 vendor/archify/renderers/shared/i18n.mjs create mode 100644 vendor/archify/renderers/shared/layout-report.mjs create mode 100644 vendor/archify/renderers/shared/legend.mjs create mode 100644 vendor/archify/renderers/shared/output-path.mjs create mode 100644 vendor/archify/renderers/shared/path-semantics.mjs create mode 100644 vendor/archify/renderers/shared/portable-path.mjs create mode 100644 vendor/archify/renderers/shared/repository-evidence.mjs create mode 100644 vendor/archify/renderers/shared/repository-location.mjs create mode 100644 vendor/archify/renderers/shared/route-quality.mjs create mode 100644 vendor/archify/renderers/shared/sidecar-path.mjs create mode 100644 vendor/archify/renderers/shared/spatial-grid.mjs create mode 100644 vendor/archify/renderers/shared/text-fit.mjs create mode 100644 vendor/archify/renderers/shared/utils.mjs create mode 100644 vendor/archify/renderers/shared/validator.mjs create mode 100644 vendor/archify/renderers/workflow/README.md create mode 100644 vendor/archify/renderers/workflow/render-workflow.mjs create mode 100644 vendor/archify/renderers/workflow/workflow-compiler.mjs create mode 100644 vendor/archify/renderers/workflow/workflow-migration-geometry.mjs create mode 100644 vendor/archify/schemas/README.md create mode 100644 vendor/archify/schemas/architecture.schema.json create mode 100644 vendor/archify/schemas/common.schema.json create mode 100644 vendor/archify/schemas/dataflow.schema.json create mode 100644 vendor/archify/schemas/lifecycle.schema.json create mode 100644 vendor/archify/schemas/sequence.schema.json create mode 100644 vendor/archify/schemas/workflow.schema.json create mode 100644 vendor/archify/scripts/check-render-output.mjs create mode 100644 vendor/archify/scripts/check-update.mjs create mode 100644 vendor/archify/scripts/delivery-update-child.mjs create mode 100644 vendor/archify/scripts/generate-brand-marks.mjs create mode 100644 vendor/archify/scripts/generate-validators.mjs create mode 100644 vendor/archify/scripts/render-examples.mjs create mode 100644 vendor/archify/scripts/update-contract.mjs create mode 100644 vendor/archify/skill-release.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..33f7413 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve byte-identical third-party sources, including upstream whitespace. +vendor/archify/** -whitespace diff --git a/.gitignore b/.gitignore index 4ee6aaa..126ad6f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build/ .env .env.* !.env.example +static/diagrams/ diff --git a/README.md b/README.md index 57ee3f1..4c20cec 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ Pull requests validate only. Pushes to `main` validate, build, and publish to th After publication, verify the exact commit’s Actions task and deployed page body; an OAuth redirect is not content verification. Future changes should use pull requests. Never commit credentials or edit generated `build/` output. +## Sections and diagrams + +Top-level sections are Organization, Strategy, and Contribution, each with an independent sidebar. The first Organization page is Structure and contains an explicitly illustrative organization chart, not confirmed Eldorado reporting lines. + +Archify 3.0.1 is vendored under `vendor/archify/` from commit `d5a1333d7447c866a765adac7d4d062f2f02e4d2`. MIT and third-party notices are preserved. `diagrams/` holds editable JSON; `pnpm diagrams:build` validates and generates local standalone viewers before every site build. The iframe is sandboxed without same-origin permission. See `docs/contribution/diagrams.md` for capability analysis, authoring, boundaries, and upgrade procedure. + ## Design -Inspired by https://www.eldorado.gg/: Lato, near-black navigation, charcoal, gold `#ffc950`, light panels, bold headings. Original documentation artwork and square panels; no official marketplace assets or business claims. See `docs/reference.md`. +Inspired by https://www.eldorado.gg/: Lato, near-black navigation, charcoal, gold `#ffc950`, light panels, bold headings. Original documentation artwork and square panels; no official marketplace assets or business claims. See `docs/core/reference.md`. diff --git a/diagrams/organization-structure.architecture.json b/diagrams/organization-structure.architecture.json new file mode 100644 index 0000000..be7ba43 --- /dev/null +++ b/diagrams/organization-structure.architecture.json @@ -0,0 +1,94 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Illustrative organization structure", + "output": "static/diagrams/organization-structure/index.html", + "quality_profile": "showcase" + }, + "components": [ + { + "id": "leadership", + "type": "backend", + "label": "Example leadership", + "sublabel": "Illustrative only", + "pos": [ + 320, + 40 + ], + "size": [ + 200, + 80 + ] + }, + { + "id": "product", + "type": "backend", + "label": "Product", + "sublabel": "Example function", + "pos": [ + 40, + 260 + ], + "size": [ + 200, + 80 + ] + }, + { + "id": "engineering", + "type": "backend", + "label": "Engineering", + "sublabel": "Example function", + "pos": [ + 320, + 260 + ], + "size": [ + 200, + 80 + ] + }, + { + "id": "operations", + "type": "backend", + "label": "Operations", + "sublabel": "Example function", + "pos": [ + 600, + 260 + ], + "size": [ + 200, + 80 + ] + } + ], + "connections": [ + { + "id": "leadership-product", + "from": "leadership", + "to": "product" + }, + { + "id": "leadership-engineering", + "from": "leadership", + "to": "engineering" + }, + { + "id": "leadership-operations", + "from": "leadership", + "to": "operations" + } + ], + "cards": [ + { + "dot": "amber", + "title": "Test diagram \u2014 not a confirmed structure", + "items": [ + "All roles and relationships are illustrative.", + "Actual Eldorado teams and reporting lines remain to be defined." + ] + } + ] +} diff --git a/docs/contribution/contribute.md b/docs/contribution/contribute.md new file mode 100644 index 0000000..345dc3f --- /dev/null +++ b/docs/contribution/contribute.md @@ -0,0 +1,19 @@ +--- +title: Contribute +description: Contribute evidence, proposals, and documentation changes to Eldorado Powered By AI. +slug: / +--- + +Use this section to contribute to **Eldorado Powered By AI**. + +- Distinguish evidence, proposals, open questions, and agreed decisions. +- Do not present example diagrams as the actual organization or architecture. +- Use a pull request for source changes; automated checks must pass before publication. + +## Maintain the portal + +[Update and publish the site](/docs/maintain/) or consult the [configuration reference](/docs/reference/). + +## Create diagrams + +[Author and embed an Archify diagram](./diagrams.md). diff --git a/docs/contribution/diagrams.md b/docs/contribution/diagrams.md new file mode 100644 index 0000000..254e3dc --- /dev/null +++ b/docs/contribution/diagrams.md @@ -0,0 +1,46 @@ +--- +title: Archify diagrams +description: Author typed JSON diagrams and embed their local interactive viewer in documentation. +--- + +## Capabilities + +[Archify](https://github.com/tt-a1i/archify) 3.0.1 is vendored at commit `d5a1333d7447c866a765adac7d4d062f2f02e4d2` under `vendor/archify/`, with its MIT license and third-party notices preserved. + +| Diagram type | Intended use | +| --- | --- | +| Architecture | Components, boundaries and relationships; also suitable for this illustrative structure test | +| Workflow | Processes, branches and approval gates | +| Sequence | Participants and ordered messages | +| Dataflow | Sources, processing and destinations | +| Lifecycle | States and transitions | + +The standalone viewer provides pan/zoom, node inspection, search/focus, relationship tracing, light/dark themes, and an export menu. Motion is opt-in. The Organization example exercises architecture mode; other diagram types use the same build interface but do not yet have portal-specific examples. Browser-dependent exports such as video are not acceptance-tested by this integration. + +## Author a diagram + +Create `diagrams/..json` using the matching schema and authoring guidance in `vendor/archify/schemas/` and `vendor/archify/references/`. Use stable IDs, explicit relationships, and honest evidence labels. Do not include private information merely because the source repository is private: Pages access is a separate platform policy. + +Set `meta.output` to `static/diagrams//index.html`. The build validates the typed source with the upstream showcase profile, renders locally, checks the artifact, and publishes the standalone HTML and a downloadable JSON source. + +```sh +pnpm diagrams:build +pnpm test +pnpm build +``` + +## Embed it in MDX + +```mdx +import ArchifyDiagram from '@site/src/components/ArchifyDiagram'; + + +``` + +Supply a text alternative below each diagram. The wrapper uses a local, base-path-aware iframe with a descriptive title, scripts and downloads enabled, and no same-origin or parent-navigation permissions. A full-viewer link provides a wider canvas. Generated output is ignored by Git and rebuilt in CI. + +## Integration boundaries + +The portal remains Docusaurus/React/TypeScript with pnpm. It uses the actual upstream renderer, not a look-alike diagram implementation. No Astro site, global agent installation, hosted rendering service, or AI API was added. The integration disables Archify update networking and does not run its installation commands. + +Upgrades are deliberate: review the upstream diff and licenses, update the provenance record and vendor checksums, then run tests, diagram validation, production build, and browser checks. Do not edit upstream files for portal styling. diff --git a/docs/maintain.md b/docs/core/maintain.md similarity index 100% rename from docs/maintain.md rename to docs/core/maintain.md diff --git a/docs/overview.md b/docs/core/overview.md similarity index 100% rename from docs/overview.md rename to docs/core/overview.md diff --git a/docs/reference.md b/docs/core/reference.md similarity index 100% rename from docs/reference.md rename to docs/core/reference.md diff --git a/docs/organization/structure.mdx b/docs/organization/structure.mdx new file mode 100644 index 0000000..b1ce824 --- /dev/null +++ b/docs/organization/structure.mdx @@ -0,0 +1,19 @@ +--- +title: Structure +description: Explore an illustrative organization diagram with the Archify viewer. +slug: /structure +--- + +import ArchifyDiagram from '@site/src/components/ArchifyDiagram'; + +:::note Illustrative integration example +This is **not Eldorado’s confirmed organization chart**. The roles and relationships below are test data demonstrating the diagram capability. Actual teams, reporting lines, and AI responsibilities will be defined separately. +::: + + + +Select a node to inspect it. Use the viewer’s search, zoom, theme, and export controls, or open the full viewer for more space. + +## Text alternative + +The example connects **Example leadership** to three sample functions: **Product**, **Engineering**, and **Operations**. These links demonstrate structure; they do not assert real reporting relationships at Eldorado. diff --git a/docs/strategy/direction.md b/docs/strategy/direction.md new file mode 100644 index 0000000..35166a8 --- /dev/null +++ b/docs/strategy/direction.md @@ -0,0 +1,11 @@ +--- +title: Direction +description: Define the strategy and direction for an AI-powered Eldorado. +slug: / +--- + +# Strategy and direction + +**Eldorado Powered By AI** is the space for shaping how AI can support Eldorado. + +Specific initiatives, priorities, measures of success, and a delivery roadmap remain to be agreed. This section is ready for that work; no proposals here should be treated as approved commitments until explicitly agreed. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 2f67b4b..27386bd 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -13,21 +13,27 @@ const config: Config = { onBrokenLinks: 'throw', markdown: {hooks: {onBrokenMarkdownLinks: 'throw'}}, presets: [['classic', { - docs: {sidebarPath: './sidebars.ts', routeBasePath: 'docs', editUrl: 'https://gitea.lego-cloud.eu/eldorado-v1/documentation/_edit/main/'}, + docs: {path: 'docs/core', sidebarPath: './sidebars.ts', routeBasePath: 'docs', editUrl: 'https://gitea.lego-cloud.eu/eldorado-v1/documentation/_edit/main/'}, blog: false, theme: {customCss: './src/css/custom.css'}, } satisfies Preset.Options]], + plugins: [ + ['@docusaurus/plugin-content-docs', {id: 'organization', path: 'docs/organization', routeBasePath: 'organization', sidebarPath: './sidebars/organization.ts'}], + ['@docusaurus/plugin-content-docs', {id: 'strategy', path: 'docs/strategy', routeBasePath: 'strategy', sidebarPath: './sidebars/strategy.ts'}], + ['@docusaurus/plugin-content-docs', {id: 'contribution', path: 'docs/contribution', routeBasePath: 'contribution', sidebarPath: './sidebars/contribution.ts'}], + ], themes: [['@easyops-cn/docusaurus-search-local', { hashed: 'filename', indexDocs: true, indexBlog: false, indexPages: true, - docsRouteBasePath: 'docs', language: ['en'], highlightSearchTermsOnTargetPage: true, + docsRouteBasePath: ['docs', 'organization', 'strategy', 'contribution'], language: ['en'], highlightSearchTermsOnTargetPage: true, }]], themeConfig: { colorMode: {defaultMode: 'light', respectPrefersColorScheme: true}, navbar: { title: 'Eldorado', logo: {alt: 'Documentation home', src: 'img/docs-mark.svg'}, items: [ - {to: '/docs/overview/', label: 'Documentation', position: 'left'}, - {to: '/docs/maintain/', label: 'Maintainer guide', position: 'left'}, + {type: 'docSidebar', docsPluginId: 'organization', sidebarId: 'organizationSidebar', label: 'Organization', position: 'left'}, + {type: 'docSidebar', docsPluginId: 'strategy', sidebarId: 'strategySidebar', label: 'Strategy', position: 'left'}, + {type: 'docSidebar', docsPluginId: 'contribution', sidebarId: 'contributionSidebar', label: 'Contribution', position: 'left'}, {href: 'https://gitea.lego-cloud.eu/eldorado-v1/documentation', label: 'Repository', position: 'right'}, ], }, diff --git a/package.json b/package.json index 398c4da..acfbfd2 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "node": ">=22" }, "scripts": { - "start": "docusaurus start", - "build": "docusaurus build", + "start": "pnpm diagrams:build && docusaurus start", + "build": "pnpm diagrams:build && docusaurus build", "serve": "docusaurus serve", "typecheck": "tsc --noEmit", - "verify": "node scripts/verify.mjs", - "verify:build": "node scripts/verify.mjs --build" + "verify": "pnpm test && node scripts/verify.mjs", + "verify:build": "node scripts/verify.mjs --build", + "diagrams:build": "node scripts/build-diagrams.mjs", + "test": "node --test scripts/portal.test.mjs" }, "dependencies": { "@docusaurus/core": "3.10.2", @@ -32,6 +34,7 @@ "@types/react": "19.1.13", "typescript": "5.9.3" }, + "pnpm": {"overrides": {"serialize-javascript": "7.0.5"}}, "browserslist": { "production": [ ">0.5%", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f4b7d8..fde3059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + serialize-javascript: 7.0.5 + importers: .: @@ -4412,9 +4415,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.0: resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} engines: {node: '>= 0.6'} @@ -4678,8 +4678,9 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.5: + resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} + engines: {node: '>=20.0.0'} serve-handler@6.1.7: resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} @@ -8836,7 +8837,7 @@ snapshots: globby: 13.2.2 normalize-path: 3.0.0 schema-utils: 4.5.0 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.5 webpack: 5.111.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.28))(html-minifier-terser@7.2.0)(postcss@8.5.28) core-js-compat@3.50.0: @@ -8902,7 +8903,7 @@ snapshots: jest-worker: 29.7.0 postcss: 8.5.28 schema-utils: 4.5.0 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.5 webpack: 5.111.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.28))(html-minifier-terser@7.2.0)(postcss@8.5.28) optionalDependencies: clean-css: 5.3.3 @@ -11291,10 +11292,6 @@ snapshots: quick-lru@5.1.1: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - range-parser@1.2.0: {} range-parser@1.2.1: {} @@ -11642,9 +11639,7 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 + serialize-javascript@7.0.5: {} serve-handler@6.1.7: dependencies: diff --git a/scripts/build-diagrams.mjs b/scripts/build-diagrams.mjs new file mode 100644 index 0000000..f445275 --- /dev/null +++ b/scripts/build-diagrams.mjs @@ -0,0 +1,30 @@ +import {readdirSync, readFileSync, mkdirSync, copyFileSync, renameSync, rmSync} from 'node:fs'; +import {join} from 'node:path'; +import {execFileSync} from 'node:child_process'; +const cli = 'vendor/archify/bin/archify.mjs'; +const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']); +// This directory is generated-only and ignored by Git. Remove stale artifacts. +rmSync('static/diagrams', {recursive: true, force: true}); +mkdirSync('static/diagrams', {recursive: true}); +mkdirSync('.cache/archify', {recursive: true}); +const names = new Set(); +for (const file of readdirSync('diagrams').sort()) { + const match = /^([a-z0-9]+(?:-[a-z0-9]+)*)\.([a-z]+)\.json$/.exec(file); + if (!match || !types.has(match[2])) throw new Error(`Unsupported diagram source: ${file}`); + const [,name,type] = match; + if (names.has(name)) throw new Error(`Duplicate diagram output: ${name}`); + names.add(name); + const input = join('diagrams',file); + const spec = JSON.parse(readFileSync(input,'utf8')); + const output = `static/diagrams/${name}/index.html`; + mkdirSync(`static/diagrams/${name}`, {recursive: true}); + if (spec.diagram_type !== type || spec.meta.output !== output) throw new Error(`Diagram type/output mismatch: ${file}`); + const run = args => execFileSync(process.execPath,[cli,...args],{stdio:'inherit',env:{...process.env,ARCHIFY_UPDATE_CHECK_DISABLED:'1'}}); + run(['validate',type,input,'--quality','showcase','--json']); + run(['deliver',type,input,output,'--quality','showcase','--json']); + run(['check',output,'--json','--require-provenance']); + renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`); + copyFileSync(input,`static/diagrams/${name}.json`); +} +copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt'); +copyFileSync('vendor/archify/THIRD_PARTY_NOTICES.md','static/diagrams/THIRD-PARTY-NOTICES.md'); diff --git a/scripts/portal.test.mjs b/scripts/portal.test.mjs new file mode 100644 index 0000000..7247ae8 --- /dev/null +++ b/scripts/portal.test.mjs @@ -0,0 +1,38 @@ +import {test} from 'node:test'; +import {createHash} from 'node:crypto'; +import assert from 'node:assert/strict'; +import {readFileSync, existsSync} from 'node:fs'; +const read = p => readFileSync(p, 'utf8'); +test('vendored source matches the reviewed upstream snapshot', () => { + const checksums = JSON.parse(read('vendor/archify-checksums.json')); + for (const [path, hash] of Object.entries(checksums)) { + assert.equal(createHash('sha256').update(readFileSync(`vendor/archify/${path}`)).digest('hex'), hash, path); + } +}); +test('three independent section menus and Structure first', () => { + const config = read('docusaurus.config.ts'); + for (const section of ['Organization', 'Strategy', 'Contribution']) assert(config.includes(`label: '${section}'`)); + assert(!config.includes("label: 'Documentation'")); + assert(!config.includes("label: 'Maintainer guide'")); + assert(read('sidebars/organization.ts').includes("organizationSidebar: ['structure']")); + for (const section of ['organization','strategy','contribution']) assert(config.includes(`id: '${section}'`)); +}); +test('Archify is pinned, licensed and rebuilt, not a remote embed', () => { + const provenance = JSON.parse(read('vendor/archify-provenance.json')); + assert.equal(provenance.commit, 'd5a1333d7447c866a765adac7d4d062f2f02e4d2'); + assert(read('vendor/archify/LICENSE').includes('MIT License')); + const scripts = JSON.parse(read('package.json')).scripts; + assert(scripts.build.startsWith('pnpm diagrams:build')); + const wrapper = read('src/components/ArchifyDiagram/index.tsx'); + assert(wrapper.includes('sandbox="allow-scripts allow-downloads"')); + assert(!wrapper.includes('allow-same-origin')); + assert(wrapper.includes('useBaseUrl')); +}); +test('example is explicitly illustrative and contains valid connected nodes', () => { + const spec = JSON.parse(read('diagrams/organization-structure.architecture.json')); + assert(spec.meta.title.includes('Illustrative')); + const ids = new Set(spec.components.map(n => n.id)); + assert.equal(ids.size, spec.components.length); + for (const edge of spec.connections) assert(ids.has(edge.from) && ids.has(edge.to)); + assert(read('docs/organization/structure.mdx').includes('not Eldorado’s confirmed organization chart')); +}); diff --git a/scripts/verify.mjs b/scripts/verify.mjs index 4246c1e..46304da 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -6,7 +6,7 @@ const config = read('docusaurus.config.ts'); assert(config.includes("baseUrl: '/eldorado-v1/documentation/'")); assert(config.includes("onBrokenLinks: 'throw'")); assert(config.includes("hashed: 'filename'")); -for (const file of readdirSync('docs')) { +for (const file of readdirSync('docs', {recursive: true}).filter(f => /\.mdx?$/.test(f))) { const doc = read(join('docs', file)); assert(/^---\n[\s\S]*?title: .+\n[\s\S]*?description: .+\n/.test(doc), `Missing metadata: ${file}`); } @@ -16,9 +16,13 @@ const workflow = read('.gitea/workflows/pages.yml'); assert(workflow.includes('mountpoint -q')); assert(workflow.includes("gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'")); if (process.argv.includes('--build')) { - for (const route of ['index.html','docs/overview/index.html','docs/maintain/index.html','docs/reference/index.html','404.html']) { + for (const route of ['index.html','docs/overview/index.html','docs/maintain/index.html','docs/reference/index.html','404.html','organization/structure/index.html','strategy/index.html','contribution/index.html','contribution/diagrams/index.html','diagrams/organization-structure/index.html']) { assert(existsSync(join('build',route)), `Missing route: ${route}`); } + assert(!readdirSync('build/diagrams', {recursive: true}).some(f => f.endsWith('.delivery.json')), 'Private build receipts must not be published'); + const diagram = read('build/diagrams/organization-structure/index.html'); + assert(diagram.includes('Example leadership') && diagram.includes('node-engineering')); + assert(!diagram.includes('/opt/data/'), 'No build paths in published diagrams'); const home = read('build/index.html'); assert(/]*>Eldorado Powered By AI<\/title>/.test(home)); assert(home.includes('Powered By AI')); diff --git a/sidebars/contribution.ts b/sidebars/contribution.ts new file mode 100644 index 0000000..8fc9619 --- /dev/null +++ b/sidebars/contribution.ts @@ -0,0 +1,2 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; +export default {contributionSidebar: ['contribute', 'diagrams']} satisfies SidebarsConfig; diff --git a/sidebars/organization.ts b/sidebars/organization.ts new file mode 100644 index 0000000..18dab5f --- /dev/null +++ b/sidebars/organization.ts @@ -0,0 +1,2 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; +export default {organizationSidebar: ['structure']} satisfies SidebarsConfig; diff --git a/sidebars/strategy.ts b/sidebars/strategy.ts new file mode 100644 index 0000000..5f416fd --- /dev/null +++ b/sidebars/strategy.ts @@ -0,0 +1,2 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; +export default {strategySidebar: ['direction']} satisfies SidebarsConfig; diff --git a/src/components/ArchifyDiagram/index.tsx b/src/components/ArchifyDiagram/index.tsx new file mode 100644 index 0000000..5e4c81c --- /dev/null +++ b/src/components/ArchifyDiagram/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function ArchifyDiagram({name, title}: {name: string; title: string}): React.ReactElement { + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) throw new Error('Invalid diagram name'); + const source = useBaseUrl(`/diagrams/${name}/`); + const json = useBaseUrl(`/diagrams/${name}.json`); + return
+ + + + + +`; +} + +function compactMessage(value) { + let text = String(value || 'Preview build failed without a diagnostic.').trim(); + const lines = text.split(/\r?\n/); + const errorLine = lines.findIndex((line) => /^Error:\s/.test(line)); + if (errorLine > 0) text = lines.slice(errorLine).join('\n'); + const relevant = text.split(/\r?\n/); + const stackLine = relevant.findIndex((line, index) => index > 0 && /^\s*at\s/.test(line)); + if (stackLine > 0) text = relevant.slice(0, stackLine).join('\n'); + return text.length > 6000 ? `${text.slice(0, 6000)}\n… diagnostic truncated` : text; +} + +function redactDiagnostic(value, paths) { + let text = compactMessage(value); + for (const [absolutePath, replacement] of paths) { + if (!absolutePath) continue; + text = text.split(absolutePath).join(replacement); + } + return text; +} + +function safeJson(value) { + return JSON.stringify(value).replace(/ { resolveClosed = resolve; }); + + function publicState() { + return JSON.parse(JSON.stringify(state)); + } + + function sendState(res) { + res.write(`event: state\ndata: ${safeJson(publicState())}\n\n`); + } + + function broadcast() { + for (const res of clients) sendState(res); + } + + const page = Buffer.from(previewPage()); + const server = http.createServer((req, res) => { + const expectedHost = `${loopbackHost}:${port}`; + if (req.headers.host !== expectedHost) { + res.writeHead(403, responseHeaders('text/plain; charset=utf-8')); + res.end('Forbidden host'); + return; + } + if (req.method !== 'GET' && req.method !== 'HEAD') { + res.writeHead(405, { ...responseHeaders('text/plain; charset=utf-8'), Allow: 'GET, HEAD' }); + res.end('Method not allowed'); + return; + } + + let url; + try { + url = new URL(req.url, `http://${expectedHost}`); + } catch { + res.writeHead(400, responseHeaders('text/plain; charset=utf-8')); + res.end('Bad request'); + return; + } + + if (url.pathname === '/') { + res.writeHead(200, { + ...responseHeaders('text/html; charset=utf-8'), + 'Content-Security-Policy': "default-src 'none'; frame-src 'self'; connect-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'", + 'Content-Length': page.byteLength, + }); + if (req.method === 'HEAD') res.end(); + else res.end(page); + return; + } + if (url.pathname === '/state') { + const body = Buffer.from(`${safeJson(publicState())}\n`); + res.writeHead(200, { ...responseHeaders('application/json; charset=utf-8'), 'Content-Length': body.byteLength }); + if (req.method === 'HEAD') res.end(); + else res.end(body); + return; + } + if (url.pathname === '/artifact.html') { + if (!artifactBuffer) { + res.writeHead(404, responseHeaders('text/plain; charset=utf-8')); + res.end('No verified artifact yet'); + return; + } + res.writeHead(200, { ...responseHeaders('text/html; charset=utf-8'), 'Content-Length': artifactBuffer.byteLength }); + if (req.method === 'HEAD') res.end(); + else res.end(artifactBuffer); + return; + } + if (url.pathname === '/events' && req.method === 'GET') { + res.writeHead(200, { + ...responseHeaders('text/event-stream; charset=utf-8'), + Connection: 'keep-alive', + }); + res.write('retry: 1000\n\n'); + clients.add(res); + sendState(res); + req.on('close', () => clients.delete(res)); + return; + } + + res.writeHead(404, responseHeaders('text/plain; charset=utf-8')); + res.end('Not found'); + }); + + server.on('connection', (socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + // A connection event already queued when force-stop begins must not keep + // server.close() waiting after the current sockets have been destroyed. + if (forceStopping) socket.destroy(); + }); + + try { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, loopbackHost, () => { + server.off('error', reject); + port = server.address().port; + resolve(); + }); + }); + } catch (error) { + try { server.close(); } catch {} + cleanupOwnedDirectory(stagingDirectory, stagingIdentity); + throw error; + } + + const url = `http://${loopbackHost}:${port}/`; + + function finishStop() { + if (stopped || child || !serverClosed) return; + stopped = true; + clearTimeout(debounceTimer); + clearInterval(pollTimer); + clearTimeout(stopGraceTimer); + clearTimeout(stopKillTimer); + try { + cleanupOwnedDirectory(stagingDirectory, stagingIdentity); + } finally { + resolveClosed(); + } + } + + function signalActiveChild(signal) { + if (!child || child.exitCode !== null || child.signalCode !== null) return; + try { + if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal); + else child.kill(signal); + } catch (error) { + if (error.code === 'ESRCH') return; + try { child.kill(signal); } catch {} + } + } + + function closeServer() { + if (serverClosing) return; + serverClosing = true; + for (const res of clients) res.end(); + clients.clear(); + server.close(() => { + serverClosed = true; + finishStop(); + }); + server.closeIdleConnections?.(); + } + + function startBoundedChildDrain() { + if (!child || stopGraceTimer || stopKillTimer) return; + stopGraceTimer = setTimeout(() => { + stopGraceTimer = undefined; + if (!child) return finishStop(); + signalActiveChild('SIGTERM'); + stopKillTimer = setTimeout(() => { + stopKillTimer = undefined; + signalActiveChild('SIGKILL'); + }, stopKillMs); + }, stopGraceMs); + } + + async function stop({ force = false } = {}) { + if (force) forceStopping = true; + if (!stopping) { + stopping = true; + clearTimeout(debounceTimer); + clearInterval(pollTimer); + watcher?.close(); + closeServer(); + } + if (forceStopping) { + for (const socket of sockets) socket.destroy(); + } + if (child && force) { + clearTimeout(stopGraceTimer); + clearTimeout(stopKillTimer); + stopGraceTimer = undefined; + stopKillTimer = undefined; + signalActiveChild('SIGKILL'); + } else if (child) { + startBoundedChildDrain(); + } else { + finishStop(); + } + return closed; + } + + function publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath) { + const repairDetails = receipt?.diagnostics + ?.slice(0, 12) + .map((entry) => { + const fix = entry.supportedFixes?.length ? `\nFix: ${entry.supportedFixes.join('; ')}` : ''; + return `[${entry.code}] ${entry.message}${fix}`; + }) || []; + const checkerDetails = receipt?.checker?.checks + ?.filter((check) => !check.ok) + .flatMap((check) => check.details || []) + .filter(Boolean) + .slice(0, 12) || []; + const diagnostic = [ + receipt?.error, + ...(repairDetails.length ? repairDetails : checkerDetails), + ].filter(Boolean).join('\n') || stderr || stdout; + state.status = 'needs-fix'; + state.failure = { + stage: receipt?.stage || 'render', + ...(receipt?.code ? { code: receipt.code } : {}), + ...(receipt?.evidence ? { evidence: receipt.evidence } : {}), + message: redactDiagnostic( + diagnostic, + [ + [inputPath, ''], + [outputPath, ''], + [snapshotPath, ''], + [candidatePath, ''], + [stagingDirectory, ''], + [path.resolve(here, '..'), ''], + [path.resolve(options.cwd || process.cwd()), ''], + ...(options.repoRoot ? [[path.resolve(options.repoRoot), '']] : []), + ], + ), + }; + broadcast(); + } + + function commitCandidate(candidatePath, receipt, generationHash, outputCapture) { + let candidate; + let sourceCandidateBinding; + let sourceCandidateIdentity; + let commitCandidatePath; + let commitCandidateIdentity; + let commitCandidateBinding; + try { + const sourceCapture = captureRegularFileBinding(candidatePath, { + subject: 'candidate', + expectedSha256: receipt?.artifact?.sha256, + expectedBytes: receipt?.artifact?.bytes, + includeContent: true, + }); + if (sourceCapture.status !== 'captured') throw atomicOutputError(sourceCapture); + sourceCandidateBinding = sourceCapture.binding; + sourceCandidateIdentity = sourceCapture.identity; + candidate = sourceCapture.content.buffer; + const digest = sourceCapture.content.sha256; + const releasedSource = releaseRegularFileBinding(sourceCandidateBinding); + sourceCandidateBinding = undefined; + if (releasedSource.status !== 'released') throw atomicOutputError(releasedSource); + resolveOutputPath(outputRequest); + const sameArtifact = state.lastVerified?.sha256 === digest; + const currentSource = sourceDigest(inputPath); + if (currentSource.hash !== generationHash) { + return { + committed: false, + supersededBy: currentSource, + candidateIdentity: sourceCandidateIdentity, + }; + } + const beforeStage = verifyAtomicOutput(outputCapture.snapshot); + if (beforeStage.status !== 'match') throw atomicOutputError(beforeStage); + ({ + candidatePath: commitCandidatePath, + identity: commitCandidateIdentity, + } = stagePreviewCommit(outputCapture.commitPath, candidate, outputCapture.mode)); + const candidateCapture = captureRegularFileBinding(commitCandidatePath, { + subject: 'candidate', + expectedSha256: digest, + expectedBytes: candidate.byteLength, + expectedIdentity: commitCandidateIdentity, + ...(outputCapture.mode === null ? {} : { expectedMode: outputCapture.mode }), + }); + if (candidateCapture.status !== 'captured') throw atomicOutputError(candidateCapture); + commitCandidateBinding = candidateCapture.binding; + const beforeCommit = verifyAtomicOutput(outputCapture.snapshot); + if (beforeCommit.status !== 'match') throw atomicOutputError(beforeCommit); + const publication = publishRegularFileBinding( + commitCandidateBinding, + commitCandidatePath, + outputCapture.snapshot, + { subject: 'candidate' }, + ); + if (!['committed', 'committed-with-warning'].includes(publication.status)) { + throw atomicOutputError(publication); + } + const releasedCandidate = releaseRegularFileBinding(commitCandidateBinding); + commitCandidateBinding = undefined; + if (releasedCandidate.status !== 'released') throw atomicOutputError(releasedCandidate); + commitCandidatePath = undefined; + commitCandidateIdentity = undefined; + artifactBuffer = candidate; + lastGoodSourceHash = generationHash; + state.status = 'verified'; + if (!sameArtifact) { + state.revision += 1; + state.lastVerified = { + sha256: digest, + bytes: candidate.byteLength, + checksPassed: receipt.validation.checksPassed, + checkCount: receipt.validation.checkCount, + compositionProfile: receipt.validation.compositionProfile, + compositionStatus: receipt.validation.compositionStatus, + }; + } + state.failure = null; + broadcast(); + return { committed: true, supersededBy: null, candidateIdentity: sourceCandidateIdentity }; + } catch (error) { + publishFailure({ + stage: 'commit', + error: `Could not publish the verified preview: ${error.message}`, + ...(error.previewFailure || {}), + }, '', '', candidatePath); + return { committed: false, supersededBy: null, candidateIdentity: sourceCandidateIdentity }; + } finally { + if (sourceCandidateBinding) releaseRegularFileBinding(sourceCandidateBinding); + if (commitCandidateBinding) releaseRegularFileBinding(commitCandidateBinding); + if (commitCandidatePath && commitCandidateIdentity) { + removeOwnedRegularFile(commitCandidatePath, commitCandidateIdentity); + } + } + } + + function beginBuild(digest, epoch) { + if (stopping || child) return; + activeHash = digest.hash; + activeEpoch = epoch; + state.generation += 1; + state.status = 'checking'; + state.failure = null; + broadcast(); + + const candidatePath = path.join(stagingDirectory, `generation-${state.generation}.html`); + const snapshotPath = path.join(stagingDirectory, `generation-${state.generation}.json`); + let snapshotIdentity; + const outputCapture = captureAtomicOutput(outputPath); + if (outputCapture.status !== 'captured') { + const failure = atomicOutputFailure(outputCapture); + publishFailure( + { stage: 'prepare', error: failure.message, ...failure }, + '', + '', + candidatePath, + snapshotPath, + ); + return; + } + const beforeBuild = verifyAtomicOutput(outputCapture.snapshot); + if (beforeBuild.status !== 'match') { + const failure = atomicOutputFailure(beforeBuild); + publishFailure( + { stage: 'prepare', error: failure.message, ...failure }, + '', + '', + candidatePath, + snapshotPath, + ); + return; + } + if (digest.bytes !== null) { + try { + fs.writeFileSync(snapshotPath, digest.bytes, { flag: 'wx', mode: 0o600 }); + const metadata = fs.lstatSync(snapshotPath, { bigint: true }); + if (metadata.isFile() && metadata.ino !== 0n) { + snapshotIdentity = { device: metadata.dev, inode: metadata.ino }; + } + } catch (error) { + publishFailure( + { stage: 'prepare', error: `Could not snapshot the observed input: ${error.message}` }, + '', + '', + candidatePath, + snapshotPath, + ); + return; + } + } + const args = [options.deliveryCli || cliPath, 'deliver', type, snapshotPath, candidatePath, '--json']; + if (options.quality) args.push('--quality', options.quality); + if (options.repoRoot) args.push('--repo-root', path.resolve(options.repoRoot)); + let stdout = ''; + let stderr = ''; + child = spawn(process.execPath, args, { + cwd: options.cwd || process.cwd(), + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + detached: process.platform !== 'win32', + }); + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk) => { stdout += chunk; }); + child.stderr.on('data', (chunk) => { stderr += chunk; }); + child.on('error', (error) => { stderr += error.message; }); + child.on('close', (code) => { + const receipt = parseReceipt(stdout); + const generationEpoch = activeEpoch; + const generationHash = activeHash; + const stale = generationEpoch !== sourceEpoch; + let supersededBy = null; + let candidateIdentity; + const deliverySidecars = captureOwnedDeliverySidecars( + candidatePath, + snapshotPath, + receipt, + ); + child = null; + clearTimeout(stopGraceTimer); + clearTimeout(stopKillTimer); + stopGraceTimer = undefined; + stopKillTimer = undefined; + if (!stopping && !stale && code === 0 && receipt?.ok) { + ({ supersededBy, candidateIdentity } = commitCandidate( + candidatePath, + receipt, + generationHash, + outputCapture, + )); + } else if (code === 0 && receipt?.ok) { + const abandonedCandidate = captureRegularFileBinding(candidatePath, { + subject: 'abandoned-preview-candidate', + expectedSha256: receipt.artifact?.sha256, + expectedBytes: receipt.artifact?.bytes, + }); + if (abandonedCandidate.status === 'captured') { + candidateIdentity = abandonedCandidate.identity; + releaseRegularFileBinding(abandonedCandidate.binding); + } + } else if (!stopping && !stale) { + publishFailure(receipt, stdout, stderr, candidatePath, snapshotPath); + } + if (candidateIdentity) removeOwnedRegularFile(candidatePath, candidateIdentity); + if (snapshotIdentity) removeOwnedRegularFile(snapshotPath, snapshotIdentity, { subject: 'snapshot' }); + for (const deliverySidecar of deliverySidecars) { + removeOwnedRegularFile(deliverySidecar.path, deliverySidecar.identity, { + subject: 'preview-delivery-sidecar', + }); + } + + if (stopping) { + finishStop(); + } else if (pendingBuild || stale || supersededBy) { + pendingBuild = false; + if (supersededBy && sourceEpoch === generationEpoch) sourceEpoch += 1; + const digest = supersededBy || sourceDigest(inputPath); + queueStableBuild(digest.hash, true); + } + }); + } + + function queueStableBuild(hash, immediate = false) { + queuedHash = hash; + clearTimeout(debounceTimer); + const launch = () => { + if (stopping) return; + const digest = sourceDigest(inputPath); + if (digest.hash !== queuedHash) { + queueStableBuild(digest.hash); + return; + } + if (digest.hash === lastGoodSourceHash) { + if (state.status !== 'verified' && state.lastVerified) { + state.status = 'verified'; + state.failure = null; + broadcast(); + } + return; + } + if (child) { + pendingBuild = true; + return; + } + beginBuild(digest, sourceEpoch); + }; + debounceTimer = setTimeout(launch, immediate ? 0 : debounceMs); + } + + function observeSource({ immediate = false } = {}) { + const digest = sourceDigest(inputPath); + if (!immediate && digest.hash === queuedHash) return; + sourceEpoch += 1; + queueStableBuild(digest.hash, immediate); + } + + if (options.watch !== false) { + try { + // `path.resolve` keeps Windows 8.3 names intact. Canonicalize short names + // and junctions before libuv opens the directory so its callback path has + // the same prefix as the watched path. + const watchedDirectory = fs.realpathSync.native(path.dirname(inputPath)); + watcher = fs.watch(watchedDirectory, (event, filename) => { + // On Windows the watcher hands us just the basename; on POSIX it can be + // null. Resolve named events against the directory libuv actually opened + // so file-level case, 8.3, and hard-link aliases still identify the input. + if ( + !filename + || sameLocation(path.join(watchedDirectory, filename.toString()), inputPath).status === 'match' + ) { + observeSource(); + } + }); + watcher.on('error', () => { + const failedWatcher = watcher; + watcher = undefined; + failedWatcher?.close(); + }); + } catch (error) { + await stop(); + throw new Error(`Could not watch the input directory: ${error.message}`); + } + } + pollTimer = setInterval(() => observeSource(), pollMs); + + let opener = null; + if (shouldOpen) { + try { + opener = openLoopbackUrl(url); + } catch { + opener = { requested: true, status: 'unsupported', target: url, method: null }; + } + } + + observeSource({ immediate: true }); + + return { + url, + input: inputPath, + output: outputPath, + opener, + state: publicState, + stop, + closed, + }; +} + +export async function runPreview(options) { + const preview = await startPreview(options); + console.log(`preview ${preview.url}`); + console.log(`watching ${preview.input}`); + console.log(`output ${preview.output}`); + if (preview.opener && preview.opener.status !== 'opened') { + console.error(`Could not open the preview (${preview.opener.status}). ${preview.opener.failure?.reason || 'Open it manually.'} Target: ${preview.url}`); + } + + let signalCount = 0; + const stop = () => { + signalCount += 1; + if (signalCount === 1) { + console.log('\nstopping preview…'); + preview.stop(); + } else { + console.log('\nforcing preview shutdown…'); + preview.stop({ force: true }); + } + }; + process.on('SIGINT', stop); + process.on('SIGTERM', stop); + await preview.closed; + process.off('SIGINT', stop); + process.off('SIGTERM', stop); +} diff --git a/vendor/archify/bin/recover-output.mjs b/vendor/archify/bin/recover-output.mjs new file mode 100644 index 0000000..9b84261 --- /dev/null +++ b/vendor/archify/bin/recover-output.mjs @@ -0,0 +1,41 @@ +import { recoverRetiredPublication } from '../renderers/shared/atomic-output.mjs'; + +function usage(stream = process.stderr) { + stream.write('Usage: node bin/recover-output.mjs [--json]\n'); +} + +const args = process.argv.slice(2); +if (args.length === 1 && ['--help', '-h'].includes(args[0])) { + usage(process.stdout); +} else { + let json = false; + let recoveryDirectory; + let invalid = false; + for (const argument of args) { + if (argument === '--json' && !json) { + json = true; + } else if (argument.startsWith('-') || recoveryDirectory !== undefined) { + invalid = true; + } else { + recoveryDirectory = argument; + } + } + + if (invalid || recoveryDirectory === undefined) { + usage(); + process.exitCode = 64; + } else { + const result = recoverRetiredPublication(recoveryDirectory); + if (json) { + process.stdout.write(`${JSON.stringify(result)}\n`); + } else { + const reason = result.reason?.code || 'publication-recovery-unknown'; + process.stdout.write(`${result.status}: ${reason}\n`); + } + // A complete prior recovery is idempotent. A preserved target is a safe, + // deliberate non-action that needs the operator to inspect the claimant. + process.exitCode = ['recovered', 'absent'].includes(result.status) + ? 0 + : result.status === 'preserved' ? 2 : 1; + } +} diff --git a/vendor/archify/bin/visual-check.mjs b/vendor/archify/bin/visual-check.mjs new file mode 100644 index 0000000..277505a --- /dev/null +++ b/vendor/archify/bin/visual-check.mjs @@ -0,0 +1,2667 @@ +import { spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + DESKTOP_READABILITY_VIEWPORT, + MIN_PROJECTED_NODE_TEXT_PX, +} from '../renderers/shared/desktop-readability.mjs'; +import { + backupPublicRegularFileBinding, + captureAtomicOutput, + captureRegularFileBinding, + quarantineRemoveLinkedRegularFileAlias, + quarantineRemoveRegularFileBinding, + releaseRegularFileBinding, + removeEmptyDirectoryWithRetry, + verifyAtomicOutput, + verifyRegularFileBinding, +} from '../renderers/shared/atomic-output.mjs'; +import { + sameEntry, + sameLocation, + sameParent, + sidecarNamespaceComponentKey, +} from '../renderers/shared/path-semantics.mjs'; +import { + boundedSidecarStem, + sidecarStemFromComponent, +} from '../renderers/shared/sidecar-path.mjs'; + +export const VISUAL_CHECK_VIEWPORTS = Object.freeze([ + DESKTOP_READABILITY_VIEWPORT, + Object.freeze({ width: 1600, height: 1000 }), + Object.freeze({ width: 1920, height: 1080 }), + Object.freeze({ width: 2048, height: 1320 }), +]); + +export const CAPTURE_VIEWPORTS = Object.freeze([ + VISUAL_CHECK_VIEWPORTS[0], + VISUAL_CHECK_VIEWPORTS[VISUAL_CHECK_VIEWPORTS.length - 1], +]); +export const THEMES = Object.freeze(['light', 'dark']); +const EXIT = Object.freeze({ pass: 0, fail: 1, skipped: 2 }); +export const CHROME_NO_SANDBOX_ENV = 'ARCHIFY_CHROME_NO_SANDBOX'; +const VISUAL_SIDECAR_SUFFIXES = Object.freeze([ + '.visual-check.json', + '.visual-check.html', + ...CAPTURE_VIEWPORTS.flatMap(({ width, height }) => THEMES.map( + (theme) => `.visual-check.${width}x${height}.${theme}.png`, + )), +]); +const visualEvidenceOwnerships = new WeakSet(); +export const CHROME_STARTUP_TIMEOUT_MS = 90000; + +function sha256(buffer) { + return createHash('sha256').update(buffer).digest('hex'); +} + +function htmlEscape(value) { + return String(value).replace(/[&<>"']/g, (char) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + })[char]); +} + +function pathEntry(file) { + try { + return { exists: true, stat: fs.lstatSync(file) }; + } catch (error) { + if (error?.code === 'ENOENT') return { exists: false }; + return { exists: null, error }; + } +} + +function visualEvidencePaths(outputs) { + return outputs.capture === false ? [] : [outputs.contactSheet, ...outputs.screenshots.map((entry) => entry.path)]; +} + +function regularFileEvidence(file, { expectedLinks = 1 } = {}) { + const captured = captureRegularFileBinding(file, { + subject: 'evidence-file', + expectedLinks, + }); + if (captured.status !== 'captured') { + const systemCode = captured.reason?.systemCode; + return { + ok: false, + reason: systemCode === 'ENOENT' + ? { code: 'missing-evidence-file' } + : captured.reason?.code === 'evidence-file-not-regular-file' + ? { code: 'evidence-path-not-regular-file' } + : { + code: 'evidence-file-unreadable', + ...(systemCode ? { systemCode } : {}), + ...(captured.reason?.code ? { bindingCode: captured.reason.code } : {}), + }, + }; + } + const released = releaseRegularFileBinding(captured.binding); + if (released.status !== 'released') { + return { + ok: false, + reason: { + code: 'evidence-file-unreadable', + ...(released.reason?.systemCode ? { systemCode: released.reason.systemCode } : {}), + ...(released.reason?.code ? { bindingCode: released.reason.code } : {}), + }, + }; + } + return { + ok: true, + evidence: { + file: path.basename(file), + sha256: captured.content.sha256, + bytes: captured.content.bytes, + }, + identity: captured.identity, + }; +} + +function readRegularFileContents(file, { subject = 'file' } = {}) { + const captured = captureRegularFileBinding(file, { + subject, + expectedLinks: 1, + includeContent: true, + }); + if (captured.status !== 'captured') return { ok: false, reason: captured.reason }; + const released = releaseRegularFileBinding(captured.binding); + if (released.status !== 'released') return { ok: false, reason: released.reason }; + return { + ok: true, + bytes: captured.content.buffer, + evidence: { + sha256: captured.content.sha256, + bytes: captured.content.bytes, + }, + identity: captured.identity, + }; +} + +function regularFileCaptureError(label, captured) { + const reason = captured?.reason || { code: 'regular-file-capture-failed' }; + const error = new Error(`${label} could not be read safely (${reason.code}).`); + if (reason.systemCode) error.code = reason.systemCode; + error.evidenceReason = reason; + return error; +} + +function screenshotKey(width, height, theme) { + return `${width}x${height}:${theme}`; +} + +function resolvedSidecarNamespace(artifact, component) { + const namespace = sidecarNamespaceComponentKey(path.dirname(artifact), component); + if (namespace.status === 'resolved') return namespace; + const error = new Error( + `Could not establish a stable sidecar namespace (${namespace.reason?.code || 'unknown'}).`, + ); + error.code = 'ARCHIFY_SIDECAR_NAMESPACE_INDETERMINATE'; + error.sidecarNamespaceReason = namespace.reason; + throw error; +} + +function portableSidecarComponentKey(component) { + return component + .normalize('NFC') + .toLocaleUpperCase('en-US') + .toLocaleLowerCase('en-US') + .normalize('NFC'); +} + +function targetSidecarNamespace(directory, component) { + const namespace = sidecarNamespaceComponentKey(directory, component); + if (namespace.status === 'resolved') return namespace.componentKey; + if (namespace.reason?.code === 'sidecar-directory-missing') { + // A pure path calculation must not create --out-dir. Use the portable + // collision envelope until the directory exists and can be probed. + return portableSidecarComponentKey(component); + } + const error = new Error( + `Could not establish target sidecar semantics (${namespace.reason?.code || 'unknown'}).`, + ); + error.code = 'ARCHIFY_SIDECAR_NAMESPACE_INDETERMINATE'; + error.sidecarNamespaceReason = namespace.reason; + throw error; +} + +function evidenceSidecarPaths(artifactPath, { outDir, command = 'visual-check', capture = true } = {}) { + let artifact = path.resolve(artifactPath); + try { + // Existing aliases, including Windows 8.3 spellings, must share the + // physical artifact's evidence namespace even when the stem is short. + artifact = fs.realpathSync.native(artifact); + } catch (error) { + if (!['ENOENT', 'ENOTDIR', 'ENAMETOOLONG'].includes(error?.code)) throw error; + // Missing artifacts still need deterministic failure sidecar paths. + } + const namespace = resolvedSidecarNamespace(artifact, path.basename(artifact)); + artifact = path.join(namespace.directoryPath, path.basename(artifact)); + const namespaceStem = sidecarStemFromComponent(namespace.componentKey); + const directory = outDir ? path.resolve(outDir) : path.dirname(artifact); + const targetComponentKey = outDir + ? targetSidecarNamespace(directory, namespace.componentKey) + : namespace.componentKey; + const portableTargetKey = outDir + ? portableSidecarComponentKey(namespace.componentKey) + : namespace.componentKey; + const targetAliasesSourceSpelling = targetComponentKey !== namespace.componentKey + || portableTargetKey !== namespace.componentKey; + const stem = boundedSidecarStem( + namespaceStem.stem, + capture ? VISUAL_SIDECAR_SUFFIXES : ['.browser-check.json'], + targetAliasesSourceSpelling ? { + force: true, + hashDomain: `${command}-artifact\0${artifact}\0target-component\0${portableTargetKey}`, + } : namespaceStem.options, + ); + const base = path.join(directory, `${stem}.${command}`); + const screenshots = (capture ? CAPTURE_VIEWPORTS : []).flatMap(({ width, height }) => THEMES.map((theme) => ({ + width, + height, + theme, + path: `${base}.${width}x${height}.${theme}.png`, + }))); + return { + base, command, capture, + receipt: `${base}.json`, + contactSheet: `${base}.html`, + screenshots, + }; +} + +export function sidecarPaths(artifactPath, options = {}) { + return evidenceSidecarPaths(artifactPath, options); +} + +export function browserCheckSidecarPaths(artifactPath, options = {}) { + return evidenceSidecarPaths(artifactPath, { ...options, command: 'browser-check', capture: false }); +} + +function evidencePathConflict(artifactPath, outputs, file, reason) { + const target = file || outputs.receipt; + const error = `${outputs.command} will not replace the existing unowned evidence path "${target}".`; + return { + ok: false, + error, + diagnostic: failureDiagnostic({ + code: 'viewer/evidence-path-conflict', + message: error, + subject: { artifact: path.resolve(artifactPath), evidencePath: target }, + evidence: { reason }, + supportedFixes: [ + `move the conflicting file aside, or choose a separate --out-dir, then rerun ${outputs.command}`, + ], + }), + }; +} + +function visualEvidenceOwnershipMatches(ownership, artifactPath, outputs) { + return visualEvidenceOwnerships.has(ownership) + && ownership.active + && sameEntry(ownership.artifact, artifactPath).status === 'match' + && sameLocation(ownership.receipt, outputs.receipt).status === 'match'; +} + +function visualEvidenceTargets(outputs) { + return [ + { kind: 'receipt', path: outputs.receipt }, + ...(outputs.capture === false ? [] : [{ kind: 'contact-sheet', path: outputs.contactSheet }]), + ...outputs.screenshots.map((entry, index) => ({ + kind: 'screenshot', + index, + path: entry.path, + })), + ]; +} + +function evidenceWriteFailure(artifactPath, message, errors) { + return { + ok: false, + error: message, + diagnostic: failureDiagnostic({ + code: 'viewer/evidence-write', + message, + subject: { artifact: path.resolve(artifactPath) }, + evidence: { errors }, + supportedFixes: ['restore write access to the evidence directory and rerun visual-check'], + }), + }; +} + +function fileIdentity(stat) { + return { + device: stat.dev, + inode: stat.ino, + mode: Number(stat.mode & 0o777n), + links: stat.nlink, + }; +} + +function identityMatches(stat, identity, { directory = false } = {}) { + return stat.dev === identity.device + && stat.ino === identity.inode + && Number(stat.mode & 0o777n) === identity.mode + && (directory ? stat.isDirectory() : stat.isFile()) + && !stat.isSymbolicLink(); +} + +function evidenceIdentityMatches(actual, expected, expectedLinks = expected?.links ?? 1) { + return actual?.device === expected?.device + && actual?.inode === expected?.inode + && actual?.mode === expected?.mode + && BigInt(actual?.links ?? 0) === BigInt(expectedLinks); +} + +function stagedOutputs(outputs, stagingDirectory) { + return { + receipt: path.join(stagingDirectory, 'receipt.json'), + contactSheet: path.join(stagingDirectory, 'contact-sheet.html'), + screenshots: outputs.screenshots.map((entry, index) => ({ + ...entry, + path: path.join(stagingDirectory, `capture-${index}.png`), + })), + }; +} + +function stagedPathFor(ownership, finalPath) { + return ownership.stagingPaths.get(finalPath); +} + +function registerStagedEvidence(ownership, finalPath) { + const stagedPath = stagedPathFor(ownership, finalPath); + let stat; + try { + stat = fs.lstatSync(stagedPath, { bigint: true }); + } catch (error) { + throw new Error(`visual-check could not inspect staged evidence "${stagedPath}": ${error.message}`); + } + if (!stat.isFile() || stat.isSymbolicLink() || stat.ino === 0n) { + throw new Error(`visual-check staged evidence is not a uniquely owned regular file: "${stagedPath}".`); + } + if (stat.nlink !== 1n) { + ownership.stagedEntries.set(finalPath, { + path: stagedPath, + identity: fileIdentity(stat), + }); + throw new Error(`visual-check staged evidence has an external hard link: "${stagedPath}".`); + } + const targetMode = ownership.targets.get(finalPath)?.mode; + if (targetMode !== null && targetMode !== undefined + && Number(stat.mode & 0o777n) !== targetMode) { + fs.chmodSync(stagedPath, targetMode); + const adjusted = fs.lstatSync(stagedPath, { bigint: true }); + if (adjusted.dev !== stat.dev || adjusted.ino !== stat.ino || !adjusted.isFile()) { + throw new Error(`visual-check staged evidence changed while preserving its mode: "${stagedPath}".`); + } + stat = adjusted; + } + const entry = { + path: stagedPath, + identity: fileIdentity(stat), + }; + ownership.stagedEntries.set(finalPath, entry); + const evidence = regularFileEvidence(stagedPath); + if (!evidence.ok || !evidenceIdentityMatches(evidence.identity, entry.identity)) { + throw new Error(`visual-check could not bind staged evidence "${stagedPath}" to its contents.`); + } + entry.evidence = { + sha256: evidence.evidence.sha256, + bytes: evidence.evidence.bytes, + }; +} + +function writeStagedEvidence(ownership, finalPath, contents) { + const stagedPath = stagedPathFor(ownership, finalPath); + const target = ownership.targets.get(finalPath); + const mode = target?.mode ?? 0o666; + let descriptor; + try { + descriptor = fs.openSync(stagedPath, 'wx', mode); + let stat = fs.fstatSync(descriptor, { bigint: true }); + if (!stat.isFile() || stat.ino === 0n) { + throw new Error('the newly created staging entry has no unique regular-file identity'); + } + if (target?.mode !== null && target?.mode !== undefined + && Number(stat.mode & 0o777n) !== target.mode) { + fs.fchmodSync(descriptor, target.mode); + const adjusted = fs.fstatSync(descriptor, { bigint: true }); + if (adjusted.dev !== stat.dev || adjusted.ino !== stat.ino || !adjusted.isFile() + || Number(adjusted.mode & 0o777n) !== target.mode) { + throw new Error('the staged evidence mode could not be preserved'); + } + stat = adjusted; + } + const entry = { + path: stagedPath, + identity: fileIdentity(stat), + }; + ownership.stagedEntries.set(finalPath, entry); + if (stat.nlink !== 1n) { + throw new Error('the newly created staging entry has an external hard link'); + } + fs.writeFileSync(descriptor, contents); + const bytes = Buffer.isBuffer(contents) ? contents : Buffer.from(contents); + entry.evidence = { sha256: sha256(bytes), bytes: bytes.byteLength }; + } finally { + if (descriptor !== undefined) fs.closeSync(descriptor); + } +} + +function unlinkOwnedEntry(file, identity, { + evidence, + expectedLinks, +} = {}) { + const verifiedLinks = Number(expectedLinks ?? identity.links ?? 1); + let captured = captureRegularFileBinding(file, { + subject: 'visual-cleanup', + expectedIdentity: identity, + expectedMode: identity.mode, + expectedLinks: verifiedLinks, + ...(evidence ? { + expectedSha256: evidence.sha256, + expectedBytes: evidence.bytes, + } : {}), + }); + const reportedLinks = Number(captured.reason?.links); + if (captured.status === 'unsupported' + && captured.reason?.code === 'visual-cleanup-hardlinked' + && Number.isSafeInteger(reportedLinks) + && reportedLinks > verifiedLinks) { + captured = captureRegularFileBinding(file, { + subject: 'visual-cleanup', + expectedIdentity: identity, + expectedMode: identity.mode, + expectedLinks: reportedLinks, + ...(evidence ? { + expectedSha256: evidence.sha256, + expectedBytes: evidence.bytes, + } : {}), + }); + } + if (captured.status !== 'captured') { + if (captured.reason?.systemCode === 'ENOENT') return { status: 'absent' }; + return { + status: captured.status === 'different' || captured.status === 'unsupported' + ? 'different' + : 'unknown', + reason: captured.reason?.code?.includes('content-changed') ? 'content' : 'identity', + error: new Error(captured.reason?.code || 'visual cleanup binding failed'), + bindingState: captured.reason, + }; + } + let removal; + let released; + try { + removal = quarantineRemoveRegularFileBinding(captured.binding, file, { + subject: 'visual-cleanup', + expectedLinks: captured.identity?.links ? Number(captured.identity.links) : verifiedLinks, + }); + } finally { + released = releaseRegularFileBinding(captured.binding); + } + if (released.status !== 'released') { + return { + status: 'unknown', + reason: 'identity', + error: new Error(released.reason?.code || 'visual cleanup binding release failed'), + bindingState: released.reason, + }; + } + if (removal.status === 'removed' || removal.status === 'absent') return removal; + return { + status: removal.status === 'preserved' || removal.status === 'different' + || removal.status === 'unsupported' ? 'different' : 'unknown', + reason: 'identity', + error: new Error(removal.reason?.code || 'visual cleanup quarantine failed'), + bindingState: removal.reason, + ...(removal.recoveryDirectory ? { + recoveryDirectory: removal.recoveryDirectory, + recoveryFile: removal.recoveryFile, + } : {}), + }; +} + +function backupOwnedPublishedEntry(file, backupPath, identity, evidence) { + const captured = captureRegularFileBinding(file, { + subject: 'previous-evidence', + expectedIdentity: identity, + expectedMode: identity.mode, + expectedSha256: evidence.sha256, + expectedBytes: evidence.bytes, + expectedLinks: 1, + }); + if (captured.status !== 'captured') { + return { ...captured, backupCreated: false, backupVerified: false }; + } + let backup; + let released; + try { + backup = backupPublicRegularFileBinding(captured.binding, file, backupPath, { + subject: 'previous-evidence', + }); + } finally { + released = releaseRegularFileBinding(captured.binding); + } + if (released.status !== 'released') { + return { + status: 'recovery-required', + reason: { + code: 'previous-evidence-binding-release-failed', + bindingState: released.reason, + }, + recoveryDirectory: backup?.recoveryDirectory || path.dirname(backupPath), + recoveryFile: backup?.recoveryFile || (backup?.backupCreated ? backupPath : undefined), + target: backup?.target || file, + backupCreated: backup?.backupCreated === true, + backupVerified: backup?.backupVerified === true, + }; + } + return backup; +} + +function publishedRemovalError(file, result) { + const code = result.reason?.code; + let reason; + if (result.status === 'preserved') { + reason = 'published entry changed during retirement; its successor was restored and preserved'; + } else if (result.status === 'recovery-required') { + reason = 'published entry changed during retirement; recovery material was preserved'; + } else if (code?.includes('content-changed')) { + reason = 'published entry content changed; it was preserved'; + } else if (code?.includes('mode-changed')) { + reason = 'published entry mode changed; it was preserved'; + } else if (result.status === 'different' || result.status === 'unsupported') { + reason = 'published entry identity changed; it was preserved'; + } else { + reason = code || 'published entry could not be safely retired; it was preserved'; + } + return { + file, + reason, + ...(code ? { code } : {}), + ...(result.recoveryDirectory ? { recoveryDirectory: result.recoveryDirectory } : {}), + ...(result.recoveryFile ? { recoveryFile: result.recoveryFile } : {}), + ...(result.target ? { target: result.target } : {}), + }; +} + +function cleanupStagedEvidence(ownership, { removeDirectory = false } = {}) { + const errors = []; + for (const [finalPath, entry] of [...ownership.stagedEntries]) { + const result = unlinkOwnedEntry(entry.path, entry.identity, { evidence: entry.evidence }); + if (result.status === 'removed' || result.status === 'absent') { + ownership.stagedEntries.delete(finalPath); + } else { + errors.push({ + file: entry.path, + reason: result.status === 'different' && result.reason === 'content' + ? 'staging entry content changed; it was preserved' + : result.status === 'different' + ? 'staging entry identity changed; it was preserved' + : result.error?.message, + }); + } + } + const directories = [ + ownership.inspection, + ...(removeDirectory ? [{ + directory: ownership.stagingDirectory, + identity: ownership.stagingIdentity, + }] : []), + ].filter(Boolean); + for (const { directory, identity } of directories) { + try { + const stat = fs.lstatSync(directory, { bigint: true }); + if (!identity || !identityMatches(stat, identity, { directory: true })) { + errors.push({ + file: directory, + recoveryDirectory: directory, + reason: 'staging directory identity changed; it was preserved', + }); + } else { + removeEmptyDirectoryWithRetry(directory); + } + } catch (error) { + if (error?.code !== 'ENOENT') { + errors.push({ file: directory, recoveryDirectory: directory, reason: error.message }); + } + } + } + return errors; +} + +function verifyVisualEvidenceTargets(artifactPath, outputs, ownership) { + for (const target of visualEvidenceTargets(outputs)) { + const captured = ownership.targets.get(target.path); + const verification = captured ? verifyAtomicOutput(captured.snapshot) : { + status: 'unknown', + reason: { code: 'evidence-snapshot-missing' }, + }; + if (verification.status !== 'match') { + return evidencePathConflict(artifactPath, outputs, target.path, verification.reason); + } + } + return { ok: true }; +} + +function beginVisualEvidenceWrite(artifactPath, artifactBytes, outputs) { + const targets = visualEvidenceTargets(outputs); + const candidates = targets.map((target) => target.path); + const previousEvidence = new Map(); + try { + fs.mkdirSync(path.dirname(outputs.receipt), { recursive: true }); + } catch (error) { + return evidenceWriteFailure( + artifactPath, + 'visual-check could not create its evidence directory.', + [{ file: path.dirname(outputs.receipt), reason: error.message }], + ); + } + + const capturedTargets = new Map(); + for (const target of targets) { + const captured = captureAtomicOutput(target.path, { + requestedEntryPolicy: 'regular-or-absent', + }); + if (captured.status !== 'captured') { + return evidencePathConflict(artifactPath, outputs, target.path, captured.reason); + } + capturedTargets.set(target.path, captured); + } + + const parent = capturedTargets.get(outputs.receipt).snapshot.slot; + for (const target of targets.slice(1)) { + const slot = capturedTargets.get(target.path).snapshot.slot; + if (slot.parentDevice !== parent.parentDevice || slot.parentInode !== parent.parentInode) { + return evidencePathConflict(artifactPath, outputs, target.path, { + code: 'evidence-parent-mismatch', + }); + } + } + + const existing = candidates.filter((file) => ( + capturedTargets.get(file).snapshot.requestedEntry.kind === 'existing' + )); + if (existing.length) { + const receiptEntry = capturedTargets.get(outputs.receipt).snapshot.requestedEntry; + if (receiptEntry.kind !== 'existing') { + return evidencePathConflict(artifactPath, outputs, existing[0], { + code: 'ownership-receipt-missing', + }); + } + + const previousReceipt = readRegularFileContents(outputs.receipt, { + subject: 'ownership-receipt', + }); + const capturedReceipt = capturedTargets.get(outputs.receipt).snapshot.target; + if (!previousReceipt.ok || !evidenceIdentityMatches(previousReceipt.identity, { + device: capturedReceipt.device, + inode: capturedReceipt.inode, + mode: capturedReceipt.mode, + links: 1, + })) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-receipt-invalid', + ...(previousReceipt.reason?.code ? { bindingCode: previousReceipt.reason.code } : {}), + ...(previousReceipt.reason?.systemCode + ? { systemCode: previousReceipt.reason.systemCode } : {}), + }); + } + const previousBytes = previousReceipt.bytes; + let previous; + try { + previous = JSON.parse(previousBytes.toString('utf8')); + } catch (error) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-receipt-invalid', + ...(error?.code ? { systemCode: error.code } : {}), + }); + } + previousEvidence.set(outputs.receipt, { + ...previousReceipt.evidence, + }); + + if (previous?.schemaVersion !== 1 || previous?.command !== outputs.command) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-receipt-schema-mismatch', + }); + } + const recordedReceiptName = previous?.sidecars?.receipt; + const recordedContactName = previous?.sidecars?.contactSheet; + const recordedDirectory = previous?.sidecars?.directory || path.dirname(previous?.artifact?.path || ''); + const receiptNameSafe = typeof recordedReceiptName === 'string' + // path-contract-allow: portable-logical-path -- receipt filenames are serialized logical basenames. + && path.basename(recordedReceiptName) === recordedReceiptName; + const contactNameSafe = outputs.capture === false || typeof recordedContactName === 'string' + // path-contract-allow: portable-logical-path -- contact-sheet filenames are serialized logical basenames. + && path.basename(recordedContactName) === recordedContactName; + if (!path.isAbsolute(recordedDirectory) || !receiptNameSafe || !contactNameSafe + || sameLocation(path.join(recordedDirectory, recordedReceiptName), outputs.receipt).status !== 'match' + || (outputs.capture !== false && sameLocation(path.join(recordedDirectory, recordedContactName), outputs.contactSheet).status !== 'match')) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-sidecar-path-mismatch', + }); + } + if (typeof previous?.artifact?.path !== 'string' + || !path.isAbsolute(previous.artifact.path) + || sameEntry(previous.artifact.path, artifactPath).status !== 'match' + || previous.artifact.sha256 !== sha256(artifactBytes) + || previous.artifact.bytes !== artifactBytes.byteLength) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-artifact-mismatch', + }); + } + + const existingEvidence = visualEvidencePaths(outputs).filter((file) => ( + capturedTargets.get(file).snapshot.requestedEntry.kind === 'existing' + )); + const recordedEvidence = previous?.sidecars?.files; + if (existingEvidence.length && !Array.isArray(recordedEvidence)) { + return evidencePathConflict(artifactPath, outputs, existingEvidence[0], { + code: 'ownership-evidence-digests-missing', + }); + } + if (Array.isArray(recordedEvidence)) { + const allowed = new Map(visualEvidencePaths(outputs).map((file) => [path.basename(file), file])); + const recorded = new Map(); + for (const item of recordedEvidence) { + if (!item || typeof item.file !== 'string' || recorded.has(item.file) || !allowed.has(item.file)) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-evidence-list-invalid', + }); + } + recorded.set(item.file, item); + } + if (recorded.size !== existingEvidence.length + || existingEvidence.some((file) => !recorded.has(path.basename(file)))) { + return evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'ownership-evidence-list-mismatch', + }); + } + for (const file of existingEvidence) { + const actual = regularFileEvidence(file); + const expected = recorded.get(path.basename(file)); + if (!actual.ok + || actual.evidence.sha256 !== expected.sha256 + || actual.evidence.bytes !== expected.bytes) { + return evidencePathConflict(artifactPath, outputs, file, { + code: actual.ok ? 'ownership-evidence-digest-mismatch' : actual.reason.code, + ...(!actual.ok && actual.reason.systemCode + ? { systemCode: actual.reason.systemCode } : {}), + }); + } + previousEvidence.set(file, { + sha256: actual.evidence.sha256, + bytes: actual.evidence.bytes, + }); + } + } + + } + + for (const target of targets) { + const verification = verifyAtomicOutput(capturedTargets.get(target.path).snapshot); + if (verification.status !== 'match') { + return evidencePathConflict(artifactPath, outputs, target.path, verification.reason); + } + } + + let stagingDirectory; + let stagingStat; + try { + stagingDirectory = fs.mkdtempSync(path.toNamespacedPath( + path.join(parent.parentPath, '.archify-visual-check-'), + )); + stagingStat = fs.lstatSync(stagingDirectory, { bigint: true }); + if (!stagingStat.isDirectory() || stagingStat.isSymbolicLink() || stagingStat.ino === 0n) { + throw new Error('the newly created staging directory has no stable directory identity'); + } + } catch (error) { + return evidenceWriteFailure( + artifactPath, + 'visual-check could not create a private evidence staging directory.', + [{ file: parent.parentPath, reason: error.message }], + ); + } + + const staged = stagedOutputs(outputs, stagingDirectory); + const ownership = { + active: true, + artifact: path.resolve(artifactPath), + receipt: outputs.receipt, + targets: capturedTargets, + stagingDirectory, + stagingIdentity: fileIdentity(stagingStat), + stagedOutputs: staged, + stagingPaths: new Map([ + [outputs.receipt, staged.receipt], + [outputs.contactSheet, staged.contactSheet], + ...outputs.screenshots.map((entry, index) => [entry.path, staged.screenshots[index].path]), + ]), + stagedEntries: new Map(), + previousEvidence, + }; + visualEvidenceOwnerships.add(ownership); + return { ok: true, ownership }; +} + +function recordVisualEvidenceFiles(receipt, outputs, ownership) { + const files = []; + for (const file of visualEvidencePaths(outputs)) { + const staged = ownership.stagedEntries.get(file); + if (!staged || !currentEvidenceMatches(staged.path, staged.identity, staged.evidence)) { + const error = new Error(`visual-check could not bind evidence file "${file}" to its receipt.`); + error.evidenceReason = { code: 'staged-evidence-content-mismatch' }; + throw error; + } + files.push({ ...staged.evidence, file: path.basename(file) }); + } + receipt.sidecars.files = files; +} + +function currentEvidenceMatches(file, identity, evidence, { + expectedLinks = identity.links ?? 1, +} = {}) { + if (!evidence) return false; + const current = regularFileEvidence(file, { expectedLinks: Number(expectedLinks) }); + return current.ok + && evidenceIdentityMatches(current.identity, identity, expectedLinks) + && current.evidence.sha256 === evidence.sha256 + && current.evidence.bytes === evidence.bytes; +} + +function restoreOwnedBackup(backup, errors) { + const expectedLinks = backup.identity.links ?? 1n; + const expectedLinkCount = Number(expectedLinks); + let backupStat; + try { + backupStat = fs.lstatSync(backup.path, { bigint: true }); + } catch { + errors.push({ file: backup.path, reason: 'backup identity changed; it was preserved' }); + return; + } + if (!identityMatches(backupStat, backup.identity) || backupStat.nlink !== expectedLinks) { + errors.push({ file: backup.path, reason: 'backup identity changed; it was preserved' }); + return; + } + let captured = backup.evidence && Number.isSafeInteger(expectedLinkCount) + ? captureRegularFileBinding(backup.path, { + subject: 'restore-backup', + expectedIdentity: backup.identity, + expectedMode: backup.identity.mode, + expectedSha256: backup.evidence.sha256, + expectedBytes: backup.evidence.bytes, + expectedLinks: expectedLinkCount, + }) + : { status: 'unknown', reason: { code: 'restore-backup-evidence-unavailable' } }; + let contentChanged = false; + if (captured.status !== 'captured' + && captured.reason?.code === 'restore-backup-content-changed') { + captured = captureRegularFileBinding(backup.path, { + subject: 'restore-backup-claimant', + expectedIdentity: backup.identity, + expectedMode: backup.identity.mode, + expectedLinks: expectedLinkCount, + }); + contentChanged = captured.status === 'captured'; + } + if (captured.status !== 'captured') { + errors.push({ + file: backup.path, + reason: 'backup bytes, mode, or identity changed; it was preserved', + ...(captured.reason?.code ? { code: captured.reason.code } : {}), + }); + return; + } + let released; + try { + const finalEntry = pathEntry(backup.finalPath); + if (finalEntry.exists === true) { + errors.push({ + file: backup.path, + reason: 'final path is occupied; the owned backup was preserved', + }); + return; + } + if (finalEntry.exists === null) { + errors.push({ file: backup.finalPath, reason: finalEntry.error?.message }); + return; + } + try { + fs.linkSync(backup.path, backup.finalPath); + const linkedCount = expectedLinkCount + 1; + const staged = verifyRegularFileBinding(captured.binding, { + filePath: backup.path, + expectedLinks: linkedCount, + }); + const published = verifyRegularFileBinding(captured.binding, { + filePath: backup.finalPath, + expectedLinks: linkedCount, + }); + if (staged.status !== 'match' || published.status !== 'match') { + const removed = quarantineRemoveRegularFileBinding( + captured.binding, + backup.finalPath, + { subject: 'restored-evidence', expectedLinks: linkedCount }, + ); + errors.push({ + file: backup.path, + reason: removed.status === 'removed' + ? 'restored entry identity could not be verified; the owned backup was preserved' + : removed.status === 'preserved' + ? 'restored entry changed during cleanup; its successor and the owned backup were preserved' + : 'restored entry changed before rollback; both entries were preserved', + ...(removed.reason?.code ? { code: removed.reason.code } : {}), + ...(removed.recoveryDirectory ? { recoveryDirectory: removed.recoveryDirectory } : {}), + ...(removed.recoveryFile ? { recoveryFile: removed.recoveryFile } : {}), + }); + return; + } + const removed = unlinkOwnedEntry(backup.path, backup.identity, { + evidence: captured.content, + expectedLinks: linkedCount, + }); + const finalMatches = verifyRegularFileBinding(captured.binding, { + filePath: backup.finalPath, + expectedLinks: expectedLinkCount, + }).status === 'match'; + if (removed.status !== 'removed' || !finalMatches) { + errors.push({ file: backup.path, reason: 'restored entry link count could not be finalized' }); + } else if (contentChanged) { + errors.push({ + file: backup.finalPath, + reason: 'the backup content changed externally; its current bytes were restored and preserved', + }); + } + } catch (error) { + errors.push({ + file: backup.path, + reason: error?.code === 'EEXIST' + ? 'final path was claimed during restore; the owned backup was preserved' + : error.message, + }); + } + } finally { + released = releaseRegularFileBinding(captured.binding); + if (released.status !== 'released') { + errors.push({ + file: backup.path, + reason: 'restored backup binding could not be released cleanly', + ...(released.reason?.code ? { code: released.reason.code } : {}), + }); + } + } +} + +function commitVisualEvidence(artifactPath, outputs, ownership, desiredPaths) { + const verification = verifyVisualEvidenceTargets(artifactPath, outputs, ownership); + if (!verification.ok) { + const cleanupErrors = cleanupStagedEvidence(ownership, { removeDirectory: true }); + if (cleanupErrors.length) verification.diagnostic.evidence.rollbackErrors = cleanupErrors; + ownership.active = false; + return verification; + } + + const desired = new Set(desiredPaths); + const stagedBindings = new Map(); + const releaseStagedBindings = () => { + const failures = []; + for (const [finalPath, binding] of stagedBindings) { + const released = releaseRegularFileBinding(binding); + if (released.status !== 'released') { + failures.push({ + file: finalPath, + reason: released.reason?.code, + bindingState: released.reason, + ...(released.reason?.recoveryDirectory + ? { recoveryDirectory: released.reason.recoveryDirectory } : {}), + }); + } + } + stagedBindings.clear(); + return failures; + }; + for (const finalPath of desired) { + const staged = ownership.stagedEntries.get(finalPath); + const captured = staged ? captureRegularFileBinding(staged.path, { + subject: 'staged-evidence', + expectedIdentity: staged.identity, + expectedMode: staged.identity.mode, + expectedSha256: staged.evidence?.sha256, + expectedBytes: staged.evidence?.bytes, + expectedLinks: 1, + }) : null; + if (!staged || captured?.status !== 'captured') { + const failure = evidenceWriteFailure( + artifactPath, + 'visual-check could not verify its staged evidence before publication.', + [{ + file: staged?.path || stagedPathFor(ownership, finalPath), + reason: captured?.reason?.code || 'staged entry missing or changed', + }], + ); + failure.diagnostic.evidence.errors.push( + ...releaseStagedBindings(), + ...cleanupStagedEvidence(ownership, { removeDirectory: true }), + ); + ownership.active = false; + return failure; + } + stagedBindings.set(finalPath, captured.binding); + } + + const backups = []; + const published = []; + let conflict = null; + try { + for (const [index, target] of visualEvidenceTargets(outputs).entries()) { + const captured = ownership.targets.get(target.path); + if (captured.snapshot.target.kind !== 'file') continue; + const backupPath = path.join(ownership.stagingDirectory, `previous-${index}`); + const identity = { + device: captured.snapshot.target.device, + inode: captured.snapshot.target.inode, + mode: captured.snapshot.target.mode, + links: 1n, + }; + const evidence = ownership.previousEvidence.get(target.path); + if (!evidence) { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: 'target-evidence-missing-during-commit', + }); + throw new Error(conflict.error); + } + const backup = { + targetPath: target.path, + finalPath: captured.commitPath, + path: backupPath, + identity, + evidence, + }; + const backupResult = backupOwnedPublishedEntry( + captured.commitPath, + backupPath, + identity, + evidence, + ); + if (backupResult.backupCreated) backups.push(backup); + if (backupResult.status !== 'backed-up' + || !backupResult.backupCreated + || !backupResult.backupVerified) { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: backupResult.reason?.code || 'target-backup-failed-during-commit', + backupCreated: backupResult.backupCreated === true, + backupVerified: backupResult.backupVerified === true, + ...(backupResult.recoveryDirectory + ? { recoveryDirectory: backupResult.recoveryDirectory } : {}), + ...(backupResult.recoveryFile ? { recoveryFile: backupResult.recoveryFile } : {}), + ...(backupResult.target ? { target: backupResult.target } : {}), + ...(backupResult.reason ? { bindingState: backupResult.reason } : {}), + }); + throw new Error(conflict.error); + } + } + + const publicationOrder = visualEvidenceTargets(outputs) + .filter((target) => desired.has(target.path)) + .sort((left, right) => Number(left.kind === 'receipt') - Number(right.kind === 'receipt')); + for (const target of publicationOrder) { + const captured = ownership.targets.get(target.path); + const staged = ownership.stagedEntries.get(target.path); + const stagedBinding = stagedBindings.get(target.path); + const beforeLink = verifyRegularFileBinding(stagedBinding, { + filePath: staged.path, + expectedLinks: 1, + }); + if (beforeLink.status !== 'match') { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: beforeLink.reason?.code?.includes('content-changed') + ? 'staged-evidence-content-mismatch' + : beforeLink.reason?.code || 'staged-evidence-content-mismatch', + }); + throw new Error(conflict.error); + } + try { + fs.linkSync(staged.path, captured.commitPath); + } catch (error) { + if (error?.code === 'EEXIST') { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: 'target-created-during-commit', + }); + } + throw error; + } + published.push({ + finalPath: captured.commitPath, + stagedPath: staged.path, + identity: staged.identity, + evidence: staged.evidence, + binding: stagedBinding, + }); + const stagedLinked = verifyRegularFileBinding(stagedBinding, { + filePath: staged.path, + expectedLinks: 2, + }); + const publishedLinked = verifyRegularFileBinding(stagedBinding, { + filePath: captured.commitPath, + expectedLinks: 2, + }); + if (stagedLinked.status !== 'match' || publishedLinked.status !== 'match') { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: 'published-entry-identity-mismatch', + stagedState: stagedLinked.reason, + publishedState: publishedLinked.reason, + }); + throw new Error(conflict.error); + } + const removed = quarantineRemoveRegularFileBinding(stagedBinding, staged.path, { + subject: 'staged-evidence', + expectedLinks: 2, + }); + if (removed.status !== 'removed' && removed.status !== 'preserved') { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: removed.reason?.code || 'published-entry-identity-mismatch', + }); + throw new Error(`visual-check could not retire staged evidence "${staged.path}".`); + } + if (removed.status === 'removed') ownership.stagedEntries.delete(target.path); + const finalized = verifyRegularFileBinding(stagedBinding, { + filePath: captured.commitPath, + expectedLinks: 1, + }); + if (finalized.status !== 'match') { + conflict = evidencePathConflict(artifactPath, outputs, target.path, { + code: 'published-entry-link-count-mismatch', + bindingState: finalized.reason, + }); + throw new Error(conflict.error); + } + } + + // Receipt publication is the linearization point for the evidence set. + // Recheck every earlier member afterwards so a writer that replaces a PNG + // or the contact sheet while later members publish cannot yield success. + for (const entry of published) { + const finalState = verifyRegularFileBinding(entry.binding, { + filePath: entry.finalPath, + expectedLinks: 1, + }); + if (finalState.status !== 'match') { + conflict = evidencePathConflict(artifactPath, outputs, entry.finalPath, { + code: 'published-set-identity-mismatch', + bindingState: finalState.reason, + }); + throw new Error(conflict.error); + } + } + + for (const backup of backups) { + if (!currentEvidenceMatches( + backup.path, + backup.identity, + ownership.previousEvidence.get(backup.targetPath), + )) { + conflict = evidencePathConflict(artifactPath, outputs, backup.targetPath, { + code: 'backup-content-changed-before-cleanup', + }); + throw new Error(conflict.error); + } + } + + // SMB keeps removed staging names pending until every bound handle closes. + // Publication and backup verification are complete before directory cleanup. + const cleanupErrors = releaseStagedBindings(); + for (const backup of backups) { + const removed = unlinkOwnedEntry(backup.path, backup.identity, { + evidence: backup.evidence, + }); + if (removed.status !== 'removed' && removed.status !== 'absent') { + cleanupErrors.push({ + file: backup.path, + reason: removed.status === 'different' && removed.reason === 'content' + ? 'backup content changed; it was preserved' + : removed.status === 'different' + ? 'backup identity changed; it was preserved' + : removed.error?.message, + }); + } + } + cleanupErrors.push(...cleanupStagedEvidence(ownership, { removeDirectory: true })); + ownership.active = false; + let recoveryDirectory; + if (cleanupErrors.length) { + try { + const retained = fs.lstatSync(ownership.stagingDirectory, { bigint: true }); + if (identityMatches(retained, ownership.stagingIdentity, { directory: true })) { + recoveryDirectory = ownership.stagingDirectory; + } + } catch {} + recoveryDirectory ||= cleanupErrors.find((entry) => entry.recoveryDirectory)?.recoveryDirectory; + } + return { + ok: true, + status: cleanupErrors.length ? 'committed-with-warning' : 'committed', + cleanupErrors, + ...(recoveryDirectory ? { recoveryDirectory } : {}), + committedReceipt: outputs.receipt, + }; + } catch (error) { + const rollbackErrors = []; + for (const entry of [...published].reverse()) { + let removed; + let ownedMismatch; + for (const expectedLinks of [2, 1]) { + const matches = verifyRegularFileBinding(entry.binding, { + filePath: entry.finalPath, + expectedLinks, + }); + if (matches.status === 'match') { + removed = quarantineRemoveRegularFileBinding( + entry.binding, + entry.finalPath, + { subject: 'published-evidence', expectedLinks }, + ); + break; + } + if (matches.reason?.code?.includes('content-changed') + || matches.reason?.code?.includes('mode-changed')) { + ownedMismatch = matches; + } else { + ownedMismatch ||= matches; + } + const reportedLinks = Number(matches.reason?.links); + if (matches.status === 'unsupported' + && matches.reason?.code === 'staged-evidence-hardlinked' + && Number.isSafeInteger(reportedLinks) + && reportedLinks > expectedLinks) { + const current = verifyRegularFileBinding(entry.binding, { + filePath: entry.finalPath, + expectedLinks: reportedLinks, + }); + if (current.status === 'match') { + removed = quarantineRemoveRegularFileBinding( + entry.binding, + entry.finalPath, + { subject: 'published-evidence', expectedLinks: reportedLinks }, + ); + break; + } + } + } + if (!removed) { + const claimantRemoval = quarantineRemoveLinkedRegularFileAlias( + entry.stagedPath, + entry.finalPath, + { subject: 'published-evidence-claimant' }, + ); + removed = claimantRemoval.status === 'removed' ? claimantRemoval : ownedMismatch; + } + if (removed.status !== 'removed' && removed.status !== 'absent') { + rollbackErrors.push(publishedRemovalError(entry.finalPath, removed)); + } + } + for (const backup of [...backups].reverse()) restoreOwnedBackup(backup, rollbackErrors); + rollbackErrors.push(...releaseStagedBindings()); + rollbackErrors.push(...cleanupStagedEvidence(ownership, { removeDirectory: true })); + ownership.active = false; + if (conflict) { + if (rollbackErrors.length) conflict.diagnostic.evidence.rollbackErrors = rollbackErrors; + return conflict; + } + return evidenceWriteFailure( + artifactPath, + 'visual-check could not atomically publish its evidence set.', + [{ reason: error.message }, ...rollbackErrors], + ); + } finally { + releaseStagedBindings(); + } +} + +function executable(file, platform = process.platform) { + if (!file) return null; + try { + fs.accessSync(file, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK); + return path.resolve(file); + } catch { + return null; + } +} + +function findOnPath(command, env, platform, resolveExecutable) { + const pathApi = platform === 'win32' ? path.win32 : path; + const directories = String(env.PATH || '').split(pathApi.delimiter).filter(Boolean); + const extensions = platform === 'win32' + ? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean) + : ['']; + for (const directory of directories) { + for (const extension of extensions) { + const candidate = pathApi.join(directory, `${command}${extension}`); + const resolved = resolveExecutable(candidate, platform); + if (resolved) return resolved; + } + } + return null; +} + +export function findChrome({ + env = process.env, + platform = process.platform, + resolveExecutable = executable, +} = {}) { + if (Object.prototype.hasOwnProperty.call(env, 'ARCHIFY_CHROME')) { + return resolveExecutable(env.ARCHIFY_CHROME, platform); + } + + const fixed = []; + const commands = []; + if (platform === 'darwin') { + fixed.push( + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + ); + } else if (platform === 'win32') { + const pathApi = path.win32; + for (const root of [env.PROGRAMFILES, env['PROGRAMFILES(X86)'], env.LOCALAPPDATA].filter(Boolean)) { + fixed.push( + pathApi.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'), + pathApi.join(root, 'Chromium', 'Application', 'chrome.exe'), + ); + } + commands.push('chrome', 'google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'); + } else { + commands.push('google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'); + } + + for (const candidate of fixed) { + const resolved = resolveExecutable(candidate, platform); + if (resolved) return resolved; + } + for (const command of commands) { + const resolved = findOnPath(command, env, platform, resolveExecutable); + if (resolved) return resolved; + } + return null; +} + +class PipeCdp { + constructor(child, { failureDetails = () => '' } = {}) { + this.child = child; + this.failureDetails = failureDetails; + this.nextId = 1; + this.buffer = ''; + this.receivedBytes = 0; + this.receivedMessages = 0; + this.completedWrites = 0; + this.writtenBytes = 0; + this.pending = new Map(); + this.waiters = []; + this.writePipe = child.stdio[3]; + this.readPipe = child.stdio[4]; + this.readPipe.setEncoding('utf8'); + this.readPipe.on('data', (chunk) => this.consume(chunk)); + this.writePipe.on('error', (error) => this.failAll(this.failure('write pipe', error))); + this.readPipe.on('error', (error) => this.failAll(this.failure('read pipe', error))); + this.readPipe.once('end', () => this.failAll(this.failure('read pipe', new Error('stream ended')))); + this.readPipe.once('close', () => this.failAll(this.failure('read pipe', new Error('stream closed')))); + this.writePipe.once('close', () => this.failAll(this.failure('write pipe', new Error('stream closed')))); + child.once('error', (error) => this.failAll(this.failure('process launch', error))); + child.once('close', (code, signal) => { + const ending = signal ? `signal ${signal}` : `exit code ${code}`; + this.failAll(this.failure('process exit', new Error(`Chrome closed with ${ending}`))); + }); + } + + failure(stage, error) { + const code = error?.code ? ` [${error.code}]` : ''; + const details = this.failureDetails(); + const failure = new Error([ + `Chrome DevTools ${stage} failed: ${error?.message || String(error)}${code}`, + details, + `Chrome transport: Node ${process.version}, libuv ${process.versions.uv}, ${process.platform}; pid=${this.child.pid ?? 'unavailable'}, exitCode=${this.child.exitCode}, signalCode=${this.child.signalCode}.`, + `Chrome read pipe: readable=${this.readPipe.readable}, ended=${this.readPipe.readableEnded}, destroyed=${this.readPipe.destroyed}, receivedBytes=${this.receivedBytes}, messages=${this.receivedMessages}, bufferedBytes=${Buffer.byteLength(this.buffer)}.`, + `Chrome write pipe: writable=${this.writePipe.writable}, ended=${this.writePipe.writableEnded}, destroyed=${this.writePipe.destroyed}, completedWrites=${this.completedWrites}, writtenBytes=${this.writtenBytes}, bufferedBytes=${this.writePipe.writableLength}.`, + ].filter(Boolean).join('\n')); + if (error?.code) failure.code = error.code; + if (error?.method) failure.method = error.method; + return failure; + } + + consume(chunk) { + this.receivedBytes += Buffer.byteLength(chunk); + this.buffer += chunk; + let boundary; + while ((boundary = this.buffer.indexOf('\0')) >= 0) { + const raw = this.buffer.slice(0, boundary); + this.buffer = this.buffer.slice(boundary + 1); + if (!raw) continue; + let message; + try { + message = JSON.parse(raw); + } catch (error) { + this.failAll(new Error(`Chrome DevTools returned invalid JSON: ${error.message}`)); + continue; + } + this.receivedMessages += 1; + if (message.id) { + const pending = this.pending.get(message.id); + if (!pending) continue; + clearTimeout(pending.timer); + this.pending.delete(message.id); + if (message.error) pending.reject(new Error(`${pending.method}: ${message.error.message}`)); + else pending.resolve(message.result || {}); + continue; + } + for (const waiter of [...this.waiters]) { + if (waiter.method !== message.method) continue; + if (waiter.sessionId && waiter.sessionId !== message.sessionId) continue; + clearTimeout(waiter.timer); + this.waiters.splice(this.waiters.indexOf(waiter), 1); + waiter.resolve(message.params || {}); + } + } + } + + send(method, params = {}, sessionId = undefined, timeoutMs = 15000) { + const id = this.nextId++; + const message = { id, method, params }; + if (sessionId) message.sessionId = sessionId; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + const error = new Error(`${method}: timed out after ${timeoutMs}ms`); + error.code = 'ERR_CHROME_CDP_TIMEOUT'; + error.method = method; + reject(this.failure('protocol timeout', error)); + }, timeoutMs); + this.pending.set(id, { method, resolve, reject, timer }); + try { + const serialized = `${JSON.stringify(message)}\0`; + this.writePipe.write(serialized, (error) => { + if (error) { + this.failAll(this.failure('write pipe', error)); + } else { + this.completedWrites += 1; + this.writtenBytes += Buffer.byteLength(serialized); + } + }); + } catch (error) { + this.failAll(this.failure('write pipe', error)); + } + }); + } + + waitFor(method, sessionId, timeoutMs = 15000) { + return new Promise((resolve, reject) => { + const waiter = { method, sessionId, resolve, reject, timer: null }; + waiter.timer = setTimeout(() => { + this.waiters.splice(this.waiters.indexOf(waiter), 1); + reject(this.failure('event timeout', new Error(`${method}: event timed out after ${timeoutMs}ms`))); + }, timeoutMs); + this.waiters.push(waiter); + }); + } + + failAll(error) { + for (const pending of this.pending.values()) { + clearTimeout(pending.timer); + pending.reject(error); + } + for (const waiter of this.waiters) { + clearTimeout(waiter.timer); + waiter.reject(error); + } + this.pending.clear(); + this.waiters = []; + } +} + +export function chromeVisualBrowserArgs(profileRoot, { + env = process.env, + getuid = typeof process.getuid === 'function' ? () => process.getuid() : null, +} = {}) { + const args = [ + '--headless=new', + '--remote-debugging-pipe', + '--disable-gpu', + '--hide-scrollbars', + '--disable-background-networking', + '--disable-component-update', + '--disable-default-apps', + '--disable-sync', + '--metrics-recording-only', + '--no-first-run', + '--no-default-browser-check', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + '--force-device-scale-factor=1', + `--user-data-dir=${profileRoot}`, + 'about:blank', + ]; + const rootUser = typeof getuid === 'function' && getuid() === 0; + const sandboxOptOut = env?.[CHROME_NO_SANDBOX_ENV] === '1'; + if (rootUser || sandboxOptOut) args.unshift('--no-sandbox'); + return args; +} + +async function evaluate(cdp, sessionId, expression, awaitPromise = false) { + const response = await cdp.send('Runtime.evaluate', { + expression, + awaitPromise, + returnByValue: true, + }, sessionId); + if (response.exceptionDetails) { + throw new Error(response.exceptionDetails.exception?.description + || response.exceptionDetails.text + || 'Runtime.evaluate failed'); + } + return response.result?.value; +} + +export class ChromeVisualBrowser { + constructor(chromePath, { + env = process.env, + getuid = typeof process.getuid === 'function' ? () => process.getuid() : null, + spawnImpl = spawn, + startupTimeoutMs = CHROME_STARTUP_TIMEOUT_MS, + } = {}) { + this.profileRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-visual-check-profile-')); + this.stderr = ''; + const args = chromeVisualBrowserArgs(this.profileRoot, { env, getuid }); + this.child = spawnImpl(chromePath, args, { stdio: ['ignore', 'ignore', 'pipe', 'pipe', 'pipe'] }); + this.child.stderr.setEncoding('utf8'); + this.child.stderr.on('data', (chunk) => { + this.stderr = `${this.stderr}${chunk}`.slice(-8000); + }); + this.child.stderr.on('error', (error) => { + this.stderr = `${this.stderr}\nChrome stderr stream failed: ${error.message}`.trim().slice(-8000); + }); + this.cdp = new PipeCdp(this.child, { + failureDetails: () => { + const exit = this.child.signalCode + ? `signal ${this.child.signalCode}` + : this.child.exitCode == null ? 'still running' : `exit code ${this.child.exitCode}`; + const stderr = this.stderr.trim(); + return [ + `Chrome process: ${exit}.`, + stderr ? `Chrome stderr:\n${stderr}` : '', + ].filter(Boolean).join('\n'); + }, + }); + this.startupTimeoutMs = startupTimeoutMs; + this.sessionPromise = this.attach(); + } + + async attach() { + // Process launch can be delayed substantially on a busy desktop. Keep the + // longer allowance inside one gate invocation so an authoring agent does + // not turn startup jitter into repeated tool calls or candidate edits. + const targets = await this.cdp.send( + 'Target.getTargets', {}, undefined, this.startupTimeoutMs, + ); + let target = targets.targetInfos?.find((item) => item.type === 'page'); + if (!target) { + const created = await this.cdp.send('Target.createTarget', { url: 'about:blank' }); + target = { targetId: created.targetId }; + } + const attached = await this.cdp.send('Target.attachToTarget', { + targetId: target.targetId, + flatten: true, + }); + await this.cdp.send('Page.enable', {}, attached.sessionId); + await this.cdp.send('Runtime.enable', {}, attached.sessionId); + return attached.sessionId; + } + + async inspect({ artifactPath, width, height, theme, screenshotPath, writeScreenshot }) { + const sessionId = await this.sessionPromise; + await this.cdp.send('Emulation.setDeviceMetricsOverride', { + width, + height, + deviceScaleFactor: 1, + mobile: false, + }, sessionId); + + const url = pathToFileURL(artifactPath); + url.searchParams.set('theme', theme); + const loaded = this.cdp.waitFor('Page.loadEventFired', sessionId); + // Navigation can fail before this waiter is awaited. Attach a rejection + // handler immediately so a later load failure never escapes as an + // unhandled rejection; awaiting `loaded` below still reports it normally. + loaded.catch(() => {}); + const navigation = await this.cdp.send('Page.navigate', { url: url.href }, sessionId); + if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`); + await loaded; + await evaluate(this.cdp, sessionId, `(function () { + document.documentElement.setAttribute('data-motion', 'still'); + var panel = document.querySelector('.diagram-container'); + if (panel) panel.setAttribute('data-detail-level', 'read'); + var fontsReady = document.fonts && document.fonts.ready + ? document.fonts.ready.catch(function () {}) + : Promise.resolve(); + if (window.Archify && Archify.layoutStability && typeof Archify.layoutStability.whenStable === 'function') { + return fontsReady.then(function () { return Archify.layoutStability.whenStable(); }); + } + return fontsReady.then(function () { + if (window.Archify && Archify.readerLayout && typeof Archify.readerLayout.whenStable === 'function') { + return Archify.readerLayout.whenStable(); + } + }).then(function () { + if (window.Archify && Archify.viewerChromeLayout && typeof Archify.viewerChromeLayout.whenStable === 'function') { + return Archify.viewerChromeLayout.whenStable(); + } + }).then(function () { + if (window.Archify && Archify.readerLayout && typeof Archify.readerLayout.whenStable === 'function') { + return Archify.readerLayout.whenStable(); + } + }).then(function () { + if (window.Archify && Archify.viewerChromeLayout && typeof Archify.viewerChromeLayout.whenStable === 'function') { + return Archify.viewerChromeLayout.whenStable(); + } + return new Promise(function (resolve) { + requestAnimationFrame(function () { requestAnimationFrame(resolve); }); + }); + }); + })()`, true); + + const metrics = await evaluate(this.cdp, sessionId, `(function () { + var reader = document.querySelector('.container'); + var diagram = document.querySelector('.diagram-container'); + var svg = diagram && ( + diagram.querySelector(':scope > svg') || + diagram.querySelector(':scope > .diagram-stage > svg') + ); + var stage = diagram && (diagram.querySelector(':scope > .diagram-stage') || svg); + var legend = svg && svg.querySelector('[data-legend]'); + var navigationDock = diagram && diagram.querySelector('.diagram-nav'); + var viewBox = svg && svg.viewBox && svg.viewBox.baseVal; + var diagramWidth = svg ? svg.getBoundingClientRect().width : 0; + var viewBoxWidth = viewBox ? viewBox.width : 0; + var scale = viewBoxWidth > 0 ? Math.min(1, diagramWidth / viewBoxWidth) : 0; + var minimum = null; + var minimumNonEdge = null; + var minimumEdge = null; + if (svg && scale > 0) { + Array.from(svg.querySelectorAll('text[data-node-label], text[data-boundary-label], text[data-detail="context"], g[data-detail="context"] text')).forEach(function (text) { + if (text.getAttribute('data-detail') === 'fine' || text.closest('[data-detail="fine"]')) return; + var edge = text.closest('[data-edge-from][data-edge-to]'); + var node = text.closest('[data-node-id]'); + var context = text.getAttribute('data-detail') === 'context' || Boolean(text.closest('g[data-detail="context"]')); + var detail = text.hasAttribute('data-node-label') + ? 'primary' + : text.hasAttribute('data-boundary-label') ? 'boundary' + : context && edge ? 'edge' : 'context'; + if (detail === 'context' && !node) return; + var sourceFontPx = parseFloat(text.getAttribute('font-size') || ''); + if (!Number.isFinite(sourceFontPx)) return; + var projectedFontPx = sourceFontPx * scale; + var entry = { + text: (text.textContent || '').trim(), + detail: detail, + owner: edge ? { + kind: 'edge', + id: edge.getAttribute('data-edge-id'), + from: edge.getAttribute('data-edge-from'), + to: edge.getAttribute('data-edge-to') + } : node ? { kind: 'node', id: node.getAttribute('data-node-id') } + : detail === 'boundary' ? { kind: 'boundary', id: null } : null, + sourceFontPx: sourceFontPx, + projectedFontPx: projectedFontPx + }; + if (!minimum || projectedFontPx < minimum.projectedFontPx) minimum = entry; + if (detail === 'edge') { + if (!minimumEdge || projectedFontPx < minimumEdge.projectedFontPx) minimumEdge = entry; + } else if (!minimumNonEdge || projectedFontPx < minimumNonEdge.projectedFontPx) { + minimumNonEdge = entry; + } + }); + } + function intersectionArea(a, b) { + if (!a || !b || !a.width || !a.height || !b.width || !b.height) return 0; + var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left)); + var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top)); + return width * height; + } + var legendRect = legend ? legend.getBoundingClientRect() : null; + var stageRect = window.Archify && Archify.viewerChromeLayout + && typeof Archify.viewerChromeLayout.stageRect === 'function' + ? Archify.viewerChromeLayout.stageRect() + : (stage ? stage.getBoundingClientRect() : null); + // The dock may lift to the viewport floor while the page scrolls; the + // stage contract is about its resting position. + var navigationDockRect = !navigationDock ? null + : window.Archify && Archify.viewerChromeLayout && typeof Archify.viewerChromeLayout.dockRect === 'function' + ? Archify.viewerChromeLayout.dockRect() + : navigationDock.getBoundingClientRect(); + var stageDockIntersectionArea = intersectionArea(stageRect, navigationDockRect); + var viewerChromeReceipt = window.Archify && Archify.viewerChromeLayout + && typeof Archify.viewerChromeLayout.receipt === 'function' + ? Archify.viewerChromeLayout.receipt() + : null; + // Read rendered boxes only; source ownership, offsets and hard pins are unknown. + var workflowLanes = svg ? Array.from(svg.querySelectorAll('rect[data-composition-frame-kind="lane"]')).map(function (lane) { + var rect = lane.getBoundingClientRect(); + var members = Array.from(svg.querySelectorAll('g[data-node-id]')).map(function (node) { + var box = node.querySelector('rect'); + if (!box) return null; + var bounds = box.getBoundingClientRect(); + if (bounds.top < rect.top - 1 || bounds.bottom > rect.bottom + 1 + || bounds.left < rect.left - 1 || bounds.right > rect.right + 1) return null; + return { id: node.getAttribute('data-node-id'), top: bounds.top, bottom: bounds.bottom }; + }).filter(Boolean); + var top = members.length ? members.reduce(function (minimum, node) { return Math.min(minimum, node.top); }, Infinity) : null; + var bottom = members.length ? members.reduce(function (maximum, node) { return Math.max(maximum, node.bottom); }, -Infinity) : null; + return { + frameId: lane.getAttribute('data-composition-frame-id'), + heightPx: Math.round(rect.height), + nodeCount: members.length, + nodeIds: members.slice(0, 12).map(function (node) { return node.id; }), + nodeSpanPx: top === null ? null : Math.round(bottom - top), + spaceAboveNodesPx: top === null ? null : Math.round(top - rect.top), + spaceBelowNodesPx: bottom === null ? null : Math.round(rect.bottom - bottom) + }; + }).sort(function (a, b) { return b.heightPx - a.heightPx; }).slice(0, 6) : []; + // Vertical page budget: every block that stacks above or below the SVG, + // so an overflow receipt can say which part must give up how many pixels. + function outerHeight(element) { + if (!element) return 0; + var style = window.getComputedStyle(element); + if (element.hidden || style.display === 'none') return 0; + return element.getBoundingClientRect().height + + (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0); + } + var bodyStyle = window.getComputedStyle(document.body); + var diagramStyle = diagram ? window.getComputedStyle(diagram) : null; + var svgHeight = svg ? svg.getBoundingClientRect().height : 0; + var notesBelowFold = Boolean(reader) && document.documentElement.getAttribute('data-reader-rail') === 'bottom'; + var pageComposition = { + bodyPaddingPx: Math.round((parseFloat(bodyStyle.paddingTop) || 0) + (parseFloat(bodyStyle.paddingBottom) || 0)), + headerPx: Math.round(outerHeight(reader && reader.querySelector('.header'))), + diagramChromePx: Math.round(outerHeight(diagram) - svgHeight), + svgPx: Math.round(svgHeight), + cardsPx: notesBelowFold ? 0 : Math.round(outerHeight(reader && reader.querySelector('.cards'))), + viewBoxHeight: viewBox ? viewBox.height : 0 + }; + var svgRect = svg && svg.getBoundingClientRect(); + var diagramRect = diagram && diagram.getBoundingClientRect(); + var documentScrollUnclipped = Boolean(svgRect && diagramRect + && svgRect.top >= diagramRect.top - 1 && svgRect.left >= diagramRect.left - 1 + && svgRect.bottom <= diagramRect.bottom + 1 && svgRect.right <= diagramRect.right + 1 + && diagram.scrollHeight <= diagram.clientHeight + 1 + && diagram.scrollWidth <= diagram.clientWidth + 1 + && [document.documentElement, document.body].every(function (element) { + return !['hidden', 'clip'].includes(window.getComputedStyle(element).overflowY); + })); + return { + pageComposition: pageComposition, + innerWidth: window.innerWidth, + innerHeight: window.innerHeight, + scrollWidth: Math.ceil(document.documentElement.scrollWidth), + // Bottom notes and index are reading material below the fold; the + // Reader excludes them from the one-screen fit, and so does this check. + scrollHeight: Math.max(window.innerHeight, Math.ceil(document.documentElement.scrollHeight - ( + notesBelowFold ? outerHeight(reader.querySelector('.reader-rail')) : 0))), + resolvedTheme: document.documentElement.getAttribute('data-theme') || '', + readerLayout: document.documentElement.getAttribute('data-reader-layout') || null, + readerOverflow: document.documentElement.getAttribute('data-reader-overflow') || null, + readerFit: svg ? svg.getAttribute('data-reader-fit') : null, + diagramType: svg ? svg.getAttribute('data-diagram-type') : null, + documentScrollUnclipped: documentScrollUnclipped, + readerWidth: reader ? reader.getBoundingClientRect().width : 0, + diagramWidth: diagramWidth, + viewBoxWidth: viewBoxWidth, + workflowLanes: workflowLanes, + minimumProjectedNodeTextPx: minimum ? minimum.projectedFontPx : null, + minimumProjectedNonEdgeTextPx: minimumNonEdge ? minimumNonEdge.projectedFontPx : null, + minimumProjectedEdgeTextPx: minimumEdge ? minimumEdge.projectedFontPx : null, + minimumProjectedNodeText: minimum ? minimum.text : null, + minimumProjectedNodeTextDetail: minimum ? minimum.detail : null, + minimumProjectedNodeTextOwner: minimum ? minimum.owner : null, + hasLegend: Boolean(legendRect && legendRect.width && legendRect.height), + hasNavigationDock: Boolean(navigationDockRect && navigationDockRect.width && navigationDockRect.height), + legendDockIntersectionArea: stageDockIntersectionArea > 0 + ? intersectionArea(legendRect, navigationDockRect) + : 0, + dockStageIntersectionArea: stageDockIntersectionArea, + dockStageGap: stageRect && navigationDockRect ? navigationDockRect.top - stageRect.bottom : null, + viewerChromeRequiredGap: viewerChromeReceipt ? viewerChromeReceipt.gap : null, + viewerChromeReserve: viewerChromeReceipt ? viewerChromeReceipt.reserve : 0, + viewerChromeActive: viewerChromeReceipt ? viewerChromeReceipt.active : false + }; + })()`); + if (!metrics || !Number.isFinite(metrics.scrollWidth) || !Number.isFinite(metrics.scrollHeight)) { + throw new Error('Chrome returned incomplete containment metrics.'); + } + + if (screenshotPath) { + const capture = await this.cdp.send('Page.captureScreenshot', { + format: 'png', + fromSurface: true, + captureBeyondViewport: false, + }, sessionId, 20000); + if (!capture.data) throw new Error('Chrome returned an empty screenshot.'); + const screenshot = Buffer.from(capture.data, 'base64'); + if (writeScreenshot) writeScreenshot(screenshot); + else fs.writeFileSync(screenshotPath, screenshot, { flag: 'wx' }); + } + return metrics; + } + + close() { + if (!this.closePromise) this.closePromise = this.finishClose(); + return this.closePromise; + } + + async finishClose() { + this.cdp.failAll(new Error('visual-check finished')); + try { + if (this.child.exitCode === null && this.child.signalCode === null) { + await new Promise((resolve) => { + let timer = null; + let settled = false; + const finish = () => { + if (settled) return; + settled = true; + if (timer) clearTimeout(timer); + this.child.removeListener('exit', finish); + resolve(); + }; + this.child.once('exit', finish); + this.child.kill('SIGTERM'); + if (settled) return; + timer = setTimeout(() => { + if (this.child.exitCode === null && this.child.signalCode === null) this.child.kill('SIGKILL'); + finish(); + }, 1500); + }); + } + } finally { + // `exit` only reports that Chrome's main process is gone. A helper may + // still hold an inherited pipe open, so explicitly release every stream + // this browser instance owns before its test cleanup hook resolves. + for (const stream of [this.child.stderr, this.child.stdio[3], this.child.stdio[4]]) { + if (stream && !stream.destroyed) stream.destroy(); + } + } + try { + fs.rmSync(this.profileRoot, { recursive: true, force: true }); + } catch { + // Chrome may briefly retain profile files on Windows; evidence is done. + } + } +} + +function observation({ width, height, theme, metrics }) { + const innerWidth = Number(metrics.innerWidth); + const innerHeight = Number(metrics.innerHeight); + const scrollWidth = Number(metrics.scrollWidth); + const scrollHeight = Number(metrics.scrollHeight); + const overflowX = scrollWidth > innerWidth; + const overflowY = scrollHeight > innerHeight; + const minimumProjectedNodeTextPx = metrics.minimumProjectedNodeTextPx == null + ? null + : Number(metrics.minimumProjectedNodeTextPx); + const minimumProjectedNonEdgeTextPx = metrics.minimumProjectedNonEdgeTextPx == null + ? null + : Number(metrics.minimumProjectedNonEdgeTextPx); + const minimumProjectedEdgeTextPx = metrics.minimumProjectedEdgeTextPx == null + ? null + : Number(metrics.minimumProjectedEdgeTextPx); + const readabilityOk = minimumProjectedNodeTextPx == null + || minimumProjectedNodeTextPx >= MIN_PROJECTED_NODE_TEXT_PX; + const readerLayout = metrics.readerLayout || null; + const readerOverflow = metrics.readerOverflow || null; + const readerFit = metrics.readerFit || null; + const verticalScrollAccepted = Boolean( + overflowY + && !overflowX + && readabilityOk + && Number.isFinite(minimumProjectedNodeTextPx) + && ((readerLayout === 'adaptive' && readerOverflow === 'authored' && readerFit === 'intrinsic-height') + || (readerFit === 'authored-height' && metrics.diagramType === 'architecture' + && metrics.documentScrollUnclipped === true)) + ); + const authoredClipped = readerFit === 'authored-height' && metrics.diagramType === 'architecture' + && metrics.documentScrollUnclipped !== true; + const containmentOk = !authoredClipped && !overflowX && (!overflowY || verticalScrollAccepted); + const legendDockIntersectionArea = Number(metrics.legendDockIntersectionArea) || 0; + const dockStageIntersectionArea = Number(metrics.dockStageIntersectionArea) || 0; + const dockStageGap = metrics.dockStageGap == null ? null : Number(metrics.dockStageGap); + const receiptDockStageGap = metrics.viewerChromeRequiredGap == null + ? null + : Number(metrics.viewerChromeRequiredGap); + const requiredDockStageGap = Number.isFinite(receiptDockStageGap) ? receiptDockStageGap : 0; + const viewerChromeStageOk = !metrics.hasNavigationDock || ( + Number.isFinite(dockStageGap) + && dockStageIntersectionArea <= 0.5 + && dockStageGap >= requiredDockStageGap - 1 + ); + const viewerChromeOk = legendDockIntersectionArea <= 0.5 && viewerChromeStageOk; + return { + width, + height, + theme, + innerWidth, + innerHeight, + scrollWidth, + scrollHeight, + overflowX, + overflowY, + verticalScrollAccepted, + overflowDisposition: authoredClipped || overflowX || (overflowY && !verticalScrollAccepted) + ? 'unexpected-overflow' + : verticalScrollAccepted ? 'readable-vertical-scroll' : 'contained', + readerLayout, + readerOverflow, + readerFit, + ...(readerFit === 'authored-height' ? { diagramType: metrics.diagramType, documentScrollUnclipped: metrics.documentScrollUnclipped === true } : {}), + ok: containmentOk, + readerWidth: Number(metrics.readerWidth) || null, + diagramWidth: Number(metrics.diagramWidth) || null, + ...(metrics.pageComposition ? { pageComposition: metrics.pageComposition } : {}), + viewBoxWidth: Number(metrics.viewBoxWidth) || null, + ...(metrics.workflowLanes?.length ? { workflowLanes: metrics.workflowLanes } : {}), + minimumProjectedNodeTextPx, + minimumProjectedNonEdgeTextPx, + minimumProjectedEdgeTextPx, + minimumProjectedNodeText: metrics.minimumProjectedNodeText || null, + minimumProjectedNodeTextDetail: metrics.minimumProjectedNodeTextDetail || null, + minimumProjectedNodeTextOwner: metrics.minimumProjectedNodeTextOwner || null, + minimumRequiredNodeTextPx: MIN_PROJECTED_NODE_TEXT_PX, + readabilityOk, + hasLegend: Boolean(metrics.hasLegend), + hasNavigationDock: Boolean(metrics.hasNavigationDock), + legendDockIntersectionArea, + dockStageIntersectionArea, + dockStageGap, + requiredDockStageGap, + viewerChromeStageOk, + viewerChromeReserve: Number(metrics.viewerChromeReserve) || 0, + viewerChromeActive: Boolean(metrics.viewerChromeActive), + viewerChromeOk, + resolvedTheme: metrics.resolvedTheme || theme, + }; +} + +function contactSheetHtml({ artifactPath, receipt, screenshots }) { + const cards = screenshots.map((entry) => ` +
+ ${htmlEscape(`${entry.theme} ${entry.width} by ${entry.height}`)} +
${htmlEscape(entry.theme.toUpperCase())} · ${entry.width}×${entry.height} · containment ${entry.ok ? 'pass' : 'fail'}${entry.verticalScrollAccepted ? ' · readable vertical scroll' : ''}
+
`).join(''); + return ` + + + + +Archify automated browser evidence · ${htmlEscape(path.basename(artifactPath))} + + + +

Automated browser evidence

${htmlEscape(path.basename(artifactPath))} · visual-check containment ${htmlEscape(receipt.containment.status)} · perceptual visual review pending

+
${cards} +
+ + +`; +} + +const publishedBrowserEvidenceReceipts = new WeakSet(); + +// Keep detailed observations in the bound receipt; expose every review image and +// every failure without repeating the same viewport metrics in CLI context. +export function summarizeBrowserEvidence(receipt) { + const directory = receipt.sidecars?.directory || path.dirname(receipt.artifact.path); + const evidencePath = (file) => file ? path.resolve(directory, file) : undefined; + const published = publishedBrowserEvidenceReceipts.has(receipt); + return { + schemaVersion: receipt.schemaVersion, + command: receipt.command, + ok: receipt.ok, + status: receipt.status, + evidenceKind: receipt.evidenceKind, + visualReview: receipt.visualReview, + artifact: receipt.artifact, + provenance: receipt.provenance, + deliveryReceiptId: receipt.deliveryReceiptId, + state: receipt.state, + chrome: receipt.chrome, + checks: Object.fromEntries(['containment', 'readability', 'viewerChrome', 'themeStates', 'captures'] + .filter((key) => receipt[key]) + .map((key) => [key, receipt[key].status])), + error: receipt.error, + publication: receipt.publication, + diagnostics: receipt.diagnostics || [], + evidence: { + receipt: published ? evidencePath(receipt.sidecars?.receipt) : undefined, + contactSheet: published ? evidencePath(receipt.captures?.contactSheet) : undefined, + screenshots: (published ? receipt.captures?.screenshots || [] : []).map((entry) => ({ + path: evidencePath(entry.file), + width: entry.width, + height: entry.height, + theme: entry.theme, + resolvedTheme: entry.resolvedTheme, + })), + }, + }; +} + +function viewportSubject(artifact, entry) { + return { + artifact, + viewport: { width: entry.width, height: entry.height, theme: entry.theme }, + }; +} + +function failureDiagnostic({ code, message, subject, evidence, supportedFixes, severity = 'error' }) { + return { code, severity, message, subject, evidence, supportedFixes }; +} + +function resolveSidecarDirectory(artifactPath, outputs, compareParents) { + const artifact = path.resolve(artifactPath); + const artifactDirectory = path.dirname(artifact); + const sidecarDirectory = path.dirname(outputs.receipt); + const comparison = compareParents(outputs.receipt, artifact); + + const sidecars = { + ...(comparison?.status === 'different' ? { directory: sidecarDirectory } : {}), + receipt: path.basename(outputs.receipt), + ...(outputs.capture === false ? {} : { contactSheet: path.basename(outputs.contactSheet) }), + }; + if (comparison?.status === 'match' || comparison?.status === 'different') { + return { ok: true, sidecars }; + } + + const reason = comparison?.reason || { code: 'invalid-path-identity-result' }; + const error = `visual-check could not determine the physical identity of its evidence directory "${sidecarDirectory}" relative to the artifact directory "${artifactDirectory}".`; + return { + ok: false, + sidecars, + error, + diagnostic: failureDiagnostic({ + code: 'viewer/sidecar-directory-identity', + message: error, + subject: { artifact, sidecarDirectory }, + evidence: { comparison: reason }, + supportedFixes: [ + 'use existing, accessible artifact and evidence directories, then rerun visual-check', + ], + }), + }; +} + +// Turn a measured vertical overflow into the pixel budget an author can act on: +// which stacked block holds the height, and what the SVG or cards must shrink to. +export function verticalBudgetFixes(entry) { + const page = entry.pageComposition; + if (!entry.overflowY || !page) return []; + const excess = entry.scrollHeight - entry.innerHeight; + const fixes = []; + const stacked = `${page.bodyPaddingPx}px body padding + ${page.headerPx}px header + ${page.diagramChromePx}px diagram chrome + ${page.svgPx}px SVG + ${page.cardsPx}px cards = ${entry.scrollHeight}px against ${entry.innerHeight}px`; + if (page.svgPx > 0 && page.viewBoxHeight > 0) { + const targetSvg = page.svgPx - excess; + const targetViewBoxHeight = Math.floor(page.viewBoxHeight * targetSvg / page.svgPx); + if (entry.readerLayout === 'adaptive') { + fixes.push(targetSvg > 0 + ? `the page is ${excess}px too tall (${stacked}); the Reader already narrowed the stage to its ${entry.diagramWidth}px minimum, so the SVG height only follows meta.viewBox: keep every node and relationship and reduce the viewBox height to at most ${targetViewBoxHeight} (from ${page.viewBoxHeight}) by tightening vertical gaps and empty rows` + : `the page is ${excess}px too tall (${stacked}) and the SVG alone exceeds the viewport at the minimum reader width; split the diagram into two`); + } else { + fixes.push(`the page is ${excess}px too tall (${stacked}); the SVG spans the full ${entry.diagramWidth}px reader width because its viewBox ratio is below 1.55 and it declares no intrinsic-height fit, so the Reader can neither narrow it nor accept readable vertical scroll: either remove meta.viewBox so the renderer sizes the canvas and declares the fit, or make the viewBox at least 1.55x wider than tall`); + } + } + if (page.cardsPx >= excess) { + fixes.push(`alternatively, conclusion cards occupy ${page.cardsPx}px below the diagram; shortening card copy or dropping a card row so cards take at most ${page.cardsPx - excess}px also fits`); + } + return fixes; +} + +function observationDiagnostics({ artifact, allObservations, readabilityObservations, command }) { + const diagnostics = []; + for (const entry of allObservations) { + if (entry.resolvedTheme !== entry.theme) { + diagnostics.push(failureDiagnostic({ + code: 'viewer/theme-state', + message: `The rendered artifact resolved ${entry.resolvedTheme || 'no theme'} instead of the requested ${entry.theme} theme at ${entry.width}x${entry.height}.`, + subject: viewportSubject(artifact, entry), + evidence: { + requestedTheme: entry.theme, + resolvedTheme: entry.resolvedTheme, + }, + supportedFixes: [ + `restore deterministic ${entry.theme} theme resolution, then rerun ${command}`, + ], + })); + } + if (!entry.ok) { + const authoredClipped = entry.readerFit === 'authored-height' && entry.diagramType === 'architecture' + && !entry.documentScrollUnclipped; + const budgetFixes = authoredClipped + ? ['restore the full SVG inside the diagram panel and allow normal document scrolling; remove internal scrollers and clipping without changing authored geometry'] + : verticalBudgetFixes(entry); + diagnostics.push(failureDiagnostic({ + code: authoredClipped ? 'viewer/diagram-clipped' : 'viewer/viewport-overflow', + message: authoredClipped + ? `The authored Architecture canvas is clipped or cannot scroll in the document at ${entry.width}x${entry.height} (${entry.theme}).` + : `The rendered artifact overflows the ${entry.width}x${entry.height} ${entry.theme} viewport.`, + subject: viewportSubject(artifact, entry), + evidence: { + innerWidth: entry.innerWidth, + innerHeight: entry.innerHeight, + scrollWidth: entry.scrollWidth, + scrollHeight: entry.scrollHeight, + overflowX: entry.overflowX, + overflowY: entry.overflowY, + overflowDisposition: entry.overflowDisposition, + readerLayout: entry.readerLayout, + readerOverflow: entry.readerOverflow, + readerFit: entry.readerFit, + ...(authoredClipped ? { documentScrollUnclipped: false } : {}), + ...(entry.overflowY && entry.pageComposition ? { + pageComposition: entry.pageComposition, + pageCompositionMeasurement: 'CSS pixels at this viewport; body padding, header, diagram chrome, SVG and cards stack vertically and sum to scrollHeight', + } : {}), + ...(entry.overflowY && entry.workflowLanes?.length ? { + workflowLanes: entry.workflowLanes, + measurement: 'CSS pixels; rendered node boxes geometrically contained in each lane frame; spaces include headers and routing, not guaranteed removable space', + } : {}), + }, + supportedFixes: [ + ...budgetFixes, + ...(entry.overflowY && entry.workflowLanes?.length ? [ + 'run validate workflow --layout-json and compare the tallest rendered lane frames with source lanes, col and yOffset; frame IDs are rendered indices, not source lane IDs', + 'where ownership and explicit geometry permit, distribute stacked steps across logical columns and meaningful lanes before increasing yOffset; preserve nodes, branches, labels and hard pins', + `read references/authoring-contract.md#workflow-viewport-repair, then validate and deliver the changed source before rerunning ${command} on the new artifact; this is inspection guidance, not a verified coordinate fix`, + ] : budgetFixes.length ? [] : [`contain the rendered layout within ${entry.width}x${entry.height}, then rerun ${command}`]), + ], + })); + } + if (entry.legendDockIntersectionArea > 0.5) { + diagnostics.push(failureDiagnostic({ + code: 'viewer/chrome-legend-clearance', + message: `The navigation Dock obscures the SVG Legend at ${entry.width}x${entry.height} (${entry.theme}).`, + subject: viewportSubject(artifact, entry), + evidence: { legendDockIntersectionArea: entry.legendDockIntersectionArea }, + supportedFixes: [ + `move the SVG Legend or Viewer Dock until legendDockIntersectionArea is 0, then rerun ${command}`, + ], + })); + } + if (!entry.viewerChromeStageOk) { + const stageOverlapsDock = entry.dockStageIntersectionArea > 0.5; + diagnostics.push(failureDiagnostic({ + code: 'viewer/chrome-stage-clearance', + message: stageOverlapsDock + ? `Navigation Dock enters the protected SVG stage at ${entry.width}x${entry.height} (${entry.theme}).` + : `Navigation Dock clearance from the protected SVG stage is below the required gap at ${entry.width}x${entry.height} (${entry.theme}).`, + subject: viewportSubject(artifact, entry), + evidence: { + dockStageIntersectionArea: entry.dockStageIntersectionArea, + dockStageGap: entry.dockStageGap, + requiredDockStageGap: entry.requiredDockStageGap, + }, + supportedFixes: [ + `adjust Viewer stage reservation or clipping until dockStageGap is at least ${entry.requiredDockStageGap} and dockStageIntersectionArea is 0, then rerun ${command}`, + ], + })); + } + } + for (const entry of readabilityObservations) { + if (entry.readabilityOk) continue; + diagnostics.push(failureDiagnostic({ + code: 'viewer/projected-text-readability', + message: `Projected ${entry.minimumProjectedNodeTextDetail || 'node'} text is below the readability floor at ${entry.width}x${entry.height}.`, + subject: viewportSubject(artifact, entry), + evidence: { + text: entry.minimumProjectedNodeText, + detail: entry.minimumProjectedNodeTextDetail, + owner: entry.minimumProjectedNodeTextOwner, + minimumProjectedNodeTextPx: entry.minimumProjectedNodeTextPx, + minimumRequiredNodeTextPx: entry.minimumRequiredNodeTextPx, + }, + supportedFixes: [ + `increase projected ${entry.minimumProjectedNodeTextOwner?.kind === 'edge' ? 'relationship label' : entry.minimumProjectedNodeTextOwner?.kind === 'boundary' ? 'boundary label' : 'node text'} to at least ${entry.minimumRequiredNodeTextPx}px at ${entry.width}x${entry.height}, then rerun ${command}`, + ], + })); + } + return diagnostics; +} + +function baseReceipt({ artifactPath, artifact, sidecars, chrome, deliveryProvenance, command, capture }) { + return { + schemaVersion: 1, + ok: false, + command, + evidenceKind: 'automated-browser', + status: 'fail', + visualReview: capture ? 'pending' : 'not-requested', + ...(deliveryProvenance ? { + provenance: deliveryProvenance.status, + ...(deliveryProvenance.receiptId ? { deliveryReceiptId: deliveryProvenance.receiptId } : {}), + } : {}), + artifact: { + path: artifactPath, + sha256: sha256(artifact), + bytes: artifact.byteLength, + }, + state: { detail: 'read', motion: 'still' }, + chrome, + diagnostics: [], + containment: { + status: 'fail', + policy: 'fit-or-reader-declared-readable-vertical-scroll', + viewports: [], + }, + themeStates: { status: 'fail', viewports: [] }, + readability: { status: 'fail', minimumProjectedNodeTextPx: MIN_PROJECTED_NODE_TEXT_PX, viewports: [] }, + viewerChrome: { status: 'fail', viewports: [] }, + captures: { status: capture ? 'fail' : 'not-requested', screenshots: [], contactSheet: null }, + sidecars: { ...sidecars, files: [] }, + }; +} + +function appendEvidencePublicationFailure(receipt, failure) { + if (!failure?.ok) { + if (!receipt.error) receipt.error = failure.error; + receipt.diagnostics = [...(receipt.diagnostics || []), failure.diagnostic]; + } +} + +function appendEvidenceCleanupWarning(receipt, publication) { + if (publication?.status !== 'committed-with-warning') return false; + const committedStatus = receipt.status; + const cleanupErrors = publication.cleanupErrors || []; + const cleanupMessage = publication.recoveryDirectory + ? `The evidence set was committed, but cleanup is incomplete. Recovery directory: "${publication.recoveryDirectory}".` + : 'The evidence set was committed, but cleanup is incomplete.'; + receipt.ok = false; + receipt.status = 'fail'; + receipt.error = receipt.error ? `${receipt.error} ${cleanupMessage}` : cleanupMessage; + receipt.publication = { + status: publication.status, + committedReceipt: publication.committedReceipt, + ...(publication.recoveryDirectory + ? { recoveryDirectory: publication.recoveryDirectory } : {}), + cleanupErrors, + committedStatus, + }; + receipt.diagnostics = [...(receipt.diagnostics || []), failureDiagnostic({ + code: 'viewer/evidence-cleanup-incomplete', + severity: 'warning', + message: cleanupMessage, + subject: { + artifact: receipt.artifact?.path, + receipt: publication.committedReceipt, + }, + evidence: { + publicationStatus: publication.status, + ...(publication.recoveryDirectory + ? { recoveryDirectory: publication.recoveryDirectory } : {}), + cleanupErrors, + }, + supportedFixes: publication.recoveryDirectory + ? [`inspect and safely retire the retained files under "${publication.recoveryDirectory}"`] + : ['resolve the reported filesystem cleanup failure before rerunning visual-check'], + })]; + return true; +} + +function publishReceiptOnly(artifactPath, outputs, receipt, ownership) { + const cleanupErrors = cleanupStagedEvidence(ownership); + if (cleanupErrors.length) { + const failure = evidenceWriteFailure( + artifactPath, + 'visual-check could not safely retire this run\'s incomplete staged evidence.', + cleanupErrors, + ); + cleanupStagedEvidence(ownership, { removeDirectory: true }); + ownership.active = false; + return failure; + } + try { + writeStagedEvidence(ownership, outputs.receipt, `${JSON.stringify(receipt, null, 2)}\n`); + } catch (error) { + const failure = evidenceWriteFailure( + artifactPath, + 'visual-check could not stage its evidence receipt.', + [{ file: stagedPathFor(ownership, outputs.receipt), reason: error.message }], + ); + cleanupStagedEvidence(ownership, { removeDirectory: true }); + ownership.active = false; + return failure; + } + return commitVisualEvidence(artifactPath, outputs, ownership, [outputs.receipt]); +} + +function persistBrowserEvidenceFailure( + artifactPath, + failure, + { outDir, compareSidecarParents = sameParent, ownership, command = 'visual-check', capture = true } = {}, +) { + const outputs = evidenceSidecarPaths(artifactPath, { outDir, command, capture }); + const directory = resolveSidecarDirectory(artifactPath, outputs, compareSidecarParents); + const capturedArtifact = readRegularFileContents(artifactPath, { + subject: 'failure-artifact', + }); + const artifactBytes = capturedArtifact.ok ? capturedArtifact.bytes : null; + const receipt = { + ...failure, + ok: false, status: 'fail', + ...(artifactBytes ? { + artifact: { + path: path.resolve(artifactPath), + sha256: sha256(artifactBytes), + bytes: artifactBytes.byteLength, + }, + } : {}), + containment: { + status: 'fail', + policy: 'fit-or-reader-declared-readable-vertical-scroll', + viewports: [], + }, + captures: { status: capture ? 'fail' : 'not-requested', screenshots: [], contactSheet: null }, + sidecars: { ...directory.sidecars, files: [] }, + }; + if (!directory.ok) { + receipt.error = directory.error; + receipt.diagnostics = [...(failure.diagnostics || []), directory.diagnostic]; + return receipt; + } + let activeOwnership = ownership; + if (!visualEvidenceOwnershipMatches(activeOwnership, artifactPath, outputs)) { + if (!artifactBytes) { + const conflict = evidencePathConflict(artifactPath, outputs, outputs.receipt, { + code: 'artifact-unreadable', + }); + receipt.error = conflict.error; + receipt.diagnostics = [...(failure.diagnostics || []), conflict.diagnostic]; + return receipt; + } + const preflight = beginVisualEvidenceWrite(artifactPath, artifactBytes, outputs); + if (!preflight.ok) { + receipt.error = preflight.error; + receipt.diagnostics = [...(failure.diagnostics || []), preflight.diagnostic]; + return receipt; + } + activeOwnership = preflight.ownership; + } + const publication = publishReceiptOnly(artifactPath, outputs, receipt, activeOwnership); + if (publication.ok) publishedBrowserEvidenceReceipts.add(receipt); + appendEvidencePublicationFailure(receipt, publication); + appendEvidenceCleanupWarning(receipt, publication); + return receipt; +} + +async function runBrowserEvidence({ + artifactPath, + outDir, + chromePath, + resolveChrome = findChrome, + browserFactory = async (resolvedChrome) => new ChromeVisualBrowser(resolvedChrome), + deliveryProvenance, + verifyArtifact, + compareSidecarParents = sameParent, + command, capture, +} = {}) { + if (!artifactPath) throw new Error(`${command} requires one delivered HTML artifact.`); + const artifact = path.resolve(artifactPath); + if (!/\.html?$/i.test(artifact)) throw new Error(`${command} requires an .html artifact.`); + const capturedArtifact = captureRegularFileBinding(artifact, { + subject: 'visual-check-artifact', + expectedLinks: 1, + includeContent: true, + }); + if (capturedArtifact.status !== 'captured') { + throw regularFileCaptureError('The visual-check artifact', capturedArtifact); + } + const artifactBytes = capturedArtifact.content.buffer; + try { + const outputs = evidenceSidecarPaths(artifact, { outDir, command, capture }); + const directory = resolveSidecarDirectory(artifact, outputs, compareSidecarParents); + const receipt = baseReceipt({ + artifactPath: artifact, + artifact: artifactBytes, + sidecars: directory.sidecars, + chrome: { status: 'not-checked', executable: null }, + deliveryProvenance, command, capture, + }); + if (!directory.ok) { + receipt.error = directory.error; + receipt.diagnostics = [directory.diagnostic]; + return { exitCode: EXIT.fail, receipt }; + } + const preflight = beginVisualEvidenceWrite(artifact, artifactBytes, outputs); + if (!preflight.ok) { + receipt.error = preflight.error; + receipt.diagnostics = [preflight.diagnostic]; + return { exitCode: EXIT.fail, receipt }; + } + const ownership = preflight.ownership; + let inspectionArtifact; + try { + // Chrome's file loader cannot reliably open long Windows/UNC paths. Keep + // the one inspected snapshot local; publish evidence on its target volume. + ownership.inspection = { + directory: fs.mkdtempSync(path.join(os.tmpdir(), 'archify-inspection-')), + }; + const stat = fs.lstatSync(ownership.inspection.directory, { bigint: true }); + if (!stat.isDirectory() || stat.isSymbolicLink() || stat.ino === 0n) { + throw new Error('the private inspection directory has no stable identity'); + } + ownership.inspection.identity = fileIdentity(stat); + inspectionArtifact = path.join(ownership.inspection.directory, 'artifact-snapshot.html'); + ownership.stagingPaths.set(artifact, inspectionArtifact); + writeStagedEvidence(ownership, artifact, artifactBytes); + } catch (error) { + return { exitCode: EXIT.fail, receipt: persistBrowserEvidenceFailure(artifact, { + ...receipt, + status: 'fail', + ok: false, + error: `visual-check could not stage its private artifact snapshot: ${error.message}`, + diagnostics: [failureDiagnostic({ + code: 'viewer/artifact-snapshot', + message: 'visual-check could not bind a private artifact snapshot.', + subject: { artifact }, + evidence: { reason: error.message }, + supportedFixes: ['retry after resolving the reported staging filesystem error'], + })], + }, { outDir, compareSidecarParents, ownership, command, capture }) }; + } + const verifyInspectionArtifact = () => { + const entry = ownership.stagedEntries.get(artifact); + if (!entry || !currentEvidenceMatches(entry.path, entry.identity, entry.evidence)) { + throw new Error('The private visual-check artifact snapshot changed during inspection.'); + } + }; + try { + verifyArtifact?.(artifactBytes); + } catch (error) { + return { exitCode: EXIT.fail, receipt: persistBrowserEvidenceFailure(artifact, { + schemaVersion: 1, command, evidenceKind: 'automated-browser', visualReview: capture ? 'pending' : 'not-requested', + artifact: { path: artifact, sha256: sha256(artifactBytes), bytes: artifactBytes.byteLength }, + provenance: error.deliveryProvenance?.status, error: error.message, + diagnostics: error.archifyDiagnostics || [], + }, { outDir, compareSidecarParents, ownership, command, capture }) }; + } + + const resolvedChrome = chromePath || resolveChrome(); + receipt.chrome = resolvedChrome + ? { status: 'available', executable: resolvedChrome } + : { status: 'unavailable', executable: null }; + + if (!resolvedChrome) { + receipt.status = 'skipped'; + receipt.containment.status = 'skipped'; + receipt.readability.status = 'skipped'; + receipt.viewerChrome.status = 'skipped'; + receipt.themeStates.status = 'skipped'; + if (capture) receipt.captures.status = 'skipped'; + receipt.error = 'Chrome or Chromium is unavailable. Set ARCHIFY_CHROME to its executable path.'; + receipt.diagnostics = [failureDiagnostic({ + code: 'viewer/chrome-unavailable', + severity: 'warning', + message: receipt.error, + subject: { artifact }, + evidence: { executable: null }, + supportedFixes: [`set ARCHIFY_CHROME to a Chrome or Chromium executable and rerun ${command}`], + })]; + const publication = publishReceiptOnly(artifact, outputs, receipt, ownership); + if (publication.ok) publishedBrowserEvidenceReceipts.add(receipt); + if (!publication.ok) { + appendEvidencePublicationFailure(receipt, publication); + return { exitCode: EXIT.fail, receipt }; + } + if (appendEvidenceCleanupWarning(receipt, publication)) { + return { exitCode: EXIT.fail, receipt }; + } + return { exitCode: EXIT.skipped, receipt }; + } + + let browser; + try { + browser = await browserFactory(resolvedChrome); + const observations = new Map(); + const screenshotsByKey = new Map(outputs.screenshots.map((entry, index) => [ + screenshotKey(entry.width, entry.height, entry.theme), + { ...entry, stagedPath: ownership.stagedOutputs.screenshots[index].path }, + ])); + + for (const viewport of VISUAL_CHECK_VIEWPORTS) { + const key = screenshotKey(viewport.width, viewport.height, 'light'); + const screenshot = screenshotsByKey.get(key); + const metrics = await browser.inspect({ + artifactPath: inspectionArtifact, + ...viewport, + theme: 'light', + ...(screenshot ? { + screenshotPath: screenshot.stagedPath, + writeScreenshot: (bytes) => writeStagedEvidence(ownership, screenshot.path, bytes), + } : {}), + }); + verifyInspectionArtifact(); + if (screenshot) { + if (screenshot && !ownership.stagedEntries.has(screenshot.path)) { + registerStagedEvidence(ownership, screenshot.path); + } else { + const staged = ownership.stagedEntries.get(screenshot.path); + if (!currentEvidenceMatches(staged.path, staged.identity, staged.evidence)) { + throw new Error('The staged visual-check screenshot changed after capture.'); + } + } + } + observations.set(key, observation({ ...viewport, theme: 'light', metrics })); + } + for (const viewport of CAPTURE_VIEWPORTS) { + const key = screenshotKey(viewport.width, viewport.height, 'dark'); + const screenshot = screenshotsByKey.get(key); + const metrics = await browser.inspect({ + artifactPath: inspectionArtifact, + ...viewport, + theme: 'dark', + ...(screenshot ? { + screenshotPath: screenshot.stagedPath, + writeScreenshot: (bytes) => writeStagedEvidence(ownership, screenshot.path, bytes), + } : {}), + }); + verifyInspectionArtifact(); + if (screenshot && !ownership.stagedEntries.has(screenshot.path)) { + registerStagedEvidence(ownership, screenshot.path); + } else if (screenshot) { + const staged = ownership.stagedEntries.get(screenshot.path); + if (!currentEvidenceMatches(staged.path, staged.identity, staged.evidence)) { + throw new Error('The staged visual-check screenshot changed after capture.'); + } + } + observations.set(key, observation({ ...viewport, theme: 'dark', metrics })); + } + + let artifactVerification = verifyRegularFileBinding(capturedArtifact.binding); + if (artifactVerification.status !== 'match') { + throw regularFileCaptureError(`The delivered artifact changed while ${command} was running`, artifactVerification); + } + verifyArtifact?.(artifactBytes); + artifactVerification = verifyRegularFileBinding(capturedArtifact.binding); + if (artifactVerification.status !== 'match') { + throw regularFileCaptureError(`The delivered artifact changed while ${command} was running`, artifactVerification); + } + + receipt.containment.viewports = VISUAL_CHECK_VIEWPORTS.map(({ width, height }) => ( + observations.get(screenshotKey(width, height, 'light')) + )); + receipt.readability.viewports = receipt.containment.viewports.map((entry) => ({ ...entry })); + receipt.viewerChrome.viewports = receipt.containment.viewports.map((entry) => ({ ...entry })); + receipt.captures.screenshots = outputs.screenshots.map((entry) => ({ + ...observations.get(screenshotKey(entry.width, entry.height, entry.theme)), + file: path.basename(entry.path), + })); + const allObservations = [...observations.values()]; + receipt.themeStates.viewports = allObservations.map((entry) => ({ + width: entry.width, + height: entry.height, + requestedTheme: entry.theme, + resolvedTheme: entry.resolvedTheme, + ok: entry.resolvedTheme === entry.theme, + })); + const themeStatePass = receipt.themeStates.viewports.every(entry => entry.ok); + const containmentPass = allObservations.every((entry) => entry.ok); + const readabilityPass = receipt.readability.viewports.every((entry) => entry.readabilityOk); + const viewerChromePass = allObservations.every((entry) => entry.viewerChromeOk); + receipt.diagnostics = observationDiagnostics({ + artifact, + allObservations, + readabilityObservations: receipt.readability.viewports, command, + }); + receipt.themeStates.status = themeStatePass ? 'pass' : 'fail'; + receipt.containment.status = containmentPass ? 'pass' : 'fail'; + receipt.readability.status = readabilityPass ? 'pass' : 'fail'; + receipt.viewerChrome.status = viewerChromePass ? 'pass' : 'fail'; + if (capture) { + receipt.captures.status = 'pass'; + receipt.captures.contactSheet = path.basename(outputs.contactSheet); + } + receipt.status = containmentPass && themeStatePass && readabilityPass && viewerChromePass ? 'pass' : 'fail'; + receipt.ok = containmentPass && themeStatePass && readabilityPass && viewerChromePass; + if (capture) writeStagedEvidence(ownership, outputs.contactSheet, contactSheetHtml({ + artifactPath: artifact, + receipt, + screenshots: receipt.captures.screenshots, + })); + recordVisualEvidenceFiles(receipt, outputs, ownership); + writeStagedEvidence(ownership, outputs.receipt, `${JSON.stringify(receipt, null, 2)}\n`); + const publication = commitVisualEvidence( + artifact, + outputs, + ownership, + visualEvidenceTargets(outputs).map((target) => target.path), + ); + if (!publication.ok) { + appendEvidencePublicationFailure(receipt, publication); + receipt.ok = false; + receipt.status = 'fail'; + return { exitCode: EXIT.fail, receipt }; + } + publishedBrowserEvidenceReceipts.add(receipt); + if (appendEvidenceCleanupWarning(receipt, publication)) { + return { exitCode: EXIT.fail, receipt }; + } + return { exitCode: receipt.ok ? EXIT.pass : EXIT.fail, receipt }; + } catch (error) { + const startupTimeout = error.code === 'ERR_CHROME_CDP_TIMEOUT' + && error.method === 'Target.getTargets'; + receipt.status = 'fail'; + receipt.ok = false; + receipt.error = error.message; + receipt.containment.status = 'fail'; + receipt.readability.status = 'fail'; + receipt.viewerChrome.status = 'fail'; + receipt.themeStates.status = 'fail'; + receipt.captures.status = capture ? 'fail' : 'not-requested'; + receipt.captures.screenshots = []; + receipt.captures.contactSheet = null; + if (error.deliveryProvenance) receipt.provenance = error.deliveryProvenance.status; + receipt.diagnostics = error.archifyDiagnostics || [failureDiagnostic({ + code: startupTimeout ? 'viewer/chrome-startup-timeout' : `viewer/${command}-runtime`, + message: startupTimeout + ? 'Chrome did not finish its initial DevTools handshake within the startup window.' + : `${command} could not complete its Chrome inspection.`, + subject: { artifact }, + evidence: { reason: error.message }, + supportedFixes: startupTimeout + ? [ + 'do not edit or simplify the artifact because this is a browser-startup failure', + `retry ${command} once after host load subsides; if it repeats, stop and report the environment failure`, + ] + : [`resolve the reported Chrome inspection error, then rerun ${command}`], + })]; + return { + exitCode: EXIT.fail, + receipt: persistBrowserEvidenceFailure(artifact, receipt, { + outDir, + compareSidecarParents, + ownership, command, capture, + }), + }; + } finally { + if (browser?.close) await browser.close(); + } + } finally { + releaseRegularFileBinding(capturedArtifact.binding); + } +} + +export function persistVisualCheckFailure(artifactPath, failure, options = {}) { + return persistBrowserEvidenceFailure(artifactPath, failure, { ...options, command: 'visual-check', capture: true }); +} +export function persistBrowserCheckFailure(artifactPath, failure, options = {}) { + return persistBrowserEvidenceFailure(artifactPath, failure, { ...options, command: 'browser-check', capture: false }); +} +export function runVisualCheck(options = {}) { + return runBrowserEvidence({ ...options, command: 'visual-check', capture: true }); +} +export function runBrowserCheck(options = {}) { + return runBrowserEvidence({ ...options, command: 'browser-check', capture: false }); +} diff --git a/vendor/archify/brand-marks/README.md b/vendor/archify/brand-marks/README.md new file mode 100644 index 0000000..83578f6 --- /dev/null +++ b/vendor/archify/brand-marks/README.md @@ -0,0 +1,31 @@ +# Built-in brand marks + +Archify ships a bounded catalogue of 107 commonly used brands for architecture, +workflow, sequence, data-flow, and lifecycle nodes. The mark is optional authored +identity: it never replaces the node's semantic `type`, color, label, or +relationships. + +Unknown sites are handled by an explicit two-stage workflow. Run +`node bin/archify.mjs brands capture --json`, then author the returned +digest-pinned `brand` value. Normal render and validate commands do not perform +an unpinned capture, and changed or unavailable content fails closed. + +Most vector paths and brand metadata are generated from Simple Icons 16.28.0. +The OpenAI mark is traced to OpenAI's official brand guidelines. Every generated +entry records its source and, when available upstream, its guidelines and license +metadata in `renderers/shared/generated-brand-marks.mjs`. + +Brand names and logos may be trademarks of their respective owners. Simple +Icons' CC0 license covers its collection work, not every underlying trademark or +artwork. Contributors must review the recorded source, current brand guidelines, +and intended referential use before adding or updating a mark. Archify does not +imply sponsorship, endorsement, or partnership. + +Edit `catalog.json`, then regenerate the committed zero-runtime-dependency bundle: + +```bash +npm run generate:brand-marks +npm run check:brand-marks +``` + +Do not hand-edit `renderers/shared/generated-brand-marks.mjs`. diff --git a/vendor/archify/brand-marks/catalog.json b/vendor/archify/brand-marks/catalog.json new file mode 100644 index 0000000..ac56989 --- /dev/null +++ b/vendor/archify/brand-marks/catalog.json @@ -0,0 +1,131 @@ +{ + "schemaVersion": 1, + "marks": [ + { + "id": "openai", + "title": "OpenAI", + "category": "ai", + "aliases": ["chatgpt", "gpt", "codex"], + "domains": ["openai.com", "chatgpt.com"], + "custom": { + "viewBox": 20, + "hex": "000000", + "path": "M11.248 18.25q-.825 0-1.568-.314a4.3 4.3 0 0 1-1.32-.874 4 4 0 0 1-1.304.214 4 4 0 0 1-2.046-.544 4.27 4.27 0 0 1-1.518-1.485 4 4 0 0 1-.56-2.095q0-.48.131-1.04A4.4 4.4 0 0 1 2.04 10.71a4.07 4.07 0 0 1 .017-3.4 4.2 4.2 0 0 1 1.056-1.418 3.8 3.8 0 0 1 1.6-.842 3.9 3.9 0 0 1 .76-1.683q.593-.759 1.451-1.188a4.04 4.04 0 0 1 1.832-.429q.825 0 1.567.313.742.314 1.32.875a4 4 0 0 1 1.304-.215q1.106 0 2.046.545a4.14 4.14 0 0 1 1.501 1.485q.578.941.578 2.095 0 .48-.132 1.04.66.61 1.023 1.419.363.792.363 1.666 0 .892-.38 1.717a4.3 4.3 0 0 1-1.072 1.435 3.8 3.8 0 0 1-1.584.825 3.8 3.8 0 0 1-.775 1.683 4.06 4.06 0 0 1-1.436 1.188 4.04 4.04 0 0 1-1.832.429m-4.076-2.062q.825 0 1.435-.347l3.103-1.782a.36.36 0 0 0 .164-.313v-1.42L7.881 14.62a.67.67 0 0 1-.726 0l-3.118-1.798a.5.5 0 0 1-.017.115v.198q0 .841.396 1.551.413.693 1.139 1.089a3.2 3.2 0 0 0 1.617.412m.165-2.69a.4.4 0 0 0 .181.05q.083 0 .165-.05l1.238-.71-3.977-2.31a.7.7 0 0 1-.363-.643v-3.58q-.825.362-1.32 1.122a2.9 2.9 0 0 0-.495 1.65q0 .809.413 1.55.412.743 1.072 1.123zm3.91 3.663q.875 0 1.585-.396a2.96 2.96 0 0 0 1.534-2.64v-3.564a.32.32 0 0 0-.165-.297l-1.254-.726v4.604a.7.7 0 0 1-.363.643l-3.119 1.799a3 3 0 0 0 1.783.577m.627-6.039V8.878L10.01 7.822 8.129 8.878v2.244l1.881 1.056zM7.057 5.859a.7.7 0 0 1 .363-.644l3.119-1.798a3 3 0 0 0-1.782-.578q-.874 0-1.584.396A2.96 2.96 0 0 0 6.05 4.324a3.07 3.07 0 0 0-.396 1.551v3.547q0 .199.165.314l1.237.726zm8.383 7.887q.825-.364 1.303-1.123.495-.758.495-1.65a3.15 3.15 0 0 0-.412-1.55q-.413-.743-1.073-1.123l-3.086-1.782q-.099-.065-.181-.049a.3.3 0 0 0-.165.05l-1.238.692 3.993 2.327a.6.6 0 0 1 .264.264.64.64 0 0 1 .1.363zm-3.317-8.382a.63.63 0 0 1 .726 0l3.135 1.831v-.297q0-.792-.396-1.501a2.86 2.86 0 0 0-1.105-1.155q-.71-.43-1.65-.43-.825 0-1.436.347L8.294 5.941a.36.36 0 0 0-.165.314v1.418z", + "source": "https://openai.com/brand/", + "guidelines": "https://openai.com/brand/" + } + }, + { "id": "claude", "category": "ai", "simpleIcon": "claude", "aliases": ["claude-ai"], "domains": ["claude.ai"] }, + { "id": "anthropic", "category": "ai", "simpleIcon": "anthropic", "domains": ["anthropic.com"] }, + { "id": "google-gemini", "category": "ai", "simpleIcon": "googlegemini", "aliases": ["gemini"], "domains": ["gemini.google.com"] }, + { "id": "deepseek", "category": "ai", "simpleIcon": "deepseek", "domains": ["deepseek.com"] }, + { "id": "qwen", "category": "ai", "simpleIcon": "qwen", "domains": ["qwen.ai"] }, + { "id": "meta", "category": "ai", "simpleIcon": "meta", "aliases": ["llama"], "domains": ["meta.com"] }, + { "id": "mistral-ai", "category": "ai", "simpleIcon": "mistralai", "aliases": ["mistral"], "domains": ["mistral.ai"] }, + { "id": "hugging-face", "category": "ai", "simpleIcon": "huggingface", "aliases": ["huggingface"], "domains": ["huggingface.co"] }, + { "id": "ollama", "category": "ai", "simpleIcon": "ollama", "domains": ["ollama.com"] }, + { "id": "openrouter", "category": "ai", "simpleIcon": "openrouter", "aliases": ["open-router"], "domains": ["openrouter.ai"] }, + { "id": "perplexity", "category": "ai", "simpleIcon": "perplexity", "domains": ["perplexity.ai"] }, + { "id": "replicate", "category": "ai", "simpleIcon": "replicate", "domains": ["replicate.com"] }, + + { "id": "google-cloud", "category": "cloud", "simpleIcon": "googlecloud", "aliases": ["gcp", "googlecloud"], "domains": ["cloud.google.com"] }, + { "id": "cloudflare", "category": "cloud", "simpleIcon": "cloudflare", "domains": ["cloudflare.com"] }, + { "id": "vercel", "category": "cloud", "simpleIcon": "vercel", "domains": ["vercel.com"] }, + { "id": "netlify", "category": "cloud", "simpleIcon": "netlify", "domains": ["netlify.com"] }, + { "id": "digitalocean", "category": "cloud", "simpleIcon": "digitalocean", "aliases": ["digital-ocean"], "domains": ["digitalocean.com"] }, + { "id": "render", "category": "cloud", "simpleIcon": "render", "domains": ["render.com"] }, + { "id": "railway", "category": "cloud", "simpleIcon": "railway", "domains": ["railway.com", "railway.app"] }, + { "id": "fly-io", "category": "cloud", "simpleIcon": "flydotio", "aliases": ["fly.io"], "domains": ["fly.io"] }, + { "id": "cloudinary", "category": "cloud", "simpleIcon": "cloudinary", "domains": ["cloudinary.com"] }, + { "id": "alibaba-cloud", "category": "cloud", "simpleIcon": "alibabacloud", "aliases": ["aliyun"], "domains": ["alibabacloud.com", "aliyun.com"] }, + { "id": "firebase", "category": "cloud", "simpleIcon": "firebase", "domains": ["firebase.google.com"] }, + { "id": "supabase", "category": "cloud", "simpleIcon": "supabase", "domains": ["supabase.com"] }, + { "id": "neon", "category": "cloud", "simpleIcon": "neon", "domains": ["neon.tech"] }, + + { "id": "github", "category": "engineering", "simpleIcon": "github", "domains": ["github.com"] }, + { "id": "gitlab", "category": "engineering", "simpleIcon": "gitlab", "domains": ["gitlab.com"] }, + { "id": "bitbucket", "category": "engineering", "simpleIcon": "bitbucket", "domains": ["bitbucket.org"] }, + { "id": "docker", "category": "engineering", "simpleIcon": "docker", "domains": ["docker.com"] }, + { "id": "kubernetes", "category": "engineering", "simpleIcon": "kubernetes", "aliases": ["k8s"], "domains": ["kubernetes.io"] }, + { "id": "terraform", "category": "engineering", "simpleIcon": "terraform", "domains": ["terraform.io"] }, + { "id": "pulumi", "category": "engineering", "simpleIcon": "pulumi", "domains": ["pulumi.com"] }, + { "id": "ansible", "category": "engineering", "simpleIcon": "ansible", "domains": ["ansible.com"] }, + { "id": "jenkins", "category": "engineering", "simpleIcon": "jenkins", "domains": ["jenkins.io"] }, + { "id": "circleci", "category": "engineering", "simpleIcon": "circleci", "aliases": ["circle-ci"], "domains": ["circleci.com"] }, + { "id": "github-actions", "category": "engineering", "simpleIcon": "githubactions" }, + { "id": "argo", "category": "engineering", "simpleIcon": "argo", "aliases": ["argocd", "argo-cd"], "domains": ["argoproj.github.io"] }, + { "id": "helm", "category": "engineering", "simpleIcon": "helm", "domains": ["helm.sh"] }, + { "id": "grafana", "category": "engineering", "simpleIcon": "grafana", "domains": ["grafana.com"] }, + { "id": "prometheus", "category": "engineering", "simpleIcon": "prometheus", "domains": ["prometheus.io"] }, + { "id": "sentry", "category": "engineering", "simpleIcon": "sentry", "domains": ["sentry.io"] }, + { "id": "datadog", "category": "engineering", "simpleIcon": "datadog", "domains": ["datadoghq.com"] }, + { "id": "pagerduty", "category": "engineering", "simpleIcon": "pagerduty", "aliases": ["pager-duty"], "domains": ["pagerduty.com"] }, + + { "id": "postgresql", "category": "data", "simpleIcon": "postgresql", "aliases": ["postgres"], "domains": ["postgresql.org"] }, + { "id": "mysql", "category": "data", "simpleIcon": "mysql", "domains": ["mysql.com"] }, + { "id": "mongodb", "category": "data", "simpleIcon": "mongodb", "aliases": ["mongo"], "domains": ["mongodb.com"] }, + { "id": "redis", "category": "data", "simpleIcon": "redis", "domains": ["redis.io"] }, + { "id": "apache-kafka", "category": "data", "simpleIcon": "apachekafka", "aliases": ["kafka"], "domains": ["kafka.apache.org"] }, + { "id": "rabbitmq", "category": "data", "simpleIcon": "rabbitmq", "aliases": ["rabbit-mq"], "domains": ["rabbitmq.com"] }, + { "id": "clickhouse", "category": "data", "simpleIcon": "clickhouse", "domains": ["clickhouse.com"] }, + { "id": "elasticsearch", "category": "data", "simpleIcon": "elasticsearch", "aliases": ["elastic"], "domains": ["elastic.co"] }, + { "id": "opensearch", "category": "data", "simpleIcon": "opensearch", "aliases": ["open-search"], "domains": ["opensearch.org"] }, + { "id": "snowflake", "category": "data", "simpleIcon": "snowflake", "domains": ["snowflake.com"] }, + { "id": "databricks", "category": "data", "simpleIcon": "databricks", "domains": ["databricks.com"] }, + { "id": "planetscale", "category": "data", "simpleIcon": "planetscale", "aliases": ["planet-scale"], "domains": ["planetscale.com"] }, + { "id": "prisma", "category": "data", "simpleIcon": "prisma", "domains": ["prisma.io"] }, + { "id": "sqlite", "category": "data", "simpleIcon": "sqlite", "domains": ["sqlite.org"] }, + { "id": "mariadb", "category": "data", "simpleIcon": "mariadb", "aliases": ["maria-db"], "domains": ["mariadb.org"] }, + { "id": "influxdb", "category": "data", "simpleIcon": "influxdb", "aliases": ["influx-db"], "domains": ["influxdata.com"] }, + { "id": "apache-airflow", "category": "data", "simpleIcon": "apacheairflow", "aliases": ["airflow"], "domains": ["airflow.apache.org"] }, + + { "id": "notion", "category": "collaboration", "simpleIcon": "notion", "domains": ["notion.so"] }, + { "id": "figma", "category": "collaboration", "simpleIcon": "figma", "domains": ["figma.com"] }, + { "id": "jira", "category": "collaboration", "simpleIcon": "jira", "domains": ["atlassian.com"] }, + { "id": "linear", "category": "collaboration", "simpleIcon": "linear", "domains": ["linear.app"] }, + { "id": "discord", "category": "collaboration", "simpleIcon": "discord", "domains": ["discord.com"] }, + { "id": "zoom", "category": "collaboration", "simpleIcon": "zoom", "domains": ["zoom.us"] }, + { "id": "trello", "category": "collaboration", "simpleIcon": "trello", "domains": ["trello.com"] }, + { "id": "asana", "category": "collaboration", "simpleIcon": "asana", "domains": ["asana.com"] }, + { "id": "airtable", "category": "collaboration", "simpleIcon": "airtable", "domains": ["airtable.com"] }, + { "id": "miro", "category": "collaboration", "simpleIcon": "miro", "domains": ["miro.com"] }, + { "id": "stripe", "category": "business", "simpleIcon": "stripe", "domains": ["stripe.com"] }, + { "id": "shopify", "category": "business", "simpleIcon": "shopify", "domains": ["shopify.com"] }, + { "id": "hubspot", "category": "business", "simpleIcon": "hubspot", "domains": ["hubspot.com"] }, + { "id": "paypal", "category": "business", "simpleIcon": "paypal", "domains": ["paypal.com"] }, + { "id": "intercom", "category": "business", "simpleIcon": "intercom", "domains": ["intercom.com"] }, + { "id": "zendesk", "category": "business", "simpleIcon": "zendesk", "domains": ["zendesk.com"] }, + { "id": "wordpress", "category": "business", "simpleIcon": "wordpress", "domains": ["wordpress.org", "wordpress.com"] }, + { "id": "woocommerce", "category": "business", "simpleIcon": "woocommerce", "aliases": ["woo-commerce"], "domains": ["woocommerce.com"] }, + + { "id": "wechat", "category": "channel", "simpleIcon": "wechat", "aliases": ["weixin", "微信"], "domains": ["weixin.qq.com"] }, + { "id": "youtube", "category": "channel", "simpleIcon": "youtube", "domains": ["youtube.com", "youtu.be"] }, + { "id": "tiktok", "category": "channel", "simpleIcon": "tiktok", "aliases": ["douyin", "抖音"], "domains": ["tiktok.com", "douyin.com"] }, + { "id": "x", "category": "channel", "simpleIcon": "x", "aliases": ["twitter"], "domains": ["x.com", "twitter.com"] }, + { "id": "instagram", "category": "channel", "simpleIcon": "instagram", "domains": ["instagram.com"] }, + { "id": "facebook", "category": "channel", "simpleIcon": "facebook", "domains": ["facebook.com"] }, + { "id": "reddit", "category": "channel", "simpleIcon": "reddit", "domains": ["reddit.com"] }, + { "id": "telegram", "category": "channel", "simpleIcon": "telegram", "domains": ["telegram.org", "t.me"] }, + { "id": "whatsapp", "category": "channel", "simpleIcon": "whatsapp", "domains": ["whatsapp.com"] }, + { "id": "pinterest", "category": "channel", "simpleIcon": "pinterest", "domains": ["pinterest.com"] }, + + { "id": "python", "category": "language", "simpleIcon": "python", "domains": ["python.org"] }, + { "id": "typescript", "category": "language", "simpleIcon": "typescript", "aliases": ["ts"], "domains": ["typescriptlang.org"] }, + { "id": "javascript", "category": "language", "simpleIcon": "javascript", "aliases": ["js"] }, + { "id": "go", "category": "language", "simpleIcon": "go", "aliases": ["golang"], "domains": ["go.dev"] }, + { "id": "rust", "category": "language", "simpleIcon": "rust", "domains": ["rust-lang.org"] }, + { "id": "node-js", "category": "framework", "simpleIcon": "nodedotjs", "aliases": ["node", "nodejs"], "domains": ["nodejs.org"] }, + { "id": "react", "category": "framework", "simpleIcon": "react", "aliases": ["reactjs"], "domains": ["react.dev"] }, + { "id": "vue", "category": "framework", "simpleIcon": "vuedotjs", "aliases": ["vuejs", "vue.js"], "domains": ["vuejs.org"] }, + { "id": "next-js", "category": "framework", "simpleIcon": "nextdotjs", "aliases": ["nextjs", "next.js"], "domains": ["nextjs.org"] }, + { "id": "pytorch", "category": "framework", "simpleIcon": "pytorch", "domains": ["pytorch.org"] }, + { "id": "tensorflow", "category": "framework", "simpleIcon": "tensorflow", "domains": ["tensorflow.org"] }, + { "id": "angular", "category": "framework", "simpleIcon": "angular", "domains": ["angular.dev"] }, + { "id": "svelte", "category": "framework", "simpleIcon": "svelte", "domains": ["svelte.dev"] }, + { "id": "django", "category": "framework", "simpleIcon": "django", "domains": ["djangoproject.com"] }, + { "id": "flask", "category": "framework", "simpleIcon": "flask", "domains": ["palletsprojects.com"] }, + { "id": "fastapi", "category": "framework", "simpleIcon": "fastapi", "domains": ["fastapi.tiangolo.com"] }, + { "id": "spring", "category": "framework", "simpleIcon": "spring", "aliases": ["spring-boot"], "domains": ["spring.io"] }, + { "id": "dotnet", "category": "framework", "simpleIcon": "dotnet", "aliases": [".net"], "domains": ["dotnet.microsoft.com"] } + ] +} diff --git a/vendor/archify/delta/architecture-delta.mjs b/vendor/archify/delta/architecture-delta.mjs new file mode 100644 index 0000000..5c608f3 --- /dev/null +++ b/vendor/archify/delta/architecture-delta.mjs @@ -0,0 +1,1313 @@ +import { parseRepositoryRemote } from '../renderers/shared/repository-location.mjs'; + +const COMPARATOR_VERSION = 1; +const CANONICAL_VERSION = 1; + +export class ArchitectureDeltaError extends Error { + constructor(code, message, details = {}) { + super(message); + this.name = 'ArchitectureDeltaError'; + this.code = code; + this.details = details; + } +} + +const codepointOrder = (left, right) => (left < right ? -1 : left > right ? 1 : 0); +const sorted = (values) => [...values].sort((left, right) => codepointOrder(String(left), String(right))); + +function canonical(value) { + if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort(codepointOrder).map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`; + } + return JSON.stringify(value); +} + +const equal = (left, right) => canonical(left) === canonical(right); + +function sortedObjects(values) { + return [...values].sort((left, right) => codepointOrder(canonical(left), canonical(right))); +} + +function sortedBy(values, keyFor) { + return [...values].sort((left, right) => codepointOrder(String(keyFor(left)), String(keyFor(right)))); +} + +function normalizeRepository(repository) { + if (!repository) return undefined; + const location = parseRepositoryRemote(repository.url, { authored: true }); + const url = location?.url || String(repository.url || ''); + return { + url: location?.provider === 'github' ? url.toLowerCase() : url, + revision: String(repository.revision || '').toLowerCase(), + ...(repository.provider !== undefined ? { provider: repository.provider } : {}), + ...(repository.link_mode !== undefined ? { link_mode: repository.link_mode } : {}), + }; +} + +const PROVENANCE_FIELDS = ['url', 'revision', 'provider', 'link_mode']; + +function provenanceSide(repository) { + if (!repository) return null; + return { + revision: repository.revision, + ...(repository.provider !== undefined ? { provider: repository.provider } : {}), + ...(repository.link_mode !== undefined ? { linkMode: repository.link_mode } : {}), + }; +} + +function provenanceChange(base, head) { + if (equal(base, head)) return undefined; + const changedFields = !base || !head + ? ['/repository'] + : PROVENANCE_FIELDS + .filter((field) => !equal(base[field], head[field])) + .map((field) => `/${field}`) + .sort(codepointOrder); + return { + changedFields, + base: provenanceSide(base), + head: provenanceSide(head), + }; +} + +function normalizeComponent(component) { + return { + ...component, + ...(Array.isArray(component.sources) ? { sources: sortedObjects(component.sources) } : {}), + }; +} + +function normalizeBoundary(boundary) { + return { ...boundary, wraps: sorted(boundary.wraps || []) }; +} + +export function canonicalArchitecture(diagram) { + const meta = { ...(diagram.meta || {}) }; + delete meta.output; + if (meta.repository) meta.repository = normalizeRepository(meta.repository); + return { + schema_version: diagram.schema_version, + diagram_type: diagram.diagram_type, + meta, + ...(diagram.layout ? { layout: diagram.layout } : {}), + components: sortedBy((diagram.components || []).map(normalizeComponent), (component) => component.id), + boundaries: sortedBy((diagram.boundaries || []).map(normalizeBoundary), boundaryKey), + connections: sortedBy(diagram.connections || [], (connection) => connection.id || ''), + ...(diagram.cards ? { cards: diagram.cards } : {}), + }; +} + +export function canonicalArchitectureJson(diagram) { + return canonical(canonicalArchitecture(diagram)); +} + +function fail(code, message, details) { + throw new ArchitectureDeltaError(code, message, details); +} + +function requireComparableShape(diagram, side) { + if (diagram?.schema_version !== 1) { + fail('delta/schema-version-mismatch', `${side} must use schema_version 1.`, { side, path: '/schema_version', actual: diagram?.schema_version }); + } + if (diagram?.diagram_type !== 'architecture') { + fail('delta/type-mismatch', `${side} must use diagram_type architecture.`, { side, path: '/diagram_type', actual: diagram?.diagram_type }); + } +} + +function stableIndex(items, collection, side, missingCode = 'delta/stable-id-required') { + const index = new Map(); + const missing = []; + const duplicates = []; + (items || []).forEach((item, itemIndex) => { + if (!item?.id) missing.push(`/${collection}/${itemIndex}/id`); + else if (index.has(item.id)) duplicates.push(item.id); + else index.set(item.id, item); + }); + if (missing.length) { + fail(missingCode, `${side} ${collection} require authored stable ids for comparison.`, { + side, + paths: sorted(missing), + supportedFixes: [`add a unique id to every ${collection} item`], + }); + } + if (duplicates.length) { + fail('delta/duplicate-stable-id', `${side} ${collection} contain duplicate ids.`, { + side, + collection, + ids: sorted(new Set(duplicates)), + supportedFixes: [`make every ${collection} id unique`], + }); + } + return index; +} + +const boundaryKey = (boundary) => `${boundary.kind}\u001f${boundary.label}`; + +function boundaryIndex(boundaries, side) { + const index = new Map(); + const ambiguous = []; + for (const boundary of boundaries || []) { + const key = boundaryKey(boundary); + if (index.has(key)) ambiguous.push(`${boundary.kind}:${boundary.label}`); + else index.set(key, boundary); + } + if (ambiguous.length) { + fail('delta/boundary-key-ambiguous', `${side} boundary kind + label keys must be unique.`, { + side, + boundaries: sorted(new Set(ambiguous)), + supportedFixes: ['rename one duplicate boundary or add stable boundary ids in a future schema version'], + }); + } + return index; +} + +function normalizedField(item, field) { + const value = item?.[field]; + if (field === 'sources' && Array.isArray(value)) return sortedObjects(value); + if (field === 'wraps' && Array.isArray(value)) return sorted(value); + return value; +} + +function fieldChanges(before, after, groups) { + const classifications = []; + const changedFields = []; + for (const [classification, fields] of Object.entries(groups)) { + const changed = fields.filter((field) => !equal(normalizedField(before, field), normalizedField(after, field))); + if (changed.length) classifications.push(classification); + changedFields.push(...changed.map((field) => `/${field}`)); + } + return { classifications: sorted(classifications), changedFields: sorted(changedFields) }; +} + +const COMPONENT_FIELDS = { + semantic: ['type', 'label', 'sublabel', 'tag', 'brand', 'icon'], + evidence: ['sources'], + geometry: ['row', 'col', 'pos', 'size'], +}; +const CONNECTION_FIELDS = { + topology: ['from', 'to'], + semantic: ['label', 'variant'], + geometry: ['fromSide', 'toSide', 'route', 'via', 'labelAt', 'labelDx', 'labelDy', 'labelSegment', 'width'], +}; +const BOUNDARY_FIELDS = { scope: ['wraps'], geometry: ['pad'] }; + +function statusFor(classifications, kind) { + if (classifications.some((value) => ['topology', 'semantic', 'scope'].includes(value))) return 'changed'; + if (classifications.includes('evidence')) return 'evidence-changed'; + if (classifications.includes('geometry')) return kind === 'connection' ? 'rerouted' : kind === 'component' ? 'moved' : 'geometry-changed'; + return 'same'; +} + +function compareEntities(baseIndex, headIndex, kind, groups, describe) { + const changes = []; + const identityClassification = kind === 'connection' ? 'topology' : kind === 'boundary' ? 'scope' : 'semantic'; + for (const id of sorted(new Set([...baseIndex.keys(), ...headIndex.keys()]))) { + const base = baseIndex.get(id); + const head = headIndex.get(id); + if (!base) changes.push({ ...describe(id, undefined, head), status: 'added', classifications: [identityClassification], changedFields: [] }); + else if (!head) changes.push({ ...describe(id, base, undefined), status: 'removed', classifications: [identityClassification], changedFields: [] }); + else { + const fields = fieldChanges(base, head, groups); + const status = statusFor(fields.classifications, kind); + if (status !== 'same') changes.push({ ...describe(id, base, head), status, ...fields }); + } + } + return changes; +} + +function summaryFor(changes, shape) { + const summary = Object.fromEntries(shape.map((key) => [key, 0])); + for (const change of changes) { + const key = change.status.replace(/-([a-z])/g, (_all, letter) => letter.toUpperCase()); + if (Object.hasOwn(summary, key)) summary[key] += 1; + } + return summary; +} + +function presentationChanged(base, head) { + const basePresentation = { + title: base.meta?.title, + locale: base.meta?.locale, + subtitle: base.meta?.subtitle, + animation: base.meta?.animation, + visual_preset: base.meta?.visual_preset, + quality_profile: base.meta?.quality_profile, + engineering_profile: base.meta?.engineering_profile, + legend: base.meta?.legend, + viewBox: base.meta?.viewBox, + layout: base.layout, + cards: base.cards, + }; + const headPresentation = { + title: head.meta?.title, + locale: head.meta?.locale, + subtitle: head.meta?.subtitle, + animation: head.meta?.animation, + visual_preset: head.meta?.visual_preset, + quality_profile: head.meta?.quality_profile, + engineering_profile: head.meta?.engineering_profile, + legend: head.meta?.legend, + viewBox: head.meta?.viewBox, + layout: head.layout, + cards: head.cards, + }; + return !equal(basePresentation, headPresentation); +} + +export function compareArchitecture(base, head, evidence = {}) { + requireComparableShape(base, 'base'); + requireComparableShape(head, 'head'); + const baseComponents = stableIndex(base.components, 'components', 'base'); + const headComponents = stableIndex(head.components, 'components', 'head'); + const shared = sorted([...baseComponents.keys()].filter((id) => headComponents.has(id))); + if (!shared.length) { + fail('delta/no-shared-component-id', 'The snapshots share no component id, so Archify cannot prove that they describe the same system.', { + supportedFixes: ['preserve at least one authored component id across snapshots'], + }); + } + + const baseConnections = stableIndex(base.connections, 'connections', 'base', 'delta/relationship-id-required'); + const headConnections = stableIndex(head.connections, 'connections', 'head', 'delta/relationship-id-required'); + const baseBoundaries = boundaryIndex(base.boundaries, 'base'); + const headBoundaries = boundaryIndex(head.boundaries, 'head'); + + const baseRepository = normalizeRepository(base.meta?.repository); + const headRepository = normalizeRepository(head.meta?.repository); + const identity = (repository) => parseRepositoryRemote(repository.url, { authored: true })?.identity || repository.url; + if (baseRepository && headRepository && identity(baseRepository) !== identity(headRepository)) { + fail('delta/repository-mismatch', 'The snapshots name different repositories.', { + baseRepository: baseRepository.url, + headRepository: headRepository.url, + supportedFixes: ['compare snapshots from the same repository or remove repository evidence from both inputs'], + }); + } + const proofLevel = baseRepository && headRepository + && evidence.baseVerified && evidence.headVerified + && /^[a-f0-9]{40}$/.test(baseRepository.revision) + && /^[a-f0-9]{40}$/.test(headRepository.revision) + ? 'revision-pinned' + : 'authored'; + + const components = compareEntities(baseComponents, headComponents, 'component', COMPONENT_FIELDS, (id, before, after) => ({ + id, + baseLabel: before?.label, + headLabel: after?.label, + })); + const connections = compareEntities(baseConnections, headConnections, 'connection', CONNECTION_FIELDS, (id, before, after) => ({ + id, + ...(before ? { base: { from: before.from, to: before.to, label: before.label || '' } } : {}), + ...(after ? { head: { from: after.from, to: after.to, label: after.label || '' } } : {}), + })); + const boundaries = compareEntities(baseBoundaries, headBoundaries, 'boundary', BOUNDARY_FIELDS, (_key, before, after) => ({ + key: `${(after || before).kind}:${(after || before).label}`, + kind: (after || before).kind, + label: (after || before).label, + })); + const provenance = provenanceChange(baseRepository, headRepository); + const provenanceChanged = Boolean(provenance); + + return { + schemaVersion: 1, + ok: true, + command: 'compare', + type: 'architecture', + comparatorVersion: COMPARATOR_VERSION, + canonicalVersion: CANONICAL_VERSION, + completeness: 'complete', + proofLevel, + base: { + title: base.meta?.title || '', + ...(evidence.baseRawSha256 ? { rawSha256: evidence.baseRawSha256 } : {}), + ...(evidence.baseSemanticSha256 ? { semanticSha256: evidence.baseSemanticSha256 } : {}), + ...(Number.isInteger(evidence.baseBytes) ? { bytes: evidence.baseBytes } : {}), + ...(baseRepository?.revision ? { revision: baseRepository.revision } : {}), + }, + head: { + title: head.meta?.title || '', + ...(evidence.headRawSha256 ? { rawSha256: evidence.headRawSha256 } : {}), + ...(evidence.headSemanticSha256 ? { semanticSha256: evidence.headSemanticSha256 } : {}), + ...(Number.isInteger(evidence.headBytes) ? { bytes: evidence.headBytes } : {}), + ...(headRepository?.revision ? { revision: headRepository.revision } : {}), + }, + summary: { + components: summaryFor(components, ['added', 'changed', 'evidenceChanged', 'removed', 'moved']), + connections: summaryFor(connections, ['added', 'changed', 'removed', 'rerouted']), + boundaries: summaryFor(boundaries, ['added', 'changed', 'removed', 'geometryChanged']), + presentationChanged: presentationChanged(base, head), + provenanceChanged, + }, + changes: { components, connections, boundaries }, + ...(provenance ? { provenance } : {}), + identity: { + components: 'components[].id', + connections: 'connections[].id (required)', + boundaries: 'boundaries[].kind + boundaries[].label (derived)', + }, + view: { visualPreset: head.meta?.visual_preset || 'classic' }, + limitations: [ + 'Authored Architecture IR only; no runtime impact, causality, risk, or mergeability is inferred.', + 'Boundary identity is conservatively derived from kind + label.', + ], + }; +} + +function esc(value) { + return String(value ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); +} + +const safeJson = (value) => JSON.stringify(value, null, 2).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026'); + +export function extractArchitectureSvg(html) { + const match = html.match(//); + if (!match) fail('delta/svg-missing', 'A validated Architecture artifact did not contain its primary SVG.'); + return match[0]; +} + +export function extractArtifactCss(html) { + const match = html.match(/Repository provenance changed${esc(provenanceDescription)}` + : ''; + const rowHtml = rows.length ? rows.map((row, index) => { + const label = row.headLabel || row.baseLabel || row.head?.label || row.base?.label || row.label || row.id; + const targetSignature = expectedReviewTargetSignature(row); + return `
  • `; + }).join('\n') : provenanceChanged + ? '
  • No component, relationship, or boundary changes; repository provenance changed.
  • ' + : '
  • No authored architecture changes.
  • '; + const baseView = baseHtml + ? `` + : baseSvg; + const headView = headHtml + ? `` + : headSvg; + const html = ` + +${esc(receipt.head.title)} Architecture Delta + +

    ARCHITECTURE DELTA · ${proof}

    See what changed
    before you merge.

    ${esc(receipt.base.title)} → ${esc(receipt.head.title)}

    ${total(receipt.summary, 'added')}ADDED
    ${total(receipt.summary, 'removed')}REMOVED
    ${changed}CHANGED
    ${provenanceNotice} +
    + ADD− DEL~ MOD↔ MOVE
    + +
    ${deltaSvg}
    +Exact authored changes · ${rows.length}
      ${rowHtml}
    +
    Stable IDs only · completeness: complete · ${proof}Authored IR only · no risk or mergeability inference
    + +`; + return html.replace(/[ \t]+$/gm, ''); +} + +export function validateArchitectureDeltaHtml(html, receipt) { + const failures = []; + const rows = architectureDeltaChangeRows(receipt); + const deltaMarkup = html.match(/
    ([\s\S]*?)<\/section>/)?.[1] || ''; + const svgTags = [...deltaMarkup.matchAll(/<\/?svg\b[^>]*>/g)]; + let svgDepth = 0; + let svgRoots = 0; + let rootStart = -1; + let rootEnd = -1; + let svgBalanced = true; + for (const match of svgTags) { + if (match[0].startsWith(' (html.match(new RegExp(`
    0; + const provenanceDescription = provenanceChanged && (legacyProvenance || validProvenanceFields) ? provenanceDetail(receipt.provenance) : ''; + const provenanceFieldLabel = legacyProvenance ? 'repository metadata' : validProvenanceFields ? provenanceFields.join(', ') : ''; + if (provenanceNotices !== (provenanceChanged ? 1 : 0) + || (!provenanceChanged && !legacyProvenance) + || (provenanceChanged && !legacyProvenance && !validProvenanceFields) + || (provenanceChanged && !html.includes(`data-provenance-fields="${esc(provenanceFieldLabel)}"`)) + || (provenanceChanged && !html.includes(`${esc(provenanceDescription)}`))) { + failures.push('provenance change notice does not match the receipt'); + } + if (provenanceChanged && rows.length === 0 && html.includes('No authored architecture changes.')) failures.push('provenance-only delta claims no authored changes'); + if (!html.includes('aria-label="Authored change review"')) failures.push('missing exact-ID change navigator'); + if ((html.match(/class="change-row"/g) || []).length !== rows.length) failures.push('change navigator row count does not match the receipt'); + if (!html.includes('id="export-svg"') || !html.includes('id="share-card"') + || !html.includes('window.Archify.deltaExport = { canonicalSvg: canonicalDeltaSvg, shareCard')) { + failures.push('missing canonical Delta SVG or Share Card export contract'); + } + for (const [index, row] of rows.entries()) { + const safeKey = esc(row.key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const rowMatches = [...html.matchAll(new RegExp(` +
    + + +
    + + + +
    + + +
    + + +
    + +
    +
    +
    +

    Product Analytics Data Flow

    +
    +
    + + + + +
    + + Product Analytics Data Flow + A data-flow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Sources + + + 02 / Ingest + + + 03 / Process + + + 04 / Store + + + 05 / Consume + + + + + + + + + + + + + + + + Web App · browser SDK · 01 / Sources · events + + + + Web App + browser SDK + events + + + + Mobile · iOS / Android · 01 / Sources · events + + + + Mobile + iOS / Android + events + + + + Edge API · collector · 02 / Ingest · TLS + + + + Edge API + collector + TLS + + + + Consent Gate · policy filter · 03 / Process · PII guard + + + + Consent Gate + policy filter + PII guard + + + + Event Stream · Kafka topic · 03 / Process · ordered + + + + Event Stream + Kafka topic + ordered + + + + PII Vault · encrypted · 04 / Store · restricted + + + + PII Vault + encrypted + restricted + + + + Warehouse · analytics tables · 04 / Store · curated + + + + Warehouse + analytics tables + curated + + + + Feature Store · daily batch · 04 / Store · derived + + + + Feature Store + daily batch + derived + + + + Dashboards · product metrics · 05 / Consume · SQL + + + + Dashboards + product metrics + SQL + + + + ML Model · ranking job · 05 / Consume · features + + + + ML Model + ranking job + features + + + + + + clickstream + user events + + + + app events + device events + + + + identity + consent + PII touch + + + + accepted events + append-only + + + + identity map + encrypted PII + + + + normalized facts + non-PII + + + + daily aggregates + batch + + + + metrics SQL + read-only + + + + feature vectors + derived + + + + restricted join + approved only + + + + + Legend + + + primary data + + + + policy / PII + + + + async batch + + + + data store + + + + data flow + + + +

    + + + + + + + + + +
    + + + +
    + + + + diff --git a/vendor/archify/examples/deployment-release.lifecycle.json b/vendor/archify/examples/deployment-release.lifecycle.json new file mode 100644 index 0000000..2967524 --- /dev/null +++ b/vendor/archify/examples/deployment-release.lifecycle.json @@ -0,0 +1,48 @@ +{ + "schema_version": 2, + "diagram_type": "lifecycle", + "meta": { + "title": "Deployment Release Lifecycle", + "output": "examples/deployment-release.html", + "animation": "trace", + "visual_preset": "signal-flow", + "quality_profile": "showcase" + }, + "lanes": [ + { "id": "main", "label": "Release phases" }, + { "id": "waiting", "label": "Approval + health wait" }, + { "id": "recovery", "label": "Rollback controller" }, + { "id": "terminal", "label": "Terminal exits" } + ], + "states": [ + { "id": "queued", "type": "start", "label": "Queued", "sublabel": "change accepted", "lane": "main", "col": 0, "step": "01", "tag": "pending" }, + { "id": "building", "type": "active", "label": "Building", "sublabel": "immutable image", "lane": "main", "col": 1, "step": "02", "tag": "running" }, + { "id": "verifying", "type": "decision", "label": "Verifying", "sublabel": "tests + policy", "lane": "main", "col": 2, "step": "03", "tag": "gate" }, + { "id": "ready", "type": "waiting", "label": "Ready", "sublabel": "promotion pending", "lane": "main", "col": 3, "step": "04", "tag": "approved" }, + { "id": "live", "type": "success", "label": "Live", "sublabel": "production healthy", "lane": "main", "col": 4, "step": "05", "tag": "success" }, + { "id": "approval", "type": "waiting", "label": "Needs Approval", "sublabel": "release owner", "lane": "waiting", "col": 2, "tag": "pause" }, + { "id": "rollback", "type": "active", "label": "Rolling Back", "sublabel": "last good image", "lane": "recovery", "col": 3, "tag": "automatic" }, + { "id": "paused", "type": "waiting", "label": "Health Paused", "sublabel": "SLO regression", "lane": "waiting", "col": 4, "tag": "observe" }, + { "id": "cancelled", "type": "failure", "label": "Cancelled", "sublabel": "approval denied", "lane": "terminal", "col": 2, "tag": "terminal" }, + { "id": "failed", "type": "failure", "label": "Failed", "sublabel": "rollback failed", "lane": "terminal", "col": 3, "tag": "terminal" }, + { "id": "rolled_back", "type": "success", "label": "Rolled Back", "sublabel": "service restored", "lane": "terminal", "col": 4, "tag": "terminal" } + ], + "transitions": [ + { "from": "queued", "to": "building" }, + { "from": "building", "to": "verifying" }, + { "from": "verifying", "to": "ready" }, + { "from": "ready", "to": "live" }, + { "from": "verifying", "to": "approval", "variant": "security", "label": "needs sign-off" }, + { "from": "approval", "to": "ready", "label": "approved" }, + { "from": "approval", "to": "cancelled", "variant": "security" }, + { "from": "ready", "to": "rollback", "variant": "security" }, + { "from": "rollback", "to": "failed", "variant": "security" }, + { "from": "live", "to": "paused", "variant": "dashed" }, + { "from": "paused", "to": "rolled_back", "variant": "emphasis" } + ], + "cards": [ + { "dot": "cyan", "title": "Promotion Rail", "items": ["The release object moves through five ordered phases", "Verification and approval remain distinct states", "Live means production health is currently proven"] }, + { "dot": "amber", "title": "Wait States", "items": ["Human approval can pause without consuming a worker", "A health regression pauses further rollout", "Every wait exposes the event required to continue"] }, + { "dot": "rose", "title": "Explicit Endings", "items": ["Denied approval ends as Cancelled", "Rollback controller failure ends as Failed", "Successful rollback is a terminal restored outcome"] } + ] +} diff --git a/vendor/archify/examples/event-stream.dataflow.json b/vendor/archify/examples/event-stream.dataflow.json new file mode 100644 index 0000000..3f653da --- /dev/null +++ b/vendor/archify/examples/event-stream.dataflow.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "diagram_type": "dataflow", + "meta": { + "title": "Order Event-stream Topology", + "output": "examples/event-stream.html", + "viewBox": [1080, 780], + "animation": "trace", + "visual_preset": "signal-flow", + "quality_profile": "showcase" + }, + "stages": [ + { "label": "Producers" }, + { "label": "Transit" }, + { "label": "Processors" }, + { "label": "State + recovery" }, + { "label": "Consumers" } + ], + "nodes": [ + { "id": "checkout", "type": "frontend", "label": "Checkout API", "sublabel": "order producer", "stage": 0, "row": 0, "tag": "team commerce" }, + { "id": "billing", "type": "backend", "label": "Billing API", "sublabel": "payment producer", "stage": 0, "row": 2, "tag": "team money" }, + { "id": "orders", "type": "messagebus", "label": "orders.v1", "sublabel": "12 partitions", "stage": 1, "row": 0, "tag": "key: order_id" }, + { "id": "payments", "type": "messagebus", "label": "payments.v2", "sublabel": "8 partitions", "stage": 1, "row": 2, "tag": "key: order_id" }, + { "id": "validate", "type": "backend", "label": "Order Validate", "sublabel": "group fulfillment", "stage": 2, "row": 0, "tag": "ordered" }, + { "id": "enrich", "type": "backend", "label": "Payment Enrich", "sublabel": "group analytics", "stage": 2, "row": 2, "tag": "at-least-once" }, + { "id": "state", "type": "database", "label": "Order State", "sublabel": "materialized view", "stage": 3, "row": 1, "tag": "idempotent" }, + { "id": "dlq", "type": "messagebus", "label": "events.dlq", "sublabel": "poison events", "stage": 3, "row": 4, "tag": "7-day retention" }, + { "id": "fulfillment", "type": "backend", "label": "Fulfillment", "sublabel": "shipping workflow", "stage": 4, "row": 0, "tag": "consumer" }, + { "id": "analytics", "type": "database", "label": "Analytics", "sublabel": "streaming facts", "stage": 4, "row": 2, "tag": "consumer" }, + { "id": "replay", "type": "security", "label": "Replay Tool", "sublabel": "approved batch", "stage": 4, "row": 4, "tag": "operator gate" }, + { "id": "ops", "type": "external", "label": "On-call", "sublabel": "DLQ owner", "stage": 4, "row": 3, "yOffset": -18, "tag": "SRE" } + ], + "flows": [ + { "from": "checkout", "to": "orders", "label": "OrderPlaced", "classification": "schema v1", "variant": "emphasis", "route": "straight" }, + { "from": "billing", "to": "payments", "label": "PaymentCaptured", "classification": "schema v2", "variant": "emphasis", "route": "straight" }, + { "from": "orders", "to": "validate", "label": "ordered orders", "classification": "consumer group", "variant": "emphasis", "route": "straight" }, + { "from": "payments", "to": "enrich", "label": "payment facts", "classification": "at-least-once", "variant": "emphasis", "route": "straight" }, + { "from": "validate", "to": "state", "label": "valid order", "classification": "idempotent", "variant": "emphasis", "route": "vertical-channel" }, + { "from": "enrich", "to": "state", "label": "enriched payment", "classification": "idempotent", "variant": "default", "route": "vertical-channel" }, + { "from": "state", "to": "fulfillment", "label": "ready orders", "classification": "read model", "variant": "emphasis", "route": "vertical-channel" }, + { "from": "state", "to": "analytics", "label": "order facts", "classification": "non-PII", "variant": "default", "route": "vertical-channel" }, + { "from": "validate", "to": "dlq", "label": "invalid event", "classification": "dead letter", "variant": "security", "fromSide": "top", "toSide": "top", "via": [[530, 80], [20, 80], [20, 550], [745, 550]], "labelAt": [300, 550] }, + { "from": "enrich", "to": "dlq", "label": "poison event", "classification": "dead letter", "variant": "security", "route": "bottom-channel", "labelDy": 30 }, + { "from": "dlq", "to": "ops", "label": "failure sample", "classification": "restricted", "variant": "security", "route": "vertical-channel" }, + { "from": "dlq", "to": "replay", "label": "approved replay", "classification": "audited batch", "variant": "dashed", "route": "straight", "labelDy": 30 } + ], + "cards": [ + { "dot": "amber", "title": "Transit Contract", "items": ["Every event and topic is named", "Partition keys preserve per-order ordering", "Consumer groups expose processing ownership"] }, + { "dot": "emerald", "title": "State + Delivery", "items": ["Processors write an idempotent materialized view", "Fulfillment and analytics consume distinct assets", "At-least-once delivery never implies duplicate business effects"] }, + { "dot": "rose", "title": "Failure Ownership", "items": ["Poison events land in a retained dead-letter topic", "On-call inspects samples before replay", "Replay is gated, batched, and auditable"] } + ] +} diff --git a/vendor/archify/examples/holiday-planning.workflow.json b/vendor/archify/examples/holiday-planning.workflow.json new file mode 100644 index 0000000..60dd641 --- /dev/null +++ b/vendor/archify/examples/holiday-planning.workflow.json @@ -0,0 +1,143 @@ +{ + "schema_version": 2, + "diagram_type": "workflow", + "meta": { + "title": "2026年剩余假期安排", + "locale": "zh-CN", + "quality_profile": "showcase", + "legend": { + "entries": { + "backend": { + "label": "假期" + }, + "security": { + "label": "补班" + }, + "external": { + "label": "时间节点" + } + } + }, + "output": "holiday-planning.html" + }, + "lanes": [ + { + "id": "calendar", + "label": "时间线" + }, + { + "id": "makeup", + "label": "调休" + } + ], + "mainPath": [ + "today", + "mid_autumn", + "national_day", + "year_end" + ], + "nodes": [ + { + "id": "today", + "lane": "calendar", + "col": 0, + "type": "external", + "label": "今天 9月22日", + "sublabel": "周二 · 距中秋3天", + "icon": "clock" + }, + { + "id": "mid_autumn", + "lane": "calendar", + "col": 1, + "type": "backend", + "label": "中秋节", + "sublabel": "9月25日–27日 · 3天", + "tag": "无需调休", + "icon": "moon" + }, + { + "id": "national_day", + "lane": "calendar", + "col": 3, + "type": "backend", + "label": "国庆节", + "sublabel": "10月1日–7日 · 7天", + "tag": "连休", + "icon": "calendar" + }, + { + "id": "year_end", + "lane": "calendar", + "col": 5, + "type": "external", + "label": "此后至年底", + "sublabel": "无法定节假日", + "icon": "flag" + }, + { + "id": "makeup_work", + "lane": "makeup", + "col": 3, + "type": "security", + "label": "调休上班", + "sublabel": "10月10日(周六)", + "tag": "补班", + "icon": "briefcase" + } + ], + "edges": [ + { + "id": "to_mid_autumn", + "from": "today", + "to": "mid_autumn", + "label": "3天后连休" + }, + { + "id": "mid_to_national", + "from": "mid_autumn", + "to": "national_day", + "label": "节后上班3天" + }, + { + "id": "to_year_end", + "from": "national_day", + "to": "year_end", + "label": "假期结束" + }, + { + "id": "makeup_branch", + "from": "national_day", + "to": "makeup_work", + "label": "国庆调休", + "role": "branch", + "variant": "security" + } + ], + "cards": [ + { + "dot": "emerald", + "title": "中秋节", + "items": [ + "9月25日(周五)至27日(周日)放假,共3天", + "与周末自然相连,无需额外调休上班" + ] + }, + { + "dot": "amber", + "title": "国庆节", + "items": [ + "10月1日(周四)至7日(周三)放假调休,共7天", + "9月20日(周日)上班日已过", + "10月10日(周六)需补班" + ] + }, + { + "dot": "slate", + "title": "年底前", + "items": [ + "国庆假期结束后,到2026年底不再有法定节假日安排" + ] + } + ] +} diff --git a/vendor/archify/examples/incident-response.workflow.json b/vendor/archify/examples/incident-response.workflow.json new file mode 100644 index 0000000..dac074f --- /dev/null +++ b/vendor/archify/examples/incident-response.workflow.json @@ -0,0 +1,59 @@ +{ + "schema_version": 1, + "diagram_type": "workflow", + "meta": { + "title": "Incident Response Runbook", + "output": "examples/incident-response.html", + "animation": "trace", + "visual_preset": "signal-flow", + "quality_profile": "showcase" + }, + "lanes": [ + { "id": "signals", "label": "Signals" }, + { "id": "responders", "label": "Incident Command" }, + { "id": "mitigation", "label": "Service Mitigation" }, + { "id": "recovery", "label": "Recovery Evidence" }, + { "id": "communication", "label": "Stakeholder Communication" }, + { "id": "exceptions", "label": "Escalation + Rollback", "variant": "exception" } + ], + "phases": [ + { "id": "detect", "label": "Detect", "fromCol": 0, "toCol": 1 }, + { "id": "respond", "label": "Triage + mitigate", "fromCol": 2, "toCol": 3, "variant": "emphasis" }, + { "id": "recover", "label": "Verify + close", "fromCol": 4, "toCol": 5, "variant": "dashed" } + ], + "groups": [ + { "id": "command", "label": "Incident command", "lane": "responders", "fromCol": 1, "toCol": 3, "variant": "emphasis" }, + { "id": "exception_actions", "label": "If impact persists", "lane": "exceptions", "fromCol": 3, "toCol": 5, "variant": "security" } + ], + "mainPath": ["alert", "page", "triage", "contain", "recover", "verify", "close"], + "nodes": [ + { "id": "alert", "lane": "signals", "col": 0, "type": "messagebus", "label": "SLO Alert", "sublabel": "burn rate" }, + { "id": "page", "lane": "responders", "col": 1, "width": 76, "type": "external", "label": "Page On-call", "sublabel": "acknowledge" }, + { "id": "triage", "lane": "responders", "col": 2, "width": 64, "type": "backend", "label": "Triage", "sublabel": "scope impact" }, + { "id": "declare", "lane": "responders", "col": 3, "type": "security", "label": "Declare", "sublabel": "assign commander", "tag": "SEV-1/2" }, + { "id": "contain", "lane": "mitigation", "col": 3, "width": 72, "type": "backend", "label": "Contain", "sublabel": "stop growth" }, + { "id": "recover", "lane": "mitigation", "col": 4, "width": 52, "type": "cloud", "label": "Recover", "sublabel": "restore" }, + { "id": "verify", "lane": "recovery", "col": 5, "type": "database", "label": "Verify", "sublabel": "SLO + traces", "tag": "15 min stable" }, + { "id": "close", "lane": "communication", "col": 5, "type": "external", "label": "Resolve", "sublabel": "final update" }, + { "id": "update", "lane": "communication", "col": 3, "type": "frontend", "label": "Status Update", "sublabel": "impact + ETA" }, + { "id": "escalate", "lane": "exceptions", "col": 3, "width": 72, "type": "security", "label": "Escalate", "sublabel": "specialist" }, + { "id": "rollback", "lane": "exceptions", "col": 5, "type": "messagebus", "label": "Rollback", "sublabel": "last good" } + ], + "edges": [ + { "from": "alert", "to": "page", "label": "page", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "page", "to": "triage", "route": "bottom-channel", "fromSide": "bottom", "toSide": "bottom" }, + { "from": "triage", "to": "contain", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "contain", "to": "recover", "route": "bottom-channel", "fromSide": "bottom", "toSide": "bottom" }, + { "from": "recover", "to": "verify", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "verify", "to": "close", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "triage", "to": "declare", "variant": "security" }, + { "from": "declare", "to": "update", "variant": "dashed", "fromSide": "top", "toSide": "left", "via": [[430, 16], [20, 16], [20, 615]] }, + { "from": "update", "to": "escalate", "variant": "security", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "verify", "to": "rollback", "variant": "security", "role": "error", "route": "outside-right", "fromSide": "right", "toSide": "right" } + ], + "cards": [ + { "dot": "rose", "title": "Ownership First", "items": ["A page is not an incident until someone owns command", "Severity and scope are explicit before mitigation spreads", "Escalation names the missing expertise"] }, + { "dot": "emerald", "title": "Recovery Is Evidence", "items": ["Mitigation can reduce impact without proving recovery", "SLOs and traces must stay healthy for a fixed window", "The final update follows verification, not optimism"] }, + { "dot": "cyan", "title": "Communication Contract", "items": ["Stakeholders receive impact, action, and next update time", "Rollback remains visible as a deliberate response", "Every branch has an owner and observable exit"] } + ] +} diff --git a/vendor/archify/examples/lifecycle-agent-run.html b/vendor/archify/examples/lifecycle-agent-run.html new file mode 100644 index 0000000..6714960 --- /dev/null +++ b/vendor/archify/examples/lifecycle-agent-run.html @@ -0,0 +1,13765 @@ + + + + + + + + Agent Run Lifecycle Diagram + + + + + + + + +
    + +
    +
    +
    +

    Agent Run Lifecycle

    +
    +
    + + + + +
    + + Agent Run Lifecycle + A lifecycle diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Lifecycle phases + + 02 / Interruptions + Recovery loop + + 03 / Terminal exits + + + + + + + + + + + + + + + Queued · request accepted · Lifecycle phases · entry + + + + + 01 + Queued + request accepted + entry + + + + Planning · build task graph · Lifecycle phases · model + + + + 02 + Planning + build task graph + model + + + + Executing · tool calls · Lifecycle phases · work + + + + 03 + Executing + tool calls + work + + + + Reviewing · quality gate · Lifecycle phases · check + + + + 04 + Reviewing + quality gate + check + + + + Completed · final response · Lifecycle phases · done + + + + + 05 + Completed + final response + done + + + + Needs Approval · human gate · Interruptions · pause + + + + Needs Approval + human gate + pause + + + + Blocked · missing input · Interruptions · wait + + + + Blocked + missing input + wait + + + + Failed · recoverable error · Recovery loop · retryable + + + + Failed + recoverable error + retryable + + + + Cancelled · user stopped · Terminal exits · terminal + + + + + Cancelled + user stopped + terminal + + + + Expired · timeout · Terminal exits · terminal + + + + + Expired + timeout + terminal + + + + + + + + + + + + + Legend + + + initial state + + + + active + + + + waiting + + + + decision + + + + success + + + + failure / exit + + + + final state + + + +

    + + + + + + + + + +
    + + + +
    + + + + diff --git a/vendor/archify/examples/locales/de.json b/vendor/archify/examples/locales/de.json new file mode 100644 index 0000000..9fd47a5 --- /dev/null +++ b/vendor/archify/examples/locales/de.json @@ -0,0 +1,425 @@ +{ + "page.title": "{title} Diagramm", + "diagram.description.architecture": "Ein von Archify erstelltes Architekturdiagramm.", + "diagram.description.workflow": "Ein von Archify erstelltes Workflow-Diagramm.", + "diagram.description.sequence": "Ein von Archify erstelltes Sequenzdiagramm.", + "diagram.description.dataflow": "Ein von Archify erstelltes Datenfluss-Diagramm.", + "diagram.description.lifecycle": "Ein von Archify erstelltes Lifecycle-Diagramm.", + "node.focus": "{label} fokussieren", + "node.focus.detail": "{label} fokussieren, {detail}", + "node.context.architecture": "Architekturkomponente", + "node.context.workflow": "Workflow-Knoten", + "node.context.sequence": "Sequenzteilnehmer", + "node.context.dataflow": "Datenfluss-Knoten", + "node.context.lifecycle": "Lifecycle-Status", + "legend.title": "Legende", + "legend.architecture.frontend": "Frontend", + "legend.architecture.backend": "Backend", + "legend.architecture.database": "Datenbank", + "legend.architecture.cloud": "Cloud", + "legend.architecture.security": "Sicherheit", + "legend.architecture.messagebus": "Nachrichtenbus", + "legend.architecture.external": "Extern", + "legend.workflow.frontend": "Benutzeroberfläche", + "legend.workflow.backend": "Agentenlogik", + "legend.workflow.security": "Richtlinie", + "legend.workflow.messagebus": "Werkzeugaktion", + "legend.workflow.database": "Kontext / Trace", + "legend.workflow.cloud": "Cloud-Dienst", + "legend.workflow.external": "Externes System", + "legend.sequence.emphasis": "Hauptanfrage", + "legend.sequence.return": "Rückgabe", + "legend.sequence.security": "Sicherheit", + "legend.sequence.dashed": "asynchroner Trace", + "legend.sequence.default": "Nachricht", + "legend.dataflow.emphasis": "Primärdaten", + "legend.dataflow.security": "Richtlinie / PII", + "legend.dataflow.dashed": "asynchroner Batch", + "legend.dataflow.database": "Datenspeicher", + "legend.dataflow.default": "Datenfluss", + "legend.lifecycle.start": "Startzustand", + "legend.lifecycle.active": "aktiv", + "legend.lifecycle.waiting": "Wartend", + "legend.lifecycle.decision": "Entscheidung", + "legend.lifecycle.success": "Erfolg", + "legend.lifecycle.failure": "Fehler / Abbruch", + "legend.lifecycle.neutral": "neutral", + "legend.lifecycle.external": "extern", + "legend.lifecycle.final": "Endzustand", + "viewer.kind.frontend": "Frontend", + "viewer.kind.backend": "Backend", + "viewer.kind.database": "Datenbank", + "viewer.kind.cloud": "Cloud", + "viewer.kind.security": "Sicherheit", + "viewer.kind.messagebus": "Nachrichtenbus", + "viewer.kind.external": "Extern", + "viewer.kind.neutral": "Neutral", + "viewer.kind.node": "Knoten", + "viewer.kind.start": "Start", + "viewer.kind.active": "Aktiv", + "viewer.kind.waiting": "Wartend", + "viewer.kind.decision": "Entscheidung", + "viewer.kind.success": "Erfolg", + "viewer.kind.failure": "Fehler", + "viewer.toolbar.actions": "Diagrammaktionen", + "viewer.theme.toggle.title": "Design wechseln (T)", + "viewer.theme.toggle": "Farbdesign wechseln", + "viewer.theme.dark": "Dunkel", + "viewer.theme.light": "Hell", + "viewer.preset.choose.title": "Visuellen Stil wählen (S wechselt durch)", + "viewer.preset.choose": "Visuellen Stil wählen", + "viewer.preset.style": "Stil", + "viewer.preset.menu": "Visueller Stil", + "viewer.preset.identity": "Visuelle Identität", + "viewer.preset.cycles": "S wechselt durch", + "viewer.preset.classic": "Klassisch", + "viewer.preset.classic.short": "Klassisch", + "viewer.preset.classic.hint": "Stabiler technischer Standard", + "viewer.preset.flow": "Signalfluss", + "viewer.preset.flow.short": "Fluss", + "viewer.preset.flow.hint": "Bewegungsbetonte Präsentation", + "viewer.preset.blueprint": "Bauplan", + "viewer.preset.blueprint.hint": "Technisches Review", + "viewer.preset.editorial": "Editorial", + "viewer.preset.editorial.hint": "Für Veröffentlichungen und Launch-Notizen", + "viewer.preset.badge.signalFlow": "SIGNALFLUSS", + "viewer.preset.badge.blueprint": "BAUPLAN / REV. 01", + "viewer.preset.badge.editorial": "EDITORIAL / FELDNOTIZ", + "viewer.preset.badge.editorialPlate": "ARCHIFY / TAFEL 04", + "viewer.preset.current": "Visueller Stil: {style}. Visuellen Stil wählen", + "viewer.motion.live": "Live", + "viewer.motion.still": "Statisch", + "viewer.motion.pause": "Bewegung pausieren", + "viewer.motion.resume": "Bewegung fortsetzen", + "viewer.motion.reduced": "Bewegung wegen reduzierter Bewegungseinstellung pausiert", + "viewer.motion.hidden": "Bewegung pausiert, während diese Seite ausgeblendet ist", + "viewer.motion.yielding": "Bewegung pausieren; derzeit an {owner} abgegeben", + "viewer.motion.yielding.title": "Live-Vorschau aktiv · an {owner} abgegeben", + "viewer.owner.route": "Routensonde", + "viewer.owner.lens": "Semantische Linse", + "viewer.owner.relationship": "Beziehungsvorschau", + "viewer.owner.intent": "Intent Trace", + "viewer.owner.focus": "semantischer Fokus", + "viewer.owner.legend": "Legendenvorschau", + "viewer.owner.reader": "Leserinteraktion", + "viewer.present.enter": "Präsentationsmodus starten", + "viewer.present.enter.title": "Präsentationsmodus (F)", + "viewer.present.exit": "Präsentationsmodus beenden", + "viewer.present.exit.title": "Präsentationsmodus beenden (F oder Escape)", + "viewer.present.present": "Präsentieren", + "viewer.present.exit.label": "Beenden", + "viewer.export.button": "Exportieren", + "viewer.export.button.title": "Diagramm exportieren (E)", + "viewer.export.diagram": "Diagramm exportieren", + "viewer.export.menu": "Exportieren", + "viewer.export.subtitle": "Portable, saubere Ausgaben", + "viewer.export.share": "Teilen", + "viewer.export.shareCard": "Freigabekarte", + "viewer.export.routeShareCard": "Routen-Freigabekarte", + "viewer.export.reachShareCard": "Reichweiten-Freigabekarte", + "viewer.export.copyDiagram": "Diagramm kopieren", + "viewer.export.clipboardPng": "PNG in Zwischenablage", + "viewer.export.raster": "Rasterbilder", + "viewer.export.image": "Bild", + "viewer.export.lossless": "Verlustfreies Bild", + "viewer.export.compact": "Kompaktes Bild", + "viewer.export.modern": "Modernes Bild", + "viewer.export.vectorMotion": "Vektor und Bewegung", + "viewer.export.vectorMotion.heading": "Vektor & Bewegung", + "viewer.export.editable": "Editierbarer Vektor", + "viewer.export.motion6s": "6-Sekunden-Bewegung", + "viewer.export.unsupported": "Von diesem Browser nicht unterstützt", + "viewer.export.clipboardUnsupported": "Bild-Zwischenablage wird von diesem Browser nicht unterstützt", + "viewer.export.clipboardUnsupported.short": "Dieser Browser unterstützt keine Bild-Zwischenablage.", + "viewer.export.motionUnavailable": "Bewegungsaufnahme in diesem Browser nicht verfügbar", + "viewer.export.webmUnavailable": "WebM in diesem Browser nicht verfügbar", + "viewer.export.failed": "Export fehlgeschlagen: {message}", + "viewer.export.unknownVariant": "Unbekannter Freigabekarten-Typ: {variant}", + "viewer.export.routeRequired": "Route verfolgen, bevor eine Routen-Freigabekarte exportiert wird", + "viewer.export.reachRequired": "Verfasste Reichweite verfolgen, bevor eine Reichweiten-Freigabekarte exportiert wird", + "viewer.export.unknown": "unbekannt", + "viewer.export.routeFailed": "Export der Routen-Freigabekarte fehlgeschlagen: {message}", + "viewer.export.reachFailed": "Export der Reichweiten-Freigabekarte fehlgeschlagen: {message}", + "viewer.export.copyFailed": "Kopieren fehlgeschlagen: {message}", + "viewer.export.copiedPng": "PNG in Zwischenablage kopiert", + "viewer.export.downloadedRoute": "Routen-Freigabekarte heruntergeladen", + "viewer.export.downloadedReach": "Reichweiten-Freigabekarte heruntergeladen", + "viewer.export.downloadedWebm": "WebM heruntergeladen", + "viewer.export.recording": "6 Sekunden Bewegung werden aufgezeichnet …", + "viewer.export.card.routeSummary.one": "Route: {source} → {target} · {count} gerichteter Hop", + "viewer.export.card.routeSummary.other": "Route: {source} → {target} · {count} gerichtete Hops", + "viewer.export.card.reachSummary": "Verfasst {direction} von {origin} · {nodes} · {links} · max. {hops}", + "viewer.export.card.node.one": "{count} Knoten", + "viewer.export.card.node.other": "{count} Knoten", + "viewer.export.card.link.one": "{count} Verbindung", + "viewer.export.card.link.other": "{count} Verbindungen", + "viewer.export.card.hop.one": "{count} Hop", + "viewer.export.card.hop.other": "{count} Hops", + "viewer.export.card.routeBadge": "ARCHIFY · ROUTE · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · {direction} REICHWEITE", + "viewer.export.direction.upstream": "Vorgelagert", + "viewer.export.direction.downstream": "Nachgelagert", + "viewer.export.error.canvasUnavailable": "Canvas für {label} nicht verfügbar", + "viewer.export.error.contextUnavailable": "2D-Canvas-Kontext für {label} nicht verfügbar", + "viewer.export.error.toBlobUnavailable": "canvas.toBlob für {label} nicht verfügbar", + "viewer.export.error.toBlobNull": "canvas.toBlob lieferte keine Daten für {label}", + "viewer.export.error.variantsCombined": "Freigabekarten-Typen können nicht kombiniert werden", + "viewer.export.error.viewerState": "Beim Export der Freigabekarte konnte der temporäre Viewer-Zustand nicht entfernt werden", + "viewer.export.error.routeState": "Beim Export der Routenkarte konnte die aufgelöste Route nicht sicher erhalten werden", + "viewer.export.error.reachState": "Beim Export der Reichweitenkarte konnte die verfasste Reichweite nicht sicher erhalten werden", + "viewer.export.error.webmRequirements": "Der WebM-Bewegungsexport erfordert eine Trace-Animation und Browser-Unterstützung für MediaRecorder", + "viewer.export.error.mediaRecorder": "MediaRecorder fehlgeschlagen", + "viewer.export.error.emptyWebm": "MediaRecorder hat ein leeres WebM erzeugt", + "viewer.export.error.webmBackground": "SVG-Hintergrund konnte für den WebM-Export nicht geladen werden", + "viewer.guide.eyebrow": "Diagrammleitfaden", + "viewer.guide.close": "Diagrammleitfaden schließen", + "viewer.guide.inspecting": "Kompilierte Semantik wird geprüft", + "viewer.guide.actions": "Diagramm-Erkundungsaktionen", + "viewer.guide.find": "Beliebigen Knoten finden", + "viewer.guide.find.hint": "Labels, Zuständigkeiten, Arten und stabile IDs durchsuchen.", + "viewer.guide.route": "Eine Route verfolgen", + "viewer.guide.route.aria": "Eine gerichtete Route verfolgen", + "viewer.guide.route.hint": "Herausfinden, wie zwei semantische Knoten in verfasster Richtung verbunden sind.", + "viewer.guide.map": "Das gesamte System sehen", + "viewer.guide.map.hint": "Semantisches Radar mit Live-Ansicht und stabilen Knoten öffnen.", + "viewer.guide.lens": "Semantische Arten vergleichen", + "viewer.guide.lens.hint": "Rollen zählen, ihren Verkehr sichtbar machen und direkte verfasste Verbindungen vergleichen.", + "viewer.guide.present": "Präsentationsmodus starten", + "viewer.guide.present.hint": "Dem Live-Diagramm die gesamte Ansicht geben, ohne den Export zu verändern.", + "viewer.guide.shortcuts": "Weitere Tastenkombinationen", + "viewer.guide.shortcut.export": "Exportieren", + "viewer.guide.shortcut.theme": "Design", + "viewer.guide.shortcut.style": "Stil", + "viewer.guide.shortcut.reset": "Zurücksetzen", + "viewer.guide.shortcut.zoomIn": "Vergrößern", + "viewer.guide.shortcut.zoomOut": "Verkleinern", + "viewer.guide.shortcut.close": "Schließen", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} semantischer Knoten", + "viewer.guide.fact.node.other": "{count} semantische Knoten", + "viewer.guide.fact.relationship.one": "{count} Beziehung", + "viewer.guide.fact.relationship.other": "{count} Beziehungen", + "viewer.guide.open": "Diagrammleitfaden öffnen", + "viewer.finder.title": "Knoten finden", + "viewer.finder.close": "Knotensuche schließen", + "viewer.finder.placeholder": "Labels oder IDs suchen", + "viewer.finder.search": "Diagrammknoten durchsuchen", + "viewer.finder.results": "Diagrammknoten", + "viewer.finder.empty": "Keine passenden Knoten", + "viewer.finder.result.focus": "{label} fokussieren", + "viewer.finder.result.routeStart": "{label} als Routenstart wählen", + "viewer.finder.result.routeTarget": "{label} als Routenziel wählen, {links}", + "viewer.finder.status.empty": "Keine passenden Knoten", + "viewer.finder.status.count.one": "{count} passender Knoten", + "viewer.finder.status.count.other": "{count} passende Knoten", + "viewer.finder.noun.nodes": "Knoten", + "viewer.finder.link.one": "{count} Verbindung", + "viewer.finder.link.other": "{count} Verbindungen", + "viewer.finder.result.focus.one": "{label} fokussieren, {count} zugehörige Verbindung", + "viewer.finder.result.focus.other": "{label} fokussieren, {count} zugehörige Verbindungen", + "viewer.finder.status.filtered": "{visible} von {available} {noun}", + "viewer.finder.status.all": "{available} {noun}", + "viewer.passport.eyebrow": "Semantischer Pass", + "viewer.passport.metadata": "Knoten-Metadaten", + "viewer.passport.evidence": "Verifizierter Quellnachweis", + "viewer.passport.verified": "Verifizierte Quelle", + "viewer.passport.verificationScope": "Verifiziert gegen lokales Git zur fixierten Revision. Remote-Zugriff wurde nicht geprüft.", + "viewer.passport.reach": "Verfasste Reichweite", + "viewer.passport.reach.trace": "Verfasste Erreichbarkeit verfolgen", + "viewer.passport.upstream": "Vorgelagert", + "viewer.passport.downstream": "Nachgelagert", + "viewer.passport.upstream.trace": "Vorgelagerte verfasste Erreichbarkeit verfolgen", + "viewer.passport.downstream.trace": "Nachgelagerte verfasste Erreichbarkeit verfolgen", + "viewer.passport.close": "Semantischen Pass schließen", + "viewer.passport.copy": "Link kopieren", + "viewer.passport.copy.focus": "Link zum fokussierten Knoten kopieren", + "viewer.passport.relations": "Beziehungen", + "viewer.passport.relations.show": "Verbundene Beziehungen anzeigen", + "viewer.passport.relations.hide": "Verbundene Beziehungen ausblenden", + "viewer.passport.relations.list": "Verbundene Beziehungen", + "viewer.passport.copyRelation": "Beziehung kopieren", + "viewer.passport.copyNode": "Knoten kopieren", + "viewer.passport.copyPinned": "Link zur angehefteten Beziehung kopieren", + "viewer.passport.copySource": "Link zum Quellknoten kopieren", + "viewer.passport.copy.focused.success": "Link des fokussierten Knotens kopiert", + "viewer.passport.copy.pinned.success": "Link der angehefteten Beziehung kopiert", + "viewer.passport.copy.focused.failed": "Link des fokussierten Knotens konnte nicht kopiert werden", + "viewer.passport.copy.pinned.failed": "Link der angehefteten Beziehung konnte nicht kopiert werden", + "viewer.passport.relationship.none": "Keine verbundenen Beziehungen", + "viewer.passport.relationship.count.one": "{count} Beziehung", + "viewer.passport.relationship.count.other": "{count} Beziehungen", + "viewer.passport.relationship.show.one": "{count} verbundene Beziehung anzeigen", + "viewer.passport.relationship.show.other": "{count} verbundene Beziehungen anzeigen", + "viewer.passport.relationship.summary": "{out} ausgehend · {in} eingehend{loops}", + "viewer.passport.relationship.loops": " · {count} Selbstschleife", + "viewer.passport.relationship.explorer": "Direkter Beziehungs-Explorer", + "viewer.passport.relationship.help": "Pfeiltasten zum Erkunden der Beziehungen verwenden. Enter oder Leertaste heftet Details an; Escape hebt die Auswahl auf.", + "viewer.passport.relationship.loopsBack": "schleift zurück", + "viewer.passport.relationship.connectsTo": "verbindet zu", + "viewer.passport.relationship.connectsFrom": "verbindet von", + "viewer.passport.relationship.pinned": "Angeheftete Beziehung · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "Beziehung {index} von {total} prüfen: {from} zu {to}, {label}. Enter für Details drücken.", + "viewer.passport.relationship.group.out": "Ausgehend", + "viewer.passport.relationship.group.in": "Eingehend", + "viewer.passport.relationship.group.loop": "Selbstschleifen", + "viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "AUS →", + "viewer.passport.relationship.direction.in": "← EIN", + "viewer.passport.relationship.direction.loop": "SCHLEIFE", + "viewer.passport.sourceCount.one": "{count} verifizierte Quellenreferenz", + "viewer.passport.sourceCount.other": "{count} verifizierte Quellenreferenzen", + "viewer.passport.sourceMarker": "QUELLE", + "viewer.passport.beacon.one": "{count} verifizierte Quelle; diesen Knoten fokussieren, um sie zu prüfen", + "viewer.passport.beacon.other": "{count} verifizierte Quellen; diesen Knoten fokussieren, um sie zu prüfen", + "viewer.passport.repository.open": "Verifizierte Repository-Revision {revision} öffnen", + "viewer.passport.source.open": "Verifizierte Quelle {path} bei Revision {revision} öffnen", + "viewer.passport.source.openLink": "Öffnen ↗", + "viewer.passport.reach.upstream.one": "{count} vorgelagerten verfassten Knoten verfolgen", + "viewer.passport.reach.upstream.other": "{count} vorgelagerte verfasste Knoten verfolgen", + "viewer.passport.reach.downstream.one": "{count} nachgelagerten verfassten Knoten verfolgen", + "viewer.passport.reach.downstream.other": "{count} nachgelagerte verfasste Knoten verfolgen", + "viewer.passport.reach.noUpstream": "Keine vorgelagerten verfassten Knoten", + "viewer.passport.reach.noDownstream": "Keine nachgelagerten verfassten Knoten", + "viewer.passport.reach.status": "{direction} · {nodes} Knoten · {links} Verbindungen · max. {hops} Hops", + "viewer.route.eyebrow": "Routensonde", + "viewer.route.start": "Startknoten wählen", + "viewer.route.start.find": "Start finden", + "viewer.route.start.find.aria": "Einen Routenstart finden", + "viewer.route.copy": "Link kopieren", + "viewer.route.copy.aria": "Link zur verfolgten Route kopieren", + "viewer.route.clear": "Löschen", + "viewer.route.clear.aria": "Routensonde löschen", + "viewer.route.traced": "Verfolgte Route", + "viewer.route.pickTwo": "Zwei semantische Knoten im Diagramm auswählen", + "viewer.route.pickOne": "Einen semantischen Knoten im Diagramm auswählen", + "viewer.route.controls": "Routen-Verlaufssteuerung", + "viewer.route.previous": "Vorherige Routenposition", + "viewer.route.play": "Routenverlauf abspielen", + "viewer.route.pause": "Routenverlauf pausieren", + "viewer.route.replay": "Routenverlauf erneut abspielen", + "viewer.route.next": "Nächste Routenposition", + "viewer.route.journey": "Verlauf", + "viewer.route.pause.label": "Pausieren", + "viewer.route.replay.label": "Erneut abspielen", + "viewer.route.overview": "Übersicht", + "viewer.route.overview.aria": "Vollständige Routenübersicht anzeigen", + "viewer.route.instructions": "Quelle wählen, dann das Ziel. Die Richtung ist entscheidend.", + "viewer.route.destination": "Ziel ausgehend von {label} wählen", + "viewer.route.destination.find": "Ziel finden", + "viewer.route.destination.find.aria": "Ein erreichbares Routenziel finden", + "viewer.route.differentDestination": "Anderes Ziel wählen", + "viewer.route.distinct": "Eine Route benötigt zwei unterschiedliche semantische Knoten.", + "viewer.route.unreachable": "Keine gerichtete Route zu {label}", + "viewer.route.unreachable.detail": "{target} ist von {source} aus nicht erreichbar. Ein hervorgehobenes Ziel wählen.", + "viewer.route.start.instructions": "Quelle auswählen. Im nächsten Schritt werden nur gerichtete Ziele angezeigt.", + "viewer.route.copy.success": "Link zur verfolgten Route kopiert", + "viewer.route.copy.failed": "Link zur verfolgten Route konnte nicht kopiert werden", + "viewer.route.position": "Routenposition {index} von {total}: {label}", + "viewer.route.step": "Schritt {index} von {total} · {phase} · {label}", + "viewer.route.motionRequired": "Automatischer Verlauf erfordert Live-Bewegung", + "viewer.route.trigger.clear": "Verfolgte Route löschen", + "viewer.route.overview.status": "{nodes} · {hops} · kürzeste verfasste Route", + "viewer.route.overview.node.one": "{count} Knoten", + "viewer.route.overview.node.other": "{count} Knoten", + "viewer.route.overview.hop.one": "{count} gerichteter Hop", + "viewer.route.overview.hop.other": "{count} gerichtete Hops", + "viewer.route.phase.playing": "Läuft", + "viewer.route.phase.complete": "Abgeschlossen", + "viewer.route.phase.inspecting": "Wird geprüft", + "viewer.route.destination.count.one": "{count} gerichtetes Ziel verfügbar. Einen hervorgehobenen Knoten wählen.", + "viewer.route.destination.count.other": "{count} gerichtete Ziele verfügbar. Einen hervorgehobenen Knoten wählen.", + "viewer.route.noOutgoing": "Hier startet keine ausgehende Route. Löschen und eine andere Quelle wählen.", + "viewer.route.result.title": "{source} nach {target}", + "viewer.route.finder.source.title": "Routenstart wählen", + "viewer.route.finder.source.placeholder": "Routenquellen suchen", + "viewer.route.finder.source.empty": "Keine passenden Routenquellen", + "viewer.route.finder.source.results": "Knoten, die eine Route starten können", + "viewer.route.finder.source.noun": "Routenquellen", + "viewer.route.finder.source.badge": "Start", + "viewer.route.finder.target.title": "Ziel ausgehend von {label}", + "viewer.route.finder.target.placeholder": "Erreichbare Ziele suchen", + "viewer.route.finder.target.empty": "Keine passenden erreichbaren Ziele", + "viewer.route.finder.target.results": "Erreichbare Routenziele", + "viewer.route.finder.target.noun": "erreichbare Ziele", + "viewer.route.hop.one": "{count} Hop", + "viewer.route.hop.other": "{count} Hops", + "viewer.lens.eyebrow": "Semantische Linse", + "viewer.lens.title": "Systemrollen vergleichen", + "viewer.lens.close": "Semantische Linse schließen", + "viewer.lens.instruction": "Bis zu zwei semantische Arten wählen. Eine zeigt ihren echten Verkehr; zwei vergleichen nur direkte verfasste Beziehungen.", + "viewer.lens.kinds": "Semantische Arten", + "viewer.lens.choose": "Eine Art wählen, um ihre Knoten und berührenden Beziehungen zu prüfen.", + "viewer.lens.copy": "Link zur semantischen Linse kopieren", + "viewer.lens.clear": "Semantische Linse löschen", + "viewer.lens.open": "Semantische Linse öffnen", + "viewer.lens.openActive": "Aktive semantische Linse öffnen", + "viewer.lens.legend": "Semantische Legende", + "viewer.lens.legend.inspect.one": "{label} prüfen, {count} Knoten", + "viewer.lens.legend.inspect.other": "{label} prüfen, {count} Knoten", + "viewer.lens.kind.count.one": "{label}, {count} Knoten", + "viewer.lens.kind.count.other": "{label}, {count} Knoten", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direkte Beziehung", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direkte Beziehungen", + "viewer.lens.single": "{nodes} · {relationships} · verbundene Nachbarn bleiben sichtbar", + "viewer.lens.node.one": "{count} {label}-Knoten", + "viewer.lens.node.other": "{count} {label}-Knoten", + "viewer.lens.relationship.one": "{count} berührende Beziehung", + "viewer.lens.relationship.other": "{count} berührende Beziehungen", + "viewer.radar.title": "Semantisches Radar", + "viewer.radar.building": "Übersicht wird erstellt", + "viewer.radar.openFull": "Vollständiges semantisches Radar öffnen", + "viewer.radar.open": "Radar öffnen", + "viewer.radar.close": "Semantisches Radar schließen", + "viewer.radar.surface": "Diagrammübersicht. Auf einen Knoten klicken, um ihn zu fokussieren, oder mit den Pfeiltasten schwenken.", + "viewer.radar.click": "Knoten anklicken", + "viewer.radar.drag": "Ziehen zum Schwenken", + "viewer.radar.space": "Semantisches Radar benötigt mehr MAP-Platz.", + "viewer.radar.nodes": "Semantische Diagramm-Radarknoten", + "viewer.radar.focus": "{label} vom semantischen Radar fokussieren", + "viewer.radar.status": "{count} Knoten · {viewport}", + "viewer.radar.fullMap": "{count} Knoten · vollständige Karte", + "viewer.radar.compacted": "Radar verkleinert, um den semantischen Pass oder die MAP-Steuerung nicht zu verdecken.", + "viewer.radar.cancelWaiting": "Wartendes semantisches Radar auf mehr MAP-Platz abbrechen", + "viewer.radar.needsSpace": "Semantisches Radar benötigt mehr sichtbaren MAP-Platz", + "viewer.radar.viewport.full": "vollständige Karte", + "viewer.radar.viewport.width": "{percent}% Breite", + "viewer.radar.viewport.scale": "{percent}% Ansicht", + "viewer.nav.controls": "Diagrammansicht-Steuerung", + "viewer.nav.route": "Eine gerichtete Route verfolgen", + "viewer.nav.route.title": "Route verfolgen (R)", + "viewer.nav.route.short": "PFAD", + "viewer.nav.radar": "Semantisches Radar öffnen", + "viewer.nav.radar.title": "Semantisches Radar (M)", + "viewer.nav.radar.short": "KARTE", + "viewer.nav.lens": "Semantische Linse öffnen", + "viewer.nav.lens.title": "Semantische Linse (L)", + "viewer.nav.lens.short": "LINSE", + "viewer.nav.find": "Knoten finden", + "viewer.nav.find.title": "Knoten finden (/)", + "viewer.nav.guide": "Diagrammleitfaden öffnen", + "viewer.nav.guide.title": "Diagrammleitfaden (?)", + "viewer.nav.zoomOut": "Verkleinern", + "viewer.nav.zoomOut.title": "Verkleinern (-)", + "viewer.nav.reset": "Diagrammansicht zurücksetzen", + "viewer.nav.reset.title": "Ansicht zurücksetzen (0)", + "viewer.nav.read": "LESEN", + "viewer.nav.zoomIn": "Vergrößern", + "viewer.nav.zoomIn.title": "Vergrößern (+)", + "viewer.nav.camera": "{hint}. Diagrammansicht zurücksetzen", + "viewer.nav.camera.title": "{semantic}{hint} · Ansicht zurücksetzen (0)", + "viewer.nav.camera.semantic": "Semantische Kamera aktiv · ", + "viewer.nav.level.map": "KARTE", + "viewer.nav.level.read": "LESEN", + "viewer.nav.level.full": "VOLL", + "viewer.nav.level.auto": "AUTO", + "viewer.nav.detail.map": "Vergrößern, um Beziehungslabels und Knotenkontext anzuzeigen", + "viewer.nav.detail.read": "Erneut vergrößern, um Tags und Anmerkungen anzuzeigen", + "viewer.nav.detail.full": "Vollständige Diagrammdetails", + "viewer.intent.summary": "{label}. {out} ausgehend, {in} eingehend{loops}. {total} Verbindungen. Enter für Details drücken.", + "viewer.intent.loops": ", {count} Selbstschleife", + "viewer.common.copied": "Kopiert", + "viewer.common.copyFailed": "Kopieren fehlgeschlagen", + "viewer.common.copyLink": "Link kopieren", + "viewer.common.clear": "Löschen", + "viewer.common.close": "Schließen" +} diff --git a/vendor/archify/examples/locales/es.json b/vendor/archify/examples/locales/es.json new file mode 100644 index 0000000..4f20b09 --- /dev/null +++ b/vendor/archify/examples/locales/es.json @@ -0,0 +1,440 @@ +{ + "page.title": "{title} · Diagrama", + "diagram.description.architecture": "Un diagrama de arquitectura generado por Archify.", + "diagram.description.workflow": "Un diagrama de flujo de trabajo generado por Archify.", + "diagram.description.sequence": "Un diagrama de secuencia generado por Archify.", + "diagram.description.dataflow": "Un diagrama de flujo de datos generado por Archify.", + "diagram.description.lifecycle": "Un diagrama de ciclo de vida generado por Archify.", + "node.focus": "Enfocar {label}", + "node.focus.detail": "Enfocar {label}, {detail}", + "node.context.architecture": "Componente de arquitectura", + "node.context.workflow": "Nodo de flujo de trabajo", + "node.context.sequence": "Participante de secuencia", + "node.context.dataflow": "Nodo de flujo de datos", + "node.context.lifecycle": "Estado del ciclo de vida", + "legend.title": "Leyenda", + "legend.architecture.frontend": "Frontend", + "legend.architecture.backend": "Backend", + "legend.architecture.database": "Base de datos", + "legend.architecture.cloud": "Nube", + "legend.architecture.security": "Seguridad", + "legend.architecture.messagebus": "Bus de mensajes", + "legend.architecture.external": "Externo", + "legend.workflow.frontend": "Interfaz de usuario", + "legend.workflow.backend": "Lógica del agente", + "legend.workflow.security": "Política", + "legend.workflow.messagebus": "Acción de herramienta", + "legend.workflow.database": "Contexto / traza", + "legend.workflow.cloud": "Servicio en la nube", + "legend.workflow.external": "Sistema externo", + "legend.sequence.emphasis": "solicitud principal", + "legend.sequence.return": "respuesta", + "legend.sequence.security": "seguridad", + "legend.sequence.dashed": "traza asíncrona", + "legend.sequence.default": "mensaje", + "legend.dataflow.emphasis": "datos primarios", + "legend.dataflow.security": "política / PII", + "legend.dataflow.dashed": "lote asíncrono", + "legend.dataflow.database": "almacén de datos", + "legend.dataflow.default": "flujo de datos", + "legend.lifecycle.start": "estado inicial", + "legend.lifecycle.active": "activo", + "legend.lifecycle.waiting": "en espera", + "legend.lifecycle.decision": "decisión", + "legend.lifecycle.success": "éxito", + "legend.lifecycle.failure": "fallo / salida", + "legend.lifecycle.neutral": "neutral", + "legend.lifecycle.external": "externo", + "legend.lifecycle.final": "estado final", + "viewer.kind.frontend": "Frontend", + "viewer.kind.backend": "Backend", + "viewer.kind.database": "Base de datos", + "viewer.kind.cloud": "Nube", + "viewer.kind.security": "Seguridad", + "viewer.kind.messagebus": "Bus de mensajes", + "viewer.kind.external": "Externo", + "viewer.kind.neutral": "Neutral", + "viewer.kind.node": "Nodo", + "viewer.kind.start": "Inicio", + "viewer.kind.active": "Activo", + "viewer.kind.waiting": "En espera", + "viewer.kind.decision": "Decisión", + "viewer.kind.success": "Éxito", + "viewer.kind.failure": "Fallo", + "viewer.toolbar.actions": "Acciones del diagrama", + "viewer.theme.toggle.title": "Cambiar tema (T)", + "viewer.theme.toggle": "Cambiar tema de color", + "viewer.theme.dark": "Oscuro", + "viewer.theme.light": "Claro", + "viewer.preset.choose.title": "Elegir estilo visual (S alterna)", + "viewer.preset.choose": "Elegir estilo visual", + "viewer.preset.style": "Estilo", + "viewer.preset.menu": "Estilo visual", + "viewer.preset.identity": "Identidad visual", + "viewer.preset.cycles": "alterna", + "viewer.preset.classic": "Clásico", + "viewer.preset.classic.short": "Clásico", + "viewer.preset.classic.hint": "Predeterminado técnico estable", + "viewer.preset.flow": "Flujo de señal", + "viewer.preset.flow.short": "Flujo", + "viewer.preset.flow.hint": "Presentación con énfasis en el movimiento", + "viewer.preset.blueprint": "Plano", + "viewer.preset.blueprint.hint": "Revisión de ingeniería", + "viewer.preset.editorial": "Editorial", + "viewer.preset.editorial.hint": "Notas de publicación y lanzamiento", + "viewer.preset.badge.signalFlow": "FLUJO DE SEÑAL", + "viewer.preset.badge.blueprint": "PLANO / REV 01", + "viewer.preset.badge.editorial": "EDITORIAL / NOTA DE CAMPO", + "viewer.preset.badge.editorialPlate": "ARCHIFY / LÁMINA 04", + "viewer.preset.current": "Estilo visual: {style}. Elegir estilo visual", + "viewer.motion.live": "Activa", + "viewer.motion.still": "Fija", + "viewer.motion.pause": "Pausar animación", + "viewer.motion.resume": "Reanudar animación", + "viewer.motion.reduced": "Animación pausada por la preferencia de movimiento reducido", + "viewer.motion.hidden": "Animación pausada mientras esta página está oculta", + "viewer.motion.yielding": "Pausar animación; cediendo a {owner}", + "viewer.motion.yielding.title": "Vista previa en vivo activada · cediendo a {owner}", + "viewer.owner.route": "Sonda de ruta", + "viewer.owner.lens": "Lente semántico", + "viewer.owner.relationship": "Vista previa de relación", + "viewer.owner.intent": "Traza de intención", + "viewer.owner.focus": "enfoque semántico", + "viewer.owner.legend": "vista previa de la leyenda", + "viewer.owner.reader": "interacción del lector", + "viewer.present.enter": "Entrar al modo presentación", + "viewer.present.enter.title": "Modo presentación (F)", + "viewer.present.exit": "Salir del modo presentación", + "viewer.present.exit.title": "Salir del modo presentación (F o Escape)", + "viewer.present.present": "Presentar", + "viewer.present.exit.label": "Salir", + "viewer.export.button": "Exportar", + "viewer.export.button.title": "Exportar diagrama (E)", + "viewer.export.diagram": "Exportar diagrama", + "viewer.export.menu": "Exportar", + "viewer.export.subtitle": "Salidas portables y limpias", + "viewer.export.share": "Compartir", + "viewer.export.shareCard": "Tarjeta para compartir", + "viewer.export.routeShareCard": "Tarjeta de ruta para compartir", + "viewer.export.reachShareCard": "Tarjeta de alcance para compartir", + "viewer.export.copyDiagram": "Copiar diagrama", + "viewer.export.clipboardPng": "PNG al portapapeles", + "viewer.export.raster": "Imágenes rasterizadas", + "viewer.export.image": "Imagen", + "viewer.export.lossless": "Imagen sin pérdida", + "viewer.export.compact": "Imagen compacta", + "viewer.export.modern": "Imagen moderna", + "viewer.export.vectorMotion": "Vector y movimiento", + "viewer.export.vectorMotion.heading": "Vector y movimiento", + "viewer.export.editable": "Vector editable", + "viewer.export.svg.auto": "SVG · Auto", + "viewer.export.svg.auto.hint": "Coincide con el tema del host", + "viewer.export.svg.light": "SVG · Claro", + "viewer.export.svg.light.hint": "Siempre claro", + "viewer.export.svg.dark": "SVG · Oscuro", + "viewer.export.svg.dark.hint": "Siempre oscuro", + "viewer.export.motion6s": "Movimiento de 6 s", + "viewer.export.unsupported": "No compatible con este navegador", + "viewer.export.clipboardUnsupported": "Este navegador no admite escribir imágenes en el portapapeles", + "viewer.export.clipboardUnsupported.short": "Escritura de imágenes en el portapapeles no admitida en este navegador.", + "viewer.export.motionUnavailable": "Captura de movimiento no disponible en este navegador", + "viewer.export.webmUnavailable": "WebM no disponible en este navegador", + "viewer.export.failed": "Falló la exportación: {message}", + "viewer.export.unknownVariant": "Variante de tarjeta para compartir desconocida: {variant}", + "viewer.export.routeRequired": "Traza una ruta antes de exportar una tarjeta de ruta para compartir", + "viewer.export.reachRequired": "Traza el alcance definido antes de exportar una tarjeta de alcance para compartir", + "viewer.export.unknown": "desconocido", + "viewer.export.routeFailed": "Falló la exportación de la tarjeta de ruta para compartir: {message}", + "viewer.export.reachFailed": "Falló la exportación de la tarjeta de alcance para compartir: {message}", + "viewer.export.copyFailed": "Error al copiar: {message}", + "viewer.export.copiedPng": "PNG copiado al portapapeles", + "viewer.export.downloadedRoute": "Tarjeta de ruta para compartir descargada", + "viewer.export.downloadedReach": "Tarjeta de alcance para compartir descargada", + "viewer.export.downloadedWebm": "WebM descargado", + "viewer.export.recording": "Grabando 6 segundos de movimiento…", + "viewer.export.card.routeSummary.one": "Ruta: {source} → {target} · {count} paso", + "viewer.export.card.routeSummary.other": "Ruta: {source} → {target} · {count} pasos", + "viewer.export.card.reachSummary": "Alcance definido {direction} desde {origin} · {nodes} · {links} · máx {hops}", + "viewer.export.card.node.one": "{count} nodo", + "viewer.export.card.node.other": "{count} nodos", + "viewer.export.card.link.one": "{count} enlace", + "viewer.export.card.link.other": "{count} enlaces", + "viewer.export.card.hop.one": "{count} salto", + "viewer.export.card.hop.other": "{count} saltos", + "viewer.export.card.routeBadge": "ARCHIFY · RUTA · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · ALCANCE {direction}", + "viewer.export.direction.upstream": "Ascendente", + "viewer.export.direction.downstream": "Descendente", + "viewer.export.error.canvasUnavailable": "Lienzo no disponible para {label}", + "viewer.export.error.contextUnavailable": "Contexto de lienzo 2D no disponible para {label}", + "viewer.export.error.toBlobUnavailable": "canvas.toBlob no disponible para {label}", + "viewer.export.error.toBlobNull": "canvas.toBlob no devolvió datos para {label}", + "viewer.export.error.variantsCombined": "Las variantes de tarjeta para compartir no se pueden combinar", + "viewer.export.error.viewerState": "La exportación de la tarjeta para compartir no pudo quitar el estado temporal del visor", + "viewer.export.error.routeState": "La exportación de la tarjeta de ruta no pudo preservar la ruta resuelta de forma segura", + "viewer.export.error.reachState": "La exportación de la tarjeta de alcance no pudo preservar el alcance definido de forma segura", + "viewer.export.error.webmRequirements": "La exportación de movimiento WebM requiere una animación de traza y compatibilidad del navegador con MediaRecorder", + "viewer.export.error.mediaRecorder": "MediaRecorder falló", + "viewer.export.error.emptyWebm": "MediaRecorder produjo un WebM vacío", + "viewer.export.error.webmBackground": "No se pudo cargar el fondo SVG para la exportación WebM", + "viewer.guide.title": "Explorar este sistema", + "viewer.focus.selectedNodes": "{count} nodos seleccionados", + "viewer.guide.eyebrow": "Guía del diagrama", + "viewer.guide.close": "Cerrar la guía del diagrama", + "viewer.guide.inspecting": "Inspeccionando la semántica compilada", + "viewer.guide.actions": "Acciones de exploración del diagrama", + "viewer.guide.find": "Buscar cualquier nodo", + "viewer.guide.find.hint": "Busca por etiquetas, responsabilidades, tipos e IDs estables.", + "viewer.guide.route": "Trazar una ruta", + "viewer.guide.route.aria": "Trazar una ruta dirigida", + "viewer.guide.route.hint": "Pregunta cómo se conectan dos nodos semánticos en el sentido definido.", + "viewer.guide.map": "Ver todo el sistema", + "viewer.guide.map.hint": "Abre el Radar semántico con un viewport en vivo y nodos estables.", + "viewer.guide.lens": "Comparar tipos semánticos", + "viewer.guide.lens.hint": "Cuenta roles, revela su tráfico y compara enlaces directos definidos.", + "viewer.guide.present": "Entrar al modo presentación", + "viewer.guide.present.hint": "Dale al diagrama en vivo todo el viewport sin cambiar la exportación.", + "viewer.guide.shortcuts": "Atajos de teclado adicionales", + "viewer.guide.shortcut.export": "Exportar", + "viewer.guide.shortcut.theme": "Tema", + "viewer.guide.shortcut.style": "Estilo", + "viewer.guide.shortcut.reset": "Restablecer", + "viewer.guide.shortcut.zoomIn": "Acercar", + "viewer.guide.shortcut.zoomOut": "Alejar", + "viewer.guide.shortcut.close": "Cerrar", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} nodo semántico", + "viewer.guide.fact.node.other": "{count} nodos semánticos", + "viewer.guide.fact.relationship.one": "{count} relación", + "viewer.guide.fact.relationship.other": "{count} relaciones", + "viewer.guide.open": "Abrir la guía del diagrama", + "viewer.finder.title": "Buscar un nodo", + "viewer.finder.close": "Cerrar el buscador de nodos", + "viewer.finder.placeholder": "Buscar etiquetas o IDs", + "viewer.finder.search": "Buscar nodos del diagrama", + "viewer.finder.results": "Nodos del diagrama", + "viewer.finder.empty": "No hay nodos coincidentes", + "viewer.finder.result.focus": "Enfocar {label}", + "viewer.finder.result.routeStart": "Elegir {label} como inicio de la ruta", + "viewer.finder.result.routeTarget": "Elegir {label} como destino de la ruta, {links}", + "viewer.finder.status.empty": "No hay nodos coincidentes", + "viewer.finder.status.count.one": "{count} nodo coincidente", + "viewer.finder.status.count.other": "{count} nodos coincidentes", + "viewer.finder.noun.nodes": "nodos", + "viewer.finder.link.one": "{count} enlace", + "viewer.finder.link.other": "{count} enlaces", + "viewer.finder.result.focus.one": "Enfocar {label}, {count} conexión relacionada", + "viewer.finder.result.focus.other": "Enfocar {label}, {count} conexiones relacionadas", + "viewer.finder.status.filtered": "{visible} de {available} {noun}", + "viewer.finder.status.all": "{available} {noun}", + "viewer.passport.eyebrow": "Pasaporte semántico", + "viewer.passport.metadata": "Metadatos del nodo", + "viewer.passport.evidence": "Evidencia de fuente verificada", + "viewer.passport.verified": "Fuente verificada", + "viewer.passport.verificationScope": "Verificado contra el Git local en la revisión fijada. No se ha comprobado el acceso remoto.", + "viewer.passport.reach": "Alcance definido", + "viewer.passport.reach.trace": "Trazar la alcanzabilidad definida", + "viewer.passport.upstream": "Ascendente", + "viewer.passport.downstream": "Descendente", + "viewer.passport.upstream.trace": "Trazar la alcanzabilidad definida ascendente", + "viewer.passport.downstream.trace": "Trazar la alcanzabilidad definida descendente", + "viewer.passport.close": "Cerrar el pasaporte semántico", + "viewer.passport.move": "Mover el pasaporte semántico. Arrastra, usa las flechas o pulsa Inicio para restablecer.", + "viewer.passport.copy": "Copiar enlace", + "viewer.passport.copy.focus": "Copiar enlace al nodo enfocado", + "viewer.passport.relations": "Relaciones", + "viewer.passport.relations.show": "Mostrar relaciones conectadas", + "viewer.passport.relations.hide": "Ocultar relaciones conectadas", + "viewer.passport.relations.list": "Relaciones conectadas", + "viewer.passport.copyRelation": "Copiar relación", + "viewer.passport.copyNode": "Copiar nodo", + "viewer.passport.copyPinned": "Copiar enlace a la relación fijada", + "viewer.passport.copySource": "Copiar enlace al nodo de origen", + "viewer.passport.copy.focused.success": "Enlace del nodo enfocado copiado", + "viewer.passport.copy.pinned.success": "Enlace de la relación fijada copiado", + "viewer.passport.copy.focused.failed": "No se pudo copiar el enlace del nodo enfocado", + "viewer.passport.copy.pinned.failed": "No se pudo copiar el enlace de la relación fijada", + "viewer.passport.relationship.none": "No hay relaciones conectadas", + "viewer.passport.relationship.count.one": "{count} relación", + "viewer.passport.relationship.count.other": "{count} relaciones", + "viewer.passport.relationship.show.one": "Mostrar {count} relación conectada", + "viewer.passport.relationship.show.other": "Mostrar {count} relaciones conectadas", + "viewer.passport.relationship.summary": "{out} salientes · {in} entrantes{loops}", + "viewer.passport.relationship.loops": " · {count} bucle", + "viewer.passport.relationship.explorer": "Explorador de relaciones directas", + "viewer.passport.relationship.help": "Usa las flechas para explorar las relaciones. Pulsa Enter o Espacio para fijar detalles; Escape las borra.", + "viewer.passport.relationship.loopsBack": "vuelve sobre sí mismo", + "viewer.passport.relationship.connectsTo": "conecta a", + "viewer.passport.relationship.connectsFrom": "conecta desde", + "viewer.passport.relationship.pinned": "Relación fijada · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "Inspeccionar relación {index} de {total}: {from} a {to}, {label}. Pulsa Enter para ver detalles.", + "viewer.passport.relationship.group.out": "Salientes", + "viewer.passport.relationship.group.in": "Entrantes", + "viewer.passport.relationship.group.loop": "Bucles propios", + "viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "SAL →", + "viewer.passport.relationship.direction.in": "← ENT", + "viewer.passport.relationship.direction.loop": "BUCLE", + "viewer.passport.sourceCount.one": "{count} referencia de fuente verificada", + "viewer.passport.sourceCount.other": "{count} referencias de fuente verificadas", + "viewer.passport.sourceMarker": "SRC", + "viewer.passport.beacon.one": "{count} fuente verificada; enfoca este nodo para inspeccionar", + "viewer.passport.beacon.other": "{count} fuentes verificadas; enfoca este nodo para inspeccionar", + "viewer.passport.repository.open": "Abrir la revisión verificada del repositorio {revision}", + "viewer.passport.source.open": "Abrir la fuente verificada {path} en la revisión {revision}", + "viewer.passport.source.openLink": "Abrir ↗", + "viewer.passport.reach.upstream.one": "Trazar {count} nodo definido ascendente", + "viewer.passport.reach.upstream.other": "Trazar {count} nodos definidos ascendentes", + "viewer.passport.reach.downstream.one": "Trazar {count} nodo definido descendente", + "viewer.passport.reach.downstream.other": "Trazar {count} nodos definidos descendentes", + "viewer.passport.reach.noUpstream": "No hay nodos definidos ascendentes", + "viewer.passport.reach.noDownstream": "No hay nodos definidos descendentes", + "viewer.passport.reach.status": "{direction} · {nodes} nodos · {links} enlaces · máx {hops} saltos", + "viewer.route.eyebrow": "Ruta", + "viewer.route.start": "Haz clic donde empieza la ruta", + "viewer.route.start.find": "Buscar inicio", + "viewer.route.start.find.aria": "Buscar un inicio de ruta", + "viewer.route.copy": "Copiar enlace", + "viewer.route.copy.aria": "Copiar enlace a la ruta trazada", + "viewer.route.clear": "Borrar", + "viewer.route.clear.aria": "Borrar la sonda de ruta", + "viewer.route.traced": "Ruta", + "viewer.route.pickTwo": "Haz clic en dos nodos del diagrama", + "viewer.route.pickOne": "Haz clic en un nodo del diagrama", + "viewer.route.controls": "Controles del recorrido de la ruta", + "viewer.route.previous": "Posición anterior de la ruta", + "viewer.route.play": "Reproducir el recorrido de la ruta", + "viewer.route.pause": "Pausar el recorrido de la ruta", + "viewer.route.replay": "Repetir el recorrido de la ruta", + "viewer.route.next": "Posición siguiente de la ruta", + "viewer.route.journey": "Recorrido", + "viewer.route.pause.label": "Pausar", + "viewer.route.replay.label": "Repetir", + "viewer.route.overview": "Vista general", + "viewer.route.overview.aria": "Mostrar la vista general completa de la ruta", + "viewer.route.instructions": "Haz clic en un inicio y luego en un final. La ruta sigue las flechas.", + "viewer.route.destination": "¿Dónde termina la ruta desde {label}?", + "viewer.route.destination.find": "Buscar destino", + "viewer.route.destination.find.aria": "Buscar un destino de ruta alcanzable", + "viewer.route.differentDestination": "Elige un destino diferente", + "viewer.route.distinct": "Elige dos nodos distintos.", + "viewer.route.unreachable": "No se llega a {label} desde aquí", + "viewer.route.unreachable.detail": "No se llega a {target} desde {source} siguiendo las flechas. Elige un nodo resaltado.", + "viewer.route.start.instructions": "Luego elige el final. Solo se iluminan los nodos alcanzables.", + "viewer.route.copy.success": "Enlace de la ruta trazada copiado", + "viewer.route.copy.failed": "No se pudo copiar el enlace de la ruta trazada", + "viewer.route.position": "Posición de la ruta {index} de {total}: {label}", + "viewer.route.step": "Paso {index} de {total} · {phase} · {label}", + "viewer.route.motionRequired": "El recorrido automático requiere la animación Activa", + "viewer.route.trigger.clear": "Borrar la ruta trazada", + "viewer.route.overview.status": "{nodes} · {hops} · ruta más corta", + "viewer.route.overview.node.one": "{count} nodo", + "viewer.route.overview.node.other": "{count} nodos", + "viewer.route.overview.hop.one": "{count} paso", + "viewer.route.overview.hop.other": "{count} pasos", + "viewer.route.phase.playing": "Reproduciendo", + "viewer.route.phase.complete": "Completo", + "viewer.route.phase.inspecting": "Inspeccionando", + "viewer.route.destination.count.one": "{count} nodo alcanzable está resaltado. Haz clic en él.", + "viewer.route.destination.count.other": "{count} nodos alcanzables están resaltados. Haz clic en uno.", + "viewer.route.noOutgoing": "Ninguna flecha sale de este nodo. Bórralo y elige otro inicio.", + "viewer.route.result.title": "{source} a {target}", + "viewer.route.finder.source.title": "Elegir el inicio de la ruta", + "viewer.route.finder.source.placeholder": "Buscar orígenes de ruta", + "viewer.route.finder.source.empty": "No hay orígenes de ruta coincidentes", + "viewer.route.finder.source.results": "Nodos que pueden iniciar una ruta", + "viewer.route.finder.source.noun": "orígenes de ruta", + "viewer.route.finder.source.badge": "inicio", + "viewer.route.finder.target.title": "Destino desde {label}", + "viewer.route.finder.target.placeholder": "Buscar destinos alcanzables", + "viewer.route.finder.target.empty": "No hay destinos alcanzables coincidentes", + "viewer.route.finder.target.results": "Destinos de ruta alcanzables", + "viewer.route.finder.target.noun": "destinos alcanzables", + "viewer.route.hop.one": "{count} salto", + "viewer.route.hop.other": "{count} saltos", + "viewer.lens.eyebrow": "Lente semántico", + "viewer.lens.title": "Comparar roles del sistema", + "viewer.lens.close": "Cerrar el lente semántico", + "viewer.lens.instruction": "Elige hasta dos tipos semánticos. Uno revela su tráfico real; dos comparan solo las relaciones directas definidas.", + "viewer.lens.kinds": "Tipos semánticos", + "viewer.lens.choose": "Elige un tipo para inspeccionar sus nodos y las relaciones que lo tocan.", + "viewer.lens.copy": "Copiar enlace al lente semántico", + "viewer.lens.clear": "Borrar el lente semántico", + "viewer.lens.open": "Abrir el lente semántico", + "viewer.lens.openActive": "Abrir el lente semántico activo", + "viewer.lens.legend": "Leyenda semántica", + "viewer.lens.legend.inspect.one": "Inspeccionar {label}, {count} nodo", + "viewer.lens.legend.inspect.other": "Inspeccionar {label}, {count} nodos", + "viewer.lens.kind.count.one": "{label}, {count} nodo", + "viewer.lens.kind.count.other": "{label}, {count} nodos", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} relación directa", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} relaciones directas", + "viewer.lens.single": "{nodes} · {relationships} · los pares conectados permanecen visibles", + "viewer.lens.node.one": "{count} nodo {label}", + "viewer.lens.node.other": "{count} nodos {label}", + "viewer.lens.relationship.one": "{count} relación en contacto", + "viewer.lens.relationship.other": "{count} relaciones en contacto", + "viewer.radar.title": "Radar semántico", + "viewer.radar.building": "Construyendo la vista general", + "viewer.radar.openFull": "Abrir el radar semántico completo", + "viewer.radar.open": "Abrir el radar", + "viewer.radar.close": "Cerrar el radar semántico", + "viewer.radar.surface": "Vista general del diagrama. Haz clic en un nodo para enfocarlo, o usa las flechas para desplazarte.", + "viewer.radar.click": "Clic en el nodo", + "viewer.radar.drag": "Arrastra para desplazar", + "viewer.radar.space": "El radar semántico necesita más espacio de MAPA.", + "viewer.radar.nodes": "Nodos del radar del diagrama semántico", + "viewer.radar.focus": "Enfocar {label} desde el Radar semántico", + "viewer.radar.status": "{count} nodos · {viewport}", + "viewer.radar.fullMap": "{count} nodos · mapa completo", + "viewer.radar.compacted": "Radar compactado para no tapar el Pasaporte semántico ni los controles de MAPA.", + "viewer.radar.cancelWaiting": "Cancelar la espera del radar semántico por más espacio de MAPA", + "viewer.radar.needsSpace": "El radar semántico necesita más espacio de MAPA visible", + "viewer.radar.viewport.full": "mapa completo", + "viewer.radar.viewport.width": "{percent}% de ancho", + "viewer.radar.viewport.scale": "{percent}% del viewport", + "viewer.nav.controls": "Controles de vista del diagrama", + "viewer.nav.route": "Trazar una ruta dirigida", + "viewer.nav.route.title": "Trazar ruta (R)", + "viewer.nav.route.short": "RUTA", + "viewer.nav.radar": "Abrir el radar semántico", + "viewer.nav.radar.title": "Radar semántico (M)", + "viewer.nav.radar.short": "MAPA", + "viewer.nav.lens": "Abrir el lente semántico", + "viewer.nav.lens.title": "Lente semántico (L)", + "viewer.nav.lens.short": "LENTE", + "viewer.nav.find": "Buscar un nodo", + "viewer.nav.find.title": "Buscar un nodo (/)", + "viewer.outline.title": "Índice de nodos", + "viewer.rail.show": "Notas e índice", + "viewer.rail.controls": "Panel lateral", + "viewer.rail.collapse": "Contraer panel lateral", + "viewer.rail.bottom": "Mover el panel debajo", + "viewer.rail.right": "Mover el panel al lado", + "viewer.nav.guide": "Abrir la guía del diagrama", + "viewer.nav.guide.title": "Guía del diagrama (?)", + "viewer.nav.zoomOut": "Alejar", + "viewer.nav.zoomOut.title": "Alejar (-)", + "viewer.nav.reset": "Restablecer la vista del diagrama", + "viewer.nav.reset.title": "Restablecer vista (0)", + "viewer.nav.read": "LEER", + "viewer.nav.zoomIn": "Acercar", + "viewer.nav.zoomIn.title": "Acercar (+)", + "viewer.nav.camera": "{hint}. Restablecer la vista del diagrama", + "viewer.nav.camera.title": "{semantic}{hint} · restablecer vista (0)", + "viewer.nav.camera.semantic": "Cámara semántica activa · ", + "viewer.nav.level.map": "MAPA", + "viewer.nav.level.read": "LEER", + "viewer.nav.level.full": "COMPLETO", + "viewer.nav.level.auto": "AUTO", + "viewer.nav.detail.map": "Acerca para revelar las etiquetas de relación y el contexto de los nodos", + "viewer.nav.detail.read": "Acerca de nuevo para revelar etiquetas y anotaciones", + "viewer.nav.detail.full": "Detalle completo del diagrama", + "viewer.intent.summary": "{label}. {out} salientes, {in} entrantes{loops}. {total} conexiones. Pulsa Enter para ver detalles.", + "viewer.intent.loops": ", {count} bucle propio", + "viewer.common.copied": "Copiado", + "viewer.common.copyFailed": "Error al copiar", + "viewer.common.copyLink": "Copiar enlace", + "viewer.common.clear": "Borrar", + "viewer.common.close": "Cerrar" +} diff --git a/vendor/archify/examples/locales/fr.json b/vendor/archify/examples/locales/fr.json new file mode 100644 index 0000000..d9b1e8d --- /dev/null +++ b/vendor/archify/examples/locales/fr.json @@ -0,0 +1,425 @@ +{ + "page.title": "Diagramme {title}", + "diagram.description.architecture": "Un diagramme d'architecture généré par Archify.", + "diagram.description.workflow": "Un diagramme de workflow généré par Archify.", + "diagram.description.sequence": "Un diagramme de séquence généré par Archify.", + "diagram.description.dataflow": "Un diagramme de flux de données généré par Archify.", + "diagram.description.lifecycle": "Un diagramme de cycle de vie généré par Archify.", + "node.focus": "Centrer sur {label}", + "node.focus.detail": "Centrer sur {label}, {detail}", + "node.context.architecture": "Composant d'architecture", + "node.context.workflow": "Nœud de workflow", + "node.context.sequence": "Participant de séquence", + "node.context.dataflow": "Nœud de flux de données", + "node.context.lifecycle": "État de cycle de vie", + "legend.title": "Légende", + "legend.architecture.frontend": "Frontend", + "legend.architecture.backend": "Backend", + "legend.architecture.database": "Base de données", + "legend.architecture.cloud": "Cloud", + "legend.architecture.security": "Sécurité", + "legend.architecture.messagebus": "Bus de messages", + "legend.architecture.external": "Externe", + "legend.workflow.frontend": "Interface utilisateur", + "legend.workflow.backend": "Logique de l'agent", + "legend.workflow.security": "Politique", + "legend.workflow.messagebus": "Action d'outil", + "legend.workflow.database": "Contexte / trace", + "legend.workflow.cloud": "Service cloud", + "legend.workflow.external": "Système externe", + "legend.sequence.emphasis": "requête principale", + "legend.sequence.return": "retour", + "legend.sequence.security": "sécurité", + "legend.sequence.dashed": "trace asynchrone", + "legend.sequence.default": "message", + "legend.dataflow.emphasis": "donnée principale", + "legend.dataflow.security": "politique / PII", + "legend.dataflow.dashed": "lot asynchrone", + "legend.dataflow.database": "entrepôt de données", + "legend.dataflow.default": "flux de données", + "legend.lifecycle.start": "état initial", + "legend.lifecycle.active": "actif", + "legend.lifecycle.waiting": "attente", + "legend.lifecycle.decision": "décision", + "legend.lifecycle.success": "succès", + "legend.lifecycle.failure": "échec / sortie", + "legend.lifecycle.neutral": "neutre", + "legend.lifecycle.external": "externe", + "legend.lifecycle.final": "état final", + "viewer.kind.frontend": "Frontend", + "viewer.kind.backend": "Backend", + "viewer.kind.database": "Base de données", + "viewer.kind.cloud": "Cloud", + "viewer.kind.security": "Sécurité", + "viewer.kind.messagebus": "Bus de messages", + "viewer.kind.external": "Externe", + "viewer.kind.neutral": "Neutre", + "viewer.kind.node": "Nœud", + "viewer.kind.start": "Début", + "viewer.kind.active": "Actif", + "viewer.kind.waiting": "En attente", + "viewer.kind.decision": "Décision", + "viewer.kind.success": "Succès", + "viewer.kind.failure": "Échec", + "viewer.toolbar.actions": "Actions du diagramme", + "viewer.theme.toggle.title": "Changer de thème (T)", + "viewer.theme.toggle": "Changer le thème de couleur", + "viewer.theme.dark": "Sombre", + "viewer.theme.light": "Clair", + "viewer.preset.choose.title": "Choisir le style visuel (S pour changer)", + "viewer.preset.choose": "Choisir le style visuel", + "viewer.preset.style": "Style", + "viewer.preset.menu": "Style visuel", + "viewer.preset.identity": "Identité visuelle", + "viewer.preset.cycles": "S pour changer", + "viewer.preset.classic": "Classic", + "viewer.preset.classic.short": "Classic", + "viewer.preset.classic.hint": "Style technique stable par défaut", + "viewer.preset.flow": "Flux de signal", + "viewer.preset.flow.short": "Flux", + "viewer.preset.flow.hint": "Présentation animée et dynamique", + "viewer.preset.blueprint": "Plan", + "viewer.preset.blueprint.hint": "Revue technique", + "viewer.preset.editorial": "Éditorial", + "viewer.preset.editorial.hint": "Notes de publication et de lancement", + "viewer.preset.badge.signalFlow": "FLUX DE SIGNAL", + "viewer.preset.badge.blueprint": "PLAN / RÉV. 01", + "viewer.preset.badge.editorial": "ÉDITORIAL / NOTE DE TERRAIN", + "viewer.preset.badge.editorialPlate": "ARCHIFY / PLANCHE 04", + "viewer.preset.current": "Style visuel : {style}. Choisir le style visuel", + "viewer.motion.live": "Animé", + "viewer.motion.still": "Statique", + "viewer.motion.pause": "Mettre l'animation en pause", + "viewer.motion.resume": "Reprendre l'animation", + "viewer.motion.reduced": "Animation suspendue en raison de la préférence de mouvement réduit", + "viewer.motion.hidden": "Animation suspendue pendant que la page est masquée", + "viewer.motion.yielding": "Mettre l'animation en pause ; cède actuellement la place à {owner}", + "viewer.motion.yielding.title": "Aperçu en direct activé · cède la place à {owner}", + "viewer.owner.route": "Sonde d'itinéraire", + "viewer.owner.lens": "Lentille sémantique", + "viewer.owner.relationship": "Aperçu de relation", + "viewer.owner.intent": "Trace d'intention", + "viewer.owner.focus": "le focus sémantique", + "viewer.owner.legend": "l'aperçu de la légende", + "viewer.owner.reader": "l'interaction du lecteur", + "viewer.present.enter": "Entrer en mode présentation", + "viewer.present.enter.title": "Mode présentation (F)", + "viewer.present.exit": "Quitter le mode présentation", + "viewer.present.exit.title": "Quitter le mode présentation (F ou Échap)", + "viewer.present.present": "Présenter", + "viewer.present.exit.label": "Quitter", + "viewer.export.button": "Exporter", + "viewer.export.button.title": "Exporter le diagramme (E)", + "viewer.export.diagram": "Exporter le diagramme", + "viewer.export.menu": "Exporter", + "viewer.export.subtitle": "Des exports portables et soignés", + "viewer.export.share": "Partager", + "viewer.export.shareCard": "Carte de partage", + "viewer.export.routeShareCard": "Carte de partage d'itinéraire", + "viewer.export.reachShareCard": "Carte de partage de portée", + "viewer.export.copyDiagram": "Copier le diagramme", + "viewer.export.clipboardPng": "PNG vers le presse-papiers", + "viewer.export.raster": "Images matricielles", + "viewer.export.image": "Image", + "viewer.export.lossless": "Image sans perte", + "viewer.export.compact": "Image compacte", + "viewer.export.modern": "Image au format moderne", + "viewer.export.vectorMotion": "Vecteur et animation", + "viewer.export.vectorMotion.heading": "Vecteur et animation", + "viewer.export.editable": "Vecteur modifiable", + "viewer.export.motion6s": "Animation de 6 s", + "viewer.export.unsupported": "Non pris en charge par ce navigateur", + "viewer.export.clipboardUnsupported": "L'écriture d'image dans le presse-papiers n'est pas prise en charge par ce navigateur", + "viewer.export.clipboardUnsupported.short": "L'écriture d'image dans le presse-papiers n'est pas prise en charge par ce navigateur.", + "viewer.export.motionUnavailable": "Capture d'animation indisponible dans ce navigateur", + "viewer.export.webmUnavailable": "WebM indisponible dans ce navigateur", + "viewer.export.failed": "Échec de l'export : {message}", + "viewer.export.unknownVariant": "Variante de carte de partage inconnue : {variant}", + "viewer.export.routeRequired": "Tracez un itinéraire avant d'exporter une carte de partage d'itinéraire", + "viewer.export.reachRequired": "Tracez une portée éditée avant d'exporter une carte de partage de portée", + "viewer.export.unknown": "inconnu", + "viewer.export.routeFailed": "Échec de l'export de la carte de partage d'itinéraire : {message}", + "viewer.export.reachFailed": "Échec de l'export de la carte de partage de portée : {message}", + "viewer.export.copyFailed": "Échec de la copie : {message}", + "viewer.export.copiedPng": "PNG copié dans le presse-papiers", + "viewer.export.downloadedRoute": "Carte de partage d'itinéraire téléchargée", + "viewer.export.downloadedReach": "Carte de partage de portée téléchargée", + "viewer.export.downloadedWebm": "WebM téléchargé", + "viewer.export.recording": "Enregistrement de 6 secondes d'animation…", + "viewer.export.card.routeSummary.one": "Itinéraire : {source} → {target} · {count} saut orienté", + "viewer.export.card.routeSummary.other": "Itinéraire : {source} → {target} · {count} sauts orientés", + "viewer.export.card.reachSummary": "Portée {direction} éditée depuis {origin} · {nodes} · {links} · max {hops}", + "viewer.export.card.node.one": "{count} nœud", + "viewer.export.card.node.other": "{count} nœuds", + "viewer.export.card.link.one": "{count} lien", + "viewer.export.card.link.other": "{count} liens", + "viewer.export.card.hop.one": "{count} saut", + "viewer.export.card.hop.other": "{count} sauts", + "viewer.export.card.routeBadge": "ARCHIFY · ITINÉRAIRE · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · PORTÉE {direction}", + "viewer.export.direction.upstream": "Amont", + "viewer.export.direction.downstream": "Aval", + "viewer.export.error.canvasUnavailable": "Canevas indisponible pour {label}", + "viewer.export.error.contextUnavailable": "Contexte de canevas 2D indisponible pour {label}", + "viewer.export.error.toBlobUnavailable": "canvas.toBlob indisponible pour {label}", + "viewer.export.error.toBlobNull": "canvas.toBlob n'a renvoyé aucune donnée pour {label}", + "viewer.export.error.variantsCombined": "Les variantes de carte de partage ne peuvent pas être combinées", + "viewer.export.error.viewerState": "L'export de la carte de partage n'a pas pu supprimer l'état temporaire du visualiseur", + "viewer.export.error.routeState": "L'export de la carte d'itinéraire n'a pas pu préserver l'itinéraire résolu en toute sécurité", + "viewer.export.error.reachState": "L'export de la carte de portée n'a pas pu préserver la portée éditée en toute sécurité", + "viewer.export.error.webmRequirements": "L'export d'animation WebM nécessite une animation de trace et la prise en charge de MediaRecorder par le navigateur", + "viewer.export.error.mediaRecorder": "Échec de MediaRecorder", + "viewer.export.error.emptyWebm": "MediaRecorder a produit un fichier WebM vide", + "viewer.export.error.webmBackground": "L'arrière-plan SVG n'a pas pu être chargé pour l'export WebM", + "viewer.guide.eyebrow": "Guide du diagramme", + "viewer.guide.close": "Fermer le guide du diagramme", + "viewer.guide.inspecting": "Inspection de la sémantique compilée", + "viewer.guide.actions": "Actions d'exploration du diagramme", + "viewer.guide.find": "Trouver un nœud", + "viewer.guide.find.hint": "Rechercher par libellés, responsabilités, types et identifiants stables.", + "viewer.guide.route": "Tracer un itinéraire", + "viewer.guide.route.aria": "Tracer un itinéraire orienté", + "viewer.guide.route.hint": "Découvrez comment deux nœuds sémantiques sont reliés dans le sens édité.", + "viewer.guide.map": "Voir tout le système", + "viewer.guide.map.hint": "Ouvrir le radar sémantique avec un viewport en direct et des nœuds stables.", + "viewer.guide.lens": "Comparer les types sémantiques", + "viewer.guide.lens.hint": "Comptez les rôles, révélez leur trafic et comparez les liens édités directs.", + "viewer.guide.present": "Entrer en mode présentation", + "viewer.guide.present.hint": "Donnez tout le viewport au diagramme en direct sans modifier l'export.", + "viewer.guide.shortcuts": "Raccourcis clavier supplémentaires", + "viewer.guide.shortcut.export": "Exporter", + "viewer.guide.shortcut.theme": "Thème", + "viewer.guide.shortcut.style": "Style", + "viewer.guide.shortcut.reset": "Réinitialiser", + "viewer.guide.shortcut.zoomIn": "Zoom avant", + "viewer.guide.shortcut.zoomOut": "Zoom arrière", + "viewer.guide.shortcut.close": "Fermer", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} nœud sémantique", + "viewer.guide.fact.node.other": "{count} nœuds sémantiques", + "viewer.guide.fact.relationship.one": "{count} relation", + "viewer.guide.fact.relationship.other": "{count} relations", + "viewer.guide.open": "Ouvrir le guide du diagramme", + "viewer.finder.title": "Trouver un nœud", + "viewer.finder.close": "Fermer le chercheur de nœuds", + "viewer.finder.placeholder": "Rechercher par libellé ou identifiant", + "viewer.finder.search": "Rechercher les nœuds du diagramme", + "viewer.finder.results": "Nœuds du diagramme", + "viewer.finder.empty": "Aucun nœud correspondant", + "viewer.finder.result.focus": "Centrer sur {label}", + "viewer.finder.result.routeStart": "Choisir {label} comme point de départ de l'itinéraire", + "viewer.finder.result.routeTarget": "Choisir {label} comme destination de l'itinéraire, {links}", + "viewer.finder.status.empty": "Aucun nœud correspondant", + "viewer.finder.status.count.one": "{count} nœud correspondant", + "viewer.finder.status.count.other": "{count} nœuds correspondants", + "viewer.finder.noun.nodes": "nœuds", + "viewer.finder.link.one": "{count} lien", + "viewer.finder.link.other": "{count} liens", + "viewer.finder.result.focus.one": "Centrer sur {label}, {count} connexion associée", + "viewer.finder.result.focus.other": "Centrer sur {label}, {count} connexions associées", + "viewer.finder.status.filtered": "{visible} sur {available} {noun}", + "viewer.finder.status.all": "{available} {noun}", + "viewer.passport.eyebrow": "Passeport sémantique", + "viewer.passport.metadata": "Métadonnées du nœud", + "viewer.passport.evidence": "Preuve de source vérifiée", + "viewer.passport.verified": "Source vérifiée", + "viewer.passport.verificationScope": "Vérifié par rapport au Git local à la révision épinglée. L'accès distant n'a pas été vérifié.", + "viewer.passport.reach": "Portée éditée", + "viewer.passport.reach.trace": "Tracer l'accessibilité éditée", + "viewer.passport.upstream": "Amont", + "viewer.passport.downstream": "Aval", + "viewer.passport.upstream.trace": "Tracer l'accessibilité éditée en amont", + "viewer.passport.downstream.trace": "Tracer l'accessibilité éditée en aval", + "viewer.passport.close": "Fermer le passeport sémantique", + "viewer.passport.copy": "Copier le lien", + "viewer.passport.copy.focus": "Copier le lien du nœud centré", + "viewer.passport.relations": "Relations", + "viewer.passport.relations.show": "Afficher les relations connectées", + "viewer.passport.relations.hide": "Masquer les relations connectées", + "viewer.passport.relations.list": "Relations connectées", + "viewer.passport.copyRelation": "Copier la relation", + "viewer.passport.copyNode": "Copier le nœud", + "viewer.passport.copyPinned": "Copier le lien de la relation épinglée", + "viewer.passport.copySource": "Copier le lien du nœud source", + "viewer.passport.copy.focused.success": "Lien du nœud centré copié", + "viewer.passport.copy.pinned.success": "Lien de la relation épinglée copié", + "viewer.passport.copy.focused.failed": "Impossible de copier le lien du nœud centré", + "viewer.passport.copy.pinned.failed": "Impossible de copier le lien de la relation épinglée", + "viewer.passport.relationship.none": "Aucune relation connectée", + "viewer.passport.relationship.count.one": "{count} relation", + "viewer.passport.relationship.count.other": "{count} relations", + "viewer.passport.relationship.show.one": "Afficher {count} relation connectée", + "viewer.passport.relationship.show.other": "Afficher {count} relations connectées", + "viewer.passport.relationship.summary": "{out} sortantes · {in} entrantes{loops}", + "viewer.passport.relationship.loops": " · {count} boucle", + "viewer.passport.relationship.explorer": "Explorateur de relations directes", + "viewer.passport.relationship.help": "Utilisez les flèches pour explorer les relations. Appuyez sur Entrée ou Espace pour épingler les détails ; Échap efface.", + "viewer.passport.relationship.loopsBack": "boucle vers", + "viewer.passport.relationship.connectsTo": "se connecte à", + "viewer.passport.relationship.connectsFrom": "se connecte depuis", + "viewer.passport.relationship.pinned": "Relation épinglée · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "Inspecter la relation {index} sur {total} : {from} vers {to}, {label}. Appuyez sur Entrée pour les détails.", + "viewer.passport.relationship.group.out": "Sortantes", + "viewer.passport.relationship.group.in": "Entrantes", + "viewer.passport.relationship.group.loop": "Boucles propres", + "viewer.passport.relationship.row": "{group} : {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "SORT →", + "viewer.passport.relationship.direction.in": "← ENTRE", + "viewer.passport.relationship.direction.loop": "BOUCLE", + "viewer.passport.sourceCount.one": "{count} référence de source vérifiée", + "viewer.passport.sourceCount.other": "{count} références de source vérifiées", + "viewer.passport.sourceMarker": "SOURCE", + "viewer.passport.beacon.one": "{count} source vérifiée ; centrez ce nœud pour l'inspecter", + "viewer.passport.beacon.other": "{count} sources vérifiées ; centrez ce nœud pour l'inspecter", + "viewer.passport.repository.open": "Ouvrir la révision {revision} du dépôt vérifié", + "viewer.passport.source.open": "Ouvrir la source vérifiée {path} à la révision {revision}", + "viewer.passport.source.openLink": "Ouvrir ↗", + "viewer.passport.reach.upstream.one": "Tracer {count} nœud édité en amont", + "viewer.passport.reach.upstream.other": "Tracer {count} nœuds édités en amont", + "viewer.passport.reach.downstream.one": "Tracer {count} nœud édité en aval", + "viewer.passport.reach.downstream.other": "Tracer {count} nœuds édités en aval", + "viewer.passport.reach.noUpstream": "Aucun nœud édité en amont", + "viewer.passport.reach.noDownstream": "Aucun nœud édité en aval", + "viewer.passport.reach.status": "{direction} · {nodes} nœuds · {links} liens · max {hops} sauts", + "viewer.route.eyebrow": "Sonde d'itinéraire", + "viewer.route.start": "Choisir un nœud de départ", + "viewer.route.start.find": "Trouver le départ", + "viewer.route.start.find.aria": "Trouver un départ d'itinéraire", + "viewer.route.copy": "Copier le lien", + "viewer.route.copy.aria": "Copier le lien de l'itinéraire tracé", + "viewer.route.clear": "Effacer", + "viewer.route.clear.aria": "Effacer la sonde d'itinéraire", + "viewer.route.traced": "Itinéraire tracé", + "viewer.route.pickTwo": "Choisissez deux nœuds sémantiques sur le diagramme", + "viewer.route.pickOne": "Choisissez un nœud sémantique sur le diagramme", + "viewer.route.controls": "Contrôles du parcours d'itinéraire", + "viewer.route.previous": "Position précédente de l'itinéraire", + "viewer.route.play": "Lire le parcours de l'itinéraire", + "viewer.route.pause": "Mettre le parcours de l'itinéraire en pause", + "viewer.route.replay": "Rejouer le parcours de l'itinéraire", + "viewer.route.next": "Position suivante de l'itinéraire", + "viewer.route.journey": "Parcours", + "viewer.route.pause.label": "Pause", + "viewer.route.replay.label": "Rejouer", + "viewer.route.overview": "Vue d'ensemble", + "viewer.route.overview.aria": "Afficher la vue d'ensemble complète de l'itinéraire", + "viewer.route.instructions": "Choisissez la source, puis la destination. Le sens compte.", + "viewer.route.destination": "Choisir une destination depuis {label}", + "viewer.route.destination.find": "Trouver une cible", + "viewer.route.destination.find.aria": "Trouver une destination d'itinéraire accessible", + "viewer.route.differentDestination": "Choisir une autre destination", + "viewer.route.distinct": "Un itinéraire nécessite deux nœuds sémantiques distincts.", + "viewer.route.unreachable": "Aucun itinéraire orienté vers {label}", + "viewer.route.unreachable.detail": "{target} n'est pas accessible depuis {source}. Choisissez une destination surlignée.", + "viewer.route.start.instructions": "Sélectionnez la source. L'étape suivante ne révélera que les destinations orientées.", + "viewer.route.copy.success": "Lien de l'itinéraire tracé copié", + "viewer.route.copy.failed": "Impossible de copier le lien de l'itinéraire tracé", + "viewer.route.position": "Position {index} sur {total} de l'itinéraire : {label}", + "viewer.route.step": "Étape {index} sur {total} · {phase} · {label}", + "viewer.route.motionRequired": "Le parcours automatique nécessite le mode Animé", + "viewer.route.trigger.clear": "Effacer l'itinéraire tracé", + "viewer.route.overview.status": "{nodes} · {hops} · itinéraire édité le plus court", + "viewer.route.overview.node.one": "{count} nœud", + "viewer.route.overview.node.other": "{count} nœuds", + "viewer.route.overview.hop.one": "{count} saut orienté", + "viewer.route.overview.hop.other": "{count} sauts orientés", + "viewer.route.phase.playing": "En lecture", + "viewer.route.phase.complete": "Terminé", + "viewer.route.phase.inspecting": "Inspection", + "viewer.route.destination.count.one": "{count} destination orientée disponible. Choisissez un nœud surligné.", + "viewer.route.destination.count.other": "{count} destinations orientées disponibles. Choisissez un nœud surligné.", + "viewer.route.noOutgoing": "Aucun itinéraire sortant ne débute ici. Effacez et choisissez une autre source.", + "viewer.route.result.title": "{source} vers {target}", + "viewer.route.finder.source.title": "Choisir le départ de l'itinéraire", + "viewer.route.finder.source.placeholder": "Rechercher des sources d'itinéraire", + "viewer.route.finder.source.empty": "Aucune source d'itinéraire correspondante", + "viewer.route.finder.source.results": "Nœuds pouvant débuter un itinéraire", + "viewer.route.finder.source.noun": "sources d'itinéraire", + "viewer.route.finder.source.badge": "départ", + "viewer.route.finder.target.title": "Destination depuis {label}", + "viewer.route.finder.target.placeholder": "Rechercher des destinations accessibles", + "viewer.route.finder.target.empty": "Aucune destination accessible correspondante", + "viewer.route.finder.target.results": "Destinations d'itinéraire accessibles", + "viewer.route.finder.target.noun": "destinations accessibles", + "viewer.route.hop.one": "{count} saut", + "viewer.route.hop.other": "{count} sauts", + "viewer.lens.eyebrow": "Lentille sémantique", + "viewer.lens.title": "Comparer les rôles du système", + "viewer.lens.close": "Fermer la lentille sémantique", + "viewer.lens.instruction": "Choisissez jusqu'à deux types sémantiques. Un seul révèle son trafic réel ; deux comparent uniquement les relations éditées directes.", + "viewer.lens.kinds": "Types sémantiques", + "viewer.lens.choose": "Choisissez un type pour inspecter ses nœuds et relations associées.", + "viewer.lens.copy": "Copier le lien de la lentille sémantique", + "viewer.lens.clear": "Effacer la lentille sémantique", + "viewer.lens.open": "Ouvrir la lentille sémantique", + "viewer.lens.openActive": "Ouvrir la lentille sémantique active", + "viewer.lens.legend": "Légende sémantique", + "viewer.lens.legend.inspect.one": "Inspecter {label}, {count} nœud", + "viewer.lens.legend.inspect.other": "Inspecter {label}, {count} nœuds", + "viewer.lens.kind.count.one": "{label}, {count} nœud", + "viewer.lens.kind.count.other": "{label}, {count} nœuds", + "viewer.lens.compare.one": "{first} → {second} : {forward} · {second} → {first} : {reverse} · {count} relation directe", + "viewer.lens.compare.other": "{first} → {second} : {forward} · {second} → {first} : {reverse} · {count} relations directes", + "viewer.lens.single": "{nodes} · {relationships} · les nœuds connectés restent visibles", + "viewer.lens.node.one": "{count} nœud {label}", + "viewer.lens.node.other": "{count} nœuds {label}", + "viewer.lens.relationship.one": "{count} relation associée", + "viewer.lens.relationship.other": "{count} relations associées", + "viewer.radar.title": "Radar sémantique", + "viewer.radar.building": "Construction de la vue d'ensemble", + "viewer.radar.openFull": "Ouvrir le radar sémantique complet", + "viewer.radar.open": "Ouvrir le radar", + "viewer.radar.close": "Fermer le radar sémantique", + "viewer.radar.surface": "Vue d'ensemble du diagramme. Cliquez sur un nœud pour le centrer, ou utilisez les flèches pour vous déplacer.", + "viewer.radar.click": "Cliquer sur le nœud", + "viewer.radar.drag": "Glisser pour se déplacer", + "viewer.radar.space": "Le radar sémantique a besoin de plus d'espace MAP.", + "viewer.radar.nodes": "Nœuds du radar du diagramme sémantique", + "viewer.radar.focus": "Centrer sur {label} depuis le radar sémantique", + "viewer.radar.status": "{count} nœuds · {viewport}", + "viewer.radar.fullMap": "{count} nœuds · carte complète", + "viewer.radar.compacted": "Radar compacté pour éviter de masquer le passeport sémantique ou les contrôles MAP.", + "viewer.radar.cancelWaiting": "Annuler l'attente du radar sémantique pour plus d'espace MAP", + "viewer.radar.needsSpace": "Le radar sémantique a besoin de plus d'espace MAP visible", + "viewer.radar.viewport.full": "carte complète", + "viewer.radar.viewport.width": "{percent} % de largeur", + "viewer.radar.viewport.scale": "{percent} % du viewport", + "viewer.nav.controls": "Contrôles de la vue du diagramme", + "viewer.nav.route": "Tracer un itinéraire orienté", + "viewer.nav.route.title": "Tracer l'itinéraire (R)", + "viewer.nav.route.short": "ROUTE", + "viewer.nav.radar": "Ouvrir le radar sémantique", + "viewer.nav.radar.title": "Radar sémantique (M)", + "viewer.nav.radar.short": "CARTE", + "viewer.nav.lens": "Ouvrir la lentille sémantique", + "viewer.nav.lens.title": "Lentille sémantique (L)", + "viewer.nav.lens.short": "LENTILLE", + "viewer.nav.find": "Trouver un nœud", + "viewer.nav.find.title": "Trouver un nœud (/)", + "viewer.nav.guide": "Ouvrir le guide du diagramme", + "viewer.nav.guide.title": "Guide du diagramme (?)", + "viewer.nav.zoomOut": "Zoom arrière", + "viewer.nav.zoomOut.title": "Zoom arrière (-)", + "viewer.nav.reset": "Réinitialiser la vue du diagramme", + "viewer.nav.reset.title": "Réinitialiser la vue (0)", + "viewer.nav.read": "LIRE", + "viewer.nav.zoomIn": "Zoom avant", + "viewer.nav.zoomIn.title": "Zoom avant (+)", + "viewer.nav.camera": "{hint}. Réinitialiser la vue du diagramme", + "viewer.nav.camera.title": "{semantic}{hint} · réinitialiser la vue (0)", + "viewer.nav.camera.semantic": "Caméra sémantique active · ", + "viewer.nav.level.map": "CARTE", + "viewer.nav.level.read": "LIRE", + "viewer.nav.level.full": "COMPLET", + "viewer.nav.level.auto": "AUTO", + "viewer.nav.detail.map": "Zoomez pour révéler les libellés des relations et le contexte des nœuds", + "viewer.nav.detail.read": "Zoomez à nouveau pour révéler les étiquettes et annotations", + "viewer.nav.detail.full": "Détail complet du diagramme", + "viewer.intent.summary": "{label}. {out} sortantes, {in} entrantes{loops}. {total} connexions. Appuyez sur Entrée pour les détails.", + "viewer.intent.loops": ", {count} boucle propre", + "viewer.common.copied": "Copié", + "viewer.common.copyFailed": "Échec de la copie", + "viewer.common.copyLink": "Copier le lien", + "viewer.common.clear": "Effacer", + "viewer.common.close": "Fermer" +} diff --git a/vendor/archify/examples/locales/fr.partial.json b/vendor/archify/examples/locales/fr.partial.json new file mode 100644 index 0000000..4a442c8 --- /dev/null +++ b/vendor/archify/examples/locales/fr.partial.json @@ -0,0 +1,22 @@ +{ + "page.title": "Diagramme {title}", + "diagram.description.architecture": "Un diagramme d'architecture généré par Archify.", + "legend.title": "Légende", + "node.focus": "Focus {label}", + "node.context.architecture": "Composant d'architecture", + "viewer.kind.frontend": "Frontend", + "viewer.kind.backend": "Backend", + "viewer.export.button": "Exporter", + "viewer.export.button.title": "Exporter le diagramme (E)", + "viewer.export.diagram": "Exporter le diagramme", + "viewer.finder.title": "Rechercher un nœud", + "viewer.route.eyebrow": "Sonde d'itinéraire", + "viewer.route.overview.node.one": "{count} nœud", + "viewer.route.overview.node.other": "{count} nœuds", + "viewer.guide.eyebrow": "Guide du diagramme", + "viewer.nav.controls": "Contrôles de vue du diagramme", + "viewer.theme.toggle": "Changer de thème", + "viewer.preset.classic": "Classique", + "viewer.common.close": "Fermer", + "viewer.common.copyLink": "Copier le lien" +} diff --git a/vendor/archify/examples/locales/it.json b/vendor/archify/examples/locales/it.json new file mode 100644 index 0000000..37fe4d1 --- /dev/null +++ b/vendor/archify/examples/locales/it.json @@ -0,0 +1,425 @@ +{ + "page.title": "{title} · Diagramma", + "diagram.description.architecture": "Un diagramma di architettura generato da Archify.", + "diagram.description.workflow": "Un diagramma di flusso di lavoro generato da Archify.", + "diagram.description.sequence": "Un diagramma di sequenza generato da Archify.", + "diagram.description.dataflow": "Un diagramma di flusso dati generato da Archify.", + "diagram.description.lifecycle": "Un diagramma di ciclo di vita generato da Archify.", + "node.focus": "Metti a fuoco {label}", + "node.focus.detail": "Metti a fuoco {label}, {detail}", + "node.context.architecture": "Componente di architettura", + "node.context.workflow": "Nodo del flusso di lavoro", + "node.context.sequence": "Partecipante alla sequenza", + "node.context.dataflow": "Nodo del flusso dati", + "node.context.lifecycle": "Stato del ciclo di vita", + "legend.title": "Legenda", + "legend.architecture.frontend": "Frontend", + "legend.architecture.backend": "Backend", + "legend.architecture.database": "Database", + "legend.architecture.cloud": "Cloud", + "legend.architecture.security": "Sicurezza", + "legend.architecture.messagebus": "Bus di messaggi", + "legend.architecture.external": "Esterno", + "legend.workflow.frontend": "Interfaccia utente", + "legend.workflow.backend": "Logica dell'agente", + "legend.workflow.security": "Criterio", + "legend.workflow.messagebus": "Azione dello strumento", + "legend.workflow.database": "Contesto / traccia", + "legend.workflow.cloud": "Servizio cloud", + "legend.workflow.external": "Sistema esterno", + "legend.sequence.emphasis": "richiesta principale", + "legend.sequence.return": "risposta", + "legend.sequence.security": "sicurezza", + "legend.sequence.dashed": "traccia asincrona", + "legend.sequence.default": "messaggio", + "legend.dataflow.emphasis": "dati primari", + "legend.dataflow.security": "criterio / PII", + "legend.dataflow.dashed": "batch asincrono", + "legend.dataflow.database": "archivio dati", + "legend.dataflow.default": "flusso dati", + "legend.lifecycle.start": "stato iniziale", + "legend.lifecycle.active": "attivo", + "legend.lifecycle.waiting": "attesa", + "legend.lifecycle.decision": "decisione", + "legend.lifecycle.success": "successo", + "legend.lifecycle.failure": "errore / uscita", + "legend.lifecycle.neutral": "neutro", + "legend.lifecycle.external": "esterno", + "legend.lifecycle.final": "stato finale", + "viewer.kind.frontend": "Frontend", + "viewer.kind.backend": "Backend", + "viewer.kind.database": "Database", + "viewer.kind.cloud": "Cloud", + "viewer.kind.security": "Sicurezza", + "viewer.kind.messagebus": "Bus di messaggi", + "viewer.kind.external": "Esterno", + "viewer.kind.neutral": "Neutro", + "viewer.kind.node": "Nodo", + "viewer.kind.start": "Inizio", + "viewer.kind.active": "Attivo", + "viewer.kind.waiting": "In attesa", + "viewer.kind.decision": "Decisione", + "viewer.kind.success": "Successo", + "viewer.kind.failure": "Errore", + "viewer.toolbar.actions": "Azioni del diagramma", + "viewer.theme.toggle.title": "Cambia tema (T)", + "viewer.theme.toggle": "Cambia tema colore", + "viewer.theme.dark": "Scuro", + "viewer.theme.light": "Chiaro", + "viewer.preset.choose.title": "Scegli lo stile visivo (S per scorrere)", + "viewer.preset.choose": "Scegli lo stile visivo", + "viewer.preset.style": "Stile", + "viewer.preset.menu": "Stile visivo", + "viewer.preset.identity": "Identità visiva", + "viewer.preset.cycles": "S per scorrere", + "viewer.preset.classic": "Classico", + "viewer.preset.classic.short": "Classico", + "viewer.preset.classic.hint": "Predefinito tecnico stabile", + "viewer.preset.flow": "Flusso di segnale", + "viewer.preset.flow.short": "Flusso", + "viewer.preset.flow.hint": "Presentazione dinamica", + "viewer.preset.blueprint": "Progetto", + "viewer.preset.blueprint.hint": "Revisione tecnica", + "viewer.preset.editorial": "Editoriale", + "viewer.preset.editorial.hint": "Note di pubblicazione e lancio", + "viewer.preset.badge.signalFlow": "FLUSSO DI SEGNALE", + "viewer.preset.badge.blueprint": "PROGETTO / REV. 01", + "viewer.preset.badge.editorial": "EDITORIALE / NOTA SUL CAMPO", + "viewer.preset.badge.editorialPlate": "ARCHIFY / TAVOLA 04", + "viewer.preset.current": "Stile visivo: {style}. Scegli lo stile visivo", + "viewer.motion.live": "Attiva", + "viewer.motion.still": "Statica", + "viewer.motion.pause": "Metti in pausa l'animazione", + "viewer.motion.resume": "Riprendi l'animazione", + "viewer.motion.reduced": "Animazione in pausa per preferenza di movimento ridotto", + "viewer.motion.hidden": "Animazione in pausa mentre la pagina è nascosta", + "viewer.motion.yielding": "Metti in pausa l'animazione; attualmente cede il posto a {owner}", + "viewer.motion.yielding.title": "Anteprima dinamica attiva · cede il posto a {owner}", + "viewer.owner.route": "Esplora percorso", + "viewer.owner.lens": "Lente semantica", + "viewer.owner.relationship": "Anteprima relazione", + "viewer.owner.intent": "Traccia intento", + "viewer.owner.focus": "focus semantico", + "viewer.owner.legend": "anteprima legenda", + "viewer.owner.reader": "interazione del lettore", + "viewer.present.enter": "Entra in modalità presentazione", + "viewer.present.enter.title": "Modalità presentazione (F)", + "viewer.present.exit": "Esci dalla modalità presentazione", + "viewer.present.exit.title": "Esci dalla modalità presentazione (F o Esc)", + "viewer.present.present": "Presenta", + "viewer.present.exit.label": "Esci", + "viewer.export.button": "Esporta", + "viewer.export.button.title": "Esporta diagramma (E)", + "viewer.export.diagram": "Esporta diagramma", + "viewer.export.menu": "Esporta", + "viewer.export.subtitle": "Output portatili e puliti", + "viewer.export.share": "Condividi", + "viewer.export.shareCard": "Scheda di condivisione", + "viewer.export.routeShareCard": "Scheda di condivisione del percorso", + "viewer.export.reachShareCard": "Scheda di condivisione della raggiungibilità", + "viewer.export.copyDiagram": "Copia diagramma", + "viewer.export.clipboardPng": "PNG negli appunti", + "viewer.export.raster": "Immagini raster", + "viewer.export.image": "Immagine", + "viewer.export.lossless": "Immagine senza perdita", + "viewer.export.compact": "Immagine compatta", + "viewer.export.modern": "Immagine moderna", + "viewer.export.vectorMotion": "Vettoriale e animazione", + "viewer.export.vectorMotion.heading": "Vettoriale e animazione", + "viewer.export.editable": "Vettoriale modificabile", + "viewer.export.motion6s": "Animazione di 6s", + "viewer.export.unsupported": "Non supportato da questo browser", + "viewer.export.clipboardUnsupported": "La scrittura di immagini negli appunti non è supportata da questo browser", + "viewer.export.clipboardUnsupported.short": "La scrittura di immagini negli appunti non è supportata in questo browser.", + "viewer.export.motionUnavailable": "Cattura dell'animazione non disponibile in questo browser", + "viewer.export.webmUnavailable": "WebM non disponibile in questo browser", + "viewer.export.failed": "Esportazione non riuscita: {message}", + "viewer.export.unknownVariant": "Variante scheda di condivisione sconosciuta: {variant}", + "viewer.export.routeRequired": "Traccia un percorso prima di esportare una scheda di condivisione del percorso", + "viewer.export.reachRequired": "Traccia la raggiungibilità prima di esportare una scheda di condivisione della raggiungibilità", + "viewer.export.unknown": "sconosciuto", + "viewer.export.routeFailed": "Esportazione della scheda di condivisione del percorso non riuscita: {message}", + "viewer.export.reachFailed": "Esportazione della scheda di condivisione della raggiungibilità non riuscita: {message}", + "viewer.export.copyFailed": "Copia non riuscita: {message}", + "viewer.export.copiedPng": "PNG copiato negli appunti", + "viewer.export.downloadedRoute": "Scheda di condivisione del percorso scaricata", + "viewer.export.downloadedReach": "Scheda di condivisione della raggiungibilità scaricata", + "viewer.export.downloadedWebm": "WebM scaricato", + "viewer.export.recording": "Registrazione di 6 secondi di animazione…", + "viewer.export.card.routeSummary.one": "Percorso: {source} → {target} · {count} salto diretto", + "viewer.export.card.routeSummary.other": "Percorso: {source} → {target} · {count} salti diretti", + "viewer.export.card.reachSummary": "{direction} generata da {origin} · {nodes} · {links} · max {hops}", + "viewer.export.card.node.one": "{count} nodo", + "viewer.export.card.node.other": "{count} nodi", + "viewer.export.card.link.one": "{count} collegamento", + "viewer.export.card.link.other": "{count} collegamenti", + "viewer.export.card.hop.one": "{count} salto", + "viewer.export.card.hop.other": "{count} salti", + "viewer.export.card.routeBadge": "ARCHIFY · PERCORSO · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · RAGGIUNGIBILITÀ {direction}", + "viewer.export.direction.upstream": "A monte", + "viewer.export.direction.downstream": "A valle", + "viewer.export.error.canvasUnavailable": "Canvas non disponibile per {label}", + "viewer.export.error.contextUnavailable": "Contesto canvas 2D non disponibile per {label}", + "viewer.export.error.toBlobUnavailable": "canvas.toBlob non disponibile per {label}", + "viewer.export.error.toBlobNull": "canvas.toBlob non ha restituito dati per {label}", + "viewer.export.error.variantsCombined": "Le varianti della scheda di condivisione non possono essere combinate", + "viewer.export.error.viewerState": "L'esportazione della scheda di condivisione non è riuscita a rimuovere lo stato temporaneo del visualizzatore", + "viewer.export.error.routeState": "L'esportazione della scheda del percorso non è riuscita a preservare in modo sicuro il percorso risolto", + "viewer.export.error.reachState": "L'esportazione della scheda di raggiungibilità non è riuscita a preservare in modo sicuro la raggiungibilità generata", + "viewer.export.error.webmRequirements": "L'esportazione dell'animazione WebM richiede un'animazione di traccia e il supporto MediaRecorder del browser", + "viewer.export.error.mediaRecorder": "MediaRecorder non riuscito", + "viewer.export.error.emptyWebm": "MediaRecorder ha prodotto un WebM vuoto", + "viewer.export.error.webmBackground": "Impossibile caricare lo sfondo SVG per l'esportazione WebM", + "viewer.guide.eyebrow": "Guida al diagramma", + "viewer.guide.close": "Chiudi la guida al diagramma", + "viewer.guide.inspecting": "Analisi della semantica compilata", + "viewer.guide.actions": "Azioni di esplorazione del diagramma", + "viewer.guide.find": "Trova un nodo qualsiasi", + "viewer.guide.find.hint": "Cerca etichette, responsabilità, tipi e ID stabili.", + "viewer.guide.route": "Traccia un percorso", + "viewer.guide.route.aria": "Traccia un percorso diretto", + "viewer.guide.route.hint": "Chiedi come si collegano due nodi semantici nella direzione generata.", + "viewer.guide.map": "Guarda l'intero sistema", + "viewer.guide.map.hint": "Apri il Radar semantico con un viewport dal vivo e nodi stabili.", + "viewer.guide.lens": "Confronta i tipi semantici", + "viewer.guide.lens.hint": "Conta i ruoli, mostra il loro traffico e confronta i collegamenti diretti generati.", + "viewer.guide.present": "Entra in modalità presentazione", + "viewer.guide.present.hint": "Assegna il viewport al diagramma dal vivo senza modificare l'esportazione.", + "viewer.guide.shortcuts": "Altre scorciatoie da tastiera", + "viewer.guide.shortcut.export": "Esporta", + "viewer.guide.shortcut.theme": "Tema", + "viewer.guide.shortcut.style": "Stile", + "viewer.guide.shortcut.reset": "Ripristina", + "viewer.guide.shortcut.zoomIn": "Zoom avanti", + "viewer.guide.shortcut.zoomOut": "Zoom indietro", + "viewer.guide.shortcut.close": "Chiudi", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} nodo semantico", + "viewer.guide.fact.node.other": "{count} nodi semantici", + "viewer.guide.fact.relationship.one": "{count} relazione", + "viewer.guide.fact.relationship.other": "{count} relazioni", + "viewer.guide.open": "Apri la guida al diagramma", + "viewer.finder.title": "Trova un nodo", + "viewer.finder.close": "Chiudi il cercatore di nodi", + "viewer.finder.placeholder": "Cerca etichette o ID", + "viewer.finder.search": "Cerca nodi del diagramma", + "viewer.finder.results": "Nodi del diagramma", + "viewer.finder.empty": "Nessun nodo corrispondente", + "viewer.finder.result.focus": "Metti a fuoco {label}", + "viewer.finder.result.routeStart": "Scegli {label} come inizio del percorso", + "viewer.finder.result.routeTarget": "Scegli {label} come destinazione del percorso, {links}", + "viewer.finder.status.empty": "Nessun nodo corrispondente", + "viewer.finder.status.count.one": "{count} nodo corrispondente", + "viewer.finder.status.count.other": "{count} nodi corrispondenti", + "viewer.finder.noun.nodes": "nodi", + "viewer.finder.link.one": "{count} collegamento", + "viewer.finder.link.other": "{count} collegamenti", + "viewer.finder.result.focus.one": "Metti a fuoco {label}, {count} connessione correlata", + "viewer.finder.result.focus.other": "Metti a fuoco {label}, {count} connessioni correlate", + "viewer.finder.status.filtered": "{visible} di {available} {noun}", + "viewer.finder.status.all": "{available} {noun}", + "viewer.passport.eyebrow": "Passaporto semantico", + "viewer.passport.metadata": "Metadati del nodo", + "viewer.passport.evidence": "Fonte verificata", + "viewer.passport.verified": "Fonte verificata", + "viewer.passport.verificationScope": "Verificato rispetto al Git locale alla revisione fissata. L'accesso remoto non è stato verificato.", + "viewer.passport.reach": "Raggiungibilità generata", + "viewer.passport.reach.trace": "Traccia la raggiungibilità generata", + "viewer.passport.upstream": "A monte", + "viewer.passport.downstream": "A valle", + "viewer.passport.upstream.trace": "Traccia la raggiungibilità generata a monte", + "viewer.passport.downstream.trace": "Traccia la raggiungibilità generata a valle", + "viewer.passport.close": "Chiudi il passaporto semantico", + "viewer.passport.copy": "Copia link", + "viewer.passport.copy.focus": "Copia il link al nodo in focus", + "viewer.passport.relations": "Relazioni", + "viewer.passport.relations.show": "Mostra le relazioni collegate", + "viewer.passport.relations.hide": "Nascondi le relazioni collegate", + "viewer.passport.relations.list": "Relazioni collegate", + "viewer.passport.copyRelation": "Copia relazione", + "viewer.passport.copyNode": "Copia nodo", + "viewer.passport.copyPinned": "Copia il link alla relazione fissata", + "viewer.passport.copySource": "Copia il link al nodo di origine", + "viewer.passport.copy.focused.success": "Link del nodo in focus copiato", + "viewer.passport.copy.pinned.success": "Link della relazione fissata copiato", + "viewer.passport.copy.focused.failed": "Impossibile copiare il link del nodo in focus", + "viewer.passport.copy.pinned.failed": "Impossibile copiare il link della relazione fissata", + "viewer.passport.relationship.none": "Nessuna relazione collegata", + "viewer.passport.relationship.count.one": "{count} relazione", + "viewer.passport.relationship.count.other": "{count} relazioni", + "viewer.passport.relationship.show.one": "Mostra {count} relazione collegata", + "viewer.passport.relationship.show.other": "Mostra {count} relazioni collegate", + "viewer.passport.relationship.summary": "{out} in uscita · {in} in entrata{loops}", + "viewer.passport.relationship.loops": " · {count} autoanello", + "viewer.passport.relationship.explorer": "Esploratore delle relazioni dirette", + "viewer.passport.relationship.help": "Usa i tasti freccia per esplorare le relazioni. Premi Invio o Spazio per fissare i dettagli; Esc per annullare.", + "viewer.passport.relationship.loopsBack": "torna su se stesso", + "viewer.passport.relationship.connectsTo": "si collega a", + "viewer.passport.relationship.connectsFrom": "si collega da", + "viewer.passport.relationship.pinned": "Relazione fissata · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "Analizza la relazione {index} di {total}: da {from} a {to}, {label}. Premi Invio per i dettagli.", + "viewer.passport.relationship.group.out": "In uscita", + "viewer.passport.relationship.group.in": "In entrata", + "viewer.passport.relationship.group.loop": "Autoanelli", + "viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "USCITA →", + "viewer.passport.relationship.direction.in": "← ENTRATA", + "viewer.passport.relationship.direction.loop": "ANELLO", + "viewer.passport.sourceCount.one": "{count} riferimento a fonte verificata", + "viewer.passport.sourceCount.other": "{count} riferimenti a fonti verificate", + "viewer.passport.sourceMarker": "FONTE", + "viewer.passport.beacon.one": "{count} fonte verificata; metti a fuoco questo nodo per analizzarla", + "viewer.passport.beacon.other": "{count} fonti verificate; metti a fuoco questo nodo per analizzarle", + "viewer.passport.repository.open": "Apri la revisione {revision} del repository verificato", + "viewer.passport.source.open": "Apri la fonte verificata {path} alla revisione {revision}", + "viewer.passport.source.openLink": "Apri ↗", + "viewer.passport.reach.upstream.one": "Traccia {count} nodo generato a monte", + "viewer.passport.reach.upstream.other": "Traccia {count} nodi generati a monte", + "viewer.passport.reach.downstream.one": "Traccia {count} nodo generato a valle", + "viewer.passport.reach.downstream.other": "Traccia {count} nodi generati a valle", + "viewer.passport.reach.noUpstream": "Nessun nodo generato a monte", + "viewer.passport.reach.noDownstream": "Nessun nodo generato a valle", + "viewer.passport.reach.status": "{direction} · {nodes} nodi · {links} collegamenti · max {hops} salti", + "viewer.route.eyebrow": "Esplora percorso", + "viewer.route.start": "Scegli un nodo di partenza", + "viewer.route.start.find": "Trova partenza", + "viewer.route.start.find.aria": "Trova una partenza del percorso", + "viewer.route.copy": "Copia link", + "viewer.route.copy.aria": "Copia il link al percorso tracciato", + "viewer.route.clear": "Cancella", + "viewer.route.clear.aria": "Cancella l'esplorazione del percorso", + "viewer.route.traced": "Percorso tracciato", + "viewer.route.pickTwo": "Scegli due nodi semantici sul diagramma", + "viewer.route.pickOne": "Scegli un nodo semantico sul diagramma", + "viewer.route.controls": "Controlli del tragitto del percorso", + "viewer.route.previous": "Posizione precedente nel percorso", + "viewer.route.play": "Riproduci il tragitto del percorso", + "viewer.route.pause": "Metti in pausa il tragitto del percorso", + "viewer.route.replay": "Riavvia il tragitto del percorso", + "viewer.route.next": "Posizione successiva nel percorso", + "viewer.route.journey": "Tragitto", + "viewer.route.pause.label": "Pausa", + "viewer.route.replay.label": "Riavvia", + "viewer.route.overview": "Panoramica", + "viewer.route.overview.aria": "Mostra la panoramica completa del percorso", + "viewer.route.instructions": "Scegli prima l'origine, poi la destinazione. La direzione è importante.", + "viewer.route.destination": "Scegli una destinazione da {label}", + "viewer.route.destination.find": "Trova destinazione", + "viewer.route.destination.find.aria": "Trova una destinazione raggiungibile del percorso", + "viewer.route.differentDestination": "Scegli una destinazione diversa", + "viewer.route.distinct": "Un percorso richiede due nodi semantici distinti.", + "viewer.route.unreachable": "Nessun percorso diretto verso {label}", + "viewer.route.unreachable.detail": "{target} non è raggiungibile da {source}. Scegli una destinazione evidenziata.", + "viewer.route.start.instructions": "Seleziona l'origine. Il passo successivo mostrerà solo le destinazioni dirette.", + "viewer.route.copy.success": "Link del percorso tracciato copiato", + "viewer.route.copy.failed": "Impossibile copiare il link del percorso tracciato", + "viewer.route.position": "Posizione {index} di {total} nel percorso: {label}", + "viewer.route.step": "Passo {index} di {total} · {phase} · {label}", + "viewer.route.motionRequired": "Il tragitto automatico richiede l'animazione attiva", + "viewer.route.trigger.clear": "Cancella il percorso tracciato", + "viewer.route.overview.status": "{nodes} · {hops} · percorso generato più breve", + "viewer.route.overview.node.one": "{count} nodo", + "viewer.route.overview.node.other": "{count} nodi", + "viewer.route.overview.hop.one": "{count} salto diretto", + "viewer.route.overview.hop.other": "{count} salti diretti", + "viewer.route.phase.playing": "In riproduzione", + "viewer.route.phase.complete": "Completato", + "viewer.route.phase.inspecting": "Analisi in corso", + "viewer.route.destination.count.one": "{count} destinazione diretta disponibile. Scegli un nodo evidenziato.", + "viewer.route.destination.count.other": "{count} destinazioni dirette disponibili. Scegli un nodo evidenziato.", + "viewer.route.noOutgoing": "Nessun percorso in uscita da qui. Cancella e scegli un'altra origine.", + "viewer.route.result.title": "{source} verso {target}", + "viewer.route.finder.source.title": "Scegli l'inizio del percorso", + "viewer.route.finder.source.placeholder": "Cerca origini del percorso", + "viewer.route.finder.source.empty": "Nessuna origine del percorso corrispondente", + "viewer.route.finder.source.results": "Nodi che possono avviare un percorso", + "viewer.route.finder.source.noun": "origini del percorso", + "viewer.route.finder.source.badge": "inizio", + "viewer.route.finder.target.title": "Destinazione da {label}", + "viewer.route.finder.target.placeholder": "Cerca destinazioni raggiungibili", + "viewer.route.finder.target.empty": "Nessuna destinazione raggiungibile corrispondente", + "viewer.route.finder.target.results": "Destinazioni raggiungibili del percorso", + "viewer.route.finder.target.noun": "destinazioni raggiungibili", + "viewer.route.hop.one": "{count} salto", + "viewer.route.hop.other": "{count} salti", + "viewer.lens.eyebrow": "Lente semantica", + "viewer.lens.title": "Confronta i ruoli del sistema", + "viewer.lens.close": "Chiudi la lente semantica", + "viewer.lens.instruction": "Scegli fino a due tipi semantici. Uno mostra il traffico reale; due confrontano solo le relazioni dirette generate.", + "viewer.lens.kinds": "Tipi semantici", + "viewer.lens.choose": "Scegli un tipo per analizzarne i nodi e le relazioni collegate.", + "viewer.lens.copy": "Copia il link alla lente semantica", + "viewer.lens.clear": "Cancella la lente semantica", + "viewer.lens.open": "Apri la lente semantica", + "viewer.lens.openActive": "Apri la lente semantica attiva", + "viewer.lens.legend": "Legenda semantica", + "viewer.lens.legend.inspect.one": "Analizza {label}, {count} nodo", + "viewer.lens.legend.inspect.other": "Analizza {label}, {count} nodi", + "viewer.lens.kind.count.one": "{label}, {count} nodo", + "viewer.lens.kind.count.other": "{label}, {count} nodi", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} relazione diretta", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} relazioni dirette", + "viewer.lens.single": "{nodes} · {relationships} · i nodi collegati restano visibili", + "viewer.lens.node.one": "{count} nodo {label}", + "viewer.lens.node.other": "{count} nodi {label}", + "viewer.lens.relationship.one": "{count} relazione a contatto", + "viewer.lens.relationship.other": "{count} relazioni a contatto", + "viewer.radar.title": "Radar semantico", + "viewer.radar.building": "Costruzione della panoramica", + "viewer.radar.openFull": "Apri il radar semantico completo", + "viewer.radar.open": "Apri radar", + "viewer.radar.close": "Chiudi il radar semantico", + "viewer.radar.surface": "Panoramica del diagramma. Clicca su un nodo per metterlo a fuoco, oppure usa i tasti freccia per scorrere.", + "viewer.radar.click": "Clicca sul nodo", + "viewer.radar.drag": "Trascina per scorrere", + "viewer.radar.space": "Il radar semantico richiede più spazio nella MAPPA.", + "viewer.radar.nodes": "Nodi del radar semantico del diagramma", + "viewer.radar.focus": "Metti a fuoco {label} dal radar semantico", + "viewer.radar.status": "{count} nodi · {viewport}", + "viewer.radar.fullMap": "{count} nodi · mappa completa", + "viewer.radar.compacted": "Radar compattato per evitare di coprire il passaporto semantico o i controlli della MAPPA.", + "viewer.radar.cancelWaiting": "Annulla l'attesa del radar semantico per più spazio nella MAPPA", + "viewer.radar.needsSpace": "Il radar semantico richiede più spazio visibile nella MAPPA", + "viewer.radar.viewport.full": "mappa completa", + "viewer.radar.viewport.width": "{percent}% di larghezza", + "viewer.radar.viewport.scale": "{percent}% del viewport", + "viewer.nav.controls": "Controlli della vista del diagramma", + "viewer.nav.route": "Traccia un percorso diretto", + "viewer.nav.route.title": "Traccia percorso (R)", + "viewer.nav.route.short": "PERCORSO", + "viewer.nav.radar": "Apri il radar semantico", + "viewer.nav.radar.title": "Radar semantico (M)", + "viewer.nav.radar.short": "MAPPA", + "viewer.nav.lens": "Apri la lente semantica", + "viewer.nav.lens.title": "Lente semantica (L)", + "viewer.nav.lens.short": "LENTE", + "viewer.nav.find": "Trova un nodo", + "viewer.nav.find.title": "Trova un nodo (/)", + "viewer.nav.guide": "Apri la guida al diagramma", + "viewer.nav.guide.title": "Guida al diagramma (?)", + "viewer.nav.zoomOut": "Zoom indietro", + "viewer.nav.zoomOut.title": "Zoom indietro (-)", + "viewer.nav.reset": "Ripristina la vista del diagramma", + "viewer.nav.reset.title": "Ripristina vista (0)", + "viewer.nav.read": "LEGGI", + "viewer.nav.zoomIn": "Zoom avanti", + "viewer.nav.zoomIn.title": "Zoom avanti (+)", + "viewer.nav.camera": "{hint}. Ripristina la vista del diagramma", + "viewer.nav.camera.title": "{semantic}{hint} · ripristina vista (0)", + "viewer.nav.camera.semantic": "Camera semantica attiva · ", + "viewer.nav.level.map": "MAPPA", + "viewer.nav.level.read": "LEGGI", + "viewer.nav.level.full": "COMPLETO", + "viewer.nav.level.auto": "AUTO", + "viewer.nav.detail.map": "Ingrandisci per mostrare le etichette delle relazioni e il contesto dei nodi", + "viewer.nav.detail.read": "Ingrandisci ancora per mostrare tag e annotazioni", + "viewer.nav.detail.full": "Dettaglio completo del diagramma", + "viewer.intent.summary": "{label}. {out} in uscita, {in} in entrata{loops}. {total} connessioni. Premi Invio per i dettagli.", + "viewer.intent.loops": ", {count} autoanello", + "viewer.common.copied": "Copiato", + "viewer.common.copyFailed": "Copia non riuscita", + "viewer.common.copyLink": "Copia link", + "viewer.common.clear": "Cancella", + "viewer.common.close": "Chiudi" +} diff --git a/vendor/archify/examples/locales/ja.json b/vendor/archify/examples/locales/ja.json new file mode 100644 index 0000000..e74d346 --- /dev/null +++ b/vendor/archify/examples/locales/ja.json @@ -0,0 +1,425 @@ +{ + "page.title": "{title} ダイアグラム", + "diagram.description.architecture": "Archifyで生成されたアーキテクチャ図です。", + "diagram.description.workflow": "Archifyで生成されたワークフロー図です。", + "diagram.description.sequence": "Archifyで生成されたシーケンス図です。", + "diagram.description.dataflow": "Archifyで生成されたデータフロー図です。", + "diagram.description.lifecycle": "Archifyで生成されたライフサイクル図です。", + "node.focus": "{label} にフォーカス", + "node.focus.detail": "{label} にフォーカス、{detail}", + "node.context.architecture": "アーキテクチャコンポーネント", + "node.context.workflow": "ワークフローノード", + "node.context.sequence": "シーケンス参加者", + "node.context.dataflow": "データフローノード", + "node.context.lifecycle": "ライフサイクル状態", + "legend.title": "凡例", + "legend.architecture.frontend": "フロントエンド", + "legend.architecture.backend": "バックエンド", + "legend.architecture.database": "データベース", + "legend.architecture.cloud": "クラウド", + "legend.architecture.security": "セキュリティ", + "legend.architecture.messagebus": "メッセージバス", + "legend.architecture.external": "外部", + "legend.workflow.frontend": "ユーザーUI", + "legend.workflow.backend": "エージェントロジック", + "legend.workflow.security": "ポリシー", + "legend.workflow.messagebus": "ツール実行", + "legend.workflow.database": "コンテキスト / トレース", + "legend.workflow.cloud": "クラウドサービス", + "legend.workflow.external": "外部システム", + "legend.sequence.emphasis": "主要リクエスト", + "legend.sequence.return": "戻り値", + "legend.sequence.security": "セキュリティ", + "legend.sequence.dashed": "非同期トレース", + "legend.sequence.default": "メッセージ", + "legend.dataflow.emphasis": "主要データ", + "legend.dataflow.security": "ポリシー / PII", + "legend.dataflow.dashed": "非同期バッチ", + "legend.dataflow.database": "データストア", + "legend.dataflow.default": "データフロー", + "legend.lifecycle.start": "初期状態", + "legend.lifecycle.active": "アクティブ", + "legend.lifecycle.waiting": "待機", + "legend.lifecycle.decision": "判断", + "legend.lifecycle.success": "成功", + "legend.lifecycle.failure": "失敗 / 終了", + "legend.lifecycle.neutral": "中立", + "legend.lifecycle.external": "外部", + "legend.lifecycle.final": "終了状態", + "viewer.kind.frontend": "フロントエンド", + "viewer.kind.backend": "バックエンド", + "viewer.kind.database": "データベース", + "viewer.kind.cloud": "クラウド", + "viewer.kind.security": "セキュリティ", + "viewer.kind.messagebus": "メッセージバス", + "viewer.kind.external": "外部", + "viewer.kind.neutral": "中立", + "viewer.kind.node": "ノード", + "viewer.kind.start": "開始", + "viewer.kind.active": "アクティブ", + "viewer.kind.waiting": "待機", + "viewer.kind.decision": "判断", + "viewer.kind.success": "成功", + "viewer.kind.failure": "失敗", + "viewer.toolbar.actions": "ダイアグラム操作", + "viewer.theme.toggle.title": "テーマ切替 (T)", + "viewer.theme.toggle": "カラーテーマを切替", + "viewer.theme.dark": "ダーク", + "viewer.theme.light": "ライト", + "viewer.preset.choose.title": "ビジュアルスタイルを選択 (Sで循環)", + "viewer.preset.choose": "ビジュアルスタイルを選択", + "viewer.preset.style": "スタイル", + "viewer.preset.menu": "ビジュアルスタイル", + "viewer.preset.identity": "ビジュアルアイデンティティ", + "viewer.preset.cycles": "Sで循環", + "viewer.preset.classic": "クラシック", + "viewer.preset.classic.short": "クラシック", + "viewer.preset.classic.hint": "安定した技術標準", + "viewer.preset.flow": "シグナルフロー", + "viewer.preset.flow.short": "フロー", + "viewer.preset.flow.hint": "動きを強調した表現", + "viewer.preset.blueprint": "ブループリント", + "viewer.preset.blueprint.hint": "エンジニアリングレビュー向け", + "viewer.preset.editorial": "エディトリアル", + "viewer.preset.editorial.hint": "発行・公開ノート向け", + "viewer.preset.badge.signalFlow": "シグナルフロー", + "viewer.preset.badge.blueprint": "ブループリント / 改訂01", + "viewer.preset.badge.editorial": "エディトリアル / フィールドノート", + "viewer.preset.badge.editorialPlate": "ARCHIFY / プレート04", + "viewer.preset.current": "ビジュアルスタイル: {style}。ビジュアルスタイルを選択", + "viewer.motion.live": "ライブ", + "viewer.motion.still": "静止", + "viewer.motion.pause": "動きを一時停止", + "viewer.motion.resume": "動きを再開", + "viewer.motion.reduced": "「視差効果を減らす」設定により動きを一時停止しました", + "viewer.motion.hidden": "このページが非表示のため動きを一時停止しました", + "viewer.motion.yielding": "動きを一時停止、現在 {owner} に譲っています", + "viewer.motion.yielding.title": "ライブプレビュー有効 · {owner} に譲っています", + "viewer.owner.route": "ルートプローブ", + "viewer.owner.lens": "セマンティックレンズ", + "viewer.owner.relationship": "関係性プレビュー", + "viewer.owner.intent": "インテントトレース", + "viewer.owner.focus": "セマンティックフォーカス", + "viewer.owner.legend": "凡例プレビュー", + "viewer.owner.reader": "読者の操作", + "viewer.present.enter": "プレゼンテーションステージに入る", + "viewer.present.enter.title": "プレゼンテーションステージ (F)", + "viewer.present.exit": "プレゼンテーションステージを終了", + "viewer.present.exit.title": "プレゼンテーションステージを終了 (F または Escape)", + "viewer.present.present": "プレゼンテーション", + "viewer.present.exit.label": "終了", + "viewer.export.button": "エクスポート", + "viewer.export.button.title": "ダイアグラムをエクスポート (E)", + "viewer.export.diagram": "ダイアグラムをエクスポート", + "viewer.export.menu": "エクスポート", + "viewer.export.subtitle": "持ち運びやすくクリーンな出力", + "viewer.export.share": "共有", + "viewer.export.shareCard": "シェアカード", + "viewer.export.routeShareCard": "ルートシェアカード", + "viewer.export.reachShareCard": "到達範囲シェアカード", + "viewer.export.copyDiagram": "ダイアグラムをコピー", + "viewer.export.clipboardPng": "PNGをクリップボードへ", + "viewer.export.raster": "ラスター画像", + "viewer.export.image": "画像", + "viewer.export.lossless": "無劣化画像", + "viewer.export.compact": "コンパクト画像", + "viewer.export.modern": "モダン画像", + "viewer.export.vectorMotion": "ベクターとモーション", + "viewer.export.vectorMotion.heading": "ベクター & モーション", + "viewer.export.editable": "編集可能なベクター", + "viewer.export.motion6s": "6秒のモーション", + "viewer.export.unsupported": "このブラウザではサポートされていません", + "viewer.export.clipboardUnsupported": "このブラウザはクリップボードへの画像書き込みに対応していません", + "viewer.export.clipboardUnsupported.short": "このブラウザはクリップボードへの画像書き込みに対応していません。", + "viewer.export.motionUnavailable": "このブラウザではモーションキャプチャを利用できません", + "viewer.export.webmUnavailable": "このブラウザではWebMを利用できません", + "viewer.export.failed": "エクスポートに失敗しました: {message}", + "viewer.export.unknownVariant": "不明なシェアカードの種類です: {variant}", + "viewer.export.routeRequired": "ルートシェアカードをエクスポートする前にルートをトレースしてください", + "viewer.export.reachRequired": "到達範囲シェアカードをエクスポートする前に編集済みの到達範囲をトレースしてください", + "viewer.export.unknown": "不明", + "viewer.export.routeFailed": "ルートシェアカードのエクスポートに失敗しました: {message}", + "viewer.export.reachFailed": "到達範囲シェアカードのエクスポートに失敗しました: {message}", + "viewer.export.copyFailed": "コピーに失敗しました: {message}", + "viewer.export.copiedPng": "PNGをクリップボードにコピーしました", + "viewer.export.downloadedRoute": "ルートシェアカードをダウンロードしました", + "viewer.export.downloadedReach": "到達範囲シェアカードをダウンロードしました", + "viewer.export.downloadedWebm": "WebMをダウンロードしました", + "viewer.export.recording": "6秒間のモーションを録画中…", + "viewer.export.card.routeSummary.one": "ルート: {source} → {target} · {count} 個の有向ホップ", + "viewer.export.card.routeSummary.other": "ルート: {source} → {target} · {count} 個の有向ホップ", + "viewer.export.card.reachSummary": "{origin} からの編集済み{direction} · {nodes} · {links} · 最大{hops}", + "viewer.export.card.node.one": "{count} 個のノード", + "viewer.export.card.node.other": "{count} 個のノード", + "viewer.export.card.link.one": "{count} 個の接続", + "viewer.export.card.link.other": "{count} 個の接続", + "viewer.export.card.hop.one": "{count} ホップ", + "viewer.export.card.hop.other": "{count} ホップ", + "viewer.export.card.routeBadge": "ARCHIFY · ROUTE · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · {direction} REACH", + "viewer.export.direction.upstream": "アップストリーム", + "viewer.export.direction.downstream": "ダウンストリーム", + "viewer.export.error.canvasUnavailable": "{label} のキャンバスを利用できません", + "viewer.export.error.contextUnavailable": "{label} の2Dキャンバスコンテキストを利用できません", + "viewer.export.error.toBlobUnavailable": "{label} でcanvas.toBlobを利用できません", + "viewer.export.error.toBlobNull": "{label} のcanvas.toBlobがデータを返しませんでした", + "viewer.export.error.variantsCombined": "シェアカードの種類は組み合わせられません", + "viewer.export.error.viewerState": "シェアカードのエクスポートで一時的なビューアー状態を削除できませんでした", + "viewer.export.error.routeState": "ルートカードのエクスポートで解決済みルートを安全に保持できませんでした", + "viewer.export.error.reachState": "到達範囲カードのエクスポートで編集済み到達範囲を安全に保持できませんでした", + "viewer.export.error.webmRequirements": "WebMモーションのエクスポートにはトレースアニメーションとブラウザのMediaRecorder対応が必要です", + "viewer.export.error.mediaRecorder": "MediaRecorderが失敗しました", + "viewer.export.error.emptyWebm": "MediaRecorderが空のWebMを生成しました", + "viewer.export.error.webmBackground": "WebMエクスポート用のSVG背景を読み込めませんでした", + "viewer.guide.eyebrow": "ダイアグラムガイド", + "viewer.guide.close": "ダイアグラムガイドを閉じる", + "viewer.guide.inspecting": "コンパイル済みセマンティクスを検査中", + "viewer.guide.actions": "ダイアグラム探索アクション", + "viewer.guide.find": "任意のノードを検索", + "viewer.guide.find.hint": "ラベル、役割、種類、固定IDで検索します。", + "viewer.guide.route": "ルートをトレース", + "viewer.guide.route.aria": "有向ルートをトレース", + "viewer.guide.route.hint": "2つのセマンティックノードが編集済みの方向でどう接続されているかを確認します。", + "viewer.guide.map": "システム全体を表示", + "viewer.guide.map.hint": "ライブビューポートと固定ノードを持つセマンティックレーダーを開きます。", + "viewer.guide.lens": "セマンティックな種類を比較", + "viewer.guide.lens.hint": "役割を数え、そのトラフィックを表示し、直接編集されたリンクを比較します。", + "viewer.guide.present": "プレゼンテーションステージに入る", + "viewer.guide.present.hint": "エクスポートを変更せずに、ライブダイアグラムにビューポート全体を割り当てます。", + "viewer.guide.shortcuts": "その他のキーボードショートカット", + "viewer.guide.shortcut.export": "エクスポート", + "viewer.guide.shortcut.theme": "テーマ", + "viewer.guide.shortcut.style": "スタイル", + "viewer.guide.shortcut.reset": "リセット", + "viewer.guide.shortcut.zoomIn": "拡大", + "viewer.guide.shortcut.zoomOut": "縮小", + "viewer.guide.shortcut.close": "閉じる", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} 個のセマンティックノード", + "viewer.guide.fact.node.other": "{count} 個のセマンティックノード", + "viewer.guide.fact.relationship.one": "{count} 件の関係", + "viewer.guide.fact.relationship.other": "{count} 件の関係", + "viewer.guide.open": "ダイアグラムガイドを開く", + "viewer.finder.title": "ノードを検索", + "viewer.finder.close": "ノード検索を閉じる", + "viewer.finder.placeholder": "ラベルまたはIDで検索", + "viewer.finder.search": "ダイアグラムノードを検索", + "viewer.finder.results": "ダイアグラムノード", + "viewer.finder.empty": "一致するノードがありません", + "viewer.finder.result.focus": "{label} にフォーカス", + "viewer.finder.result.routeStart": "{label} をルートの起点として選択", + "viewer.finder.result.routeTarget": "{label} をルートの終点として選択、{links}", + "viewer.finder.status.empty": "一致するノードがありません", + "viewer.finder.status.count.one": "一致するノード {count} 個", + "viewer.finder.status.count.other": "一致するノード {count} 個", + "viewer.finder.noun.nodes": "ノード", + "viewer.finder.link.one": "{count} 件の接続", + "viewer.finder.link.other": "{count} 件の接続", + "viewer.finder.result.focus.one": "{label} にフォーカス、関連する接続 {count} 件", + "viewer.finder.result.focus.other": "{label} にフォーカス、関連する接続 {count} 件", + "viewer.finder.status.filtered": "{available} 件の{noun}中 {visible} 件", + "viewer.finder.status.all": "{available} 件の{noun}", + "viewer.passport.eyebrow": "セマンティックパスポート", + "viewer.passport.metadata": "ノードメタデータ", + "viewer.passport.evidence": "検証済みソースの根拠", + "viewer.passport.verified": "検証済みソース", + "viewer.passport.verificationScope": "固定されたリビジョン時点のローカルGitに対して検証済みです。リモートアクセスは確認していません。", + "viewer.passport.reach": "編集済みの到達範囲", + "viewer.passport.reach.trace": "編集済みの到達可能性をトレース", + "viewer.passport.upstream": "アップストリーム", + "viewer.passport.downstream": "ダウンストリーム", + "viewer.passport.upstream.trace": "アップストリームの編集済み到達可能性をトレース", + "viewer.passport.downstream.trace": "ダウンストリームの編集済み到達可能性をトレース", + "viewer.passport.close": "セマンティックパスポートを閉じる", + "viewer.passport.copy": "リンクをコピー", + "viewer.passport.copy.focus": "フォーカス中のノードへのリンクをコピー", + "viewer.passport.relations": "関係", + "viewer.passport.relations.show": "関連する関係を表示", + "viewer.passport.relations.hide": "関連する関係を非表示", + "viewer.passport.relations.list": "関連する関係", + "viewer.passport.copyRelation": "関係をコピー", + "viewer.passport.copyNode": "ノードをコピー", + "viewer.passport.copyPinned": "固定された関係へのリンクをコピー", + "viewer.passport.copySource": "ソースノードへのリンクをコピー", + "viewer.passport.copy.focused.success": "フォーカス中のノードのリンクをコピーしました", + "viewer.passport.copy.pinned.success": "固定された関係のリンクをコピーしました", + "viewer.passport.copy.focused.failed": "フォーカス中のノードのリンクをコピーできませんでした", + "viewer.passport.copy.pinned.failed": "固定された関係のリンクをコピーできませんでした", + "viewer.passport.relationship.none": "関連する関係がありません", + "viewer.passport.relationship.count.one": "{count} 件の関係", + "viewer.passport.relationship.count.other": "{count} 件の関係", + "viewer.passport.relationship.show.one": "関連する関係 {count} 件を表示", + "viewer.passport.relationship.show.other": "関連する関係 {count} 件を表示", + "viewer.passport.relationship.summary": "出向 {out} · 入向 {in}{loops}", + "viewer.passport.relationship.loops": " · 自己ループ {count} 件", + "viewer.passport.relationship.explorer": "直接関係エクスプローラー", + "viewer.passport.relationship.help": "矢印キーで関係を探索します。EnterまたはSpaceで詳細を固定し、Escapeで解除します。", + "viewer.passport.relationship.loopsBack": "ループして戻る", + "viewer.passport.relationship.connectsTo": "接続先", + "viewer.passport.relationship.connectsFrom": "接続元", + "viewer.passport.relationship.pinned": "固定された関係 · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "第 {index}/{total} 件の関係を検査: {from} から {to} へ、{label}。詳細はEnterを押してください。", + "viewer.passport.relationship.group.out": "出向", + "viewer.passport.relationship.group.in": "入向", + "viewer.passport.relationship.group.loop": "自己ループ", + "viewer.passport.relationship.row": "{group}: {relationship}、{neighbor}", + "viewer.passport.relationship.direction.out": "出 →", + "viewer.passport.relationship.direction.in": "← 入", + "viewer.passport.relationship.direction.loop": "ループ", + "viewer.passport.sourceCount.one": "検証済みソース参照 {count} 件", + "viewer.passport.sourceCount.other": "検証済みソース参照 {count} 件", + "viewer.passport.sourceMarker": "出典", + "viewer.passport.beacon.one": "検証済みソース {count} 件。このノードにフォーカスして確認", + "viewer.passport.beacon.other": "検証済みソース {count} 件。このノードにフォーカスして確認", + "viewer.passport.repository.open": "検証済みリポジトリのリビジョン {revision} を開く", + "viewer.passport.source.open": "リビジョン {revision} の検証済みソース {path} を開く", + "viewer.passport.source.openLink": "開く ↗", + "viewer.passport.reach.upstream.one": "アップストリームの編集済みノード {count} 件をトレース", + "viewer.passport.reach.upstream.other": "アップストリームの編集済みノード {count} 件をトレース", + "viewer.passport.reach.downstream.one": "ダウンストリームの編集済みノード {count} 件をトレース", + "viewer.passport.reach.downstream.other": "ダウンストリームの編集済みノード {count} 件をトレース", + "viewer.passport.reach.noUpstream": "アップストリームの編集済みノードがありません", + "viewer.passport.reach.noDownstream": "ダウンストリームの編集済みノードがありません", + "viewer.passport.reach.status": "{direction} · ノード {nodes} 件 · 接続 {links} 件 · 最大 {hops} ホップ", + "viewer.route.eyebrow": "ルートプローブ", + "viewer.route.start": "開始ノードを選択", + "viewer.route.start.find": "開始点を検索", + "viewer.route.start.find.aria": "ルートの開始点を検索", + "viewer.route.copy": "リンクをコピー", + "viewer.route.copy.aria": "トレース済みルートへのリンクをコピー", + "viewer.route.clear": "クリア", + "viewer.route.clear.aria": "ルートプローブをクリア", + "viewer.route.traced": "トレース済みルート", + "viewer.route.pickTwo": "ダイアグラム上で2つのセマンティックノードを選択してください", + "viewer.route.pickOne": "ダイアグラム上でセマンティックノードを1つ選択してください", + "viewer.route.controls": "ルートジャーニーのコントロール", + "viewer.route.previous": "前のルート位置", + "viewer.route.play": "ルートジャーニーを再生", + "viewer.route.pause": "ルートジャーニーを一時停止", + "viewer.route.replay": "ルートジャーニーをリプレイ", + "viewer.route.next": "次のルート位置", + "viewer.route.journey": "ジャーニー", + "viewer.route.pause.label": "一時停止", + "viewer.route.replay.label": "リプレイ", + "viewer.route.overview": "概要", + "viewer.route.overview.aria": "ルート全体の概要を表示", + "viewer.route.instructions": "起点を選択してから終点を選択してください。方向が重要です。", + "viewer.route.destination": "{label} からの終点を選択", + "viewer.route.destination.find": "終点を検索", + "viewer.route.destination.find.aria": "到達可能なルートの終点を検索", + "viewer.route.differentDestination": "別の終点を選択", + "viewer.route.distinct": "ルートには2つの異なるセマンティックノードが必要です。", + "viewer.route.unreachable": "{label} への有向ルートがありません", + "viewer.route.unreachable.detail": "{source} から {target} には到達できません。ハイライトされた終点を選択してください。", + "viewer.route.start.instructions": "起点を選択してください。次のステップでは有向の終点のみが表示されます。", + "viewer.route.copy.success": "トレース済みルートのリンクをコピーしました", + "viewer.route.copy.failed": "ルートのリンクをコピーできませんでした", + "viewer.route.position": "ルート位置 {index}/{total}: {label}", + "viewer.route.step": "ステップ {index}/{total} · {phase} · {label}", + "viewer.route.motionRequired": "自動ジャーニーにはライブモーションが必要です", + "viewer.route.trigger.clear": "トレース済みルートをクリア", + "viewer.route.overview.status": "{nodes} · {hops} · 最短の編集済みルート", + "viewer.route.overview.node.one": "{count} 個のノード", + "viewer.route.overview.node.other": "{count} 個のノード", + "viewer.route.overview.hop.one": "{count} 有向ホップ", + "viewer.route.overview.hop.other": "{count} 有向ホップ", + "viewer.route.phase.playing": "再生中", + "viewer.route.phase.complete": "完了", + "viewer.route.phase.inspecting": "検査中", + "viewer.route.destination.count.one": "利用可能な有向終点 {count} 件。ハイライトされたノードを選択してください。", + "viewer.route.destination.count.other": "利用可能な有向終点 {count} 件。ハイライトされたノードを選択してください。", + "viewer.route.noOutgoing": "ここから始まる出向ルートはありません。クリアして別の起点を選択してください。", + "viewer.route.result.title": "{source} から {target}", + "viewer.route.finder.source.title": "ルートの開始点を選択", + "viewer.route.finder.source.placeholder": "ルートの起点を検索", + "viewer.route.finder.source.empty": "一致するルートの起点がありません", + "viewer.route.finder.source.results": "ルートを開始できるノード", + "viewer.route.finder.source.noun": "ルートの起点", + "viewer.route.finder.source.badge": "起点", + "viewer.route.finder.target.title": "{label} からの終点", + "viewer.route.finder.target.placeholder": "到達可能な終点を検索", + "viewer.route.finder.target.empty": "一致する到達可能な終点がありません", + "viewer.route.finder.target.results": "到達可能なルートの終点", + "viewer.route.finder.target.noun": "到達可能な終点", + "viewer.route.hop.one": "{count} ホップ", + "viewer.route.hop.other": "{count} ホップ", + "viewer.lens.eyebrow": "セマンティックレンズ", + "viewer.lens.title": "システムの役割を比較", + "viewer.lens.close": "セマンティックレンズを閉じる", + "viewer.lens.instruction": "セマンティックな種類を最大2つ選択してください。1つ選ぶと実際のトラフィックが表示され、2つ選ぶと直接の編集済み関係のみを比較します。", + "viewer.lens.kinds": "セマンティックな種類", + "viewer.lens.choose": "種類を選択すると、そのノードと接する関係を検査できます。", + "viewer.lens.copy": "セマンティックレンズへのリンクをコピー", + "viewer.lens.clear": "セマンティックレンズをクリア", + "viewer.lens.open": "セマンティックレンズを開く", + "viewer.lens.openActive": "アクティブなセマンティックレンズを開く", + "viewer.lens.legend": "セマンティック凡例", + "viewer.lens.legend.inspect.one": "{label} を検査、{count} 個のノード", + "viewer.lens.legend.inspect.other": "{label} を検査、{count} 個のノード", + "viewer.lens.kind.count.one": "{label}、{count} 個のノード", + "viewer.lens.kind.count.other": "{label}、{count} 個のノード", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · 直接関係 {count} 件", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · 直接関係 {count} 件", + "viewer.lens.single": "{nodes} · {relationships} · 接続された周辺ノードは表示されたままです", + "viewer.lens.node.one": "{label} ノード {count} 個", + "viewer.lens.node.other": "{label} ノード {count} 個", + "viewer.lens.relationship.one": "接する関係 {count} 件", + "viewer.lens.relationship.other": "接する関係 {count} 件", + "viewer.radar.title": "セマンティックレーダー", + "viewer.radar.building": "概要を構築中", + "viewer.radar.openFull": "完全なセマンティックレーダーを開く", + "viewer.radar.open": "レーダーを開く", + "viewer.radar.close": "セマンティックレーダーを閉じる", + "viewer.radar.surface": "ダイアグラムの概要です。ノードをクリックしてフォーカスするか、矢印キーでパンできます。", + "viewer.radar.click": "ノードをクリック", + "viewer.radar.drag": "ドラッグしてパン", + "viewer.radar.space": "セマンティックレーダーにはさらにMAPスペースが必要です。", + "viewer.radar.nodes": "セマンティックダイアグラムのレーダーノード", + "viewer.radar.focus": "セマンティックレーダーから {label} にフォーカス", + "viewer.radar.status": "{count} 個のノード · {viewport}", + "viewer.radar.fullMap": "{count} 個のノード · 全体マップ", + "viewer.radar.compacted": "セマンティックパスポートやMAPコントロールを覆わないよう、レーダーを縮小しました。", + "viewer.radar.cancelWaiting": "MAPスペースの確保待ちのセマンティックレーダーをキャンセル", + "viewer.radar.needsSpace": "セマンティックレーダーには表示可能なMAPスペースがさらに必要です", + "viewer.radar.viewport.full": "全体マップ", + "viewer.radar.viewport.width": "幅 {percent}%", + "viewer.radar.viewport.scale": "ビューポート {percent}%", + "viewer.nav.controls": "ダイアグラム表示コントロール", + "viewer.nav.route": "有向ルートをトレース", + "viewer.nav.route.title": "ルートをトレース (R)", + "viewer.nav.route.short": "パス", + "viewer.nav.radar": "セマンティックレーダーを開く", + "viewer.nav.radar.title": "セマンティックレーダー (M)", + "viewer.nav.radar.short": "マップ", + "viewer.nav.lens": "セマンティックレンズを開く", + "viewer.nav.lens.title": "セマンティックレンズ (L)", + "viewer.nav.lens.short": "レンズ", + "viewer.nav.find": "ノードを検索", + "viewer.nav.find.title": "ノードを検索 (/)", + "viewer.nav.guide": "ダイアグラムガイドを開く", + "viewer.nav.guide.title": "ダイアグラムガイド (?)", + "viewer.nav.zoomOut": "縮小", + "viewer.nav.zoomOut.title": "縮小 (-)", + "viewer.nav.reset": "ダイアグラム表示をリセット", + "viewer.nav.reset.title": "表示をリセット (0)", + "viewer.nav.read": "閲覧", + "viewer.nav.zoomIn": "拡大", + "viewer.nav.zoomIn.title": "拡大 (+)", + "viewer.nav.camera": "{hint}。ダイアグラム表示をリセット", + "viewer.nav.camera.title": "{semantic}{hint} · 表示をリセット (0)", + "viewer.nav.camera.semantic": "セマンティックカメラ有効 · ", + "viewer.nav.level.map": "マップ", + "viewer.nav.level.read": "閲覧", + "viewer.nav.level.full": "全体", + "viewer.nav.level.auto": "自動", + "viewer.nav.detail.map": "拡大すると関係ラベルとノードのコンテキストが表示されます", + "viewer.nav.detail.read": "さらに拡大するとタグと注釈が表示されます", + "viewer.nav.detail.full": "ダイアグラムの完全な詳細", + "viewer.intent.summary": "{label}。出向 {out} 件、入向 {in} 件{loops}。合計 {total} 件の接続。詳細はEnterを押してください。", + "viewer.intent.loops": "、自己ループ {count} 件", + "viewer.common.copied": "コピーしました", + "viewer.common.copyFailed": "コピーに失敗しました", + "viewer.common.copyLink": "リンクをコピー", + "viewer.common.clear": "クリア", + "viewer.common.close": "閉じる" +} diff --git a/vendor/archify/examples/locales/ko.json b/vendor/archify/examples/locales/ko.json new file mode 100644 index 0000000..9ba33d6 --- /dev/null +++ b/vendor/archify/examples/locales/ko.json @@ -0,0 +1,440 @@ +{ + "page.title": "{title} 다이어그램", + "diagram.description.architecture": "Archify로 생성한 아키텍처 다이어그램입니다.", + "diagram.description.workflow": "Archify로 생성한 워크플로우 다이어그램입니다.", + "diagram.description.sequence": "Archify로 생성한 시퀀스 다이어그램입니다.", + "diagram.description.dataflow": "Archify로 생성한 데이터플로우 다이어그램입니다.", + "diagram.description.lifecycle": "Archify로 생성한 라이프사이클 다이어그램입니다.", + "node.focus": "{label} 포커스", + "node.focus.detail": "{label} 포커스, {detail}", + "node.context.architecture": "아키텍처 구성요소", + "node.context.workflow": "워크플로우 노드", + "node.context.sequence": "시퀀스 참가자", + "node.context.dataflow": "데이터플로우 노드", + "node.context.lifecycle": "라이프사이클 상태", + "legend.title": "범례", + "legend.architecture.frontend": "프런트엔드", + "legend.architecture.backend": "백엔드", + "legend.architecture.database": "데이터베이스", + "legend.architecture.cloud": "클라우드", + "legend.architecture.security": "보안", + "legend.architecture.messagebus": "메시지 버스", + "legend.architecture.external": "외부", + "legend.workflow.frontend": "사용자 UI", + "legend.workflow.backend": "Agent 로직", + "legend.workflow.security": "정책", + "legend.workflow.messagebus": "도구 실행", + "legend.workflow.database": "컨텍스트 / 추적", + "legend.workflow.cloud": "클라우드 서비스", + "legend.workflow.external": "외부 시스템", + "legend.sequence.emphasis": "주요 요청", + "legend.sequence.return": "반환", + "legend.sequence.security": "보안", + "legend.sequence.dashed": "비동기 추적", + "legend.sequence.default": "메시지", + "legend.dataflow.emphasis": "주요 데이터", + "legend.dataflow.security": "정책 / PII", + "legend.dataflow.dashed": "비동기 배치", + "legend.dataflow.database": "데이터 저장소", + "legend.dataflow.default": "데이터 흐름", + "legend.lifecycle.start": "초기 상태", + "legend.lifecycle.active": "활성", + "legend.lifecycle.waiting": "대기", + "legend.lifecycle.decision": "판단", + "legend.lifecycle.success": "성공", + "legend.lifecycle.failure": "실패 / 종료", + "legend.lifecycle.neutral": "중립 상태", + "legend.lifecycle.external": "외부 상태", + "legend.lifecycle.final": "종료 상태", + "viewer.kind.frontend": "프런트엔드", + "viewer.kind.backend": "백엔드", + "viewer.kind.database": "데이터베이스", + "viewer.kind.cloud": "클라우드", + "viewer.kind.security": "보안", + "viewer.kind.messagebus": "메시지 버스", + "viewer.kind.external": "외부", + "viewer.kind.neutral": "중립", + "viewer.kind.node": "노드", + "viewer.kind.start": "시작", + "viewer.kind.active": "활성", + "viewer.kind.waiting": "대기", + "viewer.kind.decision": "판단", + "viewer.kind.success": "성공", + "viewer.kind.failure": "실패", + "viewer.toolbar.actions": "다이어그램 동작", + "viewer.theme.toggle.title": "테마 전환 (T)", + "viewer.theme.toggle": "색상 테마 전환", + "viewer.theme.dark": "다크", + "viewer.theme.light": "라이트", + "viewer.preset.choose.title": "시각 스타일 선택 (S로 순환)", + "viewer.preset.choose": "시각 스타일 선택", + "viewer.preset.style": "스타일", + "viewer.preset.menu": "시각 스타일", + "viewer.preset.identity": "시각적 정체성", + "viewer.preset.cycles": "S로 순환", + "viewer.preset.classic": "클래식", + "viewer.preset.classic.short": "클래식", + "viewer.preset.classic.hint": "안정적인 기술 기본 스타일", + "viewer.preset.flow": "시그널 플로우", + "viewer.preset.flow.short": "플로우", + "viewer.preset.flow.hint": "동적 흐름을 강조하는 프레젠테이션", + "viewer.preset.blueprint": "블루프린트", + "viewer.preset.blueprint.hint": "엔지니어링 리뷰용", + "viewer.preset.editorial": "에디토리얼", + "viewer.preset.editorial.hint": "발행 및 출시 노트에 적합", + "viewer.preset.badge.signalFlow": "시그널 플로우", + "viewer.preset.badge.blueprint": "블루프린트 / 개정 01", + "viewer.preset.badge.editorial": "에디토리얼 / 현장 노트", + "viewer.preset.badge.editorialPlate": "ARCHIFY / 도판 04", + "viewer.preset.current": "현재 시각 스타일: {style}. 시각 스타일 선택", + "viewer.motion.live": "동적", + "viewer.motion.still": "정적", + "viewer.motion.pause": "동작 일시정지", + "viewer.motion.resume": "동작 재개", + "viewer.motion.reduced": "동작 최소화 설정으로 인해 동작이 일시정지되었습니다", + "viewer.motion.hidden": "페이지가 보이지 않는 동안 동작이 일시정지되었습니다", + "viewer.motion.yielding": "동작 일시정지; 현재 {owner}에 양보 중", + "viewer.motion.yielding.title": "실시간 미리보기 활성화 · {owner}에 양보 중", + "viewer.owner.route": "경로 탐색", + "viewer.owner.lens": "시맨틱 렌즈", + "viewer.owner.relationship": "관계 미리보기", + "viewer.owner.intent": "의도 추적", + "viewer.owner.focus": "시맨틱 포커스", + "viewer.owner.legend": "범례 미리보기", + "viewer.owner.reader": "읽는 이의 상호작용", + "viewer.present.enter": "프레젠테이션 모드 진입", + "viewer.present.enter.title": "프레젠테이션 모드 (F)", + "viewer.present.exit": "프레젠테이션 모드 종료", + "viewer.present.exit.title": "프레젠테이션 모드 종료 (F 또는 Escape)", + "viewer.present.present": "프레젠테이션", + "viewer.present.exit.label": "종료", + "viewer.export.button": "내보내기", + "viewer.export.button.title": "다이어그램 내보내기 (E)", + "viewer.export.diagram": "다이어그램 내보내기", + "viewer.export.menu": "내보내기", + "viewer.export.subtitle": "가볍고 깔끔한 출력물", + "viewer.export.share": "공유", + "viewer.export.shareCard": "공유 카드", + "viewer.export.routeShareCard": "경로 공유 카드", + "viewer.export.reachShareCard": "도달범위 공유 카드", + "viewer.export.copyDiagram": "다이어그램 복사", + "viewer.export.clipboardPng": "PNG를 클립보드로", + "viewer.export.raster": "래스터 이미지", + "viewer.export.image": "이미지", + "viewer.export.lossless": "무손실 이미지", + "viewer.export.compact": "압축 이미지", + "viewer.export.modern": "최신 이미지 포맷", + "viewer.export.vectorMotion": "벡터 및 동작", + "viewer.export.vectorMotion.heading": "벡터 및 동작", + "viewer.export.editable": "편집 가능한 벡터", + "viewer.export.motion6s": "6초 동작", + "viewer.export.unsupported": "이 브라우저에서 지원하지 않습니다", + "viewer.export.clipboardUnsupported": "이 브라우저는 이미지 클립보드 쓰기를 지원하지 않습니다", + "viewer.export.clipboardUnsupported.short": "이 브라우저는 이미지 클립보드 쓰기를 지원하지 않습니다.", + "viewer.export.motionUnavailable": "이 브라우저에서는 동작을 캡처할 수 없습니다", + "viewer.export.webmUnavailable": "이 브라우저에서는 WebM을 사용할 수 없습니다", + "viewer.export.failed": "내보내기 실패: {message}", + "viewer.export.unknownVariant": "알 수 없는 공유 카드 유형: {variant}", + "viewer.export.routeRequired": "경로 공유 카드를 내보내려면 먼저 경로를 추적하세요", + "viewer.export.reachRequired": "도달범위 공유 카드를 내보내려면 먼저 편집된 도달범위를 추적하세요", + "viewer.export.unknown": "알 수 없음", + "viewer.export.routeFailed": "경로 공유 카드 내보내기 실패: {message}", + "viewer.export.reachFailed": "도달범위 공유 카드 내보내기 실패: {message}", + "viewer.export.copyFailed": "복사 실패: {message}", + "viewer.export.copiedPng": "PNG를 클립보드에 복사했습니다", + "viewer.export.downloadedRoute": "경로 공유 카드를 다운로드했습니다", + "viewer.export.downloadedReach": "도달범위 공유 카드를 다운로드했습니다", + "viewer.export.downloadedWebm": "WebM을 다운로드했습니다", + "viewer.export.recording": "6초 분량 동작을 녹화 중…", + "viewer.export.card.routeSummary.one": "경로: {source} → {target} · 방향성 홉 {count}개", + "viewer.export.card.routeSummary.other": "경로: {source} → {target} · 방향성 홉 {count}개", + "viewer.export.card.reachSummary": "{origin}에서 시작한 편집된 {direction} · {nodes} · {links} · 최대 {hops}", + "viewer.export.card.node.one": "노드 {count}개", + "viewer.export.card.node.other": "노드 {count}개", + "viewer.export.card.link.one": "연결 {count}개", + "viewer.export.card.link.other": "연결 {count}개", + "viewer.export.card.hop.one": "홉 {count}개", + "viewer.export.card.hop.other": "홉 {count}개", + "viewer.export.card.routeBadge": "ARCHIFY · 경로 · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · {direction} 도달범위", + "viewer.export.direction.upstream": "업스트림", + "viewer.export.direction.downstream": "다운스트림", + "viewer.export.error.canvasUnavailable": "{label}에 캔버스를 사용할 수 없습니다", + "viewer.export.error.contextUnavailable": "{label}에 2D 캔버스 컨텍스트를 만들 수 없습니다", + "viewer.export.error.toBlobUnavailable": "{label}에서 canvas.toBlob을 사용할 수 없습니다", + "viewer.export.error.toBlobNull": "{label}의 canvas.toBlob이 데이터를 반환하지 않았습니다", + "viewer.export.error.variantsCombined": "공유 카드 유형을 함께 조합할 수 없습니다", + "viewer.export.error.viewerState": "공유 카드 내보내기가 임시 뷰어 상태를 제거하지 못했습니다", + "viewer.export.error.routeState": "경로 카드 내보내기가 해석된 경로를 안전하게 보존하지 못했습니다", + "viewer.export.error.reachState": "도달범위 카드 내보내기가 편집된 도달범위를 안전하게 보존하지 못했습니다", + "viewer.export.error.webmRequirements": "WebM 동작 내보내기는 추적 애니메이션과 브라우저의 MediaRecorder 지원이 필요합니다", + "viewer.export.error.mediaRecorder": "MediaRecorder 실패", + "viewer.export.error.emptyWebm": "MediaRecorder가 빈 WebM을 생성했습니다", + "viewer.export.error.webmBackground": "WebM 내보내기용 SVG 배경을 불러오지 못했습니다", + "viewer.guide.eyebrow": "다이어그램 가이드", + "viewer.guide.close": "다이어그램 가이드 닫기", + "viewer.guide.inspecting": "컴파일된 시맨틱 구조 검사 중", + "viewer.guide.actions": "다이어그램 탐색 동작", + "viewer.guide.find": "아무 노드나 찾기", + "viewer.guide.find.hint": "라벨, 역할, 유형, 고정 ID로 검색합니다.", + "viewer.guide.route": "경로 추적", + "viewer.guide.route.aria": "방향성 경로 추적", + "viewer.guide.route.hint": "두 시맨틱 노드가 편집된 방향으로 어떻게 연결되는지 확인합니다.", + "viewer.guide.map": "전체 시스템 보기", + "viewer.guide.map.hint": "실시간 뷰포트와 고정 노드가 있는 시맨틱 레이더를 엽니다.", + "viewer.guide.lens": "시맨틱 유형 비교", + "viewer.guide.lens.hint": "역할 수를 세고, 트래픽을 표시하고, 직접 편집된 연결을 비교합니다.", + "viewer.guide.present": "프레젠테이션 모드 진입", + "viewer.guide.present.hint": "내보내기는 그대로 두고 실시간 다이어그램에 뷰포트 전체를 할애합니다.", + "viewer.guide.shortcuts": "추가 키보드 단축키", + "viewer.guide.shortcut.export": "내보내기", + "viewer.guide.shortcut.theme": "테마", + "viewer.guide.shortcut.style": "스타일", + "viewer.guide.shortcut.reset": "초기화", + "viewer.guide.shortcut.zoomIn": "확대", + "viewer.guide.shortcut.zoomOut": "축소", + "viewer.guide.shortcut.close": "닫기", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "시맨틱 노드 {count}개", + "viewer.guide.fact.node.other": "시맨틱 노드 {count}개", + "viewer.guide.fact.relationship.one": "관계 {count}개", + "viewer.guide.fact.relationship.other": "관계 {count}개", + "viewer.guide.open": "다이어그램 가이드 열기", + "viewer.finder.title": "노드 찾기", + "viewer.finder.close": "노드 찾기 닫기", + "viewer.finder.placeholder": "라벨 또는 ID로 검색", + "viewer.finder.search": "다이어그램 노드 검색", + "viewer.finder.results": "다이어그램 노드", + "viewer.finder.empty": "일치하는 노드가 없습니다", + "viewer.finder.result.focus": "{label} 포커스", + "viewer.finder.result.routeStart": "{label}을(를) 경로 시작점으로 선택", + "viewer.finder.result.routeTarget": "{label}을(를) 경로 도착점으로 선택, {links}", + "viewer.finder.status.empty": "일치하는 노드가 없습니다", + "viewer.finder.status.count.one": "일치하는 노드 {count}개", + "viewer.finder.status.count.other": "일치하는 노드 {count}개", + "viewer.finder.noun.nodes": "노드", + "viewer.finder.link.one": "연결 {count}개", + "viewer.finder.link.other": "연결 {count}개", + "viewer.finder.result.focus.one": "{label} 포커스, 관련 연결 {count}개", + "viewer.finder.result.focus.other": "{label} 포커스, 관련 연결 {count}개", + "viewer.finder.status.filtered": "{noun} {available}개 중 {visible}개", + "viewer.finder.status.all": "{noun} {available}개", + "viewer.passport.eyebrow": "시맨틱 패스포트", + "viewer.passport.metadata": "노드 메타데이터", + "viewer.passport.evidence": "검증된 소스 근거", + "viewer.passport.verified": "검증된 소스", + "viewer.passport.verificationScope": "고정된 리비전 기준 로컬 Git으로 검증했습니다. 원격 접근 권한은 확인하지 않았습니다.", + "viewer.passport.reach": "편집된 도달범위", + "viewer.passport.reach.trace": "편집된 도달가능성 추적", + "viewer.passport.upstream": "업스트림", + "viewer.passport.downstream": "다운스트림", + "viewer.passport.upstream.trace": "업스트림 편집 도달가능성 추적", + "viewer.passport.downstream.trace": "다운스트림 편집 도달가능성 추적", + "viewer.passport.close": "시맨틱 패스포트 닫기", + "viewer.passport.copy": "링크 복사", + "viewer.passport.copy.focus": "포커스된 노드의 링크 복사", + "viewer.passport.relations": "관계", + "viewer.passport.relations.show": "연결된 관계 표시", + "viewer.passport.relations.hide": "연결된 관계 숨기기", + "viewer.passport.relations.list": "연결된 관계", + "viewer.passport.copyRelation": "관계 복사", + "viewer.passport.copyNode": "노드 복사", + "viewer.passport.copyPinned": "고정된 관계의 링크 복사", + "viewer.passport.copySource": "소스 노드의 링크 복사", + "viewer.passport.copy.focused.success": "포커스된 노드 링크를 복사했습니다", + "viewer.passport.copy.pinned.success": "고정된 관계 링크를 복사했습니다", + "viewer.passport.copy.focused.failed": "포커스된 노드 링크를 복사할 수 없습니다", + "viewer.passport.copy.pinned.failed": "고정된 관계 링크를 복사할 수 없습니다", + "viewer.passport.relationship.none": "연결된 관계가 없습니다", + "viewer.passport.relationship.count.one": "관계 {count}개", + "viewer.passport.relationship.count.other": "관계 {count}개", + "viewer.passport.relationship.show.one": "연결된 관계 {count}개 표시", + "viewer.passport.relationship.show.other": "연결된 관계 {count}개 표시", + "viewer.passport.relationship.summary": "출향 {out}개 · 입향 {in}개{loops}", + "viewer.passport.relationship.loops": " · 자기순환 {count}개", + "viewer.passport.relationship.explorer": "직접 관계 탐색기", + "viewer.passport.relationship.help": "방향키로 관계를 탐색하세요. Enter 또는 Space로 상세정보를 고정하고, Escape로 지웁니다.", + "viewer.passport.relationship.loopsBack": "되돌아옴", + "viewer.passport.relationship.connectsTo": "연결 대상", + "viewer.passport.relationship.connectsFrom": "연결 출처", + "viewer.passport.relationship.pinned": "고정된 관계 · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "{total}개 중 {index}번 관계 검사: {from}에서 {to}로, {label}. Enter로 상세정보 확인.", + "viewer.passport.relationship.group.out": "출향", + "viewer.passport.relationship.group.in": "입향", + "viewer.passport.relationship.group.loop": "자기순환", + "viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "출 →", + "viewer.passport.relationship.direction.in": "← 입", + "viewer.passport.relationship.direction.loop": "순환", + "viewer.passport.sourceCount.one": "검증된 소스 참조 {count}개", + "viewer.passport.sourceCount.other": "검증된 소스 참조 {count}개", + "viewer.passport.sourceMarker": "출처", + "viewer.passport.beacon.one": "검증된 소스 {count}개; 이 노드를 포커스하여 확인", + "viewer.passport.beacon.other": "검증된 소스 {count}개; 이 노드를 포커스하여 확인", + "viewer.passport.repository.open": "검증된 저장소 리비전 {revision} 열기", + "viewer.passport.source.open": "리비전 {revision}의 검증된 소스 {path} 열기", + "viewer.passport.source.openLink": "열기 ↗", + "viewer.passport.reach.upstream.one": "업스트림 편집 노드 {count}개 추적", + "viewer.passport.reach.upstream.other": "업스트림 편집 노드 {count}개 추적", + "viewer.passport.reach.downstream.one": "다운스트림 편집 노드 {count}개 추적", + "viewer.passport.reach.downstream.other": "다운스트림 편집 노드 {count}개 추적", + "viewer.passport.reach.noUpstream": "업스트림 편집 노드가 없습니다", + "viewer.passport.reach.noDownstream": "다운스트림 편집 노드가 없습니다", + "viewer.passport.reach.status": "{direction} · 노드 {nodes}개 · 연결 {links}개 · 최대 {hops}홉", + "viewer.route.eyebrow": "경로 탐색", + "viewer.route.start": "시작 노드 선택", + "viewer.route.start.find": "시작점 찾기", + "viewer.route.start.find.aria": "경로 시작점 찾기", + "viewer.route.copy": "링크 복사", + "viewer.route.copy.aria": "추적된 경로의 링크 복사", + "viewer.route.clear": "지우기", + "viewer.route.clear.aria": "경로 탐색 지우기", + "viewer.route.traced": "추적된 경로", + "viewer.route.pickTwo": "다이어그램에서 시맨틱 노드 두 개를 선택하세요", + "viewer.route.pickOne": "다이어그램에서 시맨틱 노드 하나를 선택하세요", + "viewer.route.controls": "경로 이동 제어", + "viewer.route.previous": "이전 경로 위치", + "viewer.route.play": "경로 이동 재생", + "viewer.route.pause": "경로 이동 일시정지", + "viewer.route.replay": "경로 이동 다시 재생", + "viewer.route.next": "다음 경로 위치", + "viewer.route.journey": "이동 경로", + "viewer.route.pause.label": "일시정지", + "viewer.route.replay.label": "다시 재생", + "viewer.route.overview": "개요", + "viewer.route.overview.aria": "전체 경로 개요 표시", + "viewer.route.instructions": "출발지를 먼저 선택한 뒤 도착지를 선택하세요. 방향이 중요합니다.", + "viewer.route.destination": "{label}에서 출발할 도착지 선택", + "viewer.route.destination.find": "도착지 찾기", + "viewer.route.destination.find.aria": "도달 가능한 경로 도착지 찾기", + "viewer.route.differentDestination": "다른 도착지 선택", + "viewer.route.distinct": "경로에는 서로 다른 시맨틱 노드 두 개가 필요합니다.", + "viewer.route.unreachable": "{label}(으)로 가는 방향성 경로가 없습니다", + "viewer.route.unreachable.detail": "{source}에서 {target}에 도달할 수 없습니다. 강조 표시된 도착지를 선택하세요.", + "viewer.route.start.instructions": "출발지를 선택하세요. 다음 단계에서는 방향성으로 도달 가능한 도착지만 표시됩니다.", + "viewer.route.copy.success": "추적된 경로 링크를 복사했습니다", + "viewer.route.copy.failed": "경로 링크를 복사할 수 없습니다", + "viewer.route.position": "경로 위치 {total}개 중 {index}번: {label}", + "viewer.route.step": "{total}단계 중 {index}단계 · {phase} · {label}", + "viewer.route.motionRequired": "자동 이동에는 동적 모드가 필요합니다", + "viewer.route.trigger.clear": "추적된 경로 지우기", + "viewer.route.overview.status": "{nodes} · {hops} · 최단 편집 경로", + "viewer.route.overview.node.one": "노드 {count}개", + "viewer.route.overview.node.other": "노드 {count}개", + "viewer.route.overview.hop.one": "방향성 홉 {count}개", + "viewer.route.overview.hop.other": "방향성 홉 {count}개", + "viewer.route.phase.playing": "재생 중", + "viewer.route.phase.complete": "완료됨", + "viewer.route.phase.inspecting": "검사 중", + "viewer.route.destination.count.one": "방향성 도착지 {count}개 사용 가능. 강조 표시된 노드를 선택하세요.", + "viewer.route.destination.count.other": "방향성 도착지 {count}개 사용 가능. 강조 표시된 노드를 선택하세요.", + "viewer.route.noOutgoing": "여기서 시작하는 출향 경로가 없습니다. 지우고 다른 출발지를 선택하세요.", + "viewer.route.result.title": "{source}에서 {target}까지", + "viewer.route.finder.source.title": "경로 시작점 선택", + "viewer.route.finder.source.placeholder": "경로 출발지 검색", + "viewer.route.finder.source.empty": "일치하는 경로 출발지가 없습니다", + "viewer.route.finder.source.results": "경로를 시작할 수 있는 노드", + "viewer.route.finder.source.noun": "경로 출발지", + "viewer.route.finder.source.badge": "시작", + "viewer.route.finder.target.title": "{label}에서 출발할 도착지", + "viewer.route.finder.target.placeholder": "도달 가능한 도착지 검색", + "viewer.route.finder.target.empty": "일치하는 도달 가능한 도착지가 없습니다", + "viewer.route.finder.target.results": "도달 가능한 경로 도착지", + "viewer.route.finder.target.noun": "도달 가능한 도착지", + "viewer.route.hop.one": "홉 {count}개", + "viewer.route.hop.other": "홉 {count}개", + "viewer.lens.eyebrow": "시맨틱 렌즈", + "viewer.lens.title": "시스템 역할 비교", + "viewer.lens.close": "시맨틱 렌즈 닫기", + "viewer.lens.instruction": "시맨틱 유형을 최대 두 개까지 선택하세요. 하나를 선택하면 실제 트래픽을 표시하고, 두 개를 선택하면 직접 편집된 관계만 비교합니다.", + "viewer.lens.kinds": "시맨틱 유형", + "viewer.lens.choose": "유형을 선택해 해당 노드와 연결된 관계를 확인하세요.", + "viewer.lens.copy": "시맨틱 렌즈 링크 복사", + "viewer.lens.clear": "시맨틱 렌즈 지우기", + "viewer.lens.open": "시맨틱 렌즈 열기", + "viewer.lens.openActive": "활성 시맨틱 렌즈 열기", + "viewer.lens.legend": "시맨틱 범례", + "viewer.lens.legend.inspect.one": "{label} 검사, 노드 {count}개", + "viewer.lens.legend.inspect.other": "{label} 검사, 노드 {count}개", + "viewer.lens.kind.count.one": "{label}, 노드 {count}개", + "viewer.lens.kind.count.other": "{label}, 노드 {count}개", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · 직접 관계 {count}개", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · 직접 관계 {count}개", + "viewer.lens.single": "{nodes} · {relationships} · 연결된 주변 노드는 계속 표시됩니다", + "viewer.lens.node.one": "{label} 노드 {count}개", + "viewer.lens.node.other": "{label} 노드 {count}개", + "viewer.lens.relationship.one": "연결된 관계 {count}개", + "viewer.lens.relationship.other": "연결된 관계 {count}개", + "viewer.radar.title": "시맨틱 레이더", + "viewer.radar.building": "개요 구성 중", + "viewer.radar.openFull": "전체 시맨틱 레이더 열기", + "viewer.radar.open": "레이더 열기", + "viewer.radar.close": "시맨틱 레이더 닫기", + "viewer.radar.surface": "다이어그램 개요입니다. 노드를 클릭해 포커스하거나 방향키로 이동하세요.", + "viewer.radar.click": "노드 클릭", + "viewer.radar.drag": "드래그로 이동", + "viewer.radar.space": "시맨틱 레이더를 표시하려면 지도 공간이 더 필요합니다.", + "viewer.radar.nodes": "시맨틱 다이어그램 레이더 노드", + "viewer.radar.focus": "시맨틱 레이더에서 {label} 포커스", + "viewer.radar.status": "노드 {count}개 · {viewport}", + "viewer.radar.fullMap": "노드 {count}개 · 전체 지도", + "viewer.radar.compacted": "시맨틱 패스포트나 지도 컨트롤을 가리지 않도록 레이더를 축소했습니다.", + "viewer.radar.cancelWaiting": "지도 공간 확보를 기다리는 시맨틱 레이더 취소", + "viewer.radar.needsSpace": "시맨틱 레이더를 표시하려면 지도 공간이 더 필요합니다", + "viewer.radar.viewport.full": "전체 지도", + "viewer.radar.viewport.width": "너비 {percent}%", + "viewer.radar.viewport.scale": "뷰포트 {percent}%", + "viewer.nav.controls": "다이어그램 보기 제어", + "viewer.nav.route": "방향성 경로 추적", + "viewer.nav.route.title": "경로 추적 (R)", + "viewer.nav.route.short": "경로", + "viewer.nav.radar": "시맨틱 레이더 열기", + "viewer.nav.radar.title": "시맨틱 레이더 (M)", + "viewer.nav.radar.short": "지도", + "viewer.nav.lens": "시맨틱 렌즈 열기", + "viewer.nav.lens.title": "시맨틱 렌즈 (L)", + "viewer.nav.lens.short": "렌즈", + "viewer.nav.find": "노드 찾기", + "viewer.nav.find.title": "노드 찾기 (/)", + "viewer.nav.guide": "다이어그램 가이드 열기", + "viewer.nav.guide.title": "다이어그램 가이드 (?)", + "viewer.nav.zoomOut": "축소", + "viewer.nav.zoomOut.title": "축소 (-)", + "viewer.nav.reset": "다이어그램 보기 초기화", + "viewer.nav.reset.title": "보기 초기화 (0)", + "viewer.nav.read": "읽기", + "viewer.nav.zoomIn": "확대", + "viewer.nav.zoomIn.title": "확대 (+)", + "viewer.nav.camera": "{hint}. 다이어그램 보기 초기화", + "viewer.nav.camera.title": "{semantic}{hint} · 보기 초기화 (0)", + "viewer.nav.camera.semantic": "시맨틱 카메라 활성 · ", + "viewer.nav.level.map": "개요", + "viewer.nav.level.read": "읽기", + "viewer.nav.level.full": "전체", + "viewer.nav.level.auto": "자동", + "viewer.nav.detail.map": "확대하면 관계 라벨과 노드 컨텍스트가 표시됩니다", + "viewer.nav.detail.read": "다시 확대하면 태그와 주석이 표시됩니다", + "viewer.nav.detail.full": "다이어그램 전체 상세정보", + "viewer.intent.summary": "{label}. 출향 {out}개, 입향 {in}개{loops}. 연결 총 {total}개. Enter로 상세정보 확인.", + "viewer.intent.loops": ", 자기순환 {count}개", + "viewer.common.copied": "복사됨", + "viewer.common.copyFailed": "복사 실패", + "viewer.common.copyLink": "링크 복사", + "viewer.common.clear": "지우기", + "viewer.common.close": "닫기", + "viewer.export.svg.auto": "SVG · 자동", + "viewer.export.svg.auto.hint": "호스트 테마에 맞춤", + "viewer.export.svg.light": "SVG · 라이트", + "viewer.export.svg.light.hint": "항상 라이트 테마", + "viewer.export.svg.dark": "SVG · 다크", + "viewer.export.svg.dark.hint": "항상 다크 테마", + "viewer.guide.title": "이 시스템 살펴보기", + "viewer.focus.selectedNodes": "선택한 노드 {count}개", + "viewer.passport.move": "의미 정보 패널 이동. 드래그하거나 화살표 키를 사용하고, Home 키로 초기화하세요.", + "viewer.outline.title": "노드 목록", + "viewer.rail.show": "설명 및 목록", + "viewer.rail.controls": "측면 패널", + "viewer.rail.collapse": "측면 패널 접기", + "viewer.rail.bottom": "패널을 다이어그램 아래로 이동", + "viewer.rail.right": "패널을 다이어그램 옆으로 이동" +} diff --git a/vendor/archify/examples/locales/pt.json b/vendor/archify/examples/locales/pt.json new file mode 100644 index 0000000..e48a871 --- /dev/null +++ b/vendor/archify/examples/locales/pt.json @@ -0,0 +1,425 @@ +{ + "page.title": "Diagrama {title}", + "diagram.description.architecture": "Um diagrama de arquitetura gerado pelo Archify.", + "diagram.description.workflow": "Um diagrama de fluxo de trabalho gerado pelo Archify.", + "diagram.description.sequence": "Um diagrama de sequência gerado pelo Archify.", + "diagram.description.dataflow": "Um diagrama de fluxo de dados gerado pelo Archify.", + "diagram.description.lifecycle": "Um diagrama de ciclo de vida gerado pelo Archify.", + "node.focus": "Focar {label}", + "node.focus.detail": "Focar {label}, {detail}", + "node.context.architecture": "Componente de arquitetura", + "node.context.workflow": "Nó de fluxo de trabalho", + "node.context.sequence": "Participante da sequência", + "node.context.dataflow": "Nó de fluxo de dados", + "node.context.lifecycle": "Estado do ciclo de vida", + "legend.title": "Legenda", + "legend.architecture.frontend": "Frontend", + "legend.architecture.backend": "Backend", + "legend.architecture.database": "Banco de dados", + "legend.architecture.cloud": "Nuvem", + "legend.architecture.security": "Segurança", + "legend.architecture.messagebus": "Barramento de mensagens", + "legend.architecture.external": "Externo", + "legend.workflow.frontend": "UI do usuário", + "legend.workflow.backend": "Lógica do agente", + "legend.workflow.security": "Política", + "legend.workflow.messagebus": "Ação de ferramenta", + "legend.workflow.database": "Contexto / rastreamento", + "legend.workflow.cloud": "Serviço de nuvem", + "legend.workflow.external": "Sistema externo", + "legend.sequence.emphasis": "solicitação principal", + "legend.sequence.return": "retorno", + "legend.sequence.security": "segurança", + "legend.sequence.dashed": "rastreamento assíncrono", + "legend.sequence.default": "mensagem", + "legend.dataflow.emphasis": "dado principal", + "legend.dataflow.security": "política / PII", + "legend.dataflow.dashed": "lote assíncrono", + "legend.dataflow.database": "armazenamento de dados", + "legend.dataflow.default": "fluxo de dados", + "legend.lifecycle.start": "estado inicial", + "legend.lifecycle.active": "ativo", + "legend.lifecycle.waiting": "aguardando", + "legend.lifecycle.decision": "decisão", + "legend.lifecycle.success": "sucesso", + "legend.lifecycle.failure": "falha / saída", + "legend.lifecycle.neutral": "neutro", + "legend.lifecycle.external": "externo", + "legend.lifecycle.final": "estado final", + "viewer.kind.frontend": "Frontend", + "viewer.kind.backend": "Backend", + "viewer.kind.database": "Banco de dados", + "viewer.kind.cloud": "Nuvem", + "viewer.kind.security": "Segurança", + "viewer.kind.messagebus": "Barramento de mensagens", + "viewer.kind.external": "Externo", + "viewer.kind.neutral": "Neutro", + "viewer.kind.node": "Nó", + "viewer.kind.start": "Início", + "viewer.kind.active": "Ativo", + "viewer.kind.waiting": "Aguardando", + "viewer.kind.decision": "Decisão", + "viewer.kind.success": "Sucesso", + "viewer.kind.failure": "Falha", + "viewer.toolbar.actions": "Ações do diagrama", + "viewer.theme.toggle.title": "Alternar tema (T)", + "viewer.theme.toggle": "Alternar tema de cores", + "viewer.theme.dark": "Escuro", + "viewer.theme.light": "Claro", + "viewer.preset.choose.title": "Escolher estilo visual (S alterna)", + "viewer.preset.choose": "Escolher estilo visual", + "viewer.preset.style": "Estilo", + "viewer.preset.menu": "Estilo visual", + "viewer.preset.identity": "Identidade visual", + "viewer.preset.cycles": "S alterna", + "viewer.preset.classic": "Clássico", + "viewer.preset.classic.short": "Clássico", + "viewer.preset.classic.hint": "Padrão técnico estável", + "viewer.preset.flow": "Fluxo de Sinal", + "viewer.preset.flow.short": "Fluxo", + "viewer.preset.flow.hint": "Apresentação com foco em movimento", + "viewer.preset.blueprint": "Planta", + "viewer.preset.blueprint.hint": "Revisão de engenharia", + "viewer.preset.editorial": "Editorial", + "viewer.preset.editorial.hint": "Notas de publicação e lançamento", + "viewer.preset.badge.signalFlow": "FLUXO DE SINAL", + "viewer.preset.badge.blueprint": "PLANTA / REV 01", + "viewer.preset.badge.editorial": "EDITORIAL / NOTA DE CAMPO", + "viewer.preset.badge.editorialPlate": "ARCHIFY / PLACA 04", + "viewer.preset.current": "Estilo visual: {style}. Escolher estilo visual", + "viewer.motion.live": "Ao vivo", + "viewer.motion.still": "Estático", + "viewer.motion.pause": "Pausar movimento", + "viewer.motion.resume": "Retomar movimento", + "viewer.motion.reduced": "Movimento pausado pela preferência de movimento reduzido", + "viewer.motion.hidden": "Movimento pausado enquanto esta página está oculta", + "viewer.motion.yielding": "Pausar movimento; cedendo atualmente a {owner}", + "viewer.motion.yielding.title": "Pré-visualização ao vivo ativada · cedendo a {owner}", + "viewer.owner.route": "Sonda de Rota", + "viewer.owner.lens": "Lente Semântica", + "viewer.owner.relationship": "Pré-visualização de Relação", + "viewer.owner.intent": "Rastreamento de Intenção", + "viewer.owner.focus": "foco semântico", + "viewer.owner.legend": "pré-visualização da legenda", + "viewer.owner.reader": "interação do leitor", + "viewer.present.enter": "Entrar no palco de apresentação", + "viewer.present.enter.title": "Palco de apresentação (F)", + "viewer.present.exit": "Sair do palco de apresentação", + "viewer.present.exit.title": "Sair do palco de apresentação (F ou Escape)", + "viewer.present.present": "Apresentar", + "viewer.present.exit.label": "Sair", + "viewer.export.button": "Exportar", + "viewer.export.button.title": "Exportar diagrama (E)", + "viewer.export.diagram": "Exportar diagrama", + "viewer.export.menu": "Exportar", + "viewer.export.subtitle": "Saídas portáteis e limpas", + "viewer.export.share": "Compartilhar", + "viewer.export.shareCard": "Cartão de Compartilhamento", + "viewer.export.routeShareCard": "Cartão de Compartilhamento de Rota", + "viewer.export.reachShareCard": "Cartão de Compartilhamento de Alcance", + "viewer.export.copyDiagram": "Copiar diagrama", + "viewer.export.clipboardPng": "PNG para a área de transferência", + "viewer.export.raster": "Imagens raster", + "viewer.export.image": "Imagem", + "viewer.export.lossless": "Imagem sem perdas", + "viewer.export.compact": "Imagem compacta", + "viewer.export.modern": "Imagem moderna", + "viewer.export.vectorMotion": "Vetor e movimento", + "viewer.export.vectorMotion.heading": "Vetor e movimento", + "viewer.export.editable": "Vetor editável", + "viewer.export.motion6s": "Movimento de 6s", + "viewer.export.unsupported": "Não suportado por este navegador", + "viewer.export.clipboardUnsupported": "Este navegador não suporta gravação de imagem na área de transferência", + "viewer.export.clipboardUnsupported.short": "Gravação de imagem na área de transferência não suportada neste navegador.", + "viewer.export.motionUnavailable": "Captura de movimento indisponível neste navegador", + "viewer.export.webmUnavailable": "WebM indisponível neste navegador", + "viewer.export.failed": "Falha na exportação: {message}", + "viewer.export.unknownVariant": "Variante de Cartão de Compartilhamento desconhecida: {variant}", + "viewer.export.routeRequired": "Trace uma rota antes de exportar um Cartão de Compartilhamento de Rota", + "viewer.export.reachRequired": "Trace o alcance definido antes de exportar um Cartão de Compartilhamento de Alcance", + "viewer.export.unknown": "desconhecido", + "viewer.export.routeFailed": "Falha ao exportar o Cartão de Compartilhamento de Rota: {message}", + "viewer.export.reachFailed": "Falha ao exportar o Cartão de Compartilhamento de Alcance: {message}", + "viewer.export.copyFailed": "Falha ao copiar: {message}", + "viewer.export.copiedPng": "PNG copiado para a área de transferência", + "viewer.export.downloadedRoute": "Cartão de Compartilhamento de Rota baixado", + "viewer.export.downloadedReach": "Cartão de Compartilhamento de Alcance baixado", + "viewer.export.downloadedWebm": "WebM baixado", + "viewer.export.recording": "Gravando 6 segundos de movimento…", + "viewer.export.card.routeSummary.one": "Rota: {source} → {target} · {count} salto direcionado", + "viewer.export.card.routeSummary.other": "Rota: {source} → {target} · {count} saltos direcionados", + "viewer.export.card.reachSummary": "{direction} definido a partir de {origin} · {nodes} · {links} · máx {hops}", + "viewer.export.card.node.one": "{count} nó", + "viewer.export.card.node.other": "{count} nós", + "viewer.export.card.link.one": "{count} conexão", + "viewer.export.card.link.other": "{count} conexões", + "viewer.export.card.hop.one": "{count} salto", + "viewer.export.card.hop.other": "{count} saltos", + "viewer.export.card.routeBadge": "ARCHIFY · ROTA · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · ALCANCE {direction}", + "viewer.export.direction.upstream": "Upstream", + "viewer.export.direction.downstream": "Downstream", + "viewer.export.error.canvasUnavailable": "Canvas indisponível para {label}", + "viewer.export.error.contextUnavailable": "Contexto de canvas 2D indisponível para {label}", + "viewer.export.error.toBlobUnavailable": "canvas.toBlob indisponível para {label}", + "viewer.export.error.toBlobNull": "canvas.toBlob não retornou dados para {label}", + "viewer.export.error.variantsCombined": "As variantes do Cartão de Compartilhamento não podem ser combinadas", + "viewer.export.error.viewerState": "A exportação do Cartão de Compartilhamento não conseguiu remover o estado temporário do visualizador", + "viewer.export.error.routeState": "A exportação do Cartão de Rota não conseguiu preservar a rota resolvida com segurança", + "viewer.export.error.reachState": "A exportação do Cartão de Alcance não conseguiu preservar o alcance definido com segurança", + "viewer.export.error.webmRequirements": "A exportação de movimento em WebM requer uma animação de rastreamento e suporte a MediaRecorder no navegador", + "viewer.export.error.mediaRecorder": "Falha no MediaRecorder", + "viewer.export.error.emptyWebm": "O MediaRecorder produziu um WebM vazio", + "viewer.export.error.webmBackground": "Não foi possível carregar o plano de fundo SVG para a exportação em WebM", + "viewer.guide.eyebrow": "Guia do diagrama", + "viewer.guide.close": "Fechar guia do diagrama", + "viewer.guide.inspecting": "Inspecionando semântica compilada", + "viewer.guide.actions": "Ações de exploração do diagrama", + "viewer.guide.find": "Encontrar qualquer nó", + "viewer.guide.find.hint": "Pesquise rótulos, responsabilidades, tipos e IDs estáveis.", + "viewer.guide.route": "Traçar uma rota", + "viewer.guide.route.aria": "Traçar uma rota direcionada", + "viewer.guide.route.hint": "Descubra como dois nós semânticos se conectam na direção definida.", + "viewer.guide.map": "Ver o sistema inteiro", + "viewer.guide.map.hint": "Abra o Radar Semântico com uma janela de visualização ativa e nós estáveis.", + "viewer.guide.lens": "Comparar tipos semânticos", + "viewer.guide.lens.hint": "Conte papéis, revele seu tráfego e compare links diretos definidos.", + "viewer.guide.present": "Entrar no Palco de Apresentação", + "viewer.guide.present.hint": "Dê ao diagrama ao vivo a janela de visualização inteira sem alterar a exportação.", + "viewer.guide.shortcuts": "Atalhos de teclado adicionais", + "viewer.guide.shortcut.export": "Exportar", + "viewer.guide.shortcut.theme": "Tema", + "viewer.guide.shortcut.style": "Estilo", + "viewer.guide.shortcut.reset": "Redefinir", + "viewer.guide.shortcut.zoomIn": "Ampliar", + "viewer.guide.shortcut.zoomOut": "Reduzir", + "viewer.guide.shortcut.close": "Fechar", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} nó semântico", + "viewer.guide.fact.node.other": "{count} nós semânticos", + "viewer.guide.fact.relationship.one": "{count} relação", + "viewer.guide.fact.relationship.other": "{count} relações", + "viewer.guide.open": "Abrir guia do diagrama", + "viewer.finder.title": "Encontrar um nó", + "viewer.finder.close": "Fechar buscador de nós", + "viewer.finder.placeholder": "Pesquisar rótulos ou IDs", + "viewer.finder.search": "Pesquisar nós do diagrama", + "viewer.finder.results": "Nós do diagrama", + "viewer.finder.empty": "Nenhum nó correspondente", + "viewer.finder.result.focus": "Focar {label}", + "viewer.finder.result.routeStart": "Escolher {label} como início da rota", + "viewer.finder.result.routeTarget": "Escolher {label} como destino da rota, {links}", + "viewer.finder.status.empty": "Nenhum nó correspondente", + "viewer.finder.status.count.one": "{count} nó correspondente", + "viewer.finder.status.count.other": "{count} nós correspondentes", + "viewer.finder.noun.nodes": "nós", + "viewer.finder.link.one": "{count} conexão", + "viewer.finder.link.other": "{count} conexões", + "viewer.finder.result.focus.one": "Focar {label}, {count} conexão relacionada", + "viewer.finder.result.focus.other": "Focar {label}, {count} conexões relacionadas", + "viewer.finder.status.filtered": "{visible} de {available} {noun}", + "viewer.finder.status.all": "{available} {noun}", + "viewer.passport.eyebrow": "Passaporte semântico", + "viewer.passport.metadata": "Metadados do nó", + "viewer.passport.evidence": "Evidência de origem verificada", + "viewer.passport.verified": "Origem verificada", + "viewer.passport.verificationScope": "Verificado com o Git local na revisão fixada. O acesso remoto não foi verificado.", + "viewer.passport.reach": "Alcance definido", + "viewer.passport.reach.trace": "Traçar alcançabilidade definida", + "viewer.passport.upstream": "Upstream", + "viewer.passport.downstream": "Downstream", + "viewer.passport.upstream.trace": "Traçar alcançabilidade definida upstream", + "viewer.passport.downstream.trace": "Traçar alcançabilidade definida downstream", + "viewer.passport.close": "Fechar passaporte semântico", + "viewer.passport.copy": "Copiar link", + "viewer.passport.copy.focus": "Copiar link do nó em foco", + "viewer.passport.relations": "Relações", + "viewer.passport.relations.show": "Mostrar relações conectadas", + "viewer.passport.relations.hide": "Ocultar relações conectadas", + "viewer.passport.relations.list": "Relações conectadas", + "viewer.passport.copyRelation": "Copiar relação", + "viewer.passport.copyNode": "Copiar nó", + "viewer.passport.copyPinned": "Copiar link da relação fixada", + "viewer.passport.copySource": "Copiar link do nó de origem", + "viewer.passport.copy.focused.success": "Link do nó em foco copiado", + "viewer.passport.copy.pinned.success": "Link da relação fixada copiado", + "viewer.passport.copy.focused.failed": "Não foi possível copiar o link do nó em foco", + "viewer.passport.copy.pinned.failed": "Não foi possível copiar o link da relação fixada", + "viewer.passport.relationship.none": "Nenhuma relação conectada", + "viewer.passport.relationship.count.one": "{count} relação", + "viewer.passport.relationship.count.other": "{count} relações", + "viewer.passport.relationship.show.one": "Mostrar {count} relação conectada", + "viewer.passport.relationship.show.other": "Mostrar {count} relações conectadas", + "viewer.passport.relationship.summary": "{out} de saída · {in} de entrada{loops}", + "viewer.passport.relationship.loops": " · {count} laço", + "viewer.passport.relationship.explorer": "Explorador de relações diretas", + "viewer.passport.relationship.help": "Use as setas para explorar relações. Pressione Enter ou Espaço para fixar detalhes; Escape limpa.", + "viewer.passport.relationship.loopsBack": "retorna em laço", + "viewer.passport.relationship.connectsTo": "conecta-se a", + "viewer.passport.relationship.connectsFrom": "conecta-se de", + "viewer.passport.relationship.pinned": "Relação fixada · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "Inspecionar relação {index} de {total}: {from} para {to}, {label}. Pressione Enter para detalhes.", + "viewer.passport.relationship.group.out": "Saída", + "viewer.passport.relationship.group.in": "Entrada", + "viewer.passport.relationship.group.loop": "Laços próprios", + "viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "SAÍDA →", + "viewer.passport.relationship.direction.in": "← ENTRADA", + "viewer.passport.relationship.direction.loop": "LAÇO", + "viewer.passport.sourceCount.one": "{count} referência de origem verificada", + "viewer.passport.sourceCount.other": "{count} referências de origem verificadas", + "viewer.passport.sourceMarker": "ORIG", + "viewer.passport.beacon.one": "{count} origem verificada; foque este nó para inspecionar", + "viewer.passport.beacon.other": "{count} origens verificadas; foque este nó para inspecionar", + "viewer.passport.repository.open": "Abrir revisão verificada do repositório {revision}", + "viewer.passport.source.open": "Abrir origem verificada {path} na revisão {revision}", + "viewer.passport.source.openLink": "Abrir ↗", + "viewer.passport.reach.upstream.one": "Traçar {count} nó definido upstream", + "viewer.passport.reach.upstream.other": "Traçar {count} nós definidos upstream", + "viewer.passport.reach.downstream.one": "Traçar {count} nó definido downstream", + "viewer.passport.reach.downstream.other": "Traçar {count} nós definidos downstream", + "viewer.passport.reach.noUpstream": "Nenhum nó definido upstream", + "viewer.passport.reach.noDownstream": "Nenhum nó definido downstream", + "viewer.passport.reach.status": "{direction} · {nodes} nós · {links} conexões · máx {hops} saltos", + "viewer.route.eyebrow": "Sonda de rota", + "viewer.route.start": "Escolher um nó inicial", + "viewer.route.start.find": "Encontrar início", + "viewer.route.start.find.aria": "Encontrar um início de rota", + "viewer.route.copy": "Copiar link", + "viewer.route.copy.aria": "Copiar link da rota traçada", + "viewer.route.clear": "Limpar", + "viewer.route.clear.aria": "Limpar sonda de rota", + "viewer.route.traced": "Rota traçada", + "viewer.route.pickTwo": "Escolha dois nós semânticos no diagrama", + "viewer.route.pickOne": "Escolha um nó semântico no diagrama", + "viewer.route.controls": "Controles da jornada de rota", + "viewer.route.previous": "Posição anterior da rota", + "viewer.route.play": "Reproduzir jornada de rota", + "viewer.route.pause": "Pausar jornada de rota", + "viewer.route.replay": "Reproduzir jornada de rota novamente", + "viewer.route.next": "Próxima posição da rota", + "viewer.route.journey": "Jornada", + "viewer.route.pause.label": "Pausar", + "viewer.route.replay.label": "Repetir", + "viewer.route.overview": "Visão geral", + "viewer.route.overview.aria": "Mostrar visão geral completa da rota", + "viewer.route.instructions": "Escolha a origem e depois o destino. A direção importa.", + "viewer.route.destination": "Escolher um destino a partir de {label}", + "viewer.route.destination.find": "Encontrar destino", + "viewer.route.destination.find.aria": "Encontrar um destino de rota alcançável", + "viewer.route.differentDestination": "Escolher um destino diferente", + "viewer.route.distinct": "Uma rota precisa de dois nós semânticos distintos.", + "viewer.route.unreachable": "Nenhuma rota direcionada para {label}", + "viewer.route.unreachable.detail": "{target} não é alcançável a partir de {source}. Escolha um destino destacado.", + "viewer.route.start.instructions": "Selecione a origem. O próximo passo revelará apenas destinos direcionados.", + "viewer.route.copy.success": "Link da rota traçada copiado", + "viewer.route.copy.failed": "Não foi possível copiar o link da rota traçada", + "viewer.route.position": "Posição {index} de {total} na rota: {label}", + "viewer.route.step": "Passo {index} de {total} · {phase} · {label}", + "viewer.route.motionRequired": "A jornada automática requer movimento Ao vivo", + "viewer.route.trigger.clear": "Limpar rota traçada", + "viewer.route.overview.status": "{nodes} · {hops} · rota mais curta definida", + "viewer.route.overview.node.one": "{count} nó", + "viewer.route.overview.node.other": "{count} nós", + "viewer.route.overview.hop.one": "{count} salto direcionado", + "viewer.route.overview.hop.other": "{count} saltos direcionados", + "viewer.route.phase.playing": "Reproduzindo", + "viewer.route.phase.complete": "Concluído", + "viewer.route.phase.inspecting": "Inspecionando", + "viewer.route.destination.count.one": "{count} destino direcionado disponível. Escolha um nó destacado.", + "viewer.route.destination.count.other": "{count} destinos direcionados disponíveis. Escolha um nó destacado.", + "viewer.route.noOutgoing": "Nenhuma rota de saída começa aqui. Limpe e escolha outra origem.", + "viewer.route.result.title": "{source} até {target}", + "viewer.route.finder.source.title": "Escolher início da rota", + "viewer.route.finder.source.placeholder": "Pesquisar origens de rota", + "viewer.route.finder.source.empty": "Nenhuma origem de rota correspondente", + "viewer.route.finder.source.results": "Nós que podem iniciar uma rota", + "viewer.route.finder.source.noun": "origens de rota", + "viewer.route.finder.source.badge": "início", + "viewer.route.finder.target.title": "Destino a partir de {label}", + "viewer.route.finder.target.placeholder": "Pesquisar destinos alcançáveis", + "viewer.route.finder.target.empty": "Nenhum destino alcançável correspondente", + "viewer.route.finder.target.results": "Destinos de rota alcançáveis", + "viewer.route.finder.target.noun": "destinos alcançáveis", + "viewer.route.hop.one": "{count} salto", + "viewer.route.hop.other": "{count} saltos", + "viewer.lens.eyebrow": "Lente semântica", + "viewer.lens.title": "Comparar papéis do sistema", + "viewer.lens.close": "Fechar lente semântica", + "viewer.lens.instruction": "Escolha até dois tipos semânticos. Um revela seu tráfego real; dois comparam apenas relações diretas definidas.", + "viewer.lens.kinds": "Tipos semânticos", + "viewer.lens.choose": "Escolha um tipo para inspecionar seus nós e relações que o tocam.", + "viewer.lens.copy": "Copiar link da lente semântica", + "viewer.lens.clear": "Limpar lente semântica", + "viewer.lens.open": "Abrir lente semântica", + "viewer.lens.openActive": "Abrir lente semântica ativa", + "viewer.lens.legend": "Legenda semântica", + "viewer.lens.legend.inspect.one": "Inspecionar {label}, {count} nó", + "viewer.lens.legend.inspect.other": "Inspecionar {label}, {count} nós", + "viewer.lens.kind.count.one": "{label}, {count} nó", + "viewer.lens.kind.count.other": "{label}, {count} nós", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} relação direta", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} relações diretas", + "viewer.lens.single": "{nodes} · {relationships} · nós conectados permanecem visíveis", + "viewer.lens.node.one": "{count} nó {label}", + "viewer.lens.node.other": "{count} nós {label}", + "viewer.lens.relationship.one": "{count} relação que toca", + "viewer.lens.relationship.other": "{count} relações que tocam", + "viewer.radar.title": "Radar semântico", + "viewer.radar.building": "Construindo visão geral", + "viewer.radar.openFull": "Abrir radar semântico completo", + "viewer.radar.open": "Abrir radar", + "viewer.radar.close": "Fechar radar semântico", + "viewer.radar.surface": "Visão geral do diagrama. Clique em um nó para focá-lo, ou use as setas para navegar.", + "viewer.radar.click": "Clicar no nó", + "viewer.radar.drag": "Arrastar para navegar", + "viewer.radar.space": "O radar semântico precisa de mais espaço no MAPA.", + "viewer.radar.nodes": "Nós do radar do diagrama semântico", + "viewer.radar.focus": "Focar {label} a partir do Radar Semântico", + "viewer.radar.status": "{count} nós · {viewport}", + "viewer.radar.fullMap": "{count} nós · mapa completo", + "viewer.radar.compacted": "Radar compactado para não cobrir o Passaporte Semântico ou os controles do MAPA.", + "viewer.radar.cancelWaiting": "Cancelar radar semântico aguardando mais espaço no MAPA", + "viewer.radar.needsSpace": "O radar semântico precisa de mais espaço visível no MAPA", + "viewer.radar.viewport.full": "mapa completo", + "viewer.radar.viewport.width": "{percent}% de largura", + "viewer.radar.viewport.scale": "{percent}% da janela de visualização", + "viewer.nav.controls": "Controles de visualização do diagrama", + "viewer.nav.route": "Traçar uma rota direcionada", + "viewer.nav.route.title": "Traçar rota (R)", + "viewer.nav.route.short": "ROTA", + "viewer.nav.radar": "Abrir radar semântico", + "viewer.nav.radar.title": "Radar semântico (M)", + "viewer.nav.radar.short": "MAPA", + "viewer.nav.lens": "Abrir lente semântica", + "viewer.nav.lens.title": "Lente semântica (L)", + "viewer.nav.lens.short": "LENTE", + "viewer.nav.find": "Encontrar um nó", + "viewer.nav.find.title": "Encontrar um nó (/)", + "viewer.nav.guide": "Abrir guia do diagrama", + "viewer.nav.guide.title": "Guia do diagrama (?)", + "viewer.nav.zoomOut": "Reduzir", + "viewer.nav.zoomOut.title": "Reduzir (-)", + "viewer.nav.reset": "Redefinir visualização do diagrama", + "viewer.nav.reset.title": "Redefinir visualização (0)", + "viewer.nav.read": "LEITURA", + "viewer.nav.zoomIn": "Ampliar", + "viewer.nav.zoomIn.title": "Ampliar (+)", + "viewer.nav.camera": "{hint}. Redefinir visualização do diagrama", + "viewer.nav.camera.title": "{semantic}{hint} · redefinir visualização (0)", + "viewer.nav.camera.semantic": "Câmera semântica ativa · ", + "viewer.nav.level.map": "MAPA", + "viewer.nav.level.read": "LEITURA", + "viewer.nav.level.full": "COMPLETO", + "viewer.nav.level.auto": "AUTO", + "viewer.nav.detail.map": "Amplie para revelar rótulos de relações e contexto do nó", + "viewer.nav.detail.read": "Amplie novamente para revelar tags e anotações", + "viewer.nav.detail.full": "Detalhe completo do diagrama", + "viewer.intent.summary": "{label}. {out} de saída, {in} de entrada{loops}. {total} conexões. Pressione Enter para detalhes.", + "viewer.intent.loops": ", {count} laço próprio", + "viewer.common.copied": "Copiado", + "viewer.common.copyFailed": "Falha ao copiar", + "viewer.common.copyLink": "Copiar link", + "viewer.common.clear": "Limpar", + "viewer.common.close": "Fechar" +} diff --git a/vendor/archify/examples/locales/ru.json b/vendor/archify/examples/locales/ru.json new file mode 100644 index 0000000..d86e14f --- /dev/null +++ b/vendor/archify/examples/locales/ru.json @@ -0,0 +1,425 @@ +{ + "page.title": "{title} — диаграмма", + "diagram.description.architecture": "Диаграмма архитектуры, созданная в Archify.", + "diagram.description.workflow": "Диаграмма рабочего процесса, созданная в Archify.", + "diagram.description.sequence": "Диаграмма последовательности, созданная в Archify.", + "diagram.description.dataflow": "Диаграмма потока данных, созданная в Archify.", + "diagram.description.lifecycle": "Диаграмма жизненного цикла, созданная в Archify.", + "node.focus": "Фокус: {label}", + "node.focus.detail": "Фокус: {label}, {detail}", + "node.context.architecture": "Компонент архитектуры", + "node.context.workflow": "Узел рабочего процесса", + "node.context.sequence": "Участник последовательности", + "node.context.dataflow": "Узел потока данных", + "node.context.lifecycle": "Состояние жизненного цикла", + "legend.title": "Легенда", + "legend.architecture.frontend": "Фронтенд", + "legend.architecture.backend": "Бэкенд", + "legend.architecture.database": "База данных", + "legend.architecture.cloud": "Облако", + "legend.architecture.security": "Безопасность", + "legend.architecture.messagebus": "Шина сообщений", + "legend.architecture.external": "Внешний", + "legend.workflow.frontend": "Интерфейс пользователя", + "legend.workflow.backend": "Логика агента", + "legend.workflow.security": "Политика", + "legend.workflow.messagebus": "Действие инструмента", + "legend.workflow.database": "Контекст / трассировка", + "legend.workflow.cloud": "Облачный сервис", + "legend.workflow.external": "Внешняя система", + "legend.sequence.emphasis": "основной запрос", + "legend.sequence.return": "возврат", + "legend.sequence.security": "безопасность", + "legend.sequence.dashed": "асинхронная трассировка", + "legend.sequence.default": "сообщение", + "legend.dataflow.emphasis": "основные данные", + "legend.dataflow.security": "политика / ПДн", + "legend.dataflow.dashed": "асинхронный пакет", + "legend.dataflow.database": "хранилище данных", + "legend.dataflow.default": "поток данных", + "legend.lifecycle.start": "начальное состояние", + "legend.lifecycle.active": "активно", + "legend.lifecycle.waiting": "ожидание", + "legend.lifecycle.decision": "решение", + "legend.lifecycle.success": "успех", + "legend.lifecycle.failure": "ошибка / выход", + "legend.lifecycle.neutral": "нейтральное", + "legend.lifecycle.external": "внешнее", + "legend.lifecycle.final": "конечное состояние", + "viewer.kind.frontend": "Фронтенд", + "viewer.kind.backend": "Бэкенд", + "viewer.kind.database": "База данных", + "viewer.kind.cloud": "Облако", + "viewer.kind.security": "Безопасность", + "viewer.kind.messagebus": "Шина сообщений", + "viewer.kind.external": "Внешний", + "viewer.kind.neutral": "Нейтральный", + "viewer.kind.node": "Узел", + "viewer.kind.start": "Начало", + "viewer.kind.active": "Активно", + "viewer.kind.waiting": "Ожидание", + "viewer.kind.decision": "Решение", + "viewer.kind.success": "Успех", + "viewer.kind.failure": "Ошибка", + "viewer.toolbar.actions": "Действия с диаграммой", + "viewer.theme.toggle.title": "Переключить тему (T)", + "viewer.theme.toggle": "Переключить цветовую тему", + "viewer.theme.dark": "Тёмная", + "viewer.theme.light": "Светлая", + "viewer.preset.choose.title": "Выбрать визуальный стиль (S — переключение)", + "viewer.preset.choose": "Выбрать визуальный стиль", + "viewer.preset.style": "Стиль", + "viewer.preset.menu": "Визуальный стиль", + "viewer.preset.identity": "Визуальная идентичность", + "viewer.preset.cycles": "S — переключение", + "viewer.preset.classic": "Классический", + "viewer.preset.classic.short": "Классика", + "viewer.preset.classic.hint": "Стабильный технический стиль по умолчанию", + "viewer.preset.flow": "Поток сигналов", + "viewer.preset.flow.short": "Поток", + "viewer.preset.flow.hint": "Презентация с акцентом на анимацию", + "viewer.preset.blueprint": "Чертёж", + "viewer.preset.blueprint.hint": "Для инженерного обзора", + "viewer.preset.editorial": "Редакционный", + "viewer.preset.editorial.hint": "Для публикаций и анонсов", + "viewer.preset.badge.signalFlow": "ПОТОК СИГНАЛОВ", + "viewer.preset.badge.blueprint": "ЧЕРТЁЖ / РЕД. 01", + "viewer.preset.badge.editorial": "РЕДАКЦИЯ / ЗАМЕТКА", + "viewer.preset.badge.editorialPlate": "ARCHIFY / ПЛАСТИНА 04", + "viewer.preset.current": "Визуальный стиль: {style}. Выбрать визуальный стиль", + "viewer.motion.live": "Динамика", + "viewer.motion.still": "Статика", + "viewer.motion.pause": "Приостановить анимацию", + "viewer.motion.resume": "Возобновить анимацию", + "viewer.motion.reduced": "Анимация приостановлена из-за настройки сниженного движения", + "viewer.motion.hidden": "Анимация приостановлена, пока страница скрыта", + "viewer.motion.yielding": "Приостановить анимацию; сейчас уступает место: {owner}", + "viewer.motion.yielding.title": "Динамический просмотр включён · уступает место: {owner}", + "viewer.owner.route": "Зонд маршрута", + "viewer.owner.lens": "Семантическая линза", + "viewer.owner.relationship": "Предпросмотр связи", + "viewer.owner.intent": "Трассировка намерения", + "viewer.owner.focus": "семантический фокус", + "viewer.owner.legend": "предпросмотр легенды", + "viewer.owner.reader": "взаимодействие читателя", + "viewer.present.enter": "Войти в режим презентации", + "viewer.present.enter.title": "Режим презентации (F)", + "viewer.present.exit": "Выйти из режима презентации", + "viewer.present.exit.title": "Выйти из режима презентации (F или Escape)", + "viewer.present.present": "Презентация", + "viewer.present.exit.label": "Выход", + "viewer.export.button": "Экспорт", + "viewer.export.button.title": "Экспортировать диаграмму (E)", + "viewer.export.diagram": "Экспортировать диаграмму", + "viewer.export.menu": "Экспорт", + "viewer.export.subtitle": "Портативные, аккуратные файлы", + "viewer.export.share": "Поделиться", + "viewer.export.shareCard": "Карточка для публикации", + "viewer.export.routeShareCard": "Карточка маршрута", + "viewer.export.reachShareCard": "Карточка достижимости", + "viewer.export.copyDiagram": "Копировать диаграмму", + "viewer.export.clipboardPng": "PNG в буфер обмена", + "viewer.export.raster": "Растровые изображения", + "viewer.export.image": "Изображение", + "viewer.export.lossless": "Изображение без потерь", + "viewer.export.compact": "Компактное изображение", + "viewer.export.modern": "Современный формат изображения", + "viewer.export.vectorMotion": "Вектор и анимация", + "viewer.export.vectorMotion.heading": "Вектор и анимация", + "viewer.export.editable": "Редактируемый вектор", + "viewer.export.motion6s": "Анимация 6 с", + "viewer.export.unsupported": "Не поддерживается этим браузером", + "viewer.export.clipboardUnsupported": "Этот браузер не поддерживает запись изображений в буфер обмена", + "viewer.export.clipboardUnsupported.short": "Запись изображений в буфер обмена не поддерживается в этом браузере.", + "viewer.export.motionUnavailable": "Запись анимации недоступна в этом браузере", + "viewer.export.webmUnavailable": "WebM недоступен в этом браузере", + "viewer.export.failed": "Ошибка экспорта: {message}", + "viewer.export.unknownVariant": "Неизвестный тип карточки: {variant}", + "viewer.export.routeRequired": "Постройте маршрут перед экспортом карточки маршрута", + "viewer.export.reachRequired": "Постройте заданную достижимость перед экспортом карточки достижимости", + "viewer.export.unknown": "неизвестно", + "viewer.export.routeFailed": "Не удалось экспортировать карточку маршрута: {message}", + "viewer.export.reachFailed": "Не удалось экспортировать карточку достижимости: {message}", + "viewer.export.copyFailed": "Не удалось скопировать: {message}", + "viewer.export.copiedPng": "PNG скопирован в буфер обмена", + "viewer.export.downloadedRoute": "Карточка маршрута загружена", + "viewer.export.downloadedReach": "Карточка достижимости загружена", + "viewer.export.downloadedWebm": "WebM загружен", + "viewer.export.recording": "Запись анимации, 6 секунд…", + "viewer.export.card.routeSummary.one": "Маршрут: {source} → {target} · {count} направленный переход", + "viewer.export.card.routeSummary.other": "Маршрут: {source} → {target} · {count} направленных переходов", + "viewer.export.card.reachSummary": "Заданная достижимость ({direction}) от {origin} · {nodes} · {links} · макс. {hops}", + "viewer.export.card.node.one": "{count} узел", + "viewer.export.card.node.other": "{count} узлов", + "viewer.export.card.link.one": "{count} связь", + "viewer.export.card.link.other": "{count} связей", + "viewer.export.card.hop.one": "{count} переход", + "viewer.export.card.hop.other": "{count} переходов", + "viewer.export.card.routeBadge": "ARCHIFY · МАРШРУТ · {hops}", + "viewer.export.card.reachBadge": "ARCHIFY · ДОСТИЖИМОСТЬ ({direction})", + "viewer.export.direction.upstream": "Восходящий", + "viewer.export.direction.downstream": "Нисходящий", + "viewer.export.error.canvasUnavailable": "Canvas недоступен для {label}", + "viewer.export.error.contextUnavailable": "2D-контекст canvas недоступен для {label}", + "viewer.export.error.toBlobUnavailable": "canvas.toBlob недоступен для {label}", + "viewer.export.error.toBlobNull": "canvas.toBlob не вернул данные для {label}", + "viewer.export.error.variantsCombined": "Типы карточек нельзя комбинировать", + "viewer.export.error.viewerState": "Не удалось удалить временное состояние просмотра при экспорте карточки", + "viewer.export.error.routeState": "Не удалось безопасно сохранить построенный маршрут при экспорте карточки", + "viewer.export.error.reachState": "Не удалось безопасно сохранить заданную достижимость при экспорте карточки", + "viewer.export.error.webmRequirements": "Экспорт анимации в WebM требует анимации трассировки и поддержки MediaRecorder в браузере", + "viewer.export.error.mediaRecorder": "Ошибка MediaRecorder", + "viewer.export.error.emptyWebm": "MediaRecorder создал пустой файл WebM", + "viewer.export.error.webmBackground": "Не удалось загрузить фон SVG для экспорта WebM", + "viewer.guide.eyebrow": "Гид по диаграмме", + "viewer.guide.close": "Закрыть гид по диаграмме", + "viewer.guide.inspecting": "Анализ скомпилированной семантики", + "viewer.guide.actions": "Действия для изучения диаграммы", + "viewer.guide.find": "Найти любой узел", + "viewer.guide.find.hint": "Поиск по меткам, ролям, типам и постоянным ID.", + "viewer.guide.route": "Построить маршрут", + "viewer.guide.route.aria": "Построить направленный маршрут", + "viewer.guide.route.hint": "Узнайте, как два семантических узла связаны в заданном направлении.", + "viewer.guide.map": "Просмотреть всю систему", + "viewer.guide.map.hint": "Откройте семантический радар с живым видом и постоянными узлами.", + "viewer.guide.lens": "Сравнить семантические типы", + "viewer.guide.lens.hint": "Подсчитайте роли, покажите их связи и сравните прямые заданные связи.", + "viewer.guide.present": "Войти в режим презентации", + "viewer.guide.present.hint": "Отдать весь экран живой диаграмме без изменения экспорта.", + "viewer.guide.shortcuts": "Дополнительные горячие клавиши", + "viewer.guide.shortcut.export": "Экспорт", + "viewer.guide.shortcut.theme": "Тема", + "viewer.guide.shortcut.style": "Стиль", + "viewer.guide.shortcut.reset": "Сброс", + "viewer.guide.shortcut.zoomIn": "Увеличить", + "viewer.guide.shortcut.zoomOut": "Уменьшить", + "viewer.guide.shortcut.close": "Закрыть", + "viewer.guide.facts": "{nodes} · {relationships}", + "viewer.guide.fact.node.one": "{count} семантический узел", + "viewer.guide.fact.node.other": "{count} семантических узлов", + "viewer.guide.fact.relationship.one": "{count} связь", + "viewer.guide.fact.relationship.other": "{count} связей", + "viewer.guide.open": "Открыть гид по диаграмме", + "viewer.finder.title": "Найти узел", + "viewer.finder.close": "Закрыть поиск узлов", + "viewer.finder.placeholder": "Поиск по меткам или ID", + "viewer.finder.search": "Поиск узлов диаграммы", + "viewer.finder.results": "Узлы диаграммы", + "viewer.finder.empty": "Совпадений не найдено", + "viewer.finder.result.focus": "Фокус: {label}", + "viewer.finder.result.routeStart": "Выбрать {label} как начало маршрута", + "viewer.finder.result.routeTarget": "Выбрать {label} как конец маршрута, {links}", + "viewer.finder.status.empty": "Совпадений не найдено", + "viewer.finder.status.count.one": "{count} совпадающий узел", + "viewer.finder.status.count.other": "{count} совпадающих узлов", + "viewer.finder.noun.nodes": "узлов", + "viewer.finder.link.one": "{count} связь", + "viewer.finder.link.other": "{count} связей", + "viewer.finder.result.focus.one": "Фокус: {label}, {count} связанное соединение", + "viewer.finder.result.focus.other": "Фокус: {label}, {count} связанных соединений", + "viewer.finder.status.filtered": "{visible} из {available} {noun}", + "viewer.finder.status.all": "{available} {noun}", + "viewer.passport.eyebrow": "Семантический паспорт", + "viewer.passport.metadata": "Метаданные узла", + "viewer.passport.evidence": "Проверенные источники", + "viewer.passport.verified": "Проверенный источник", + "viewer.passport.verificationScope": "Проверено по локальному Git на фиксированной ревизии. Удалённый доступ не проверялся.", + "viewer.passport.reach": "Заданная достижимость", + "viewer.passport.reach.trace": "Отследить заданную достижимость", + "viewer.passport.upstream": "Восходящий", + "viewer.passport.downstream": "Нисходящий", + "viewer.passport.upstream.trace": "Отследить восходящую заданную достижимость", + "viewer.passport.downstream.trace": "Отследить нисходящую заданную достижимость", + "viewer.passport.close": "Закрыть семантический паспорт", + "viewer.passport.copy": "Скопировать ссылку", + "viewer.passport.copy.focus": "Скопировать ссылку на выделенный узел", + "viewer.passport.relations": "Связи", + "viewer.passport.relations.show": "Показать связанные отношения", + "viewer.passport.relations.hide": "Скрыть связанные отношения", + "viewer.passport.relations.list": "Связанные отношения", + "viewer.passport.copyRelation": "Копировать связь", + "viewer.passport.copyNode": "Копировать узел", + "viewer.passport.copyPinned": "Скопировать ссылку на закреплённую связь", + "viewer.passport.copySource": "Скопировать ссылку на исходный узел", + "viewer.passport.copy.focused.success": "Ссылка на выделенный узел скопирована", + "viewer.passport.copy.pinned.success": "Ссылка на закреплённую связь скопирована", + "viewer.passport.copy.focused.failed": "Не удалось скопировать ссылку на выделенный узел", + "viewer.passport.copy.pinned.failed": "Не удалось скопировать ссылку на закреплённую связь", + "viewer.passport.relationship.none": "Нет связанных отношений", + "viewer.passport.relationship.count.one": "{count} связь", + "viewer.passport.relationship.count.other": "{count} связей", + "viewer.passport.relationship.show.one": "Показать {count} связанное отношение", + "viewer.passport.relationship.show.other": "Показать {count} связанных отношений", + "viewer.passport.relationship.summary": "{out} исходящих · {in} входящих{loops}", + "viewer.passport.relationship.loops": " · {count} петля", + "viewer.passport.relationship.explorer": "Обзор прямых связей", + "viewer.passport.relationship.help": "Используйте стрелки для просмотра связей. Enter или пробел — закрепить детали; Escape — очистить.", + "viewer.passport.relationship.loopsBack": "замыкается", + "viewer.passport.relationship.connectsTo": "соединяется с", + "viewer.passport.relationship.connectsFrom": "соединяется от", + "viewer.passport.relationship.pinned": "Закреплённая связь · {from} → {to} · {label}", + "viewer.passport.relationship.inspect": "Просмотр связи {index} из {total}: {from} → {to}, {label}. Enter — подробности.", + "viewer.passport.relationship.group.out": "Исходящие", + "viewer.passport.relationship.group.in": "Входящие", + "viewer.passport.relationship.group.loop": "Петли", + "viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}", + "viewer.passport.relationship.direction.out": "ИСХ →", + "viewer.passport.relationship.direction.in": "← ВХ", + "viewer.passport.relationship.direction.loop": "ПЕТЛЯ", + "viewer.passport.sourceCount.one": "{count} проверенная ссылка на источник", + "viewer.passport.sourceCount.other": "{count} проверенных ссылок на источники", + "viewer.passport.sourceMarker": "ИСТ", + "viewer.passport.beacon.one": "{count} проверенный источник; выделите узел для просмотра", + "viewer.passport.beacon.other": "{count} проверенных источников; выделите узел для просмотра", + "viewer.passport.repository.open": "Открыть проверенную ревизию репозитория {revision}", + "viewer.passport.source.open": "Открыть проверенный источник {path} на ревизии {revision}", + "viewer.passport.source.openLink": "Открыть ↗", + "viewer.passport.reach.upstream.one": "Отследить {count} восходящий заданный узел", + "viewer.passport.reach.upstream.other": "Отследить {count} восходящих заданных узлов", + "viewer.passport.reach.downstream.one": "Отследить {count} нисходящий заданный узел", + "viewer.passport.reach.downstream.other": "Отследить {count} нисходящих заданных узлов", + "viewer.passport.reach.noUpstream": "Нет восходящих заданных узлов", + "viewer.passport.reach.noDownstream": "Нет нисходящих заданных узлов", + "viewer.passport.reach.status": "{direction} · узлов: {nodes} · связей: {links} · макс. переходов: {hops}", + "viewer.route.eyebrow": "Зонд маршрута", + "viewer.route.start": "Выбрать начальный узел", + "viewer.route.start.find": "Найти начало", + "viewer.route.start.find.aria": "Найти начало маршрута", + "viewer.route.copy": "Скопировать ссылку", + "viewer.route.copy.aria": "Скопировать ссылку на построенный маршрут", + "viewer.route.clear": "Очистить", + "viewer.route.clear.aria": "Очистить зонд маршрута", + "viewer.route.traced": "Построенный маршрут", + "viewer.route.pickTwo": "Выберите два семантических узла на диаграмме", + "viewer.route.pickOne": "Выберите семантический узел на диаграмме", + "viewer.route.controls": "Управление воспроизведением маршрута", + "viewer.route.previous": "Предыдущая позиция маршрута", + "viewer.route.play": "Воспроизвести маршрут", + "viewer.route.pause": "Приостановить маршрут", + "viewer.route.replay": "Повторить маршрут", + "viewer.route.next": "Следующая позиция маршрута", + "viewer.route.journey": "Путь", + "viewer.route.pause.label": "Пауза", + "viewer.route.replay.label": "Повтор", + "viewer.route.overview": "Обзор", + "viewer.route.overview.aria": "Показать полный обзор маршрута", + "viewer.route.instructions": "Сначала выберите источник, затем цель. Направление важно.", + "viewer.route.destination": "Выбрать цель от {label}", + "viewer.route.destination.find": "Найти цель", + "viewer.route.destination.find.aria": "Найти достижимую цель маршрута", + "viewer.route.differentDestination": "Выбрать другую цель", + "viewer.route.distinct": "Для маршрута нужны два разных семантических узла.", + "viewer.route.unreachable": "Нет направленного маршрута к {label}", + "viewer.route.unreachable.detail": "{target} недостижим из {source}. Выберите подсвеченную цель.", + "viewer.route.start.instructions": "Выберите источник. На следующем шаге будут показаны только направленно достижимые цели.", + "viewer.route.copy.success": "Ссылка на построенный маршрут скопирована", + "viewer.route.copy.failed": "Не удалось скопировать ссылку на маршрут", + "viewer.route.position": "Позиция маршрута {index} из {total}: {label}", + "viewer.route.step": "Шаг {index} из {total} · {phase} · {label}", + "viewer.route.motionRequired": "Автоматическое воспроизведение требует режима «Динамика»", + "viewer.route.trigger.clear": "Очистить построенный маршрут", + "viewer.route.overview.status": "{nodes} · {hops} · кратчайший заданный маршрут", + "viewer.route.overview.node.one": "{count} узел", + "viewer.route.overview.node.other": "{count} узлов", + "viewer.route.overview.hop.one": "{count} направленный переход", + "viewer.route.overview.hop.other": "{count} направленных переходов", + "viewer.route.phase.playing": "Воспроизведение", + "viewer.route.phase.complete": "Завершено", + "viewer.route.phase.inspecting": "Просмотр", + "viewer.route.destination.count.one": "Доступна {count} направленная цель. Выберите подсвеченный узел.", + "viewer.route.destination.count.other": "Доступно {count} направленных целей. Выберите подсвеченный узел.", + "viewer.route.noOutgoing": "Отсюда не начинается ни один исходящий маршрут. Очистите и выберите другой источник.", + "viewer.route.result.title": "{source} → {target}", + "viewer.route.finder.source.title": "Выбрать начало маршрута", + "viewer.route.finder.source.placeholder": "Поиск источников маршрута", + "viewer.route.finder.source.empty": "Совпадающих источников маршрута не найдено", + "viewer.route.finder.source.results": "Узлы, с которых можно начать маршрут", + "viewer.route.finder.source.noun": "источников маршрута", + "viewer.route.finder.source.badge": "начало", + "viewer.route.finder.target.title": "Цель от {label}", + "viewer.route.finder.target.placeholder": "Поиск достижимых целей", + "viewer.route.finder.target.empty": "Совпадающих достижимых целей не найдено", + "viewer.route.finder.target.results": "Достижимые цели маршрута", + "viewer.route.finder.target.noun": "достижимых целей", + "viewer.route.hop.one": "{count} переход", + "viewer.route.hop.other": "{count} переходов", + "viewer.lens.eyebrow": "Семантическая линза", + "viewer.lens.title": "Сравнить роли системы", + "viewer.lens.close": "Закрыть семантическую линзу", + "viewer.lens.instruction": "Выберите до двух семантических типов. Один тип показывает реальные связи; два — сравнивают только прямые заданные связи.", + "viewer.lens.kinds": "Семантические типы", + "viewer.lens.choose": "Выберите тип, чтобы просмотреть его узлы и смежные связи.", + "viewer.lens.copy": "Скопировать ссылку на семантическую линзу", + "viewer.lens.clear": "Очистить семантическую линзу", + "viewer.lens.open": "Открыть семантическую линзу", + "viewer.lens.openActive": "Открыть активную семантическую линзу", + "viewer.lens.legend": "Семантическая легенда", + "viewer.lens.legend.inspect.one": "Просмотр {label}, {count} узел", + "viewer.lens.legend.inspect.other": "Просмотр {label}, {count} узлов", + "viewer.lens.kind.count.one": "{label}, {count} узел", + "viewer.lens.kind.count.other": "{label}, {count} узлов", + "viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} прямая связь", + "viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} прямых связей", + "viewer.lens.single": "{nodes} · {relationships} · связанные узлы остаются видимыми", + "viewer.lens.node.one": "{count} узел типа {label}", + "viewer.lens.node.other": "{count} узлов типа {label}", + "viewer.lens.relationship.one": "{count} смежная связь", + "viewer.lens.relationship.other": "{count} смежных связей", + "viewer.radar.title": "Семантический радар", + "viewer.radar.building": "Построение обзора", + "viewer.radar.openFull": "Открыть полный семантический радар", + "viewer.radar.open": "Открыть радар", + "viewer.radar.close": "Закрыть семантический радар", + "viewer.radar.surface": "Обзор диаграммы. Щёлкните узел, чтобы выделить его, или используйте стрелки для панорамирования.", + "viewer.radar.click": "Щёлкнуть узел", + "viewer.radar.drag": "Перетащите для панорамирования", + "viewer.radar.space": "Семантическому радару нужно больше места на карте.", + "viewer.radar.nodes": "Узлы семантического радара диаграммы", + "viewer.radar.focus": "Фокус: {label} (из семантического радара)", + "viewer.radar.status": "Узлов: {count} · {viewport}", + "viewer.radar.fullMap": "Узлов: {count} · полная карта", + "viewer.radar.compacted": "Радар уменьшен, чтобы не перекрывать семантический паспорт или элементы управления картой.", + "viewer.radar.cancelWaiting": "Отменить ожидание семантическим радаром дополнительного места", + "viewer.radar.needsSpace": "Семантическому радару нужно больше видимого места на карте", + "viewer.radar.viewport.full": "полная карта", + "viewer.radar.viewport.width": "ширина {percent}%", + "viewer.radar.viewport.scale": "масштаб {percent}%", + "viewer.nav.controls": "Управление видом диаграммы", + "viewer.nav.route": "Построить направленный маршрут", + "viewer.nav.route.title": "Построить маршрут (R)", + "viewer.nav.route.short": "ПУТЬ", + "viewer.nav.radar": "Открыть семантический радар", + "viewer.nav.radar.title": "Семантический радар (M)", + "viewer.nav.radar.short": "КАРТА", + "viewer.nav.lens": "Открыть семантическую линзу", + "viewer.nav.lens.title": "Семантическая линза (L)", + "viewer.nav.lens.short": "ЛИНЗА", + "viewer.nav.find": "Найти узел", + "viewer.nav.find.title": "Найти узел (/)", + "viewer.nav.guide": "Открыть гид по диаграмме", + "viewer.nav.guide.title": "Гид по диаграмме (?)", + "viewer.nav.zoomOut": "Уменьшить", + "viewer.nav.zoomOut.title": "Уменьшить (-)", + "viewer.nav.reset": "Сбросить вид диаграммы", + "viewer.nav.reset.title": "Сбросить вид (0)", + "viewer.nav.read": "ЧТЕНИЕ", + "viewer.nav.zoomIn": "Увеличить", + "viewer.nav.zoomIn.title": "Увеличить (+)", + "viewer.nav.camera": "{hint}. Сбросить вид диаграммы", + "viewer.nav.camera.title": "{semantic}{hint} · сбросить вид (0)", + "viewer.nav.camera.semantic": "Семантическая камера активна · ", + "viewer.nav.level.map": "КАРТА", + "viewer.nav.level.read": "ЧТЕНИЕ", + "viewer.nav.level.full": "ПОЛНЫЙ", + "viewer.nav.level.auto": "АВТО", + "viewer.nav.detail.map": "Увеличьте, чтобы увидеть подписи связей и контекст узлов", + "viewer.nav.detail.read": "Увеличьте ещё, чтобы увидеть теги и аннотации", + "viewer.nav.detail.full": "Полная детализация диаграммы", + "viewer.intent.summary": "{label}. {out} исходящих, {in} входящих{loops}. Всего связей: {total}. Enter — подробности.", + "viewer.intent.loops": ", {count} петля", + "viewer.common.copied": "Скопировано", + "viewer.common.copyFailed": "Не удалось скопировать", + "viewer.common.copyLink": "Скопировать ссылку", + "viewer.common.clear": "Очистить", + "viewer.common.close": "Закрыть" +} diff --git a/vendor/archify/examples/product-analytics.dataflow.json b/vendor/archify/examples/product-analytics.dataflow.json new file mode 100644 index 0000000..6cc4db6 --- /dev/null +++ b/vendor/archify/examples/product-analytics.dataflow.json @@ -0,0 +1,71 @@ +{ + "schema_version": 1, + "diagram_type": "dataflow", + "meta": { + "title": "Product Analytics Data Flow", + "output": "examples/dataflow-product-analytics.html", + "viewBox": [1080, 520], + "animation": "trace", + "quality_profile": "showcase" + }, + "stages": [ + { "label": "Sources" }, + { "label": "Ingest" }, + { "label": "Process" }, + { "label": "Store" }, + { "label": "Consume" } + ], + "nodes": [ + { "id": "web", "type": "frontend", "label": "Web App", "sublabel": "browser SDK", "stage": 0, "row": 0, "tag": "events" }, + { "id": "mobile", "type": "frontend", "label": "Mobile", "sublabel": "iOS / Android", "stage": 0, "row": 2, "tag": "events" }, + { "id": "edge", "type": "cloud", "label": "Edge API", "sublabel": "collector", "stage": 1, "row": 1, "tag": "TLS" }, + { "id": "consent", "type": "security", "label": "Consent Gate", "sublabel": "policy filter", "stage": 2, "row": 0, "tag": "PII guard" }, + { "id": "stream", "type": "messagebus", "label": "Event Stream", "sublabel": "Kafka topic", "stage": 2, "row": 2, "tag": "ordered" }, + { "id": "pii", "type": "security", "label": "PII Vault", "sublabel": "encrypted", "stage": 3, "row": 0, "tag": "restricted" }, + { "id": "warehouse", "type": "database", "label": "Warehouse", "sublabel": "analytics tables", "stage": 3, "row": 1, "tag": "curated" }, + { "id": "features", "type": "database", "label": "Feature Store", "sublabel": "daily batch", "stage": 3, "row": 2, "tag": "derived" }, + { "id": "dashboard", "type": "backend", "label": "Dashboards", "sublabel": "product metrics", "stage": 4, "row": 0, "tag": "SQL" }, + { "id": "model", "type": "backend", "label": "ML Model", "sublabel": "ranking job", "stage": 4, "row": 2, "tag": "features" } + ], + "flows": [ + { "id": "web-clickstream", "from": "web", "to": "edge", "label": "clickstream", "classification": "user events", "variant": "emphasis", "fromSide": "right", "toSide": "left", "via": [[205, 157], [205, 271]], "labelAt": [204, 190] }, + { "id": "mobile-events", "from": "mobile", "to": "edge", "label": "app events", "classification": "device events", "variant": "default", "fromSide": "right", "toSide": "left", "via": [[222, 385], [222, 271]], "labelAt": [220, 342] }, + { "id": "consent-enrichment", "from": "edge", "to": "consent", "label": "identity + consent", "classification": "PII touch", "variant": "security", "fromSide": "top", "toSide": "left", "via": [[315, 112], [450, 112], [450, 157]], "labelAt": [382, 100] }, + { "id": "accepted-events", "from": "edge", "to": "stream", "label": "accepted events", "classification": "append-only", "variant": "emphasis", "fromSide": "right", "toSide": "left", "via": [[420, 271], [420, 385]], "labelAt": [438, 324] }, + { "id": "identity-map", "from": "consent", "to": "pii", "label": "identity map", "classification": "encrypted PII", "variant": "security", "route": "straight", "labelAt": [638, 144] }, + { "id": "normalized-facts", "from": "stream", "to": "warehouse", "label": "normalized facts", "classification": "non-PII", "variant": "emphasis", "fromSide": "right", "toSide": "left", "via": [[638, 385], [638, 271]], "labelAt": [638, 326] }, + { "id": "daily-aggregates", "from": "warehouse", "to": "features", "label": "daily aggregates", "classification": "batch", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "route": "straight", "labelAt": [745, 326] }, + { "id": "metrics-query", "from": "warehouse", "to": "dashboard", "label": "metrics SQL", "classification": "read-only", "variant": "default", "fromSide": "right", "toSide": "bottom", "via": [[852, 271], [960, 271]], "labelAt": [876, 258] }, + { "id": "feature-vectors", "from": "features", "to": "model", "label": "feature vectors", "classification": "derived", "variant": "dashed", "route": "straight", "labelAt": [852, 372] }, + { "id": "restricted-join", "from": "pii", "to": "dashboard", "label": "restricted join", "classification": "approved only", "variant": "security", "route": "straight", "labelAt": [852, 144] } + ], + "cards": [ + { + "dot": "emerald", + "title": "Primary Data Path", + "items": [ + "Events move left to right through source, ingest, process, store, and consume stages", + "The hot path stays visually clear even with secondary batch flows", + "Labels name data assets instead of generic API verbs" + ] + }, + { + "dot": "rose", + "title": "Sensitive Boundary", + "items": [ + "Consent and PII paths are styled as security flows", + "PII lands in a restricted vault, separate from the analytics warehouse", + "Restricted joins are visible without implying default access" + ] + }, + { + "dot": "orange", + "title": "Derived Consumers", + "items": [ + "Dashboards read curated facts from the warehouse", + "Feature vectors are derived by batch from analytics tables", + "Consumption paths stay distinct from collection and consent handling" + ] + } + ] +} diff --git a/vendor/archify/examples/production-deployment.architecture.json b/vendor/archify/examples/production-deployment.architecture.json new file mode 100644 index 0000000..bbcf9bd --- /dev/null +++ b/vendor/archify/examples/production-deployment.architecture.json @@ -0,0 +1,51 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Production Deployment Ownership", + "output": "examples/production-deployment.html", + "visual_preset": "blueprint", + "animation": "trace", + "quality_profile": "showcase", + "engineering_profile": "deployment-ownership" + }, + "components": [ + { "id": "clients", "type": "external", "label": "Customers", "sublabel": "web + mobile", "pos": [38, 300], "size": [122, 60] }, + { "id": "edge", "type": "cloud", "label": "Global Edge", "sublabel": "CDN + WAF", "pos": [230, 300], "size": [126, 60], "tag": "edge team" }, + { "id": "gateway", "type": "security", "label": "API Gateway", "sublabel": "public :443", "pos": [430, 300], "size": [128, 60], "tag": "platform" }, + { "id": "api_a", "type": "backend", "label": "API Pods / AZ-a", "sublabel": "private subnet", "pos": [630, 195], "size": [136, 62], "tag": "app team" }, + { "id": "api_b", "type": "backend", "label": "API Pods / AZ-b", "sublabel": "private subnet", "pos": [630, 405], "size": [136, 62], "tag": "app team" }, + { "id": "redis", "type": "database", "label": "Redis", "sublabel": "multi-AZ cache", "pos": [840, 195], "size": [126, 62], "tag": "platform" }, + { "id": "postgres", "type": "database", "label": "PostgreSQL", "sublabel": "primary / encrypted", "pos": [840, 405], "size": [126, 62], "tag": "data team" }, + { "id": "events", "type": "messagebus", "label": "Event Bus", "sublabel": "orders.v1", "pos": [1040, 300], "size": [126, 60], "tag": "platform" }, + { "id": "worker", "type": "backend", "label": "Workers", "sublabel": "private workload", "pos": [1190, 300], "size": [126, 60], "tag": "app team" }, + { "id": "replica", "type": "database", "label": "DR Replica", "sublabel": "eu-west-1", "pos": [1040, 578], "size": [126, 62], "tag": "data team" }, + { "id": "audit", "type": "cloud", "label": "Audit Archive", "sublabel": "immutable objects", "pos": [1190, 450], "size": [126, 62], "tag": "security" }, + { "id": "observability", "type": "external", "label": "Observability", "sublabel": "metrics + traces", "pos": [1190, 85], "size": [126, 62], "tag": "SRE" } + ], + "boundaries": [ + { "kind": "region", "label": "AWS us-east-1 / production", "wraps": ["edge", "gateway", "api_a", "api_b", "redis", "postgres", "events", "worker", "audit"], "pad": 20 }, + { "kind": "security-group", "label": "private application network", "wraps": ["api_a", "api_b", "redis", "postgres", "events", "worker"], "pad": 14 }, + { "kind": "region", "label": "AWS eu-west-1 / disaster recovery", "wraps": ["replica"] }, + { "kind": "security-group", "label": "DR private subnet", "wraps": ["replica"], "pad": 14 } + ], + "connections": [ + { "from": "clients", "to": "edge", "label": "HTTPS", "variant": "emphasis" }, + { "from": "edge", "to": "gateway", "label": "mTLS", "variant": "security" }, + { "from": "gateway", "to": "api_a", "label": "VPC route", "variant": "emphasis", "route": "orthogonal-h", "labelAt": [594, 275] }, + { "from": "gateway", "to": "api_b", "label": "VPC route", "variant": "emphasis", "route": "orthogonal-h", "labelAt": [594, 385] }, + { "from": "api_a", "to": "redis", "label": "cache", "route": "straight" }, + { "from": "api_b", "to": "postgres", "label": "SQL", "route": "straight" }, + { "from": "api_a", "to": "events", "label": "publish", "variant": "dashed", "fromSide": "top", "toSide": "top", "via": [[698, 170], [1103, 170]] }, + { "from": "api_b", "to": "events", "variant": "dashed", "fromSide": "top", "toSide": "bottom", "via": [[698, 380], [1103, 380]] }, + { "from": "events", "to": "worker", "variant": "emphasis" }, + { "from": "postgres", "to": "replica", "label": "cross-region WAL", "variant": "security", "route": "orthogonal-v", "labelAt": [1003, 529] }, + { "from": "worker", "to": "audit", "label": "evidence", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 58 }, + { "from": "worker", "to": "observability", "label": "OTLP", "variant": "dashed", "route": "orthogonal-v" } + ], + "cards": [ + { "dot": "cyan", "title": "Runtime Ownership", "items": ["Platform owns the edge, gateway, cache, and event bus", "Application teams own API pods and workers", "Data owns primary and disaster-recovery state"] }, + { "dot": "rose", "title": "Named Crossings", "items": ["Public HTTPS terminates at the managed edge", "mTLS crosses into the application network", "Cross-region WAL is explicit and encrypted"] }, + { "dot": "emerald", "title": "Operational Evidence", "items": ["Workers emit traces to SRE-owned observability", "Audit evidence lands in immutable storage", "Unknown placement should remain marked, never invented"] } + ] +} diff --git a/vendor/archify/examples/release-delivery.workflow.json b/vendor/archify/examples/release-delivery.workflow.json new file mode 100644 index 0000000..09dc8cc --- /dev/null +++ b/vendor/archify/examples/release-delivery.workflow.json @@ -0,0 +1,57 @@ +{ + "schema_version": 1, + "diagram_type": "workflow", + "meta": { + "title": "Release Delivery Workflow", + "output": "examples/release-delivery.html", + "animation": "trace", + "quality_profile": "showcase" + }, + "lanes": [ + { "id": "dev", "label": "Developer" }, + { "id": "ci", "label": "Continuous Integration" }, + { "id": "approval", "label": "Release Governance" }, + { "id": "environment", "label": "Production Environment" }, + { "id": "communication", "label": "Release Communication" }, + { "id": "exceptions", "label": "Failure + Rollback", "variant": "exception" } + ], + "phases": [ + { "id": "change", "label": "Change", "fromCol": 0, "toCol": 1 }, + { "id": "verify", "label": "Build + verify", "fromCol": 2, "toCol": 3, "variant": "emphasis" }, + { "id": "promote", "label": "Promote + observe", "fromCol": 4, "toCol": 5, "variant": "dashed" } + ], + "groups": [ + { "id": "blocking_checks", "label": "Blocking checks", "lane": "ci", "fromCol": 2, "toCol": 3, "variant": "emphasis" }, + { "id": "rollback_work", "label": "Recovery path", "lane": "exceptions", "fromCol": 3, "toCol": 5, "variant": "security" } + ], + "mainPath": ["commit", "pull_request", "build", "checks", "approval", "deploy", "verify_prod", "announce"], + "nodes": [ + { "id": "commit", "lane": "dev", "col": 0, "type": "frontend", "label": "Commit", "sublabel": "signed change" }, + { "id": "pull_request", "lane": "dev", "col": 1, "type": "frontend", "label": "Pull Request", "sublabel": "reviewed diff" }, + { "id": "build", "lane": "ci", "col": 2, "type": "backend", "label": "Build", "sublabel": "locked inputs", "tag": "reproducible" }, + { "id": "checks", "lane": "ci", "col": 3, "type": "security", "label": "Quality Gates", "sublabel": "test + scan", "tag": "blocking" }, + { "id": "approval", "lane": "approval", "col": 4, "type": "security", "label": "Approve", "sublabel": "release owner", "tag": "human gate" }, + { "id": "deploy", "lane": "environment", "col": 4, "type": "cloud", "label": "Deploy", "sublabel": "canary 10%", "tag": "production" }, + { "id": "verify_prod", "lane": "environment", "col": 5, "type": "backend", "label": "Verify", "sublabel": "smoke + SLO" }, + { "id": "announce", "lane": "communication", "col": 5, "type": "external", "label": "Announce", "sublabel": "status + notes" }, + { "id": "failed", "lane": "exceptions", "col": 2, "type": "security", "label": "Stop Release", "sublabel": "gate failed" }, + { "id": "rollback", "lane": "exceptions", "col": 4, "width": 64, "type": "messagebus", "label": "Rollback", "sublabel": "last good image", "tag": "owner: on-call" } + ], + "edges": [ + { "from": "commit", "to": "pull_request" }, + { "from": "pull_request", "to": "build", "label": "merge", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "build", "to": "checks" }, + { "from": "checks", "to": "approval", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "approval", "to": "deploy", "variant": "security", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "deploy", "to": "verify_prod" }, + { "from": "verify_prod", "to": "announce", "label": "healthy", "variant": "emphasis", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "checks", "to": "failed", "label": "red", "variant": "security", "role": "error", "route": "drop", "fromSide": "bottom", "toSide": "top" }, + { "from": "verify_prod", "to": "rollback", "variant": "security", "role": "error", "route": "outside-right", "fromSide": "right", "toSide": "right" }, + { "from": "rollback", "to": "deploy", "label": "restore", "variant": "dashed", "role": "return", "route": "return-left", "fromSide": "left", "toSide": "left" } + ], + "cards": [ + { "dot": "emerald", "title": "One Happy Path", "items": ["Every change is reviewed before a reproducible build", "Blocking checks must be green before human approval", "Production is complete only after smoke and SLO verification"] }, + { "dot": "rose", "title": "Stop Conditions", "items": ["Test or security failure stops promotion", "Production health can reverse a release", "Rollback ownership is visible before an incident"] }, + { "dot": "cyan", "title": "Release Evidence", "items": ["Approval, immutable image, and check results are retained", "The release announcement follows verification", "The main path remains readable without hiding failure"] } + ] +} diff --git a/vendor/archify/examples/sequence-cache-miss-request.html b/vendor/archify/examples/sequence-cache-miss-request.html new file mode 100644 index 0000000..84686bc --- /dev/null +++ b/vendor/archify/examples/sequence-cache-miss-request.html @@ -0,0 +1,13835 @@ + + + + + + + + Cache Miss Request Sequence Diagram + + + + + + + + +
    + +
    +
    +
    +

    Cache Miss Request Sequence

    +
    +
    + + + + +
    + + Cache Miss Request Sequence + A sequence diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + open page + + + + + + + + GET /dashboard + + + + + + + + verify JWT + + + + + + + + claims ok + + + + + + + + read cache + + + + + + + + miss + + + + + + + + query profile + metrics + + + + + + + + rows + + + + + + + + set cache + + + + + + + + emit trace + + + + + + + + 200 JSON + + + + + + + + render + + + + + + + Request + + + + Fallback + + + + Response + trace + + + + + User · browser session · Sequence participant + + + + User + browser session + + + + Web App · React UI · Sequence participant + + + + Web App + React UI + + + + API · request handler · Sequence participant + + + + API + request handler + + + + Auth · JWT verify · Sequence participant + + + + Auth + JWT verify + + + + Redis · cache · Sequence participant + + + + Redis + cache + + + + Postgres · source of truth · Sequence participant + + + + Postgres + source of truth + + + + Trace · async event · Sequence participant + + + + Trace + async event + + + + + Legend + + + main request + + + + return + + + + security + + + + async trace + + + + message + + + +

    + + + + + + + + + +
    + + + +
    + + + + diff --git a/vendor/archify/examples/web-app-rendered.html b/vendor/archify/examples/web-app-rendered.html new file mode 100644 index 0000000..e424176 --- /dev/null +++ b/vendor/archify/examples/web-app-rendered.html @@ -0,0 +1,13808 @@ + + + + + + + + Sample Web App Diagram + + + + + + + + +
    + +
    +
    +
    +

    Sample Web App

    +
    +
    + + + + +
    + + Sample Web App + An architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Users · Browser / Mobile · Architecture component + + + + Users + Browser / Mobile + + + + Auth Provider · OAuth 2.0 · Architecture component · JWT + PKCE + + + + Auth Provider + OAuth 2.0 + JWT + PKCE + + + + CloudFront · CDN · AWS Region: us-west-2 + + + + CloudFront + CDN + + + + Load Balancer · HTTPS :443 · AWS Region: us-west-2 › sg-api :443/:8000 + + + + Load Balancer + HTTPS :443 + + + + API Server · FastAPI :8000 · AWS Region: us-west-2 › sg-api :443/:8000 + + + + API Server + FastAPI :8000 + + + + Redis · cache :6379 · AWS Region: us-west-2 + + + + Redis + cache :6379 + + + + PostgreSQL · primary :5432 · AWS Region: us-west-2 + + + + PostgreSQL + primary :5432 + + + + S3 · static assets · AWS Region: us-west-2 · OAI protected + + + + S3 + static assets + OAI protected + + + + SQS · job queue · AWS Region: us-west-2 + + + + SQS + job queue + + + + Worker · async jobs · AWS Region: us-west-2 + + + + Worker + async jobs + + + + + + HTTPS + + + + verify JWT + + + + + static + + + + + read-through + + + + SQL + + + + enqueue + + + + + + + AWS Region: us-west-2 + + + + + sg-api :443/:8000 + + + + + Legend + + + Backend + + + + Database + + + + Cloud + + + + Security + + + + Message bus + + + + External + + + +

    + + + + + + + + + +
    + + + +
    + + + + diff --git a/vendor/archify/examples/web-app.architecture.json b/vendor/archify/examples/web-app.architecture.json new file mode 100644 index 0000000..f370a41 --- /dev/null +++ b/vendor/archify/examples/web-app.architecture.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Sample Web App", + "output": "web-app-rendered.html", + "quality_profile": "showcase" + }, + "components": [ + { "id": "users", "type": "external", "label": "Users", "sublabel": "Browser / Mobile", "pos": [40, 300], "size": [120, 60] }, + { "id": "auth", "type": "security", "label": "Auth Provider", "sublabel": "OAuth 2.0", "pos": [40, 110], "size": [120, 64], "tag": "JWT + PKCE" }, + { "id": "cdn", "type": "cloud", "label": "CloudFront", "sublabel": "CDN", "pos": [250, 300], "size": [130, 60] }, + { "id": "lb", "type": "cloud", "label": "Load Balancer", "sublabel": "HTTPS :443", "pos": [460, 300], "size": [130, 60] }, + { "id": "api", "type": "backend", "label": "API Server", "sublabel": "FastAPI :8000", "pos": [670, 300], "size": [130, 60] }, + { "id": "cache", "type": "database", "label": "Redis", "sublabel": "cache :6379", "pos": [670, 150], "size": [130, 60] }, + { "id": "db", "type": "database", "label": "PostgreSQL", "sublabel": "primary :5432", "pos": [880, 300], "size": [130, 60] }, + { "id": "s3", "type": "cloud", "label": "S3", "sublabel": "static assets", "pos": [250, 440], "size": [130, 60], "tag": "OAI protected" }, + { "id": "queue", "type": "messagebus", "label": "SQS", "sublabel": "job queue", "pos": [670, 440], "size": [130, 60] }, + { "id": "worker", "type": "backend", "label": "Worker", "sublabel": "async jobs", "pos": [880, 440], "size": [130, 60] } + ], + "boundaries": [ + { "kind": "region", "label": "AWS Region: us-west-2", "wraps": ["cdn", "lb", "api", "cache", "db", "s3", "queue", "worker"] }, + { "kind": "security-group", "label": "sg-api :443/:8000", "wraps": ["lb", "api"] } + ], + "connections": [ + { "id": "users-to-cdn", "from": "users", "to": "cdn", "label": "HTTPS", "variant": "emphasis" }, + { "id": "jwt-verification", "from": "auth", "to": "api", "label": "verify JWT", "variant": "security", "fromSide": "right", "toSide": "top", "via": [[620, 142], [620, 246], [735, 246]] }, + { "id": "cdn-to-lb", "from": "cdn", "to": "lb" }, + { "id": "static-assets", "from": "cdn", "to": "s3", "label": "static", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 58 }, + { "id": "lb-to-api", "from": "lb", "to": "api" }, + { "id": "cache-read-through", "from": "api", "to": "cache", "label": "read-through", "fromSide": "top", "toSide": "bottom", "labelDy": -68 }, + { "id": "api-sql", "from": "api", "to": "db", "label": "SQL" }, + { "id": "enqueue-job", "from": "api", "to": "queue", "label": "enqueue", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 58 }, + { "id": "queue-to-worker", "from": "queue", "to": "worker" } + ], + "cards": [ + { "dot": "cyan", "title": "Edge", "items": ["CloudFront CDN fronts all traffic", "S3 serves static assets via OAI"] }, + { "dot": "emerald", "title": "Application", "items": ["FastAPI behind an HTTPS load balancer", "Redis read-through cache", "Async work drained from SQS by a worker"] }, + { "dot": "rose", "title": "Security", "items": ["OAuth 2.0 with JWT + PKCE", "API + LB isolated in a security group"] } + ] +} diff --git a/vendor/archify/examples/workflow-agent-tool-call-rendered.html b/vendor/archify/examples/workflow-agent-tool-call-rendered.html new file mode 100644 index 0000000..a702481 --- /dev/null +++ b/vendor/archify/examples/workflow-agent-tool-call-rendered.html @@ -0,0 +1,13826 @@ + + + + + + + + Agent Tool Call Workflow Diagram + + + + + + + + +
    + +
    +
    +
    +

    Agent Tool Call Workflow

    +
    +
    + + + + +
    + + Agent Tool Call Workflow + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / User Interface + + + 02 / Agent Runtime + + + + EX / Policy & Recovery + + + 04 / Tool Execution & Evidence + + + + + Intake + + + Plan + route + + + Execute + report + + + + Planning loop + + Human or policy stop + + Evidence path + + Tool work + + + + + + + + + + + + + + + + + User · asks for work · User Interface › Intake + + + + User + asks for work + + + + Chat Surface · thread + files · User Interface › Intake + + + + Chat Surface + thread + files + + + + Final Reply · answer + changes · User Interface › Execute + report + + + + Final Reply + answer + changes + + + + Agent Planner · plan next step · Agent Runtime › Planning loop › Plan + route · context aware + + + + Agent Planner + plan next step + context aware + + + + Tool Router · choose capability · Agent Runtime › Planning loop › Plan + route + + + + Tool Router + choose capability + + + + Approval Gate · scope + consent · Policy & Recovery › Human or policy stop › Plan + route · block risky ops + + + + Approval Gate + scope + consent + block risky ops + + + + Blocked · wait or reject · Policy & Recovery › Human or policy stop › Execute + report + + + + Blocked + wait or reject + + + + Retry Path · revise request · Policy & Recovery › Human or policy stop › Execute + report + + + + Retry Path + revise request + + + + Context Store · repo + memory · Tool Execution & Evidence › Evidence path › Intake + + + + Context Store + repo + memory + + + + Trace Log · events + output · Tool Execution & Evidence › Evidence path › Plan + route + + + + Trace Log + events + output + + + + Tool Call · shell / browser / MCP · Tool Execution & Evidence › Tool work › Execute + report · structured result + + + + Tool Call + shell / browser / MCP + structured result + + + + External API · network service · Tool Execution & Evidence › Tool work › Execute + report + + + + External API + network service + + + + + + needs approval? + + + + denied + + + + + + plan + + + + + record result + + + + + + + trace + memory + + + + + Legend + + + User UI + + + + Agent logic + + + + Policy + + + + Tool action + + + + Context / trace + + + + Cloud service + + + + External system + + + +

    + + + + + + + + + +
    + + + +
    + + + + diff --git a/vendor/archify/migrations/workflow-v2.mjs b/vendor/archify/migrations/workflow-v2.mjs new file mode 100644 index 0000000..e9731fc --- /dev/null +++ b/vendor/archify/migrations/workflow-v2.mjs @@ -0,0 +1,280 @@ +import { compileWorkflow } from '../renderers/workflow/workflow-compiler.mjs'; +import { + createMappedWorkflowCandidate, + intrinsicWorkflow, + planningWorkflow, +} from '../renderers/workflow/workflow-migration-geometry.mjs'; +import { validateSchema } from '../renderers/shared/validator.mjs'; + +export { createHorizontalRankMapper } from '../renderers/workflow/workflow-migration-geometry.mjs'; + +const TARGET_SCHEMA_VERSION = 2; + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function diagnostic({ code, message, subject = {}, evidence = {}, supportedFixes = [] }) { + return { + code, + severity: 'error', + message, + subject, + evidence, + supportedFixes, + }; +} + +function schemaDiagnostics(workflow) { + try { + validateSchema('workflow', workflow); + return []; + } catch (error) { + if (Array.isArray(error?.archifyDiagnostics)) { + return error.archifyDiagnostics.map((entry) => ({ ...entry })); + } + throw error; + } +} + +function legacyLayoutProbe(workflow, qualityProfile) { + // The probe discovers fixed-v1 rank centers, not authored canvas capacity. + // Omitting viewBox lets a capacity-only legacy failure reach the v2 compiler, + // which can measure and monotonically expand the real migrated document. + const probe = { + schema_version: 1, + diagram_type: 'workflow', + meta: { + title: workflow.meta.title, + output: workflow.meta.output, + ...(workflow.meta.locale ? { locale: workflow.meta.locale } : {}), + legend: { mode: 'hidden' }, + }, + lanes: clone(workflow.lanes), + nodes: [{ + id: 'migration_probe', + lane: workflow.lanes[0].id, + col: 0, + type: 'backend', + label: 'Probe', + }], + edges: [], + }; + return compileWorkflow({ workflow: probe, qualityProfile }); +} + +function legacyRequirementProbe(workflow, qualityProfile) { + // Measure the complete fixed-v1 document without treating an authored + // viewBox as its intrinsic requirement. The authored viewBox remains a + // migration capacity and is preserved separately on the migrated document. + const probe = clone(workflow); + delete probe.meta.viewBox; + return compileWorkflow({ workflow: probe, qualityProfile }); +} + +function requiredViewBoxFrom(result) { + if (Array.isArray(result?.receipt?.requiredViewBox)) { + return [...result.receipt.requiredViewBox]; + } + const required = result?.diagnostics + ?.map((entry) => entry?.evidence?.requiredViewBox) + .find((candidate) => Array.isArray(candidate) && candidate.length === 2); + return required ? [...required] : null; +} + +function expandableViewBox(result) { + if (result.ok || !result.diagnostics?.length) return null; + if (!result.diagnostics.every((entry) => entry.code === 'workflow/viewbox-capacity')) return null; + return requiredViewBoxFrom(result); +} + +function result({ + ok, + document, + fromSchemaVersion = 1, + preExistingDiagnostics = [], + migrationDiagnostics = [], + newSchemaDiagnostics = [], + changedCoordinates = [], + oldRequiredViewBox = null, + newRequiredViewBox = null, +}) { + return { + ok, + ...(document ? { document } : {}), + fromSchemaVersion, + toSchemaVersion: TARGET_SCHEMA_VERSION, + preExistingDiagnostics, + migrationDiagnostics, + newSchemaDiagnostics, + changedCoordinates, + oldRequiredViewBox, + newRequiredViewBox, + }; +} + +export function migrateWorkflowDocument(inputWorkflow) { + if (!inputWorkflow || typeof inputWorkflow !== 'object' || Array.isArray(inputWorkflow)) { + return result({ + ok: false, + migrationDiagnostics: [diagnostic({ + code: 'migration/source-document', + message: 'Workflow migration requires one parsed JSON object.', + supportedFixes: ['provide one workflow schema v1 JSON document'], + })], + }); + } + // Migration has no quality override: the authored policy (or effective + // standard default) must validate the document after it leaves this process. + const qualityProfile = inputWorkflow.meta?.quality_profile || 'standard'; + + const workflow = clone(inputWorkflow); + const preExistingDiagnostics = schemaDiagnostics(workflow); + if (preExistingDiagnostics.length) { + return result({ + ok: false, + fromSchemaVersion: workflow.schema_version, + preExistingDiagnostics, + }); + } + if (workflow.schema_version === TARGET_SCHEMA_VERSION) { + const compiled = compileWorkflow({ workflow: clone(workflow), qualityProfile }); + const requiredViewBox = requiredViewBoxFrom(compiled); + if (!compiled.ok) { + return result({ + ok: false, + fromSchemaVersion: TARGET_SCHEMA_VERSION, + preExistingDiagnostics: compiled.diagnostics, + oldRequiredViewBox: requiredViewBox, + newRequiredViewBox: requiredViewBox, + }); + } + return result({ + ok: true, + document: workflow, + fromSchemaVersion: TARGET_SCHEMA_VERSION, + oldRequiredViewBox: requiredViewBox, + newRequiredViewBox: requiredViewBox, + }); + } + if (workflow.schema_version !== 1) { + return result({ + ok: false, + fromSchemaVersion: workflow.schema_version, + migrationDiagnostics: [diagnostic({ + code: 'migration/source-schema-version', + message: 'Workflow migration to schema v2 requires a schema v1 or v2 source.', + subject: { path: '/schema_version' }, + evidence: { actual: workflow.schema_version, expected: [1, 2] }, + supportedFixes: ['use an unchanged schema v1 workflow or an already migrated schema v2 workflow as the source'], + })], + }); + } + + const legacy = compileWorkflow({ workflow: clone(workflow), qualityProfile }); + const legacyProbe = legacyLayoutProbe(workflow, qualityProfile); + if (!legacyProbe.ok) { + return result({ + ok: false, + preExistingDiagnostics: legacy.ok ? [] : legacy.diagnostics, + migrationDiagnostics: legacyProbe.diagnostics, + }); + } + const legacyRequirement = legacyRequirementProbe(workflow, qualityProfile); + const oldRequiredViewBox = requiredViewBoxFrom(legacyRequirement) + || requiredViewBoxFrom(legacyProbe) + || requiredViewBoxFrom(legacy); + const preExistingLayoutDiagnostics = legacy.ok ? [] : legacy.diagnostics; + + let planned = compileWorkflow({ workflow: intrinsicWorkflow(workflow), qualityProfile }); + if (!planned.ok) { + // Old absolute pins can be invalid at the new rank centers before their X + // coordinates are mapped. Obtain the same rank plan from an automatic-route + // projection, then validate every authored pin again after mapping. + planned = compileWorkflow({ workflow: planningWorkflow(workflow), qualityProfile }); + } + if (!planned.ok) { + return result({ + ok: false, + preExistingDiagnostics: preExistingLayoutDiagnostics, + newSchemaDiagnostics: planned.diagnostics, + oldRequiredViewBox, + newRequiredViewBox: requiredViewBoxFrom(planned), + }); + } + + let mappedCandidate; + try { + mappedCandidate = createMappedWorkflowCandidate( + workflow, + legacyProbe.receipt.columns, + planned.receipt.columns, + ); + } catch (error) { + return result({ + ok: false, + preExistingDiagnostics: preExistingLayoutDiagnostics, + migrationDiagnostics: [diagnostic({ + code: 'migration/rank-mapping', + message: 'Could not construct a stable horizontal rank mapping.', + evidence: { reason: error.message }, + supportedFixes: ['report the workflow and compiler receipts to the Archify maintainers'], + })], + oldRequiredViewBox, + newRequiredViewBox: requiredViewBoxFrom(planned), + }); + } + + const { document: migrated, changedCoordinates } = mappedCandidate; + + let compiled = compileWorkflow({ workflow: migrated, qualityProfile }); + const requiredExpansion = migrated.meta.viewBox ? expandableViewBox(compiled) : null; + if (requiredExpansion) { + const current = migrated.meta.viewBox; + const expanded = [ + Math.max(current[0], requiredExpansion[0]), + Math.max(current[1], requiredExpansion[1]), + ]; + if (expanded[0] > current[0] || expanded[1] > current[1]) { + migrated.meta.viewBox = expanded; + compiled = compileWorkflow({ workflow: migrated, qualityProfile }); + } + } + + const newRequiredViewBox = requiredViewBoxFrom(compiled) || requiredViewBoxFrom(planned); + if (!compiled.ok) { + return result({ + ok: false, + preExistingDiagnostics: preExistingLayoutDiagnostics, + newSchemaDiagnostics: compiled.diagnostics, + changedCoordinates, + oldRequiredViewBox, + newRequiredViewBox, + }); + } + + const migratedSchemaDiagnostics = schemaDiagnostics(migrated); + if (migratedSchemaDiagnostics.length) { + return result({ + ok: false, + preExistingDiagnostics: preExistingLayoutDiagnostics, + newSchemaDiagnostics: migratedSchemaDiagnostics, + changedCoordinates, + oldRequiredViewBox, + newRequiredViewBox, + }); + } + + return result({ + ok: true, + document: migrated, + preExistingDiagnostics: preExistingLayoutDiagnostics, + changedCoordinates, + oldRequiredViewBox, + newRequiredViewBox, + }); +} + +export function serializeMigratedWorkflow(workflow) { + return `${JSON.stringify(workflow, null, 2)}\n`; +} diff --git a/vendor/archify/package.json b/vendor/archify/package.json new file mode 100644 index 0000000..6d2ab91 --- /dev/null +++ b/vendor/archify/package.json @@ -0,0 +1,40 @@ +{ + "name": "archify", + "version": "3.0.1", + "private": true, + "type": "module", + "description": "JSON-IR diagram renderers (architecture / workflow / sequence / dataflow / lifecycle).", + "license": "MIT", + "bin": { + "archify": "./bin/archify.mjs" + }, + "engines": { + "node": ">=18" + }, + "scripts": { + "generate:viewer": "node ../scripts/generate-viewer.mjs", + "check:viewer": "node ../scripts/generate-viewer.mjs --check", + "generate:brand-marks": "node scripts/generate-brand-marks.mjs", + "check:brand-marks": "node scripts/generate-brand-marks.mjs --check", + "generate:validators": "node scripts/generate-validators.mjs", + "check:validators": "node scripts/generate-validators.mjs --check", + "check:release-identity": "node ../scripts/check-release-identity.mjs", + "build:gallery": "node ../scripts/build-gallery.mjs ../docs", + "build:guide": "node ../scripts/build-guide.mjs ../docs/guide.html", + "build:start": "node ../scripts/build-start.mjs ../docs/start.html", + "build:readme-showcase": "node ../scripts/build-readme-showcase.mjs", + "test:webm": "node test/webm-artifact.smoke.mjs && node --test test/site-language-integration.mjs", + "test:browser": "node ../scripts/run-browser-tests.mjs", + "test": "npm run check:viewer && npm run check:brand-marks && npm run check:validators && npm run check:release-identity && node test/golden.mjs && node ../scripts/run-tests.mjs", + "render:examples": "node scripts/render-examples.mjs ../examples" + }, + "devDependencies": { + "ajv": "^8.17.1", + "parse5": "7.3.0", + "saxes": "6.0.0", + "simple-icons": "16.28.0" + }, + "overrides": { + "fast-uri": "^3.1.7" + } +} diff --git a/vendor/archify/recipes/scenarios.mjs b/vendor/archify/recipes/scenarios.mjs new file mode 100644 index 0000000..f8372b7 --- /dev/null +++ b/vendor/archify/recipes/scenarios.mjs @@ -0,0 +1,430 @@ +const RAW_RECIPES = [ + { + id: 'system-overview', type: 'architecture', proof: 'web-app', + presentation: { preset: 'classic', motion: 'static' }, + start: { + en: { descriptionPrompt: 'Use Archify to turn this plain-language system description into a high-level architecture diagram: [describe the users, core components, primary path, external dependencies, and boundaries]. No repository is required. Ask only for missing facts that would materially change the diagram, mark any remaining unknowns instead of inventing them, and keep one obvious primary path across 8–12 core components.' }, + zh: { descriptionPrompt: '用 Archify 把下面这段自然语言系统描述画成高层架构图:[在这里描述用户、核心组件、主要路径、外部依赖和边界]。不需要代码库。只追问会实质影响图的缺失信息,其余不确定内容要标明而不是编造;保留 8–12 个核心组件和一条一眼可见的主路径。' }, + }, + signals: [['system overview', 12], ['architecture', 10], ['components', 6], ['services', 4], ['repository', 5], ['trust boundary', 8], ['架构', 10], ['系统总览', 12], ['组件', 6], ['服务', 4], ['仓库', 5], ['信任边界', 8]], + en: { + title: 'System overview', question: 'What exists, who owns it, and how is it connected?', + summary: 'A bounded map of core components, external dependencies, primary paths, and trust boundaries.', + useWhen: 'Onboarding, design reviews, repository orientation, or explaining a service landscape.', + avoidWhen: 'The audience needs exact call order, state transitions, or row-level data lineage.', + include: ['8–12 core components', 'one primary path', 'external dependencies', 'trust boundaries'], + prompt: 'Analyze this repository, then use Archify to create a high-level architecture diagram. Show 8–12 core runtime components, one primary request or data path, external dependencies, ownership or trust boundaries, and put supporting detail in cards instead of adding more edges.', + }, + zh: { + title: '系统总览', question: '系统里有什么、归谁负责、彼此如何连接?', + summary: '用一张有边界的图展示核心组件、外部依赖、主路径和信任边界。', + useWhen: '适合新人上手、方案评审、仓库梳理和服务全景说明。', + avoidWhen: '如果重点是精确调用顺序、状态流转或字段级血缘,请换其他配方。', + include: ['8–12 个核心组件', '一条主路径', '外部依赖', '归属或信任边界'], + prompt: '分析这个仓库,然后用 Archify 生成高层系统架构图。展示 8–12 个核心运行时组件、一条主要请求或数据路径、外部依赖、归属或信任边界;支持性细节放进卡片,不要继续堆连线。', + }, + }, + { + id: 'deployment-ownership', type: 'architecture', proof: 'deployment-ownership', + presentation: { preset: 'blueprint', motion: 'trace' }, + signals: [['deployment topology', 14], ['region', 7], ['vpc', 9], ['cluster', 6], ['availability zone', 8], ['ownership', 7], ['cloud deployment', 12], ['部署拓扑', 14], ['区域', 6], ['集群', 6], ['可用区', 8], ['资源归属', 9], ['跨区', 8]], + en: { + title: 'Deployment ownership', question: 'Where does each workload run, and what crosses a boundary?', + summary: 'A deployment-focused map of regions, networks, clusters, workloads, stores, and cross-boundary mechanisms.', + useWhen: 'Cloud reviews, production readiness, multi-region planning, or infrastructure ownership handoffs.', + avoidWhen: 'Deployment facts are unknown or the real question is application behavior rather than placement.', + include: ['regions and networks', 'workload ownership', 'stateful services', 'named boundary crossings'], + prompt: 'Use Archify to draw the production deployment topology. Group resources by region, network, cluster, and owner; show workloads and stateful services; label every cross-boundary mechanism. Do not invent deployment facts—mark unknown areas explicitly. If the user wants a fail-closed deployment review, ask before setting meta.engineering_profile to deployment-ownership; otherwise leave the engineering profile unset.', + }, + zh: { + title: '部署与归属', question: '每个工作负载运行在哪里,哪些连接跨越了边界?', + summary: '围绕 Region、网络、集群、工作负载、存储和跨边界机制组织部署图。', + useWhen: '适合云上评审、生产就绪、多区域规划和基础设施交接。', + avoidWhen: '部署事实不清楚,或真正问题是应用行为而不是资源位置时不要使用。', + include: ['区域与网络', '工作负载归属', '有状态服务', '明确的跨边界机制'], + prompt: '用 Archify 绘制生产部署拓扑。按区域、网络、集群和负责人分组,展示工作负载与有状态服务,并标注每一种跨边界机制。不要编造部署事实,不确定的区域要明确标出。如果用户需要失败即阻断的部署评审,先征得确认,再把 meta.engineering_profile 设为 deployment-ownership;否则不要启用工程画像。', + }, + }, + { + id: 'agent-tool-call', type: 'workflow', proof: 'agent-tool-call', + presentation: { preset: 'signal-flow', motion: 'trace' }, + start: { + en: { descriptionPrompt: 'Use Archify workflow mode to turn this description into a diagram: [paste the actors, main steps, decisions, approvals, and exception paths]. Use lanes for distinct owners, keep one unmistakable happy path, and mark missing ownership or unresolved branches instead of inventing them.' }, + zh: { descriptionPrompt: '用 Archify 工作流模式把下面的描述画成图:[粘贴参与者、主要步骤、决策、审批和异常路径]。不同负责方使用独立泳道,保留一条明确的成功主路径,缺失的负责人或未定分支要标明而不是编造。' }, + }, + signals: [['agent tool call', 16], ['tool call', 12], ['approval gate', 10], ['human in the loop', 9], ['mcp', 7], ['planner', 6], ['agent loop', 10], ['智能体工具调用', 16], ['工具调用', 12], ['审批门', 10], ['人在回路', 9], ['规划器', 6], ['智能体循环', 10]], + en: { + title: 'Agent tool-call loop', question: 'How does an agent plan, get permission, act, recover, and report?', + summary: 'A lane-based agent loop with policy gates, tool execution, exception recovery, evidence, and final response.', + useWhen: 'Explaining agent runtimes, MCP/tool orchestration, approvals, retries, or observability.', + avoidWhen: 'The goal is only to show static agent components or exact API message timing.', + include: ['request and planning', 'policy or approval gate', 'tool execution', 'exception and evidence paths'], + prompt: 'Use Archify workflow mode to explain this agent tool-call loop. Separate user surface, agent runtime, policy boundary, exception handling, tool execution, and observability into lanes. Make the successful path primary and show approval, retry, blocked, and evidence paths explicitly.', + }, + zh: { + title: '智能体工具调用', question: '智能体如何规划、获批、执行、恢复并汇报?', + summary: '用泳道表达策略门、工具执行、异常恢复、证据和最终回复。', + useWhen: '适合解释 Agent Runtime、MCP/工具编排、审批、重试和可观测性。', + avoidWhen: '如果只想看静态组件,或重点是精确 API 消息时序,请换其他配方。', + include: ['请求与规划', '策略或审批门', '工具执行', '异常与证据路径'], + prompt: '用 Archify 工作流模式解释这段智能体工具调用。把用户界面、Agent Runtime、策略边界、异常处理、工具执行和可观测性分成泳道;突出成功主路径,并明确展示审批、重试、阻塞和证据路径。', + }, + }, + { + id: 'delivery-workflow', type: 'workflow', proof: 'delivery-workflow', + presentation: { preset: 'classic', motion: 'trace' }, + signals: [['ci/cd', 14], ['release workflow', 14], ['deployment pipeline', 11], ['pull request', 7], ['staging', 7], ['rollback', 8], ['发布流程', 14], ['流水线', 9], ['上线', 7], ['预发', 7], ['回滚', 8], ['审批发布', 10]], + en: { + title: 'Delivery workflow', question: 'How does a change move safely from commit to production?', + summary: 'A delivery flow with build, checks, environments, approvals, smoke tests, rollback, and ownership lanes.', + useWhen: 'CI/CD design, release reviews, deployment governance, or onboarding developers to delivery.', + avoidWhen: 'The question is where infrastructure runs or what states a deployment object can occupy.', + include: ['trigger and build', 'blocking checks', 'approval and environments', 'rollback and verification'], + prompt: 'Use Archify workflow mode to draw this delivery process from commit to production. Separate developer, CI, approval, environment, and exception lanes; mark blocking checks, smoke tests, ownership, and the rollback path. Keep one unmistakable happy path.', + }, + zh: { + title: '研发交付流程', question: '一次变更如何安全地从提交走到生产?', + summary: '展示构建、检查、环境、审批、冒烟、回滚和负责人泳道。', + useWhen: '适合 CI/CD 设计、发布评审、部署治理和研发新人上手。', + avoidWhen: '如果重点是基础设施位置或部署对象的状态集合,请换架构图或生命周期图。', + include: ['触发与构建', '阻断检查', '审批与环境', '回滚与验证'], + prompt: '用 Archify 工作流模式绘制从代码提交到生产发布的流程。拆分开发者、CI、审批、环境和异常泳道;标出阻断检查、冒烟测试、负责人和回滚路径,并保留一条一眼可见的成功主路径。', + }, + }, + { + id: 'incident-runbook', type: 'workflow', proof: 'incident-runbook', + presentation: { preset: 'signal-flow', motion: 'trace' }, + signals: [['incident response', 15], ['runbook', 12], ['outage', 9], ['triage', 8], ['mitigation', 8], ['escalation', 7], ['事故处置', 15], ['故障', 9], ['应急预案', 12], ['排障', 9], ['缓解', 7], ['升级响应', 8]], + en: { + title: 'Incident runbook', question: 'How do responders detect, triage, mitigate, verify, and escalate?', + summary: 'An operational workflow that separates signals, responders, mitigation, communications, and recovery proof.', + useWhen: 'Incident playbooks, on-call handoffs, reliability reviews, and tabletop exercises.', + avoidWhen: 'The audience needs live metrics or a post-incident component topology instead of response actions.', + include: ['detection signal', 'triage owner', 'mitigation and rollback', 'verification and communication'], + prompt: 'Use Archify workflow mode to turn this incident runbook into responder lanes. Show detection, triage, mitigation, escalation, communication, rollback, and recovery verification. Separate decision gates from actions and make missing ownership visible.', + }, + zh: { + title: '事故处置 Runbook', question: '响应者如何发现、分诊、缓解、验证并升级?', + summary: '把信号、响应者、缓解动作、沟通和恢复证据拆成可执行流程。', + useWhen: '适合故障预案、On-call 交接、稳定性评审和桌面演练。', + avoidWhen: '如果受众需要实时指标仪表盘或事故后的组件拓扑,而不是响应动作,请换其他视图。', + include: ['发现信号', '分诊负责人', '缓解与回滚', '恢复验证与沟通'], + prompt: '用 Archify 工作流模式把事故处置预案画成响应者泳道。展示发现、分诊、缓解、升级、沟通、回滚和恢复验证;把决策门与操作分开,并让缺失的负责人清晰可见。', + }, + }, + { + id: 'api-request', type: 'sequence', proof: 'cache-miss', + presentation: { preset: 'classic', motion: 'trace' }, + start: { + en: { descriptionPrompt: 'Use Archify sequence mode to draw this interaction: [paste the participants, calls, returns, fallback, and asynchronous side effects]. Keep message order unambiguous, labels short, and unknown behavior explicit. No repository is required.' }, + zh: { descriptionPrompt: '用 Archify 时序模式绘制下面的交互:[粘贴参与者、调用、返回、回退和异步副作用]。确保消息顺序无歧义、标签简短,并明确标注未知行为。不需要代码库。' }, + }, + signals: [['api request', 14], ['request response', 12], ['call chain', 11], ['cache miss', 13], ['jwt', 8], ['who calls whom', 12], ['api 请求', 14], ['请求响应', 12], ['调用链', 11], ['缓存未命中', 13], ['谁调用谁', 12], ['鉴权链路', 9]], + en: { + title: 'API request chain', question: 'Who calls whom, in what order, and what returns?', + summary: 'A time-ordered request path with authentication, cache fallback, persistence, return traffic, and async trace.', + useWhen: 'API documentation, debugging request latency, auth reviews, or explaining cache fallback.', + avoidWhen: 'Order is unimportant and the audience only needs the stable service topology.', + include: ['callers and callees', 'request and return messages', 'fallback or error path', 'async side effects'], + prompt: 'Use Archify sequence mode to show this request from caller to final response. Include authentication, cache hit or miss, persistence fallback, return messages, and asynchronous trace or event emission. Keep message labels short and order unambiguous.', + }, + zh: { + title: 'API 请求链', question: '谁调用谁、顺序如何、最终返回什么?', + summary: '按时间展示鉴权、缓存回退、持久化、返回流量和异步追踪。', + useWhen: '适合 API 文档、请求耗时排查、鉴权评审和缓存回退说明。', + avoidWhen: '如果顺序不重要,受众只需要稳定的服务拓扑,请用架构图。', + include: ['调用方与被调用方', '请求与返回消息', '回退或错误路径', '异步副作用'], + prompt: '用 Archify 时序模式展示从调用方到最终响应的完整请求。包含鉴权、缓存命中或未命中、持久化回退、返回消息,以及异步 Trace 或事件上报;消息标签保持简短,顺序必须明确。', + }, + }, + { + id: 'async-roundtrip', type: 'sequence', proof: 'async-roundtrip', + presentation: { preset: 'signal-flow', motion: 'trace' }, + signals: [['async roundtrip', 14], ['webhook', 10], ['callback', 10], ['acknowledgement', 8], ['timeout', 7], ['retry message', 8], ['异步回调', 14], ['回调', 10], ['确认消息', 8], ['超时', 7], ['消息重试', 9], ['webhook', 10]], + en: { + title: 'Async roundtrip', question: 'What happens after the initial request returns?', + summary: 'A sequence view of enqueue, acknowledgement, background work, callbacks, retries, timeout, and final consistency.', + useWhen: 'Webhooks, jobs, queues, payment callbacks, eventual consistency, or async API contracts.', + avoidWhen: 'The primary question is topic topology and consumer ownership rather than time order.', + include: ['initial acknowledgement', 'queue or scheduler', 'background work', 'callback, retry, and timeout'], + prompt: 'Use Archify sequence mode to explain this asynchronous roundtrip. Show the initial acknowledgement, enqueue or scheduling step, background processing, callback or polling, retry and timeout behavior, and the point where the caller can observe final consistency.', + }, + zh: { + title: '异步往返链路', question: '初始请求返回之后,后台还会发生什么?', + summary: '按时间展示入队、确认、后台处理、回调、重试、超时和最终一致。', + useWhen: '适合 Webhook、后台任务、队列、支付回调、最终一致和异步 API 契约。', + avoidWhen: '如果重点是 Topic 拓扑和消费者归属,而不是时间顺序,请用事件数据流配方。', + include: ['初始确认', '队列或调度器', '后台处理', '回调、重试与超时'], + prompt: '用 Archify 时序模式解释这段异步往返链路。展示初始确认、入队或调度、后台处理、回调或轮询、重试与超时,以及调用方何时能观察到最终一致结果。', + }, + }, + { + id: 'data-lineage', type: 'dataflow', proof: 'product-analytics', + presentation: { preset: 'classic', motion: 'trace' }, + signals: [['data lineage', 15], ['etl', 12], ['warehouse', 9], ['pii', 11], ['governance', 9], ['analytics pipeline', 12], ['数据血缘', 15], ['数据管道', 11], ['数仓', 9], ['治理', 9], ['隐私数据', 10], ['用户同意', 9]], + en: { + title: 'Data lineage', question: 'Where does data come from, how does it change, and who consumes it?', + summary: 'A governed path from sources through consent, transforms, sensitive stores, warehouse, and consumers.', + useWhen: 'Analytics architecture, ETL/ELT review, PII assessment, warehouse design, or model feature lineage.', + avoidWhen: 'The audience needs request timing or operational task ownership rather than data assets.', + include: ['sources and assets', 'transform stages', 'classification or consent', 'stores and consumers'], + prompt: 'Use Archify dataflow mode to map this data lineage. Name every data asset and transform, show consent or classification boundaries, distinguish streaming from batch paths, and identify stores plus downstream consumers. Do not use unlabeled flows.', + }, + zh: { + title: '数据血缘', question: '数据从哪里来、如何变化、最终被谁消费?', + summary: '从来源经过同意、转换、敏感存储、数仓直到消费者的治理路径。', + useWhen: '适合分析架构、ETL/ELT 评审、PII 评估、数仓设计和特征血缘。', + avoidWhen: '如果受众需要请求时序或操作负责人,而不是数据资产,请换其他配方。', + include: ['数据来源与资产', '转换阶段', '分类或同意边界', '存储与消费者'], + prompt: '用 Archify 数据流模式梳理这段数据血缘。为每个数据资产和转换命名,展示用户同意或数据分类边界,区分流式与批处理路径,并标明存储和下游消费者;所有数据流都必须有标签。', + }, + }, + { + id: 'event-stream', type: 'dataflow', proof: 'event-stream', + presentation: { preset: 'signal-flow', motion: 'trace' }, + start: { + en: { descriptionPrompt: 'Use Archify dataflow mode to map this data journey: [paste the sources, data assets, transforms, stores, boundaries, and consumers]. Label every flow, distinguish streaming from batch where relevant, and mark unknown classifications or ownership instead of inventing them.' }, + zh: { descriptionPrompt: '用 Archify 数据流模式梳理下面的数据路径:[粘贴来源、数据资产、转换、存储、边界和消费者]。为每条数据流标注名称,在有意义时区分流式与批处理,未知的分类或归属要标明而不是编造。' }, + }, + signals: [['event stream', 15], ['kafka topology', 14], ['topic', 8], ['consumer group', 11], ['dead letter', 10], ['dlq', 10], ['事件流', 15], ['kafka 拓扑', 14], ['主题', 7], ['消费者组', 11], ['死信', 10], ['事件地铁图', 12]], + en: { + title: 'Event-stream topology', question: 'Which events move through which topics, processors, groups, and failure paths?', + summary: 'A stream map of producers, topics, ordered processors, consumer groups, state, replay, and DLQ.', + useWhen: 'Kafka/event-platform design, stream processing reviews, ownership, replay, and failure handling.', + avoidWhen: 'Topic names, consumer groups, and delivery semantics are not known—use a generic workflow instead.', + include: ['producers and event names', 'topics and ordering', 'processors and consumer groups', 'state, replay, and DLQ'], + prompt: 'Use Archify dataflow mode to draw this event-stream topology. Name producers, events, topics, ordered processors, consumer groups, state stores, replay paths, and the DLQ. Show ownership and delivery semantics only when supported by evidence.', + }, + zh: { + title: '事件流拓扑', question: '哪些事件经过哪些 Topic、处理器、消费者组和失败路径?', + summary: '展示生产者、Topic、有序处理器、消费者组、状态、重放和 DLQ。', + useWhen: '适合 Kafka/事件平台设计、流处理评审、归属、重放和失败处理。', + avoidWhen: '如果 Topic、消费者组和投递语义都不清楚,请先用通用工作流,不要编造事件拓扑。', + include: ['生产者与事件名', 'Topic 与顺序', '处理器与消费者组', '状态、重放与 DLQ'], + prompt: '用 Archify 数据流模式绘制这段事件流拓扑。命名生产者、事件、Topic、有序处理器、消费者组、状态存储、重放路径和 DLQ;只有在证据充分时才标注归属和投递语义。', + }, + }, + { + id: 'object-lifecycle', type: 'lifecycle', proof: 'agent-run', + presentation: { preset: 'classic', motion: 'trace' }, + start: { + en: { descriptionPrompt: 'Use Archify lifecycle mode to model this object: [paste its states, transition events, waits, retries, cancellation, and terminal outcomes]. Separate active, waiting, recoverable-failure, and terminal states, and never hide an ending. No repository is required.' }, + zh: { descriptionPrompt: '用 Archify 生命周期模式建模这个对象:[粘贴它的状态、转换事件、等待、重试、取消和终态]。分开执行、等待、可恢复失败和终态,不要隐藏任何结束方式。不需要代码库。' }, + }, + signals: [['state machine', 15], ['object lifecycle', 14], ['status transition', 11], ['terminal state', 9], ['retry state', 8], ['状态机', 15], ['生命周期', 13], ['状态流转', 11], ['终态', 9], ['等待态', 8], ['重试状态', 8]], + en: { + title: 'Object lifecycle', question: 'Which states exist, what events move between them, and how does it end?', + summary: 'A state model with active work, waits, retries, cancellation, failure, and explicit terminal outcomes.', + useWhen: 'Tasks, orders, tickets, subscriptions, jobs, agent runs, or any durable object with status.', + avoidWhen: 'The object has no durable state and the real question is participant interaction over time.', + include: ['start and active states', 'event-labelled transitions', 'wait and retry states', 'all terminal outcomes'], + prompt: 'Use Archify lifecycle mode to model this object. Separate main progress, waiting or interruption states, and terminal outcomes. Label transitions with events, include retry, cancellation, timeout, success, and failure where real, and never hide an ending.', + }, + zh: { + title: '对象生命周期', question: '有哪些状态、什么事件触发流转、最终如何结束?', + summary: '展示执行、等待、重试、取消、失败以及明确终态的状态模型。', + useWhen: '适合任务、订单、工单、订阅、作业、Agent Run 等带持久状态的对象。', + avoidWhen: '对象没有持久状态,真正问题是参与者随时间的交互时,请使用时序图。', + include: ['开始与执行态', '带事件的转换', '等待与重试态', '所有终态'], + prompt: '用 Archify 生命周期模式建模这个对象。分开主进度、等待或中断状态和终态;用事件标注转换,并在真实存在时展示重试、取消、超时、成功和失败,不能隐藏任何结束方式。', + }, + }, + { + id: 'deployment-lifecycle', type: 'lifecycle', proof: 'deployment-lifecycle', + presentation: { preset: 'signal-flow', motion: 'trace' }, + signals: [['deployment lifecycle', 15], ['release state', 10], ['promotion state', 9], ['approval status', 8], ['rollback state', 10], ['部署生命周期', 15], ['发布状态', 10], ['晋级', 7], ['审批状态', 8], ['回滚状态', 10]], + en: { + title: 'Deployment lifecycle', question: 'What state is a release in, and what can happen next?', + summary: 'A deployment state model covering queued, building, verifying, approval, promotion, rollback, and terminal outcomes.', + useWhen: 'Release controllers, GitOps reconciliation, environment promotion, or deployment status APIs.', + avoidWhen: 'The question is the human/CI sequence of delivery actions rather than the deployment object state.', + include: ['queued and running states', 'verification and approval', 'promotion and rollback', 'success, failure, cancellation'], + prompt: 'Use Archify lifecycle mode to model the deployment object. Show queued, building, verifying, waiting for approval, promoting, rolling back, and every terminal outcome. Label the events and guards that permit each transition.', + }, + zh: { + title: '部署生命周期', question: '一次发布当前处于什么状态,下一步可能发生什么?', + summary: '覆盖排队、构建、验证、审批、晋级、回滚和终态的部署状态模型。', + useWhen: '适合发布控制器、GitOps 对账、环境晋级和部署状态 API。', + avoidWhen: '如果重点是人员与 CI 的交付动作顺序,而不是部署对象状态,请用交付工作流。', + include: ['排队与执行态', '验证与审批', '晋级与回滚', '成功、失败与取消'], + prompt: '用 Archify 生命周期模式建模部署对象。展示排队、构建、验证、等待审批、晋级、回滚以及所有终态,并标注允许每次状态转换的事件和守卫条件。', + }, + }, + { + id: 'layout-repair', type: 'architecture', proof: 'web-app', + presentation: { preset: 'classic', motion: 'static' }, + signals: [ + ['layout repair', 20], ['repair order', 20], ['fix order', 20], + ['viewport overflow', 20], ['overflow', 16], ['scrollheight', 16], ['scrollwidth', 16], + ['overlap', 16], ['label overlap', 20], ['edge through node', 20], ['crossing', 8], + ['via', 7], ['waypoint', 14], + ['布局修复', 20], ['修复顺序', 20], ['视口溢出', 20], ['溢出', 16], ['滚动高度', 16], ['滚动宽度', 16], + ['重叠', 16], ['标签重叠', 20], ['连线穿过节点', 20], ['连线穿节点', 20], ['交叉', 8], + ['途经点', 14], ['路径点', 14], ['拐点', 14], + ], + en: { + title: 'Layout repair', question: 'Why does the existing diagram overflow, overlap, or route through nodes, and what should be fixed first?', + summary: 'Repair an existing diagram in its current mode using validation diagnostics, explicit waypoint semantics, and a measured desktop viewport budget.', + useWhen: 'An existing architecture, workflow, sequence, dataflow, or lifecycle diagram needs layout repair; keep its diagram type and presentation settings.', + avoidWhen: 'The task is choosing a new diagram type. Do not change topology, delete meaningful labels, or hide overflow just to pass checks.', + include: [ + 'repair order: schema → overlap → direction → crossings → labels', + 'via contract: absolute [x, y] intermediate points; route = [start, ...via, end]', + 'desktop viewport budget for the complete page, including header and necessary cards', + 'validate after each edit, then inspect the final HTML in a browser', + ], + prompt: 'Use Archify to repair this existing diagram while preserving its diagram type, topology, meaningful labels, and presentation settings. Follow references/authoring-contract.md in this order: (1) schema and missing/invalid meta.quality_profile; (2) node overlap or out-of-range placement; (3) edge-through-node and endpoint-direction errors; (4) crossings, ambiguous corridors, border runs, excessive detours, and route rhythm; (5) labels: label-to-node, label-to-label, then label-to-route clearance. Run validate after every edit and use diagnostics[] code, subject, evidence, and supportedFixes; apply one diagnosed geometry control at a time. Where the current schema supports via, it is an ordered array of absolute SVG [x, y] intermediate points: the route is [start, ...via, end], with start/end supplied by the node anchors. Explicit via points override automatic routing; they are not offsets or a request for automatic obstacle avoidance. For an orthogonal repair, align adjacent points on the same x or y and make the first/final segment respect fromSide/toSide; use only controls supported by the current diagram mode. Follow references/delivery-contract.md for the viewport budget: check 1440×900, 1600×1000, 1920×1080, and 2048×1320; require document.documentElement.scrollWidth <= window.innerWidth. Prefer document.documentElement.scrollHeight <= window.innerHeight, but preserve a Reader-declared readable vertical page scroll when browser-check explicitly accepts it after reaching the projected-text floor. Budget the entire page, including header, diagram, and necessary cards. Repair every other overflow by removing only redundant content or compacting spacing first; do not hide overflow, clip content, introduce an internal diagram scroller, stretch the SVG, or shrink typography to force a pass. Let finalize run the required browser evidence; run perceptual visual review only when requested or escalated by the delivery contract.', + }, + zh: { + title: '布局修复', question: '现有图为什么仍然溢出、重叠或连线穿过节点,应该先修什么?', + summary: '保留现有图表模式,依据验证诊断、途经点语义和实测桌面视口预算修复布局。', + useWhen: '已有架构图、工作流、时序图、数据流或生命周期图需要修复布局;保留原来的图表类型和表现设置。', + avoidWhen: '任务是为新图选择类型时不要使用。不要为了通过检查改变拓扑、删除有意义的标签或隐藏溢出。', + include: [ + '修复顺序:schema → 重叠 → 方向 → 交叉 → 标签', + 'via 契约:绝对 [x, y] 中间点;路径 = [start, ...via, end]', + '包含标题和必要卡片的整页桌面视口预算', + '每次修改后 validate,最终在浏览器中检查 HTML', + ], + prompt: '用 Archify 修复这张现有图,保留图表类型、拓扑、有意义的标签和表现设置。遵循 references/authoring-contract.md 的顺序:(1) schema 错误及缺失或无效的 meta.quality_profile;(2) 节点重叠或越界;(3) 连线穿过节点及端点方向错误;(4) 交叉、含混的共享通道、贴边走线、过度绕行和转弯节奏;(5) 标签与节点、标签与标签、标签与连线的间距。每次修改后运行 validate,依据 diagnostics[] 的 code、subject、evidence 和 supportedFixes,每次只应用一项有诊断依据的几何控制。当前 schema 支持 via 时,它是按顺序排列的绝对 SVG [x, y] 中间点数组:路径为 [start, ...via, end],起终点由节点锚点提供。显式 via 会覆盖自动路由,不是偏移量,也不会请求自动绕障。修复正交走线时,相邻点应共享 x 或 y,首尾线段应遵守 fromSide/toSide;只使用当前模式支持的控制字段。视口预算遵循 references/delivery-contract.md:检查 1440×900、1600×1000、1920×1080 和 2048×1320,要求 document.documentElement.scrollWidth <= window.innerWidth。优先满足 document.documentElement.scrollHeight <= window.innerHeight;如果 browser-check 明确确认 Reader 已达到投影文字下限并接受可读的页面纵向滚动,则保留该滚动。预算覆盖整页,包括标题、主图和必要卡片。其他溢出先通过移除冗余内容或压缩间距修复,不得靠隐藏溢出、裁切、内部滚动区、拉伸 SVG 或缩小字体强行通过。让 finalize 执行必需的浏览器证据;只在用户要求或 delivery contract 规定的升级条件下进行感知视觉审阅。', + }, + }, +]; + +export const SCENARIO_RECIPES = Object.freeze(RAW_RECIPES.map((recipe) => Object.freeze({ + ...recipe, + presentation: Object.freeze({ ...recipe.presentation }), + ...(recipe.start ? { start: Object.freeze({ + en: Object.freeze({ ...recipe.start.en }), + zh: Object.freeze({ ...recipe.start.zh }), + }) } : {}), + signals: Object.freeze(recipe.signals.map((signal) => Object.freeze(signal.slice()))), + en: Object.freeze({ ...recipe.en, include: Object.freeze(recipe.en.include.slice()) }), + zh: Object.freeze({ ...recipe.zh, include: Object.freeze(recipe.zh.include.slice()) }), +}))); + +export function detectGuideLanguage(value = '') { + return /[\u3400-\u9fff]/u.test(value) ? 'zh' : 'en'; +} + +export function startPromptsFor(recipe, lang = 'en') { + const language = lang === 'zh' ? 'zh' : 'en'; + const copy = recipe[language]; + const descriptionPrompt = recipe.start?.[language]?.descriptionPrompt; + if (!descriptionPrompt) { + throw new Error(`Scenario recipe ${JSON.stringify(recipe.id)} does not define a ${language} start prompt.`); + } + const repositoryPrompt = recipe.type === 'architecture' + ? copy.prompt + : language === 'zh' + ? `先检查这个仓库里的相关证据,然后${copy.prompt}不要编造代码无法支持的行为。` + : `Inspect this repository for evidence, then ${copy.prompt.charAt(0).toLowerCase()}${copy.prompt.slice(1)} Do not invent behavior that the code does not support.`; + return { descriptionPrompt, repositoryPrompt }; +} + +function normalized(value) { + return String(value || '').normalize('NFKC').toLowerCase().replace(/[\s_]+/g, ' ').trim(); +} + +function localized(recipe, lang) { + const copy = recipe[lang === 'zh' ? 'zh' : 'en']; + return { + id: recipe.id, + type: recipe.type, + proof: recipe.proof, + presentation: { ...recipe.presentation }, + ...copy, + include: copy.include.slice(), + }; +} + +export function listScenarioRecipes(lang = 'en') { + return SCENARIO_RECIPES.map((recipe) => localized(recipe, lang)); +} + +function scoreRecipe(recipe, query) { + const text = normalized(query); + if (!text) return { recipe, score: 0, matched: [] }; + if (text === recipe.id || text === recipe.id.replace(/-/g, ' ')) { + return { recipe, score: 100, matched: [recipe.id] }; + } + let score = 0; + const matched = []; + for (const [signal, weight] of recipe.signals) { + if (text.includes(normalized(signal))) { + score += weight; + matched.push(signal); + } + } + return { recipe, score, matched }; +} + +export function recommendScenario(query, options = {}) { + const lang = options.lang === 'zh' || options.lang === 'en' ? options.lang : detectGuideLanguage(query); + const ranked = SCENARIO_RECIPES.map((recipe) => scoreRecipe(recipe, query)) + .sort((left, right) => right.score - left.score || SCENARIO_RECIPES.indexOf(left.recipe) - SCENARIO_RECIPES.indexOf(right.recipe)); + const winner = ranked[0].score > 0 ? ranked[0] : { recipe: SCENARIO_RECIPES[0], score: 0, matched: [] }; + const confidence = winner.score >= 14 ? 'high' : winner.score >= 7 ? 'medium' : 'low'; + return { + ok: true, + mode: 'recommendation', + lang, + query: String(query || ''), + confidence, + matchedSignals: winner.matched.slice(), + recommendation: localized(winner.recipe, lang), + alternatives: ranked.filter((entry) => entry.recipe.id !== winner.recipe.id && entry.score > 0) + .slice(0, 2) + .map((entry) => ({ ...localized(entry.recipe, lang), score: entry.score })), + }; +} + +export function formatScenarioList(lang = 'en') { + const isZh = lang === 'zh'; + const heading = isZh + ? `Archify 场景配方(${SCENARIO_RECIPES.length})` + : `Archify scenario recipes (${SCENARIO_RECIPES.length})`; + const intro = isZh + ? '先选择你要回答的问题,再选择图表类型。可运行:archify guide "你的场景"' + : 'Choose the question before the diagram type. Run: archify guide "your scenario"'; + return [heading, '', intro, '', ...listScenarioRecipes(lang).flatMap((recipe) => [ + `${recipe.id} [${recipe.type}] ${recipe.title}`, + ` ${recipe.question}`, + ])].join('\n'); +} + +export function formatScenarioRecommendation(result) { + const isZh = result.lang === 'zh'; + const recipe = result.recommendation; + const labels = isZh ? { + heading: '推荐', question: '要回答的问题', use: '适合', avoid: '不要这样用', include: '必须包含', presentation: '表现建议', prompt: '可直接复制的提示词', alternatives: '其他可能', confidence: '置信度', + } : { + heading: 'Recommendation', question: 'Question answered', use: 'Use when', avoid: 'Avoid when', include: 'Must include', presentation: 'Presentation', prompt: 'Copy-ready prompt', alternatives: 'Other possible fits', confidence: 'Confidence', + }; + const lines = [ + `${labels.heading}: ${recipe.title} [${recipe.type}]`, + `${labels.confidence}: ${result.confidence}`, + `${labels.question}: ${recipe.question}`, + '', + `${labels.use}: ${recipe.useWhen}`, + `${labels.avoid}: ${recipe.avoidWhen}`, + `${labels.include}: ${recipe.include.join(isZh ? '、' : '; ')}`, + `${labels.presentation}: ${recipe.presentation.preset} · ${recipe.presentation.motion}`, + '', + `${labels.prompt}:`, + recipe.prompt, + ]; + if (result.alternatives.length) { + lines.push('', `${labels.alternatives}: ${result.alternatives.map((item) => `${item.title} [${item.type}]`).join(' · ')}`); + } + return lines.join('\n'); +} + +export function publicGuideData() { + return SCENARIO_RECIPES.map((recipe) => ({ + ...localized(recipe, 'en'), + en: recipe.en, + zh: recipe.zh, + signals: recipe.signals.map(([signal, weight]) => [signal, weight]), + })); +} diff --git a/vendor/archify/references/architecture-layout-repair.md b/vendor/archify/references/architecture-layout-repair.md new file mode 100644 index 0000000..d7792cf --- /dev/null +++ b/vendor/archify/references/architecture-layout-repair.md @@ -0,0 +1,18 @@ +# Architecture layout repair + +Use this after actual visual review finds several tangled routes. A successful machine receipt does not settle composition. Work on the existing candidate, retaining all required components, relationships, labels, evidence, boundaries, and node sizes. + +## Choose the repair scope + +If the main and secondary chains already read clearly, repair the isolated defect locally. If a main chain is blocked, several routes tangle, or a local fix moves the defect onto another route, reflow the connected scene in one edit. Preserve all semantics and user-fixed geometry; agent-generated positions and route controls may change. After moving nodes, remove stale generated route overrides so automatic routing can use the new placement. + +The receipt's `directCorridorBlockers`, when present, names nodes between an edge's aligned endpoints. This is geometric evidence, not proof that the edge is the main path or a new validation failure. Trace the reader's actual main path first. When a listed blocker interrupts that path, reposition the connected group instead of adding another detour. + +## One coherent repair + +1. Trace the affected relationships and their endpoints in the JSON. Before choosing coordinates, write the reader’s main path as an ordered list of existing edges, then identify shared state and real feedback cycles. Every neighboring pair in that main path must have the relationship being explained; place other components on branches beside their actual owner. Use `validate architecture --layout-json --repo-root ` once if the compact receipt and screenshot do not reveal the needed route or label geometry; omit `--repo-root` only for a design without repository evidence. Do not guess repeated waypoint coordinates. +2. Place those connected main-path neighbors in reading order. Put shared state between its readers/writers, on an adjacent row if necessary, so one writer does not need a line across the whole execution area. Arrange a feedback cycle in its actual edge order around an open rectangle, with other consumers beside their owner. For example, if the edges are `client → API → dispatcher → worker → collector → runner → dispatcher`, put the first four on the upper row, collector below worker, and runner below dispatcher: the return then uses the lower row and a short upward edge. This illustrates adjacency, not a graph or set of coordinates to copy; use only relationships present in the candidate. Preserve every edge and its actual direction. +3. Keep automatic endpoints for the new placement. Constrain a side only when a branch or return needs a specific corridor; check that corridor against every affected relationship, including storage branches. If that local constraint introduces another conflict, return to the connected placement instead of cycling through side combinations. Only use detailed `via` or label coordinates for a remaining measured defect. Keep external actors outside the resolved boundary rectangle, including its padding; not listing a node in `wraps` does not visually exclude it. Keep an internal relationship and its label inside the shared boundary unless crossing it conveys a real fact; do not imply an external hop merely to avoid another route. +4. When `composition/label-gap` reports a measured minimum, enlarge that clear gap or move the connected group onto another readable row. Keep the full label beside its own route; placing it in distant empty space does not repair the relationship. Compact unused gaps while preserving measured label space and the previous text size. Keep the main interaction and all required nodes readable in the default desktop view; do not trade crossings for a large blank canvas, tiny text, or a chain that doubles back without a semantic reason. + +Run the complete `finalize` once after the edit, then `visual-check` on the successful artifact and inspect its desktop captures. When replacing an already reviewed artifact, use one fresh `--out-dir` for both commands as described in [the delivery contract](delivery-contract.md#a-new-candidate-at-an-existing-output-path). Trace the main path, each secondary chain, and every affected arrow and label. A bounded second repair may address a remaining specific defect. If it still fails visual acceptance, retain the candidate and report the concrete gap; do not count it as a successful repair or continue blind coordinate changes. diff --git a/vendor/archify/references/authoring-contract.md b/vendor/archify/references/authoring-contract.md new file mode 100644 index 0000000..623473f --- /dev/null +++ b/vendor/archify/references/authoring-contract.md @@ -0,0 +1,419 @@ +# Authoring contract + +Read this reference only after the Fast authoring path calls for more detail. The schemas and examples remain authoritative. + +## Composition repair + +When correcting an authored overview's abstraction, map every affected role, relationship direction, protocol, boundary, condition, and source reference to its surviving node or relationship before regrouping. A startup citation does not prove a message protocol. Preserve each claim's inspected evidence. Keep user-supplied or agreed topology fixed; fewer routes alone do not justify merging. A boundary around one node requires an explicit isolation fact and must not merely repeat its label. + +## Label repair + +When a relationship label collides, move the label, adjust the route or spacing, then shorten the wording while preserving meaning. Omit wording only when both endpoints fully imply it and it conveys no protocol, action, direction, synchronous or asynchronous behavior, or cross-boundary mechanism. Spacing means clear gap rather than center distance; measured mask width takes precedence. The first-draft gap budget is in [Layout and routing](authoring-defaults.md#layout-and-routing). + +For a disproportionate sublabel, keep its exact role or protocol concise and place the supplementary fact in a note or card. Preserve every required responsibility, protocol, and boundary fact. Use the first-draft node-width budget in [Layout and routing](authoring-defaults.md#layout-and-routing). + +## Schema lookup + +Read both the mode schema and `schemas/common.schema.json`. The mode schemas use `$ref`, so the common file is where shared enums live. + +- `componentType`: `frontend`, `backend`, `database`, `cloud`, `security`, `messagebus`, `external` +- `variant`: `default`, `emphasis`, `security`, `dashed` +- Relationship IDs use the shared identifier pattern and must be unique in their collection. + +Do not invent fields. Before writing any new field, enum, or constrained text, read its schema definition, including common `$ref` targets. In particular, check boundary kinds, repository identity, and source-reference shapes. An example demonstrates structure; it does not enumerate every valid value. Author fresh IDs, wording, facts, and layout. + +## Workflow layout contracts + +Use schema v2 for new workflows and keep schema v1 when an existing source must +retain fixed geometry. In both versions, `col` stays in `0..5` and semantic +edge labels are never deleted as a spacing repair. Do not change only +`schema_version` when absolute coordinates exist: follow the canonical +[migration and layout-receipt contract](../renderers/workflow/README.md#migration-and-layout-receipt). +The complete normative invariants live in the workflow renderer's +[layout contracts](../renderers/workflow/README.md#layout-contracts). +For sequential stages stacked in one container, use one v2 lane and group, +omit `meta.viewBox`, and center nodes around the lane content with symmetric +`yOffset` values such as `-90 / 0 / 90`. Keep semantic edge labels and act on +compiler diagnostics. + +## Legend contract + +Omit `meta.legend` for the truthful default: `auto` lists only semantic kinds +present in typed IR. Use `mode: "all"` for a renderer reference or +`mode: "hidden"` to remove the full legend. Under `entries`, only keys listed +by the selected mode schema are valid; each key accepts `label`, `visible`, or +both. `visible: true` may show an unused supported convention, while +`visible: false` hides it. `hidden` cannot be overridden. + +A label override changes reader wording only. Never infer a kind from prose or +use the legend to compensate for missing nodes, states, messages, or flows. +Long labels are measured and wrap into deterministic rows. Architecture's +implicit automatic viewBox grows from that same measured footprint. For +backwards compatibility, a legacy document with no `meta.legend` may omit an +implicit auto legend that cannot fit its explicit viewBox; this never changes +its typed topology. Adding `meta.legend` makes the presentation intentional and +strict: if its resolved labels cannot fit the authored viewBox, shorten or hide +them, or widen the viewBox using the emitted diagnostic. + +## Language consistency + +Choose one primary authored language. An explicit user choice wins; otherwise +use the language of the request, or the conversation's dominant language when +the request itself is language-neutral. Separately choose the Viewer locale. +Always write the matching `meta.locale` as a well-formed language tag: `"en"` +for English, `"zh-CN"` for Simplified Chinese, `"es"` for Spanish, or any +other tag for another language. The renderer consumes the authored locale without inferring language +from diagram strings. Documents that omit it remain valid and default to +English. + +`meta.locale` controls only renderer-owned reader surfaces: ``, the +document-title suffix, default SVG description and focus labels, default legend +labels, and fixed Viewer controls, statuses, accessibility names, and errors. +It never translates authored content. Apply the primary language separately to +titles, subtitles, node and relationship copy, boundaries, lanes, groups, +legend label overrides, and cards. A bilingual diagram still +chooses one primary locale for the Viewer; follow an explicit primary-language +request, then prompt order or conversation dominance. + +`en` and `zh-CN` are built-in Viewer catalogs and need nothing further. For +every other `meta.locale`, also set `meta.translations`: an object mapping the +renderer's canonical message keys (`catalogKeys()` in +`renderers/shared/i18n.mjs`) to translated strings whose `{placeholder}` tokens +match the English source exactly. Reuse suitable translations from `examples/locales/` or a previously reviewed +catalog; Spanish uses `examples/locales/es.json`. Translate missing keys or adapt terminology when the diagram needs it; +use the English source to check keys and placeholders. Example catalogs may +lag new Viewer keys; validation reports those gaps and uses English for them. A key that is missing, unrecognized, or has mismatched +placeholders falls back to its English string — `validate`/`render`/`deliver` +report the resulting coverage to stderr — rather than breaking the render or +silently shipping an untranslated string as if it were translated. + +For older dev inputs using only `meta.locale: "es"`, copy the Spanish catalog +into `meta.translations` before rendering again. Existing standalone HTML +keeps its embedded translations. + +For a requested language you cannot supply `meta.translations` for, do not +write a `meta.locale` with no built-in catalog and no translations. Keep every +reader-facing authored string in the requested language, omit `meta.locale` so +the renderer safely uses English, and explicitly tell the user that fixed +Viewer UI and `` remain English and the artifact is not fully localized. +The fallback applies only to renderer-owned surfaces; it never +permits authored copy to fall back to English. Do not silently substitute +`zh-CN` for another language or Chinese locale, and do not machine-translate +`meta.translations` values without disclosing that they are unreviewed. + +Keep exact product names, code identifiers, commands, protocols, API paths, and +environment names intact. Those terms may remain English inside localized copy, +but surrounding explanatory prose must still use the selected language. +Renderer-owned default legend labels follow `meta.locale`; author a +`meta.legend.entries.*.label` override only when the diagram needs different +domain wording, and keep that authored override in the primary language. + +## Visual preset default + +Omit `meta.visual_preset` by default. The renderer then opens the diagram in +`classic` for both light and dark color modes. Color mode and visual preset are +independent viewer state: switching Light / Dark must preserve the current +preset. Author `signal-flow`, `blueprint`, or `editorial` only when the user +explicitly requests that visual style. + +## Engineering profile default + +Omit `meta.engineering_profile` for an ordinary system architecture. Region, +cluster, and security boundary wording do not by themselves enable an +engineering profile. Enable `deployment-ownership` only when the user +explicitly asks for a production deployment topology, ownership handoff, or +fail-closed deployment review and the source facts are known. Once enabled, +do not remove the engineering profile merely to pass validation; repair the +authored facts or report the diagnostics truthfully. + +## Title hierarchy + +Use one concise title and let the diagram carry the explanation. Omit +`meta.subtitle` by default, and never use it to restate the title, nodes, edges, +or cards. Include one short supporting line only when the user explicitly asks +for a subtitle; an omitted or blank subtitle must not leave an empty visual row +in the generated viewer. + +## Executable geometry rules + +Generate one responsive artifact for laptops and external displays, preserving the authored SVG/viewBox, proportions, semantic geometry, and normal document flow. Use meaningful content rows and the Reader-declared readable page-scroll behavior when the complete diagram needs more height; viewport fitting does not authorize alternate topology or smaller typography. + +- Node anchors start at side midpoints. `left`/`right` change the horizontal endpoint; `top`/`bottom` change the vertical endpoint. For an automatic Architecture relationship, unobstructed facing ports whose axis offset is under 16px may share one horizontal or vertical axis when both endpoints retain the 16px corner gutter. If exactly one endpoint belongs to a spread group, only its unshared counterpart moves; relationships spread at both endpoints keep their distinct ports and outside bridge unless a reciprocal facing pair can jointly use separate straight lanes while preserving endpoint spacing, labels, and all surrounding route and obstacle clearances. +- A side is a direction contract. The first and final route segment must be perpendicular and outward/inward in the named direction. +- In architecture, data-flow, and lifecycle diagrams, explicit `route: "straight"` requests one direct segment, which may be diagonal when endpoint sides are not pinned. The artifact checker preserves this intent; explicit sides, opaque-node clearance, and other quality gates still apply. `via` takes precedence and retains existing rules, including data-flow's requirement for orthogonal via segments. +- Automatic Port Spread is a default renderer behavior for architecture, workflow, data-flow, and lifecycle diagrams. Shared automatic endpoints spread deterministically and symmetrically with a 16px corner gutter. It does not apply to sequence messages, single relationships, or explicit `via`, `channelX`, `channelY`, `labelAt`, or non-`auto` routes. +- Showcase route rhythm: every nonzero segment must be at least 8px; every interior segment must be at least 16px. When spread ports are nearly parallel, the router uses a 24px endpoint stub and a 16px outside bridge instead of manufacturing a tiny dogleg. +- Showcase route compactness: an explicit Architecture route fails with `composition/excessive-route-detour` when its orthogonal length is at least 2.5 times an obstacle-aware legal route, adds at least 200px, and sends a control point at least 96px beyond the content envelope. The evidence records both lengths, ratio, excess, bounds, and excursion. Remove an unnecessary `via` or move the diagnosed corridor inward instead of enlarging the canvas. Related relationships that overlap on the same outer corridor by at least 32px are treated as an intentional bus and remain valid. +- Shared endpoint corridors are allowed only when they remain semantically unambiguous. Unrelated collinear overlap of 8px or more fails showcase. +- Container borders are intentional pass-through geometry, but a long edge running along a structural border is not. +- An edge crossing an unrelated opaque node is always a hard failure, independent of quality profile. + +### Explicit `via` coordinates + +Use the resolved departure anchor `S = [sx, sy]` and arrival anchor +`T = [tx, ty]`. Anchors start at side midpoints, but automatic routing and +Port Spread can move them as described above; do not assume an anchor copied +from an automatic route is the anchor of a newly authored explicit route. +Explicit `via` routes do not receive automatic Port Spread. + +For the first waypoint `F = via[0]` and last waypoint `L = via[via.length - 1]`, +use these alignments and directions (SVG y increases downward): + +| Side | Departure (`fromSide`): `S` → `F` | Arrival (`toSide`): `L` → `T` | +| --- | --- | --- | +| `top` | `F[0] === sx`, `F[1] < sy` | `L[0] === tx`, `L[1] < ty` | +| `bottom` | `F[0] === sx`, `F[1] > sy` | `L[0] === tx`, `L[1] > ty` | +| `left` | `F[1] === sy`, `F[0] < sx` | `L[1] === ty`, `L[0] < tx` | +| `right` | `F[1] === sy`, `F[0] > sx` | `L[1] === ty`, `L[0] > tx` | + +For example, given a bottom departure anchor `S = [180, 160]` and a left +arrival anchor `T = [360, 260]`, this relationship fragment leaves downward +and enters the target rightward: + +```json +{ + "from": "source", + "to": "target", + "fromSide": "bottom", + "toSide": "left", + "via": [[180, 200], [300, 200], [300, 260]] +} +``` + +The full path is `[180, 160] → [180, 200] → [300, 200] → [300, 260] → [360, 260]`. +Changing only the first waypoint to `[200, 200]` makes the departure diagonal; +changing it to `[180, 120]` keeps its x aligned but leaves upward through the +source instead of outward from its bottom. Both violate `fromSide: "bottom"` +and produce `clean-flow/endpoint-side-direction`. The example establishes +endpoint direction only: keep the full route clear of unrelated nodes and +apply the other geometry rules above. + +### Spacing and labels + +In showcase Architecture, an unpinned connection label keeps its default position +when clear. If it collides, the renderer tries a bounded set of nearby positions +along the existing route, avoiding nodes, boundary titles, other labels and +other routes within the resolved canvas. Explicit `labelAt`, `labelDx`, `labelDy` +or `labelSegment` (including zero) disables this fallback. Routes and topology +stay unchanged; if no nearby position is clear, validation reports the original +collision. Inspect resolved labels with `--layout-json` before adding controls. +Standard placement retains its existing behavior. + +Spacing recommendations mean clear gap between boxes, not center distance. A 200px center distance between 165px-wide nodes leaves only 35px of clear gap. + +For a relationship label, require: + +```text +clear gap > label mask width + 8px breathing room +label mask width ≈ 6.5px × ASCII units + 13px +CJK characters count as two units +``` + +Relationship labels are semantic data. If the gap is too small, move the label, +adjust the route or spacing, then shorten the wording while preserving meaning. +Omit only wording already fully implied by both endpoints and carrying no +protocol, action, direction, synchronous/asynchronous behavior, or +cross-boundary mechanism. Preserve every meaningful label. +Deleting it is not a spacing repair. If a relationship starts unlabeled because +its endpoints fully imply it, explain why the wording is redundant; this is a +semantic authoring choice, not a spacing repair. In workflow v2, let the compiler +allocate its measured mask before applying a diagnosed `labelAt`, +`labelDx`/`labelDy`, or `labelSegment`. Apply one diagnosed geometry control at +a time unless several edges share a constrained channel. In that case, plan the smallest coupled change from measured geometry and +validate it together. Architecture/workflow provide layout evidence through +`validate --layout-json`; for other types, use validation +diagnostics and the rendered SVG geometry. +Before adding manual routes, check whether unnecessary agent-added controls +disable automatic port spread; preserve user-required route intent. Use the +measured clearance rules above rather than guessing coordinates. + +### Repair evidence + +For architecture, `validate architecture --layout-json` exposes the +resolved component boxes, boundary frames, connection points, and label positions. +A measurable rejected layout also returns these fields, with `ok: false`, +`contract: "archify-architecture-layout-v1"`, diagnostics, and exit 1. This is +repair evidence, not artifact acceptance; it writes no HTML. Malformed input or +an implementation failure retains the ordinary failure receipt without layout. + +Use the measured failing side for `layout/boundary-out-of-bounds`. Left/top +negative coordinates need an inward move; increasing viewBox width/height only +addresses right/bottom overflow. Boundaries may wrap members across rows. Keep +real membership intact and recheck connected routes after moving members. + +Automatic architecture canvases include route points as well as nodes, frames, +and labels. An authored viewBox remains authoritative. In showcase, +`layout/route-out-of-bounds` identifies clipped route points; negative coordinates +need an inward route, while right/bottom overflow can also use a larger authored +canvas. Recheck desktop readability after enlarging a canvas. + +When several crossing/corridor diagnoses involve the same nodes, consider their +placement together before adding route controls. Apply one coherent repair and +validate it; independent label nudges cannot fix a shared layout bottleneck. +Compare diagnostics by code, subject, and stage instead of total count alone. + +### Repair order + +1. Fix missing/invalid `meta.quality_profile` and schema errors. +2. Fix node overlap or out-of-range placement. +3. Fix edge-through-node and endpoint-direction errors. +4. Fix crossings, ambiguous corridors, border runs, excessive detours, and route rhythm. +5. Fix label-to-node, label-to-label, then label-to-route clearance. +6. Fix labels that leave the canvas: move the label with `labelAt`/`labelDx`/`labelDy`/`labelSegment`, or widen `meta.viewBox`. Suggested `labelDx`/`labelDy` values replace the authored field; they are not added to it. + +Run `validate` after every edit. Consume `diagnostics[]` by stable `code`, exact `subject`, measured `evidence`, and `supportedFixes`. If the diagnostic gives `labelAt`, use that point instead of estimating another offset. + +## Mode placement + +### Architecture + +Choose overview or mechanism detail using [Composition and meaning](authoring-defaults.md#composition-and-meaning). Use one obvious primary reading path, which may step across meaningful rows when the requested topology needs room. Keep the overview readable at its chosen abstraction; expand implementation details when they answer the reader's question. Group only real ownership, trust, process, or deployment boundaries. Boundaries do not replace relationships. + +Grid placement is preferred when the schema supports it. Free positions are appropriate for a bounded exception, not for prose-level coordinate planning. Keep external actors outside the system boundary when that is factually true. + +### Workflow + +Lanes express responsibility or phase. Columns `0..5` express logical +progression. Start new workflows on `readable-v2`; retain `fixed-v1` only for +legacy geometry compatibility. Keep the happy path monotonic, preserve semantic +edge labels, and route retries and exception returns outside the main lane +corridor. + +#### Workflow viewport repair + +When `viewer/viewport-overflow` includes `workflowLanes`, inspect the tallest +rendered frames and their node span before changing the source. Measurements +are CSS pixels; space above/below nodes includes lane titles and routing, so it +is not a removable-space budget. Frame IDs identify rendered lane indices. + +Run `validate workflow --layout-json` and match those frames to +source lanes and nodes. Check whether many steps share the last logical column +and use large `yOffset` values. Readable-v2 currently reserves symmetric space +around offsets and shares the base content height between lanes, so increasing +one offset can enlarge otherwise sparse lanes. + +Where the source's ownership and explicit geometry permit, redistribute steps +across logical columns and meaningful lanes, keeping the main path monotonic. +Preserve every required node, relationship, label and semantic check. If ownership +or absolute pins prevent reflow, report that constraint instead of merging lanes +or moving pins automatically. Validate the changed JSON, deliver a fresh HTML, +then rerun browser checks and inspect the first screen; a static pass alone does +not settle viewport fit. These are repair directions, not guaranteed coordinates. + +### Sequence + +Participants are ordered by conversation role. Messages own their vertical order. Use return/async/security variants for meaning, not decoration; sequence does not use Automatic Port Spread. + +### Dataflow + +Stages express transformation or custody. Rows separate parallel streams. Label only data contracts, classifications, or cross-boundary movement that is not obvious. + +### Lifecycle + +Schema v2 (new diagrams): each populated lane is one row, `main` first, +`terminal` last, others in `lanes[]` order. `col` `0..4` is one shared x grid, +so a state placed in the column of the state it leaves gets a straight vertical +transition. Every transition, including the main path, is authored; there is no +implied rail. The renderer sizes the canvas, widens a column gap for a +same-row label, and routes automatic transitions orthogonally through row gaps. +Keep labels short: a gap carrying several parallel lines has little room. + +Schema v1 (legacy): main phases use columns `0..4`; event and terminal bands +use columns `0..2`, and event/terminal column `N` aligns with main column +`N + 2`. Every lane other than `main` and `terminal` shares one middle band; +states in the same column there need distinct `yOffset` values. + +In both versions a recoverable failure needs a real transition back to an +active state. A card saying “retry” is not topology. + +## Repository evidence + +When the diagram must reflect real code, inspect repository entrypoints, +runtime boundaries, storage, transports, and deployment configuration before +authoring. Record only evidence you actually verified. `--repo-root ` is +accepted by `render`, `validate`, `deliver`, and `preview` for every diagram +type, by architecture `compare`, and by workflow `migrate`; every mode verifies `meta.repository` and +node `sources` the same way. Migrating a source-backed workflow requires the same +`--repo-root` so its candidate is verified before replacing the destination. +Never infer runtime causality from file proximity +or naming alone. + +Declare `meta.repository.url` and one full 40-character `revision`, then attach +`sources` to the mode's node collection (Architecture `components[]`, Workflow +and Data Flow `nodes[]`, Sequence `participants[]`, Lifecycle `states[]`) with +repository-relative `path`, optional `line`, `end_line`, and `label`. +Verification reads blobs at that commit, independently of working-tree edits. +Verification ignores local Git replacement refs, including those selected by +`GIT_REPLACE_REF_BASE`, and always reads the original objects at the pinned SHA. +It does not change repository configuration or delete replacement refs. +A matching local origin, available commit, bounded path, +blob, and valid line range are required in every link mode. Verification is +local and makes no remote requests; it establishes neither public availability +nor the current reader's access rights. + +`link_mode` defaults to `web`. GitHub and Gitee HTTPS repository URLs generate +revision-pinned links; their public hosts select the provider automatically. +Optional `provider: "github"` or `"gitee"` must agree with the host. Existing +GitHub declarations and default delivery receipt fields remain compatible. + +```json +{ + "url": "https://gitee.com/team/service", + "revision": "0123456789abcdef0123456789abcdef01234567", + "provider": "gitee" +} +``` + +For an internal or unsupported forge, select `link_mode: "local-only"`. The +Viewer retains SRC markers, searchable file paths, line ranges, and revision +labels without repository or source hyperlinks. The evidence receipt adds +`linkMode: "local-only"`. `url` remains required as the expected origin identity; +local-only disables links, not identity verification. A repository without an +origin is not supported. + +```json +{ + "url": "http://git.internal:3000/Platform/Services/service", + "revision": "0123456789abcdef0123456789abcdef01234567", + "link_mode": "local-only" +} +``` + +Local-only accepts HTTP(S), `git@host:path`, and `ssh://git@host[:port]/path` +addresses, including nested namespaces. Declare a credential-free address; +HTTP(S) credentials on the checkout's origin are ignored for identity and +redacted from diagnostics. Hostnames compare case-insensitively; repository +paths retain case except for the existing GitHub behavior. A trailing slash +normalizes away. Only GitHub and Gitee normalize a terminal `.git` and match +standard HTTPS/443 with Git SSH/22. For other hosts, use the actual clone address: +transport, port, `.git` suffix, and remote-relative versus absolute paths must +match. For example, `git@host:Team/repo` differs from +`ssh://git@host/Team/repo`; `git@host:/Team/repo` matches the latter. SCP-style +paths preserve literal percent escapes, while URI paths decode them. SSH host +aliases and forge-specific browse/clone prefixes are not guessed. +GitLab/Gitea/Forgejo/Bitbucket web links are not implemented in this version; +use local-only until a tested link provider is available. Unknown web providers +fail with a diagnostic rather than emitting a guessed link. + +## Hand-placed fallback + +Use only when no renderer can run. Start from `assets/template.html`, keep semantic CSS classes, preserve the inline SVG/accessibility structure, and run the delivery visual checklist. Never introduce inline literal colors that break dark/light parity. + +## Node icons + +For domain-specific diagrams, set an optional `icon` on architecture components, +workflow/dataflow nodes, sequence participants, or lifecycle states. Choose +`calendar`, `clock`, `person`, `briefcase`, `flag`, or `moon` for everyday concepts; +the complete catalog (including existing technical and lifecycle symbols) is +`common.schema.json#/$defs/nodeIcon`. Use `icon: "none"` to hide the corner symbol. +Omitting `icon` keeps the type-based default. These inline SVG symbols are +renderer-owned and export with the diagram; URLs and raw SVG are not accepted. + +Icon selection changes only the corner symbol. The node's type still determines +color and semantic grouping; brand marks remain independent. For a holiday +workflow, pair `type: "backend", icon: "calendar"` with +`meta.legend.entries.backend.label: "假期"`, and use `icon: "briefcase"` plus +an appropriate legend label for make-up work. Keep the node label meaningful: +icons are decorative and are hidden from assistive technology. + +See [holiday planning](../examples/holiday-planning.workflow.json) for a complete workflow example. diff --git a/vendor/archify/references/authoring-defaults.md b/vendor/archify/references/authoring-defaults.md new file mode 100644 index 0000000..ea3b41a --- /dev/null +++ b/vendor/archify/references/authoring-defaults.md @@ -0,0 +1,41 @@ +# Authoring defaults + +Read once before writing a fresh candidate. An existing frozen candidate going straight to `finalize` needs this only if repair changes its authorship. + +## Composition and meaning + +For Architecture, default to a system overview led by the main user journey unless the user asks for a narrower mechanism, module map, or deployment topology. Group cooperating roles in accurately named subsystems when that still explains the requested interaction. Separate roles when grouping would hide control ownership, a trust or persistence boundary, lifecycle behavior, or another distinction the reader asked about. Keep secondary and opt-in capabilities in concise sourced notes unless their path matters to the requested question. Name a deliberately narrower scope in the title. + +Preserve every requested responsibility, relationship direction, protocol, and behavior-changing condition. Show approval, authorization, and state-transition gates on the affected node or relationship; a card alone cannot qualify an otherwise unconditional arrow. Keep source evidence with each asserted claim. Boundaries express real isolation, ownership, runtime, or persistence facts. Cards answer additional reader questions; they do not replace required topology. There is no node, edge, source, card, or boundary quota. If an authored overview needs regrouping after failure, use [Composition repair](authoring-contract.md#composition-repair); user-supplied or agreed topology remains fixed. + +Relationship labels carry meaning. Give them clear space and preserve action, protocol, direction, async behavior, or cross-boundary meaning. A label may start absent only when both endpoints already fully imply it; a collision calls for spacing or routing repair. See [Label repair](authoring-contract.md#label-repair) when measured evidence reports a collision. + +## Layout and routing + +Place Architecture nodes by their actual connections before assigning coordinates; the router cannot rearrange boxes, so placement decides whether lines stay straight. Classify each relationship first, then place: + +- **Main path**: the reader's main journey, neighbors adjacent in reading order. Let a medium path step through meaningful rows instead of making a shallow horizontal strip. +- **Branch or store**: directly above or below the node that owns, reads, or writes it, centered on that node so the edge is one straight segment. Keep all stores and branches of one row on the same side of it. +- **Return** (back to an earlier main-path node): put its source on the side of the main path with no branches or stores, so it runs through an empty corridor instead of crossing them. +- **Second entrance** into a node that already has an incoming edge: place the new source so it reaches that node from another side, usually directly below or above it. +- **Fan-out**: a side with k relationships needs at least `32 + 14 × (k − 1)`px (four need 74px). Spread a hub's counterparts over two or three sides, or enlarge the hub. Center a parent on its children and align a child with its only parent. + +Before writing positions, trace each non-main relationship: its straight or one-bend corridor must not pass another node or cross another relationship. If it does, move the endpoint that is off the main path. Start the main actor and its first connected step together near the canvas origin; use content rows for vertical rhythm. Omit `meta.viewBox` for a fresh Architecture so the Reader measures intrinsic height. Keep supplied fixed geometry authoritative. + +Start with automatic routes and endpoint sides. Pin a side only for a necessary branch, return, or supplied geometry. Reserve `via`, `channelX`, `channelY`, and label coordinates for measured defects. Before writing positions, budget each labeled main-path edge at `6.5px × ASCII units + 21px` of clear gap, counting CJK as two units; use its own label length, not a row-wide fixed gap. Size Architecture sublabels for their preferred 9px text at `5.4px × text units + 8px`, with CJK counting twice; keep supporting copy concise without dropping required facts. Do not trade readability or meaning for fewer crossings. The [Geometry reference](authoring-contract.md#executable-geometry-rules) has measured spacing, port, canvas, and route rules for a diagnosed layout problem. + +## Evidence and schema + +For a real repository, follow [Repository authoring](repository-authoring.md) while inspecting source. Freeze its credential-free origin and 40-character commit in `meta.repository`, attach inspected repository-relative `sources` to each key semantic node, and pass `--repo-root` to the first `finalize`. Each reference proves only the fact visible at that location. Follow material relationships and conditions to their actual source; do not reuse a startup citation as protocol or persistence evidence. + +Examples show field shape, not legal values or source facts. Read the mode schema and shared definition before adding a field, enum, or constrained text. In particular, inspect Architecture boundary kinds. Keep longer evidence in a card while retaining the fact. See [Schema lookup](authoring-contract.md#schema-lookup) for details. + +## Presentation and modes + +Use one primary authored language from the user's choice or the request/conversation. Set `meta.locale` for built-in English (`en`) or Simplified Chinese (`zh-CN`); for other languages, including Spanish (`es`), supply `meta.translations` with reusable UI translations, or disclose the fixed Viewer UI and `` English fallback. Keep exact product, code, protocol, command, API, and environment names while localizing surrounding explanation. See [Language consistency](authoring-contract.md#language-consistency) for bilingual cases. + +Omit `meta.visual_preset` for classic, `meta.subtitle` for a title-only header, `meta.legend` for truthful auto, and `meta.engineering_profile` for an ordinary system overview. Explicit styles and a subtitle require a user request. Use legend or deployment ownership under the [legend](authoring-contract.md#legend-contract) or [engineering profile](authoring-contract.md#engineering-profile-default) contracts. Branding is optional and explicit when a node names a real product; [Brand marks](brand-marks.md) gives lookup and capture rules for that branch. Never let a badge replace semantic type, label, or relationship facts. Set required `meta.output` to a portable POSIX-relative `.html` path within the working directory; see [Output path contracts](delivery-contract.md#output-path-contracts) for native path exceptions. + +For new Workflow use schema v2, preserving v1 for a fixed legacy source; use its [layout contracts](../renderers/workflow/README.md#layout-contracts) when lane or group geometry needs detail. For Sequence start with fixed columns; use `spread` when a wide viewBox leaves unused horizontal space or meaningful labels need width. For new Lifecycle use schema v2: every lane is its own row (`main` first, `terminal` last) and `col` `0..4` is the same x in every row, so place an interruption or exit in the column of the state it leaves; author the main path as transitions, omit `viewBox`, and keep transition labels short; a recoverable failure needs a real transition back. Read [Mode placement](authoring-contract.md#mode-placement) when a mode-specific placement or viewport problem needs more detail. + +`finalize` performs the browser gate. Keep the complete drawing comfortably readable on desktop, with zero horizontal overflow. Use meaningful vertical rows and intrinsic-height page scroll when necessary. The 6px projected-text check is a failure floor; at 1440px, aim for ordinary context text around 7.5px or larger. See [Automated browser evidence](delivery-contract.md#automated-browser-evidence) when viewport evidence fails. diff --git a/vendor/archify/references/brand-marks.md b/vendor/archify/references/brand-marks.md new file mode 100644 index 0000000..ae054cd --- /dev/null +++ b/vendor/archify/references/brand-marks.md @@ -0,0 +1,82 @@ +# Brand marks + +Use a brand mark only when a real product, provider, model family, channel, or +service identity helps the reader. Semantic `type` still explains what the node +does; `brand` explains whose product it is. + +## Agent decision path + +1. Search the built-in catalogue when the request names a recognizable brand: + + ```bash + node bin/archify.mjs brands "Claude" --json + ``` + +2. Put the returned canonical ID in the node, participant, or state: + + ```json + { + "id": "planner", + "type": "backend", + "label": "Claude", + "brand": "claude" + } + ``` + +3. If there is no catalogue match and the user supplied the official website, + capture its icon explicitly: + + ```bash + node bin/archify.mjs brands capture "https://partner.example.com" --json + ``` + + Put the command's digest-pinned `brand` value in the authored node: + + ```json + { + "id": "partner", + "type": "external", + "label": "Partner portal", + "brand": { + "url": "https://partner.example.com", + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + } + } + ``` + +4. If there is no match and no user-provided URL, omit `brand`. Do not invent a + URL or silently assign a visually similar company. + +Known-brand URLs resolve to the bundled vector instead of using the network. +For discovered icon `href` attributes, capture decodes the basic named references +`amp`, `quot`, `apos`, `lt`, `gt` (and their defined uppercase aliases), plus +decimal and hexadecimal numeric references, once before URL resolution. Thus +`/icon.png?v=1&size=32` requests `/icon.png?v=1&size=32`. URL percent escapes +remain intact; nested escapes are not decoded recursively. This bounded decoder +does not add a general HTML parser or support every named HTML entity. +HTML reads stop at an explicit head ending outside comments, raw-text elements +and quoted attributes, including when those tokens span network chunks. The +256 KiB head limit and capture deadline still apply; a larger body after the +head is not read for icon discovery. +Unknown URL capture accepts only bounded raster image formats, blocks +credentials, nonstandard public ports, and private or link-local destinations, +uses bounded concurrency and one total deadline, and returns the captured +content digest. Later render and validate operations require that exact digest; +blocked, unavailable, changed, oversized, or unsafe content fails closed instead +of silently changing the artifact. + +Page, icon and redirect requests send `Accept-Encoding: identity`. Capture does +not decompress response bodies: a successful response declaring another content +coding is closed and rejected explicitly. This keeps the existing byte limits +and pinned digest tied to the unencoded representation. A later usable icon may +still succeed; otherwise an encoding error is retained instead of being hidden +by an unrelated favicon 404. + +The final artifact never fetches a brand asset when opened. Preset vectors and +digest-verified captured site icons remain embedded in SVG, PNG, WebP, JPEG, +Share Card, and WebM exports. + +Use `node bin/archify.mjs brands --json` to inspect all canonical IDs, aliases, +categories, domains, and provenance. Current categories cover AI, cloud, +engineering, data, collaboration, business systems, channels, languages, and +frameworks. diff --git a/vendor/archify/references/delivery-contract.md b/vendor/archify/references/delivery-contract.md new file mode 100644 index 0000000..1cde644 --- /dev/null +++ b/vendor/archify/references/delivery-contract.md @@ -0,0 +1,588 @@ +# Delivery contract + +## Failed finalize and candidate repair + +`finalize` stops at the first non-passing gate. Use compact stdout or `evidence.summaryReceipt`; read its full sidecar only when the summary lacks evidence needed for a coherent repair. A receipt with four artifact checks is basic validation, not showcase acceptance: require all nine checks, zero composition errors, and zero warnings. Fix `meta.quality_profile` and schema errors before geometry. + +For a validation failure, edit the existing JSON in the connected neighborhood named by diagnostics before rerunning a command. Preserve requested semantics, meaningful labels, source evidence, and fixed or agreed topology. Several routes sharing nodes call for one placement repair; read [Architecture layout repair](architecture-layout-repair.md) for that case. Reflow a blocked main path rather than nudging unrelated labels. Keep unrelated geometry when its composition already reads clearly. Use `--layout-json` before editing only when compact evidence lacks needed measurements. Workflow v2 uses its stable compiler receipt, not solver internals, as authoring evidence. + +After the edit, rerun the complete `finalize` command with `--quality showcase` and, for repository-backed work, `--repo-root `. If the output path already has browser evidence from another candidate, use a fresh `--out-dir .review-` for both the new `finalize` and any `visual-check`. Omit an earlier `--candidate-sha256` after editing because it binds the previous candidate. Compare diagnostics by code, subject, stage, and evidence, never by declining error count alone. If an issue survives two focused repairs, inspect measured geometry or the relevant contract; after one evidence-based retry, report the concrete gap. + +Use standalone `validate` only for focused diagnosis, passing `--repo-root` for repository-backed work. Its passing receipt marks `candidateFrozen: true`; run `nextAction.arguments`, replacing only ``, without editing, revalidating, or rereading the candidate. Retry later environmental or evidence failures against those frozen bytes. A measured reason to edit creates a new candidate and calls for the complete `finalize` without the old hash. + +## Validate and deliver + +`render` and direct renderer entry points print classified authoring failures +to stderr as readable diagnostics and exit 1. Input read/JSON parse failures +use `input/read` or `input/json-parse`; output filesystem failures use +`output/write` and identify the output path. Schema and layout failures keep +their existing rule codes. Use the advertised `validate --json` or +`deliver --json` interface for a machine receipt; `render` has no `--json` flag. +Unexpected implementation failures retain debugging information in human +mode and remain `internal/unclassified` in machine receipts. + +Each delivered output has two artifact-specific metadata paths. When an output +stem is too long for those derived filenames, Archify shortens it and appends a +stable hash: + +- `.delivery.json` records the latest completed attempt. +- `.delivery-pending.json` is the recovery journal for an attempt + in progress. + +For a literal artifact stem that already matches Archify's reserved bounded-name +marker, a pre-namespace raw provenance sidecar remains a read fallback when the +encoded sidecar is absent. A pre-namespace raw pending journal is an independent +fail-closed barrier: it blocks checks and redelivery even when an encoded pending +journal also exists, and neither journal is silently replaced. + +One directory-wide `.archify-delivery-lock.json` serializes every delivery that +resolves into the same physical output directory. This deliberately prevents +case, Unicode-normalization, Windows short-name, and symbolic-link aliases from +creating independent owners for one filesystem location. The tradeoff is that +deliveries to different artifact names in one directory also run serially; +provenance and pending journals remain artifact-specific. + +For migration safety, Archify also detects and preserves a legacy +`.delivery-lock.json` beside the requested artifact. An existing +legacy entry is a fail-closed recovery barrier. While a new delivery owns the +directory mutex, it also holds temporary legacy-format fences for the requested +spelling and an existing artifact's physical target spelling. It acquires the +directory lock first, then all required compatibility fences before writing a +journal or artifact, and removes the directory lock before those fences during +release. This blocks an older Archify binary using +either known spelling from entering the delivery. A legacy fence whose raw +HEAD-era filename exceeds the host component limit is omitted because the old +binary could not create that lock or deliver that artifact on the host either. + +`deliver` acquires the lock by exclusive `open(..., "wx")` before creating or +replacing the recovery journal. A successful exclusive create yields an +internal opaque ownership capability bound to that attempt. Journal creation, +failed-provenance recording, pair commit, rollback, journal finalization, and +lock release each verify the current capability inside the operation that +would mutate shared state. A rejected contender does not create a journal or +write failed provenance. + +An existing directory or legacy lock is handled without automatic recovery: + +| Observed lock state | Required `deliver` result | +| --- | --- | +| No directory entry | Attempt exclusive creation; only its success grants ownership. | +| Valid schema-v1 lock whose PID is running, or whose death cannot be established | Exit 1 with `delivery/concurrent-attempt`; preserve every shared path. | +| Valid schema-v1 lock whose PID is known to have exited | Exit 1 with `delivery/lock-stale`; preserve the lock, artifact, journal, and current provenance exactly. | +| Unreadable, malformed, symlink, dangling symlink, directory, or other non-regular lock entry | Exit 1 with `delivery/lock-invalid`; preserve the entry and every other shared path. | +| An acquired capability no longer matches the current lock or journal | Exit 1 with `delivery/ownership-lost`; stop all shared-path mutation. | +| The matching owner cannot remove its lock | Exit 1 with `delivery/lock-release`; preserve the lock. | + +A `delivery/lock-stale` diagnostic identifies the absolute output and lock +paths plus the original PID and receipt ID. Recovery is deliberately explicit +and serial: stop all delivery attempts for that physical output directory, +confirm that no active delivery owns it and that the reported stale entry has +not been replaced, remove only the reported lock, then rerun `deliver`. Do not +remove an artifact, current provenance, or pending journal as part of +stale-lock recovery. + +The lock protocol targets Node.js 18 or later on a local filesystem with +cooperating Archify processes. PID, receipt, and file-identity comparisons are +defensive checks, not an atomic compare-and-swap. Compatibility fences cover +the requested spelling and an existing physical-target spelling; they cannot +enumerate arbitrary hard-link names or previously unknown filesystem aliases, +so mixed-version delivery through such aliases remains out of scope. This +contract does not claim distributed-lock correctness on NFS, SMB, or other +network filesystems, and it cannot prevent an external process that ignores +the protocol from replacing shared paths. + +Every no-clobber HTML publisher (`render`, `deliver`, `compare`, and `preview`) +captures the requested directory entry, canonical write slot, physical parent, +and existing target type, device/inode identity, and mode before staging, then +revalidates that snapshot immediately before replacement. An existing write +target must be a regular file with exactly one hard-link name. A target with +multiple hard-link names fails closed with `output/target-hardlinked`: replacing +the requested name cannot update unknown sibling names as one publication. +Hard links remain supported for read identity and input/alias collision checks; +they are unsupported only as write targets. A symbolic link to a single-link regular +file remains supported: publication preserves the symbolic-link entry and +applies the same protocol to its resolved target. Directory, FIFO, socket, +device, changing mode, new claimant, and indeterminate identity cases fail +before replacement. + +Publication is no-clobber and recoverable, not crash-atomic replacement of an +existing target. To avoid overwriting a claimant that appears after the last +identity check, Archify first retains the bound old file in a private recovery +backup, removes the public name through identity-bound quarantine, and then +creates the new public name with an exclusive hard link. A caught failure rolls +back when the public slot and recovery binding still permit it. A process +interruption between those namespace operations can instead leave the public +path absent while the verified previous bytes remain in an adjacent private +recovery backup. Single-artifact publication records the original slot/alias +identity and backup inode, mode, SHA-256, and byte count in private +`.archify-remove-*/publication-recovery-v1.json`, beside `previous`. To make a +specific interrupted publication visible again, stop concurrent writers and run: + +```bash +node bin/recover-output.mjs /absolute/path/to/.archify-remove- --json +``` + +This is explicit recovery, not a directory scanner. Before linking, the helper +checks for a changed parent or alias, an altered/hardlinked record or backup, +digest or inode mismatch, and any existing public target. It restores only by +no-clobber hard link, so a new claimant is preserved rather than overwritten; +it never recursively removes unknown entries. A completed recovery is +idempotent. The record is evidence to be independently verified, not an +authority to restore arbitrary private bytes: the helper accepts it only from +the recorded generated child of the original physical target parent, with the +same directory identity. Name the exact directory reported by the interrupted +process and inspect an uncertain record manually. A non-cooperating process can +still swap pathnames after those checks and before Node.js `linkSync`; Node does +not expose a descriptor-bound link operation. Post-link identity verification +then fails closed and retains recovery evidence, rather than claiming recovery +or deleting an uncertain name. If recovery itself is interrupted after the +link, the old public bytes and private backup can both remain; a later recovery +run preserves the public target and needs explicit operator resolution. The +record is fsynced before the old public name is retired on platforms supporting +directory sync, and the tested guarantee is recovery after a killed process; +this is not a claim of power-loss, storage-controller, NFS, or SMB durability. +Paired flows retain their backup in private transaction staging. For `deliver`, +the pending journal and lock keep strict checkers fail-closed. The portable +Node.js filesystem API has no pathname +compare-and-swap that both replaces an existing name atomically and refuses to +overwrite a late claimant: `rename` would close the visibility gap only by +overwriting that claimant. + +After ownership is established, `deliver` creates the journal before rendering +and keeps it through the recoverable HTML/sidecar pair commit. It removes the +journal only after that commit completes. A validation, render, or pair-commit +failure, or a process interruption, may therefore leave a journal. The journal +is a safety barrier: `check`, `browser-check`, and `visual-check` fail closed when any directory +entry exists at the journal or lock path, including an unreadable file, +symlink, or dangling symlink. Run deliveries targeting the same physical output +directory serially; one attempt must finish or be recovered before another +begins. + +A successful sidecar has `schemaVersion: 1`, `status: "current"`, +`command: "deliver"`, a unique `receiptId`, the diagram `type`, an absolute +`input` path, an absolute `output` path matching the inspected +artifact, and specification/artifact SHA-256 and byte counts. Checkers treat a +missing, malformed, unsupported, or inconsistent field as invalid. They also +reject a sidecar symlink, including a dangling one. A checker binds provenance +to the artifact bytes it actually checks and verifies that binding again before +reporting success; a concurrent byte change fails. The provenance directory +entry itself must be a single-link regular file: `deliver` and strict check fail +closed with `delivery/provenance-hardlink-unsupported` when it has another hard +link, without scanning for or guessing the sibling name. + +If a currently verified owner fails after an older HTML exists, Archify writes +a new `status: "failed"` sidecar and leaves the journal until recovery is +complete. An unreadable old HTML does not prevent that marker; its artifact hash +and byte count may be absent. If the sidecar is locked or otherwise unwritable, +Archify keeps the prior sidecar rather than deleting evidence, and the journal +prevents checkers from trusting it. A rejected concurrent, stale, or invalid +lock attempt does not write failed provenance. If ownership is lost, Archify +reports `delivery/ownership-lost`, does not overwrite or remove the successor's +artifact, provenance, journal, or lock, and does not claim recorded failed +provenance; a failure receipt may report `provenance: "unrecorded"`. If every +metadata path is unavailable, the same unrecorded status applies; no tool can +preserve that fact across processes. Restore metadata-path access and complete +a successful `deliver` before trusting the output. + +Artifacts with no sidecar, journal, or lock remain supported for backward +compatibility and for the lower-level `render` command. Their checker receipts +report `provenance: "unknown"`; use `--require-provenance` to turn that state +into a non-zero failure when the workflow requires a successfully delivered +artifact: + +```bash +node bin/archify.mjs check --require-provenance +node bin/archify.mjs browser-check --json --require-provenance +``` + +## Output path contracts + +Archify intentionally separates durable authored paths from command-line paths: + +- Required authored `meta.output` is a portable POSIX-relative path such as + `reports/diagram.html`. It uses `/`, ends in a non-empty `.html` basename, + and cannot contain an absolute or drive-relative prefix, URI, backslash, + empty or dot segment, control character, unpaired UTF-16 surrogate, Windows + alternate-data-stream separator or invalid filename character, trailing dot + or space, DOS device name, or a component over either the 255-byte UTF-8 or + 255-code-unit UTF-16 limit. It resolves from the current working directory + and must remain physically inside that directory, with an `.html` target, + after symbolic links are followed. The durable output/archive profile also + conservatively rejects a Windows 8.3 short-name shape such as `PROGRA~1`; + descriptive repo/Git POSIX paths use a separate profile and are exempt. +- Explicit CLI output arguments use the active host's native syntax. They may + be relative or absolute, use native separators, and resolve outside the + current working directory. On Windows, ordinary drive-absolute, UNC, and + relative paths (including ordinary `.` and `..` navigation) are supported. + A system-resolved 8.3 spelling of an existing file or directory is accepted + when Archify can prove its physical identity; this native alias support does + not relax the durable output/archive profile's 8.3-shaped-name rejection. + Extended-length paths are limited to raw backslash-only `\\?\C:\...` and + `\\?\UNC\server\share\...` forms without dot segments; device namespaces, + malformed roots, drive-relative paths such as `C:file.html`, current-drive + roots such as `\file.html`, alternate data streams, reserved device names, + invalid or trailing filename characters, and overlong components fail + closed. POSIX CLI paths retain POSIX filename rules rather than inheriting + Windows spelling restrictions. Every host rejects NUL, unpaired surrogates, + and components that exceed its supported bound. + +These contracts are not interchangeable: an explicit CLI output does not hide +an invalid durable `meta.output` (including a missing value), and `validate` +checks the authored output even when it does not publish to that path. A +workflow v1-to-v2 migration may explicitly receive a portable durable +replacement through `migrate workflow old.json new.json --to-schema 2 --output +reports/diagram.html`; that value is written only to its separate verified v2 +destination. This migration-candidate exception does not repair the source or +bypass any non-output schema or compiler error. For every other repair, add a +portable POSIX-relative `.html` path to `meta.output`; no schema-version change +is otherwise required. + +Run `finalize` directly on a complete first candidate and after every repair edit. Its embedded validation checks the candidate before delivery; use standalone `validate` only for focused diagnosis. After an edit, omit any earlier `--candidate-sha256`, which binds the previous candidate. CLI HTML output paths must end in +`.html`, including after symbolic-link resolution. Compare receipt paths must +end in `.json`. A type mismatch fails before writing with +`output/cli-extension` or `output/cli-resolved-extension`. These checks prevent +accidental file-type overwrites; they do not sandbox explicit CLI directories +or prevent replacement of an existing artifact of the expected type. + +Use final verified delivery only after the candidate is frozen: + +```bash +node bin/archify.mjs deliver --quality showcase --json +``` + +Deliver reads the specification once, writes those exact bytes to a private same-directory candidate snapshot, renders that snapshot, runs the complete artifact checker, and only replaces the target after all artifact checks pass. The JSON receipt includes SHA-256 and byte counts for both `specification` and `artifact`. + +For the ordinary agent handoff path, prefer the finalizer: + +```bash +node bin/archify.mjs finalize --quality showcase --json +``` + +`finalize` invokes verified `deliver` once, reuses its embedded showcase +validation result, then runs strict `check --require-provenance` and +`browser-check --require-provenance`. It stops at the first failed or skipped stage +and preserves that stage's full receipt. Its stdout is one compact JSON +object with gate statuses, bounded actionable diagnostics, artifact identity, +and evidence paths. The same compact object is written atomically to +`.finalize-summary.json`; use that file for normal failure repair. +Complete stage receipts and timings remain available for auditing in +`.finalize.json`. With `--out-dir`, both files are written there; +`--receipt ` overrides the full receipt path and derives a distinct +`-summary.json`. Read the full receipt only when the compact summary is +truncated and its shown subjects and evidence cannot identify a coherent local +repair, or when complete audit evidence was explicitly requested. The compact +receipt reports `visualReview: "not-requested"`; the automated gate does not create images or require a perceptual reviewer. A compact `visualReviewRecommendation` retains positive crossover and route-detour metrics from the strict check so the author can apply the review escalation below without reading the full receipt. A recommendation does not change the machine exit code or claim that review happened. Its `affectedRoutes` identifies crossing pairs and detours (up to eight of each, with a truncation flag); the full strict-check `composition.routeReview` retains all affected relationships. Use these IDs to trace the routes in the captured default viewport. Detours may include `directCorridorBlockers`, identifying nodes between aligned endpoints. These are geometric review clues, not new validation failures or inferred main-path semantics. For a blocked main path or several tangled routes, follow [Architecture layout repair](architecture-layout-repair.md) and reflow the connected scene before tuning individual sides or labels. Preserve every semantic fact; retain unrelated positions only when their surrounding composition is already accepted. + +For a measured automatic Architecture with a large unused leading area, the +compact receipt may include `layoutReviewRecommendation`. Its +`composition.leadingSpace` evidence accounts for nodes, boundary titles, +routes and labels. Check whether that space is intentional; if not, reposition +the connected scene while preserving meaning and user-fixed geometry, then +finalize again. This suggestion changes no gate or exit status and requires +no screenshot. A fixed canvas or uncertain measurement receives no suggestion. + +A passing finalizer receipt is sufficient evidence for all four gates. Merely +naming the gates or requiring each one to pass does not require replaying their +standalone commands. Replay an individual command only when the request +explicitly requires separate executions or focused failure diagnosis needs it. + +The individual commands remain authoritative and backward compatible. Use +them directly for focused diagnosis, recovery, or when only one gate is +required. A finalize failure does not relax any gate and does not turn a +preserved older artifact into a current successful delivery. + +`finalize` overlaps private Chrome startup with delivery and strict checking. +It loads the artifact only after those gates pass and current provenance is +verified. The browser gate retains every viewport, theme, and stability check; +the browser closes at completion or an earlier failure. Its full stage receipt +records `execution: "in-process"` and the equivalent standalone `command` for +replay. Use total finalize duration to compare performance because Chrome +startup overlaps the earlier stages. + +The pair commit is recoverable, not a claim that two filesystem paths change +atomically or are durable across power loss. Journal finalization is part of +that commit: a caught failure while verifying or removing the journal rolls +back the replaced files when possible and while ownership remains current. If +ownership is lost, the old attempt immediately stops renaming, rolling back, +finalizing the journal, recording failure provenance, or cleaning up shared +paths. Any private staging or recoverable backups remain available and are +identified by the failure diagnostic. If restoration fails for another reason, +the failure receipt likewise identifies retained backups for recovery. A +process interruption can leave the journal, backups, or private staging behind; +checkers then fail closed. Follow the reported recovery evidence before rerunning +`deliver` serially on the same output. A failed attempt exits non-zero and never invokes an opener; it never +authorizes visual evidence collection. + +If exclusive creation succeeds but lock initialization fails, Archify may +record failed provenance and remove the incomplete lock only while its +capability still identifies that exact entry. A replacement is preserved. +Filesystem and cleanup errors are reported separately from an active concurrent +delivery. An active, stale, unrecognized, or otherwise preserved lock +independently prevents checkers from accepting the prior artifact. Fix the +reported filesystem error before retrying, and use another physical output +directory if the lock path contains unrelated data. + +Lock release is part of delivery completion. If the artifact/provenance pair +has committed and the journal has finalized but the matching lock cannot be +removed, `deliver` exits 1 with `delivery/lock-release`, preserves the lock, +does not print a success receipt, and does not invoke an opener. The preserved +lock keeps strict checkers fail-closed. Only after pair commit, journal +finalization, and lock release all succeed may `deliver` exit zero, print its +success receipt, or run `--open`. + +Run strict `check` after `deliver` exits zero. Run `browser-check` or optional +`visual-check` only after that strict check exits zero. A failed marker, +recovery journal, or delivery lock makes every checker fail before accepting +the preserved HTML; report the diagnostics and complete a successful recovery +delivery before collecting new browser evidence. + +The delivery interface exposes four separate claims: + +1. `deliver` proves deterministic artifact checks and byte identity. +2. `browser-check` collects required automated browser evidence from the exact artifact without capturing images. +3. `visual-check` optionally adds artifact-bound screenshots and a contact sheet. +4. Perceptual visual review records a human or image-capable reviewer's judgment. + +Passing one claim never implies the others. Never claim that the deterministic receipt includes browser or perceptual review evidence. + +## Recovering a failed comparison + +`compare` commits an HTML artifact and its JSON receipt as a pair. If that commit +fails, it attempts to restore the previous files. A complete rollback removes +the temporary directory as usual. + +If a previous file cannot be restored, compare exits non-zero with +`delta/commit-rollback-failed` and retains the recovery directory. In the JSON +failure receipt, `diagnostics[].evidence.recoveryDirectory` identifies that +directory and `recoveryFiles` lists `{ backup, target }` paths for the files whose +restoration failed. Human-readable diagnostics also print the recovery paths. + +Resolve the filesystem error, inspect the current targets, and restore each +listed backup to its corresponding target before retrying. Keep the recovery +directory until both previous files have been recovered and verified; it can +also contain rejected candidate files, which must not be mistaken for backups. +Successful comparisons and failures before commit retain their normal cleanup. + +## Automated browser evidence + +`finalize` runs the required browser gate against the exact trusted HTML without +rerendering or modifying it. For focused diagnosis, the equivalent standalone +command is: + +```bash +node bin/archify.mjs browser-check --json --require-provenance +``` + +The zero-dependency command uses Chrome/Chromium through the DevTools pipe. It +measures light-theme containment at 1440×900, 1600×1000, 1920×1080, and +2048×1320, and verifies the requested light theme at all four viewports, the dark +theme at both endpoints, and READ/Still runtime states. A requested theme that +resolves to a different theme fails with measured evidence. It creates one +`.browser-check.json` receipt and no screenshots or contact sheet. +Pass `--out-dir ` to place the receipt in a separate evidence directory. +The receipt binds the artifact SHA-256 and byte count, identifies +`evidenceKind: "automated-browser"`, and reports +`visualReview: "not-requested"`. + +Horizontal overflow always fails. Normal document-level vertical scrolling is +accepted only with a renderer-declared contract and measured readable text. +Automatic canvases declare `data-reader-fit="intrinsic-height"`; their adaptive +Reader must reach its readable width and expose `data-reader-overflow="authored"`. +Architecture with an explicit `meta.viewBox` instead declares +`data-diagram-type="architecture"` and `data-reader-fit="authored-height"`: +its SVG coordinates, aspect ratio and existing Reader width behavior stay +unchanged. Its full SVG must remain inside the diagram panel without internal +scrolling or clipping, and the document must permit vertical scrolling. +The receipt records `verticalScrollAccepted: true` and +`overflowDisposition: "readable-vertical-scroll"`. Missing or unknown declarations, +explicit viewBoxes in other modes, unreadable text, horizontal overflow, +clipping and Viewer chrome collisions remain failures. Do not add an internal +diagram scroller or hide overflow. + +`browser_evidence` in the handoff records only the outcome of this automated +command: + +- `passed` maps from exit 0 and receipt `status: "pass"` after every required measurement completes and passes. +- `failed` maps from exit 1 and receipt `status: "fail"` when the inspection finds a defect, the command fails, or a runtime error leaves the evidence incomplete. +- `skipped` maps only from exit 2 and receipt `status: "skipped"` when Chrome/Chromium is unavailable and the inspection does not run. + +Runtime failures leave incomplete evidence and must not be normalized to +`skipped`. They do not invalidate an already successful deterministic delivery. +Retry an environmental failure in a browser-capable execution context when +practical. Keep the packaged transport unchanged unless the failure reproduces +through that seam in a capable environment. + +A provenance failure exits before browser inspection and persists a failed +browser-check receipt bound to the attempted artifact. If the failure receipt +cannot be written, the diagnostic names that incomplete evidence. + +Both browser commands inspect the exact delivered HTML without modifying or rerendering it. + +## Sequence width review + +A passing `finalize` may report `layoutReviewRecommendation.action: "inspect-sequence-width"`. Its `evidence` measures the fixed participant columns' unused right-hand space after accounting for message labels, notes and segment titles. This advice adds no warning, failure, screenshot requirement or automatic geometry change. + +For a newly authored candidate with omitted `meta.column_fit` and no user-fixed column geometry, save the candidate, set only `meta.column_fit` to `"spread"`, and rerun the complete `finalize` once with `--out-dir /width-review`. Keep participant order, messages and their y positions, labels, notes, sources and canvas dimensions. If that attempt fails, restore the candidate and finalize it with `--out-dir /width-restore`; report the remaining layout suggestion rather than iterating. Preserve an explicitly fixed layout or a supplied legacy candidate and disclose the suggestion without changing it. This review is about horizontal composition; a passing receipt still does not claim perceptual approval. + +## Optional capture evidence + +`visual-check` remains backward compatible for a requested or escalated +perceptual review: + +```bash +node bin/archify.mjs visual-check --summary --require-provenance +``` + +`--summary` returns compact JSON with all diagnostics and absolute paths to the complete receipt, contact sheet, and every screenshot. For a chosen visual review, inspect the relevant captures; capture success is not perceptual approval. `--json` retains the full receipt output for existing consumers. Both modes run the same checks and keep the same exit status. If cleanup fails after publication, the summary retains the final failure diagnostics and `publication` recovery details; the linked receipt records the earlier committed evidence. + +It performs the same automated browser measurements, captures light/dark +screenshots at 1440×900 and 2048×1320, and writes four viewport PNG sidecars, +one relative-path HTML contact sheet, and one JSON receipt. `--out-dir ` moves all of these sidecars together. +Open the HTML contact sheet in a browser or inspect the viewport PNGs with an image reader. Its receipt reports `visualReview: "pending"` because captures do not themselves +make a perceptual judgment. Capture and provenance failures follow the ownership rules below. + +The receipt, contact sheet, and four PNGs form one owned evidence set. Before +capture, `visual-check` freezes every requested directory entry, its +canonical write slot and physical parent, and the target's absent/file state, +type, device/inode identity, and mode. Hard-linked evidence targets are not safe +write targets. All candidate files are created exclusively inside one random, +private staging directory beneath the physical evidence directory; the receipt +is published last. Each staged candidate must have exactly one hard-link name +before publication. The no-clobber publish link temporarily gives the staged +and final names a link count of two; unlinking the verified staged name must +leave the final entry with a link count of one. An unexpected external hard +link fails closed and its alias is never removed. + +Chrome inspects one identity- and content-checked copy of the captured artifact +in a private local temporary directory, so browser file loading does not depend +on UNC or long-path support. The six publication candidates remain on the +evidence volume. Both temporary directories are cleaned without recursively +deleting unknown contents; retained entries include their recovery locations. + +Immediately before committing anything, `visual-check` re-resolves and verifies +the complete six-path set. An absent-path claimant, existing-path replacement, +symbolic-link or dangling-link retarget, parent-topology change, hard link, or +indeterminate identity fails closed with `viewer/evidence-path-conflict`. The +claimant and every other final evidence path remain untouched. Cleanup removes +only this run's staged or published entries after rechecking their captured +identities; a changed or unknown entry is preserved. + +An existing visual evidence set is replaceable only when a regular +`visual-check` receipt proves ownership of the same artifact and evidence +directory, and its exact sidecar manifest matches every existing contact-sheet +or PNG byte count and SHA-256 digest. A missing, malformed, unknown, mismatched, +or incomplete ownership record never authorizes deletion. Failed and skipped +runs retire prior screenshots/contact sheets only as part of the same verified +transaction when that ownership proof succeeds; otherwise they preserve all +unknown evidence and report `viewer/evidence-path-conflict`. + +This rule also applies when Chrome is unavailable or provenance fails before +browser inspection: neither path may blindly delete stale-looking evidence. A +verified owned set may be recoverably retired before publishing a skipped or +failed receipt; unowned evidence remains intact. These outcomes do not invalidate an already +successful deterministic delivery and do not turn a perceptual visual review +into passed or failed. Retry an environmental failure through the supported +command in a browser-capable execution context when practical. Keep the +packaged transport unchanged unless the failure reproduces through that seam in +a capable environment. + +`browser-check` applies the same private-snapshot, identity, ownership, and no-clobber rules to its single JSON receipt. Its namespace is separate from `visual-check`, so a browser-only rerun cannot remove capture evidence. + +## A new candidate at an existing output path + +Browser evidence belongs to exact artifact bytes. After editing a candidate whose previous HTML already has browser evidence, choose a fresh evidence directory before running the next `finalize`; this preserves the old receipts and captures without an avoidable ownership-conflict retry: + +```bash +node bin/archify.mjs finalize architecture candidate.json diagram.html --quality showcase --repo-root --out-dir diagram.review-2 --json +node bin/archify.mjs visual-check diagram.html --out-dir diagram.review-2 --summary --require-provenance +``` + +Keep the requested HTML path stable. Use a new revision directory for each changed candidate, and retain the same directory for retries of unchanged bytes. For a diagram without repository evidence, omit `--repo-root`. Let the commands create their output directory. A prior validation failure that produced no HTML or browser evidence needs no new directory. Never remove unknown evidence to make a retry pass. + +## Optional opening + +Add `--open` only when the user wants an immediate local preview. It runs after +the verified pair commit has completed, its recovery journal has been removed, +and the delivery lock has been released successfully. It uses one argument-array +OS opener with a five-second bound on macOS and Linux, and a fifteen-second bound +for PowerShell startup on Windows. The receipt records `open.status`; failed or +unavailable launch attempts also include normalized `open.failure` details. +Keep it off for CI, unattended agents, and non-interactive environments. +Failure or unsupported opening does not invalidate delivery; its +status proves only whether the local opener invocation succeeded. + +## Last-Good Live Preview + +For an active desktop authoring loop only: + +```bash +node bin/archify.mjs preview .json .html --quality showcase +``` + +Preview watches one explicit input on loopback, binds each stable digest to a private snapshot, and advances only after the existing verified delivery pipeline passes. Invalid, half-written, deleted, or superseded input leaves the previous verified revision on screen and on disk. Identical bytes do not rebuild or reload. + +The preview runtime ships inside the zero-dependency Skill ZIP and must work without `node_modules`. + +Never start it by default. Do not use it for CI, unattended agents, remote sharing, or mobile use. `--no-open` is only for a user who will open the printed local URL or for loop testing. Stop it with Ctrl-C before handoff. The first Ctrl-C drains the active delivery without publishing it; a second Ctrl-C forces shutdown of both delivery processes and HTTP connections, including incomplete requests. Shutdown preserves the last verified artifact and removes only staging files whose ownership can be verified. If delivery is interrupted before its receipt reaches Preview, unconfirmed files and recovery material may remain in the private staging directory; shutdown does not recursively delete unknown contents. Server state, port, source path, diagnostics, error text, and reload tokens must never enter the generated artifact or any export. + +## Perceptual review + +The automated path ends with the deterministic browser gate and reports +`visual_review: not_requested`. Ordinary generation does not require screenshots +or an image-reading step, including newly authored or repositioned Architecture. +Perceptual review is optional; use it for an explicit request or a concrete visual +investigation. Possible reasons include: + +- the compact finalizer includes `visualReviewRecommendation` for crossings or detours (advisory, not a delivery gate); +- the user explicitly requests an aesthetic or visual review; +- a template, renderer, or Viewer change needs visual regression evidence; +- a novel layout or browser diagnostic leaves low confidence; +- the run is selected for sampled audit or dogfood. + +For the default standalone desktop viewer, measure 1440×900, 1600×1000, 1920×1080, and 2048×1320. Require `document.documentElement.scrollWidth <= window.innerWidth` at every checked size. Prefer `scrollHeight <= window.innerHeight`; accept page-level vertical scrolling only through the Reader-declared readable exception defined above. At the largest checked viewport, inspect the rendered composition for a conspicuous empty lower band: the main panel and necessary conclusion cards should use the available height as a balanced whole, not collapse into a shallow strip. For unexpected overflow, repair the authored composition by removing only genuinely redundant content or compacting spacing before shrinking nodes, labels, or the main panel. Do not hide overflow, clip content, introduce an internal diagram scroller, or reduce node/label typography to make the measurement pass. Narrow/mobile containment may retain vertical page scrolling. + +For an escalation, run `visual-check` on the current finalized artifact, inspect +its contact sheet with a capable image reader or human, and check both endpoint +themes, the default READ view, line crossings/corridors, label masks, node/card +fit, focus/search/passport closure, and export cleanliness. This review is +supplementary and never changes `browser_evidence`. An unconstrained browser +glance can support perceptual review only. + +Report one truthful optional-review status: + +- `visual_review: not_requested` — no review trigger applies; this is not a visual acceptance claim. +- `visual_review: passed` — only after inspecting the rendered artifact. +- `visual_review: skipped (image reader unavailable)` — a requested or triggered review could not run. +- `visual_review: failed` — with the concrete visible defect. + +For an escalated review, use `correction_rounds: 0`, `correction_rounds: 1`, or +`correction_rounds: 2`; never exceed two focused correction rounds. When review +is not requested, use `correction_rounds: 0`. Never report +`visual_review: passed` without inspecting the artifact. If perceptual review +changes the candidate, rerun `finalize` because the previous specification and +artifact receipts are no longer current. + +## Handoff receipt + +Return: + +```text +diagram_type: architecture|workflow|sequence|dataflow|lifecycle +output: /absolute/path/to/file.html +specification_sha256: +artifact_sha256: +validation: 9/9 showcase, 0 errors, 0 warnings +browser_evidence: passed|failed|skipped +visual_review: not_requested|passed|skipped (image reader unavailable)|failed +correction_rounds: 0|1|2 +``` + +Derive `browser_evidence` only from the latest artifact-bound `browser-check` +receipt, normally the stage embedded by `finalize`. Record optional capture or +manual browser work separately with its artifact binding, viewport/theme scope, +and observations; never use it or `visual_review` to overwrite the automated +status. + +Opening, preview status, Share Cards, and other viewer exports are not validation claims. + +Finalize receipt publication uses the same identity-bound, no-clobber publisher and explicit recovery records described above. The full and summary receipts must be distinct from the candidate, artifact, delivery metadata, and browser receipt. Their targets must be absent or single-link regular files; symlink receipt entries and hardlinked targets fail closed. A later claimant or changed parent stops publication and remains untouched. Default receipt names share the physical artifact namespace and are bounded for the host filename limit. The two receipts are published individually, not as a crash-atomic pair; only a completed passing command is a successful handoff. diff --git a/vendor/archify/references/repository-authoring.md b/vendor/archify/references/repository-authoring.md new file mode 100644 index 0000000..b73033b --- /dev/null +++ b/vendor/archify/references/repository-authoring.md @@ -0,0 +1,105 @@ +# Repository-backed architecture authoring + +Use this reference when a diagram must explain a real repository. The source is +the authority for responsibilities, calls, boundaries, and persistence. The +diagram is complete when the requested meaning is covered and every asserted +fact has supporting source evidence. + +## Explore on demand + +1. **Freeze identity.** From the target repository, record `git rev-parse + HEAD`, `git remote get-url origin`, and `git status --short`. Remove HTTP(S) + userinfo (including usernames, passwords, and tokens) before recording the + origin or placing it in the candidate. Preserve its transport, port, path and + `.git` suffix; do not rewrite an internal SSH origin as HTTPS. Pin the credential-free URL and + forty-character revision in `meta.repository`. Use `link_mode: "local-only"` + for an SSH origin, unsupported forge, intentionally local-only source links, + or a local fixture whose HTTPS URL is only a repository identity; retain the + URL and revision. Web links require a supported GitHub or Gitee HTTPS origin. If the + worktree is dirty, record the changed paths. Repository evidence is verified + against committed bytes at the pinned revision, not working-tree edits: + inspect a clean checkout at that revision for any cited changed path. Do not + present uncommitted bytes as evidence for `HEAD`; `local-only` does not record + a verifiable snapshot of those bytes. + +2. **Map the slice.** Use project instructions, manifests, entry points, + registrations, and deployment configuration to locate candidate runtime + units. Read the entry, configuration, and modules relevant to the request. + Follow imports and call sites + until the requested responsibility reaches its actual input, output, or + side effect. Read a small connected slice instead of scanning the repository + for a convenient label. + +3. **Trace ownership.** Derive runtime and I/O relationships from the observed + actor, operation, and target at their call sites; deployment and trust + relationships use the corresponding configuration or enforcement evidence. Distinguish the controller requesting + an operation from the runtime that executes it and the store receiving bytes. + For a file or database edge, the source must identify its actual reader or + writer; a responsibility statement such as “maintains tasks” does not prove + direct I/O. Keep these facts with the source locations while reading, without + a separate planning artifact. Choose which distinctions need separate + nodes using [Composition and meaning](authoring-defaults.md#composition-and-meaning); + discovering an implementation role does not automatically add it to the overview. + A configured provider, an injected adapter, a local stub, and a durable + service are different claims; label the one the source supports. + +4. **Record evidence while reading.** Keep exact repository-relative paths and + inclusive line ranges for each component and meaningful relationship. Follow + actual branches, retries, fallbacks, and error handling. A function that is + exported or configured but never called by the normal path is an optional + capability, not a required runtime edge. For a claim about authoritative + state change or control ownership, trace to the actual write or execution + site and the conditions that permit it; an upstream caller alone does not + establish those conditions. + +5. **Name uncertainty.** Write unresolved questions beside the claim they + affect: for example, “`writeFile` is called here; durability is unknown.” + Resolve a question by reading the next relevant source range or preserve it + as an explicit unknown. Never turn a label, package description, or config + value into an unobserved service or behavior. + +Stop exploring when every requested responsibility, relationship, and boundary +has supporting source entailment and the remaining unknowns cannot change that +coverage. There is no node, edge, citation, view, card, or boundary count to +hit. Do not add a summary step merely to signal completion. + +Batch independent relevant files when known. Each additional read should answer +an unresolved question that can change the diagram. Reuse concise facts and +their source ranges already verified in this task; across revisions, recheck +the affected entry points, configuration, dependencies, and evidence. + +## Choose an example by structure + +Select the main example in the [Type router](../SKILL.md#type-router) before +loading its content, using the request and repository metadata already needed +for source inspection. Selection fits the existing read batch and needs no extra +message, command, or repository-wide scan. For mixed or unclear tasks, use the +requested responsibilities and entry points as they become known in normal +inspection; keep their actual roles. Read another example when a necessary +capability remains unexplained. Examples teach shape, not facts: a library need +not acquire filesystem nodes, and finished showcases still follow the +first-draft automatic-routing rule. + +## Author from evidence + +Use the mode's complete JSON shape, including repository identity, +components, and connections; every repository-backed component needs supporting source +references, while boundaries or cards are added only when they +answer a real reader question. Let automatic routes and automatic +viewBox sizing work first. Keep the primary path readable, put exception paths +beside their owner, and leave filesystem stores outside a control boundary when +the source shows a separate responsibility. + +An existing example teaches field shape, not facts or arbitrary values. It does +not authorize a new boundary kind, a long note, a viewBox size, or a route +control. Consult the specific mode schema and `schemas/common.schema.json` +whether or not the selected example already contains the field; use the +schema's enum, length, identifier, and repository rules. Architecture +boundaries currently use `kind: "region"` or `kind: "security-group"`; source +references use `path`, `line`, and optional `end_line`. + +Repository-backed components need concise, truthful `sources` references. Preserve +control ownership when summarizing filesystem I/O: the code that reads or writes +a file owns that action, while a pure in-memory transform receives and returns +values. This fact-check does not require a separate overview node for every helper. Use the existing examples for valid field shape, then replace all +identifiers, wording, source paths, and claims with inspected repository facts. diff --git a/vendor/archify/references/update-awareness.md b/vendor/archify/references/update-awareness.md new file mode 100644 index 0000000..4f11feb --- /dev/null +++ b/vendor/archify/references/update-awareness.md @@ -0,0 +1,12 @@ +# Update awareness + +Read this file when a `finalize` or standalone `deliver` receipt has `update.noticeRequired: true`. + +Keep one compact line in the final response, in the user's language, with `installedVersion`, `availableVersion`, and the official `releaseNotes` link. Say that the installed Skill has not changed and that the user can ask to snooze or ignore the reminder. If `source` is `cache`, say that a previous check at `checkedAt` found the update. A process message or tool output does not replace this final line. +For `severity: "security"`, label it as a security update without making installation automatic or urgent by default. + +You may translate the fixed local `noticeText`. Never quote, summarize, or translate the remote manifest's summary. + +When the user explicitly asks to pause or stop this reminder, run `node scripts/check-update.mjs --snooze ""` (seven days) or `--ignore ""` (this exact release only) from the Skill directory with the receipt's `update.eventKey`, then report the returned status. Never run them on your own initiative; `--ack` is a no-op. These commands do not install an update, and a newer release notifies again. + +The notice is information, not permission. Keep the installed version unchanged. This workflow never downloads, installs, or executes an update, and silence is never consent. diff --git a/vendor/archify/references/viewer-runtime.md b/vendor/archify/references/viewer-runtime.md new file mode 100644 index 0000000..afcd03d --- /dev/null +++ b/vendor/archify/references/viewer-runtime.md @@ -0,0 +1,35 @@ +# Viewer Runtime reference + +Read this only when the user asks for a reader-facing capability. Ordinary generation does not require implementing or re-documenting these features; they are already in the generated HTML. + +## Exploration + +- Diagram Guide lists current actions and shortcuts. +- Reading Depth starts at READ at the default 100% scale, reveals FULL detail at 175%, and falls back to MAP only below 100%. Focus, route, and semantic interactions reveal their exact facts at any scale. +- Semantic Lens summarizes selected node/relationship kinds without changing authored geometry. +- Intent Trace previews a fine-pointer or keyboard target before committed focus. +- Node Finder searches labels and stable IDs. +- Semantic Passport opens on focus, shows authored upstream/downstream facts, supports a copyable deep link, has an explicit close action, closes on true outside activation and Escape, and never enters canonical export. +- Semantic Radar mirrors the visible viewport and authored graph without becoming a second source of truth. +- Direct Relationship Pin makes a unique compiled relationship operable while preserving the authored line and stable relationship identity. It must fail closed on conflicting source/target/label/ID metadata. +- Route Probe resolves exactly two endpoints over authored directed relationships. It never infers a route from geometry. + +## Motion and presentation + +`meta.animation: "trace"` enables a finite reader-controlled Live/Still trace. Static is the default. Still, reduced motion, page hiding, print, and canonical export preserve complete static meaning. Presentation Stage changes viewer chrome and framing, never authored geometry. This is not a mobile product feature; narrow layouts get containment only. + +## Canonical exports + +The export menu can copy/download full-diagram PNG, download JPEG/WebP, download a dual-theme SVG, and record a trace-enabled WebM. Viewer state—Guide, Lens, finder, focus, route, camera, radar, presentation, motion ownership, and temporary overlays—must be removed from canonical export. + +### Route Share Card + +After a real directed Route Probe resolves, the reader may use **Export → Route Share Card**. It reuses the exact ordered route snapshot and the shared Share Card seam: `format=share-card`, `variant=route`. The isolated clone may use only static `data-share-route-*` decoration. It is download-only, fails closed for stale/unreachable/conflicting routes, and never becomes the canonical artifact. + +### Reach Share Card + +After a non-empty authored reachability query, the reader may use **Export → Reach Share Card**. It consumes the already resolved upstream/downstream node and edge set without rerunning traversal: `format=share-card`, `variant=reach`. The isolated clone may use only static `data-share-reach-*` decoration. It is download-only. Call it authored reachability—not impact, blast radius, breakage, or runtime causality. + +## Truth boundary + +Viewer exports are communication assets. They do not replace the checked HTML, the deterministic delivery receipt, or a real visual review. Do not add a hosted service, storage surface, dependency, schema branch, or mobile product surface for these viewer-only capabilities. diff --git a/vendor/archify/renderers/architecture/grid.mjs b/vendor/archify/renderers/architecture/grid.mjs new file mode 100644 index 0000000..807a1b8 --- /dev/null +++ b/vendor/archify/renderers/architecture/grid.mjs @@ -0,0 +1,62 @@ +/** Grid placement for architecture IR (#8). Not auto-layout — fixed cell math only. */ + +export const DEFAULT_GRID = { + mode: 'grid', + origin: [40, 80], + cols: 4, + gapX: 30, + gapY: 40, + cellW: 130, + cellH: 64, +}; + +export function gridLayout(arch) { + const raw = arch.layout; + if (!raw || raw.mode !== 'grid') return null; + return { ...DEFAULT_GRID, ...raw }; +} + +export function resolveComponentPos(component, grid) { + if (Array.isArray(component.pos) && component.pos.length === 2) { + return component.pos; + } + if (!grid) return [NaN, NaN]; + if (!Number.isInteger(component.row) || !Number.isInteger(component.col)) { + return [NaN, NaN]; + } + const [ox, oy] = grid.origin; + const stepX = grid.cellW + grid.gapX; + const stepY = grid.cellH + grid.gapY; + return [ox + component.col * stepX, oy + component.row * stepY]; +} + +export function validateGridPlacement(arch, grid, problems) { + if (!grid) return; + if (arch.layout !== undefined && arch.layout.mode !== 'grid') { + problems.push('layout.mode must be "grid" when layout is set (free placement omits layout entirely).'); + return; + } + const seen = new Map(); + for (const c of arch.components ?? []) { + const hasPos = Array.isArray(c.pos) && c.pos.length === 2; + const hasCell = Number.isInteger(c.row) && Number.isInteger(c.col); + if (hasPos) continue; // pos wins; row/col are optional hints only + if (!hasPos && !hasCell) { + problems.push(`Component "${c.id}" needs pos [x,y] or grid row/col when layout.mode is "grid".`); + continue; + } + if (c.row < 0 || c.col < 0) { + problems.push(`Component "${c.id}" row/col must be non-negative integers.`); + continue; + } + if (c.col >= grid.cols) { + problems.push(`Component "${c.id}" col ${c.col} exceeds layout.cols ${grid.cols} (valid: 0..${grid.cols - 1}).`); + } + const key = `${c.row},${c.col}`; + if (seen.has(key)) { + problems.push(`Components "${seen.get(key)}" and "${c.id}" share grid cell row ${c.row} col ${c.col}.`); + } else { + seen.set(key, c.id); + } + } +} diff --git a/vendor/archify/renderers/architecture/labels.mjs b/vendor/archify/renderers/architecture/labels.mjs new file mode 100644 index 0000000..fa819d2 --- /dev/null +++ b/vendor/archify/renderers/architecture/labels.mjs @@ -0,0 +1,215 @@ +import { normalizeRoutePoints, rectsOverlap, segmentRectClearanceWithin } from '../shared/geometry.mjs'; +import { createSpatialGrid } from '../shared/spatial-grid.mjs'; + +// A bounded fallback for an unpinned label whose usual position collides. +// It never routes an edge, moves a node, expands the canvas, or rewrites input. +export function placeAutomaticLabels({ + labels, routes, components, titles, viewBox, placementBottom = viewBox[1], fallbackRing = true, keepFallbackNearRoute = false, + gridSweep = false, +}) { + const placed = [...labels]; + const obstacles = [...components, ...titles]; + const segments = routes.flatMap(({ relationIndex, points }) => { + const normalized = normalizeRoutePoints(points); + return normalized.slice(1).map((end, index) => ({ relationIndex, start: normalized[index], end })); + }); + const inside = rect => ( + rect.x >= 0 && rect.y >= 0 + && rect.x + rect.width <= viewBox[0] && rect.y + rect.height <= viewBox[1] + ); + // The mask test asked every segment about every candidate position. Segments + // go into a uniform grid once, and a candidate only asks the cells it covers. + const SEGMENT_CELL = 120; + const segmentGrid = createSpatialGrid(SEGMENT_CELL); + for (const segment of segments) { + const [sx, sy] = segment.start; + const [ex, ey] = segment.end; + segmentGrid.insert({ + minX: Math.min(sx, ex), maxX: Math.max(sx, ex), + minY: Math.min(sy, ey), maxY: Math.max(sy, ey), + }, segment); + } + const segmentsNear = (rect, margin) => segmentGrid.query({ + minX: rect.x - margin, maxX: rect.x + rect.width + margin, + minY: rect.y - margin, maxY: rect.y + rect.height + margin, + }); + const masksRoute = rect => { + for (const segment of segmentsNear(rect, 4)) { + if (segment.relationIndex === rect.relationIndex) continue; + if (segmentRectClearanceWithin(segment, rect, 4) + 0.0001 < 4) return true; + } + return false; + }; + const overlapsLabel = (rect, index, gap = 0) => placed.some((other, otherIndex) => ( + otherIndex !== index && rectsOverlap(rect, other, gap) + )); + const clear = (rect, index) => ( + inside(rect) && rect.y + rect.height <= placementBottom + && !obstacles.some(obstacle => rectsOverlap(rect, obstacle, 2)) + && !overlapsLabel(rect, index, 2) && !masksRoute(rect) + ); + const rectAt = (label, lx, ly) => ({ + ...label, lx, ly, x: lx - label.width / 2, y: ly - 10, + }); + // An opted-in grid layout runs parallel lines through shared gaps. Rank + // every position along all of the label's own segments: beside the line + // first, then centred on its own line (the plate interrupts only that + // line), then stepping outward past neighbouring parallels. + const gridCandidates = (label) => { + const fractions = [0.5, 0.25, 0.75, 0.375, 0.625, 0.125, 0.875]; + const ranked = []; + // A close parallel of another relationship on one side (a reciprocal + // pair) makes a label on that side read as the neighbour's: prefer the + // far side. + const parallelOnLowSide = (a, b, axis) => { + const across = 1 - axis; + const [low, high] = [Math.min(a[axis], b[axis]), Math.max(a[axis], b[axis])]; + let nearest = null; + for (const other of segments) { + if (other.relationIndex === label.relationIndex) continue; + if (Math.abs(other.start[across] - other.end[across]) > 0.0001) continue; + const distance = other.start[across] - a[across]; + if (Math.abs(distance) < 0.0001 || Math.abs(distance) > 36) continue; + const overlap = Math.min(high, Math.max(other.start[axis], other.end[axis])) + - Math.max(low, Math.min(other.start[axis], other.end[axis])); + if (overlap <= 0) continue; + if (nearest === null || Math.abs(distance) < Math.abs(nearest)) nearest = distance; + } + return nearest !== null && nearest < 0; + }; + for (const { start: a, end: b } of segments.filter(segment => segment.relationIndex === label.relationIndex)) { + if (Math.abs(a[1] - b[1]) < 0.0001 && Math.abs(a[0] - b[0]) >= label.width + 16) { + const [above, below] = parallelOnLowSide(a, b, 0) ? [1, 0] : [0, 1]; + for (const fraction of fractions) { + const x = a[0] + (b[0] - a[0]) * fraction; + if (Math.min(Math.abs(x - a[0]), Math.abs(x - b[0])) < label.width / 2 + 6) continue; + ranked.push([above, x, a[1] - 10], [below, x, a[1] + 20], [2, x, a[1] + 3], [3, x, a[1] - 18], [3, x, a[1] + 28]); + } + } else if (Math.abs(a[0] - b[0]) < 0.0001 && Math.abs(a[1] - b[1]) >= label.height + 16) { + const leftFirst = !parallelOnLowSide(a, b, 1); + for (const fraction of fractions) { + const y = a[1] + (b[1] - a[1]) * fraction; + if (Math.min(Math.abs(y - a[1]), Math.abs(y - b[1])) < label.height / 2 + 6) continue; + ranked.push([2, a[0], y + 3]); + [6, 14, 22, 30, 38, 46, 54].forEach((offset, step) => { + const tier = step === 0 ? 0 : step === 1 ? 1 : 2 + step; + const left = [tier + (leftFirst ? 0 : 0.5), a[0] - label.width / 2 - offset, y + 3]; + const right = [tier + (leftFirst ? 0.5 : 0), a[0] + label.width / 2 + offset, y + 3]; + ranked.push(left, right); + }); + } + } + } + return ranked.map((entry, order) => [...entry, order]) + .sort((left, right) => left[0] - right[0] || left[3] - right[3]) + .map(([, lx, ly]) => [lx, ly]); + }; + + for (const [index, label] of placed.entries()) { + const relation = label.relation; + if (['labelAt', 'labelDx', 'labelDy', 'labelSegment'].some(key => relation[key] !== undefined)) continue; + // Match actual defect thresholds before searching; a valid placement is + // not a reason to restyle the diagram. New placements leave extra space. + // The grid ranking already starts from the preferred position, so it + // also re-ranks labels whose default spot is merely valid. + if (!gridSweep && inside(label) && !components.some(component => rectsOverlap(label, component, -2)) + && !titles.some(title => rectsOverlap(label, title)) + && !overlapsLabel(label, index) && !masksRoute(label)) continue; + if (gridSweep) { + // Callers measure the plate one pixel higher than rectAt; test with a + // pixel of slack so the chosen position also passes their clearance. + const replacement = gridCandidates(label).map(([lx, ly]) => rectAt(label, lx, ly)) + .find(rect => clear({ ...rect, x: rect.x - 1, y: rect.y - 1, width: rect.width + 2, height: rect.height + 2 }, index)); + if (replacement) { + placed[index] = replacement; + continue; + } + } + for (const segment of segments.filter(segment => segment.relationIndex === label.relationIndex)) { + const [a, b] = [segment.start, segment.end]; + let candidates = []; + if (Math.abs(a[1] - b[1]) < 0.0001 && Math.abs(a[0] - b[0]) >= label.width + 16) { + candidates = [0.5, 0.25, 0.75, 0.125, 0.875].flatMap(fraction => { + const x = a[0] + (b[0] - a[0]) * fraction; + if (Math.min(Math.abs(x - a[0]), Math.abs(x - b[0])) < 8) return []; + return [ + [x, a[1] - 10], + [x, a[1] + 20], + [x, a[1] - 18], + [x, a[1] + 28], + ]; + }); + } else if (Math.abs(a[0] - b[0]) < 0.0001 && Math.abs(a[1] - b[1]) >= label.height + 16) { + candidates = [0.5, 0.25, 0.75, 0.125, 0.875].flatMap(fraction => { + const y = a[1] + (b[1] - a[1]) * fraction; + if (Math.min(Math.abs(y - a[1]), Math.abs(y - b[1])) < 8) return []; + return [ + [a[0] - label.width / 2 - 6, y + 3], + [a[0] + label.width / 2 + 6, y + 3], + [a[0] - label.width / 2 - 14, y + 3], + [a[0] + label.width / 2 + 14, y + 3], + ]; + }); + } + const replacement = candidates.map(([lx, ly]) => rectAt(label, lx, ly)) + .find(rect => clear(rect, index)); + if (replacement) { + placed[index] = replacement; + break; + } + } + if (placed[index] !== label || !fallbackRing) continue; + + // Dense but valid topologies can leave every point directly beside the + // relationship occupied by another route. Search a small deterministic + // ring around the current anchor and the relationship's segment centres. + // A collision-free island above a node is not a readable edge label. + // Architecture opts into keeping the mask within two label heights of + // its own route; shared callers retain their existing policy. If no nearby + // slot fits, retain the collision so validation can request more space. + const ownSegments = segments.filter(segment => segment.relationIndex === label.relationIndex); + const baseAnchors = [ + [label.lx, label.ly], + ...ownSegments.map(segment => [ + (segment.start[0] + segment.end[0]) / 2, + (segment.start[1] + segment.end[1]) / 2, + ]), + ]; + const horizontalStep = label.width / 2 + 12; + const ringOffsets = [ + [0, -28], [0, 38], + [-horizontalStep, -28], [horizontalStep, -28], + [-horizontalStep, 38], [horizontalStep, 38], + [-(label.width + 20), -52], [label.width + 20, -52], + [-(label.width + 20), 62], [label.width + 20, 62], + [-(label.width + 20), -76], [label.width + 20, -76], + [-(label.width + 20), 86], [label.width + 20, 86], + ]; + const fallback = baseAnchors.flatMap(([baseX, baseY]) => ( + ringOffsets.map(([dx, dy]) => rectAt(label, baseX + dx, baseY + dy)) + )).find(rect => clear(rect, index) && (!keepFallbackNearRoute || ownSegments.some(segment => ( + segmentRectClearanceWithin(segment, rect, label.height * 2) <= label.height * 2 + )))); + if (fallback) placed[index] = fallback; + } + return placed; +} + +// The rect a single unpinned label would occupy given only its own route and +// the nodes: what the planner reserves before the remaining routes are laid. +// Only a placement beside the route itself is worth reserving; a label that +// would already need the fallback ring is left to the final placement pass. +export function reservedLabelRect({ + label, points, routes = [], labels = [], components, viewBox = [Infinity, Infinity], placementBottom = Infinity, +}) { + const [rect] = placeAutomaticLabels({ + labels: [{ ...label, relationIndex: -1 }, ...labels.map(other => ({ ...other, relationIndex: -2 }))], + routes: [{ relationIndex: -1, points }, ...routes], + components, + titles: [], + viewBox, + placementBottom, + fallbackRing: false, + }); + return components.some(component => rectsOverlap(rect, component, -2)) ? null : rect; +} diff --git a/vendor/archify/renderers/architecture/render-architecture.mjs b/vendor/archify/renderers/architecture/render-architecture.mjs new file mode 100644 index 0000000..0133350 --- /dev/null +++ b/vendor/archify/renderers/architecture/render-architecture.mjs @@ -0,0 +1,1001 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs'; +import { animateAttr, focusEdgeAttrs, focusNodeAttrs, focusNodeTitle, loadDiagramWithBrandMarks, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs'; +import { componentBox, boundaryBox, connectionPath } from '../shared/layout-report.mjs'; +import { rendererFailure, throwDiagnosticProblems } from '../shared/diagnostics.mjs'; +import { legendFootprint, relationshipLegendObstacles, resolveLegend, renderLegend as renderResolvedLegend } from '../shared/legend.mjs'; +import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth } from '../shared/text-fit.mjs'; +import { brandLabelFitWidth, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs'; +import { minimumReadableSourceTextPx } from '../shared/desktop-readability.mjs'; +import { translateMessage as i18nText } from '../shared/i18n.mjs'; +import { gridLayout, resolveComponentPos, validateGridPlacement } from './grid.mjs'; +import { createRouter } from './routing.mjs'; +import { placeAutomaticLabels, reservedLabelRect } from './labels.mjs'; +import { cleanRouteDetourProblems } from '../shared/route-quality.mjs'; +import { + asArray, + isFinitePoint, + rectsOverlap, + cleanEndpointSideProblems, + cleanFlowProblems, + cleanCrossingProblems, + cleanAmbiguousCorridorProblems, + collectArrowheadCollisions, + cleanBorderRunProblems, + cleanRouteRhythmProblems, + cleanLabelRouteClearanceProblems, + cleanLabelCanvasContainmentProblems, + suggestLabelObstacleFix, + suggestComponentSeparation, + polylinePath, + routePointsValue, + authoredStraightRouteAttrs, + labelPoint, + componentFill, + componentText, + arrowClassMap, + edgeLabelAccent, +} from '../shared/geometry.mjs'; + +const componentTextFit = { + sublabelPreferred: 9, + sublabelMinimum: 6, + tagPreferred: 7, + tagMinimum: 6, +}; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const layoutJsonMode = process.argv.includes('--layout-json'); +const cliArgs = process.argv.filter((arg) => arg !== '--layout-json'); +const { diagram: arch, template, outPath, sourceEvidence } = await loadDiagramWithBrandMarks({ + rendererDir: __dirname, + diagramType: 'architecture', + defaultExample: 'web-app.architecture.json', + argv: cliArgs, +}); + +const grid = gridLayout(arch); + +const layout = { + defaultW: 120, + defaultH: 60, + margin: 40, + // Boundary padding — the 30/50 rule that was a hand-arithmetic footgun + // (CHANGELOG v2.2.1): 30px on top/left/right, plus 20px extra at the bottom. + boundaryPad: 30, + boundaryExtraBottom: 20, + boundaryLabelBaseline: 18, + boundaryLabelClearance: 4, + boundaryLabelFontPreferred: 9, + boundaryLabelFontMinimum: 6, + boundaryLabelMaskHeight: 16, + boundaryLabelRailGap: 2, + boundaryLabelFrameInset: 4, + legendH: 28, +}; + +const LEGEND_CATALOG = [ + 'frontend', + 'backend', + 'database', + 'cloud', + 'security', + 'messagebus', + 'external', +].map((kind) => ({ kind, label: i18nText(arch.meta.locale, `legend.architecture.${kind}`) })); + +// ---- Measure components from free coordinates -------------------------------- +function measureComponent(c) { + const [x, y] = resolveComponentPos(c, grid); + const [w, h] = Array.isArray(c.size) ? c.size : [layout.defaultW, layout.defaultH]; + return { ...c, x, y, width: w, height: h, cx: x + w / 2, cy: y + h / 2 }; +} + +const components = new Map(asArray(arch.components).map((c) => [c.id, measureComponent(c)])); +const enforcesBoundaryTitleComposition = Boolean(arch.meta?.quality_profile); +const componentSteps = new Map(); +for (const [index, conn] of asArray(arch.connections).entries()) { + if (!componentSteps.has(conn.from)) componentSteps.set(conn.from, index); + if (!componentSteps.has(conn.to)) componentSteps.set(conn.to, index + 1); +} +for (const [index, c] of asArray(arch.components).entries()) { + if (!componentSteps.has(c.id)) componentSteps.set(c.id, index); +} + +// ---- Boundaries computed from the `wraps` id list --------------------------- +function boundaryRect(boundary) { + const members = asArray(boundary.wraps).map((id) => components.get(id)).filter(Boolean); + if (!members.length) return null; + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const member of members) { + minX = Math.min(minX, member.x); + minY = Math.min(minY, member.y); + maxX = Math.max(maxX, member.x + member.width); + maxY = Math.max(maxY, member.y + member.height); + } + const pad = boundary.pad ?? layout.boundaryPad; + const topPad = Math.max( + pad, + layout.boundaryLabelBaseline + layout.boundaryLabelClearance, + ); + return { + ...boundary, + x: minX - pad, + y: minY - topPad, + width: maxX - minX + pad * 2, + height: maxY - minY + topPad + layout.boundaryExtraBottom, + memberTop: minY, + }; +} + +function rectContains(outer, inner) { + const epsilon = 1e-9; + return outer.x <= inner.x + epsilon + && outer.y <= inner.y + epsilon + && outer.x + outer.width + epsilon >= inner.x + inner.width + && outer.y + outer.height + epsilon >= inner.y + inner.height; +} + +function boundaryLabelWidth(label, fontSize) { + return Math.max(30, textUnits(label) * fontSize * 0.6 + 10); +} + +const architectureLegendEntries = resolveLegend( + arch.meta?.legend, + LEGEND_CATALOG, + new Set([...components.values()].map((component) => component.type)), +); + +// One source for connection label geometry: the rect the containment rule +// measures is the rect the SVG mask draws, the auto canvas covers, the legend +// avoids, and the layout report publishes. +const resolvedLabelPoints = new Map(); +function connectionLabelBox(conn) { + if (!conn.label) return null; + return connectionLabelBoxAt(conn, resolvedLabelPoints.get(conn) || labelPoint(conn, pathFor(conn).points)); +} + +function connectionLabelBoxAt(conn, [lx, ly]) { + const width = Math.max(30, textUnits(conn.label) * 4.8 + 10); + return { x: lx - width / 2, y: ly - 10, width, height: 14, lx, ly }; +} + +function connectionLabelRects() { + const rects = []; + for (const [relationIndex, conn] of asArray(arch.connections).entries()) { + if (!components.has(conn.from) || !components.has(conn.to)) continue; + const box = connectionLabelBox(conn); + if (!box) continue; + rects.push({ relation: conn, relationIndex, label: conn.label, ...box }); + } + return rects; +} + +function autoViewBoxFor(candidateBoundaries, extraRects = []) { + let maxX = 0; + let maxY = 0; + for (const rects of [components.values(), candidateBoundaries, extraRects]) { + for (const rect of rects) { + maxX = Math.max(maxX, rect.x + rect.width); + maxY = Math.max(maxY, rect.y + rect.height); + } + } + let width = Math.ceil(maxX + layout.margin); + let footprint = legendFootprint(architectureLegendEntries, { + width: Math.max(1, width - layout.margin * 2), + }); + if (footprint.minWidth > width - layout.margin * 2) { + width = Math.ceil(footprint.minWidth + layout.margin * 2); + footprint = legendFootprint(architectureLegendEntries, { + width: width - layout.margin * 2, + }); + } + return [ + width, + Math.ceil(maxY + layout.margin + layout.legendH + footprint.extraHeight), + ]; +} + +function resolvedViewBoxWidth(candidateBoundaries) { + if (Array.isArray(arch.meta?.viewBox) && Number.isFinite(arch.meta.viewBox[0])) { + return arch.meta.viewBox[0]; + } + return autoViewBoxFor(candidateBoundaries, connectionGeometry)[0]; +} + +function expandBoundaryForReadableTitle(boundary, minimumFontSize) { + if (!enforcesBoundaryTitleComposition) return boundary; + const requiredWidth = boundaryLabelWidth(boundary.label, minimumFontSize) + + layout.boundaryLabelFrameInset * 2; + const extra = Math.max(0, requiredWidth - boundary.width); + if (!extra) return boundary; + return { + ...boundary, + x: boundary.x - extra / 2, + width: boundary.width + extra, + }; +} + +function measureBoundaryTitle(boundary, minimumFontSize) { + const availableWidth = Math.max(0, boundary.width - layout.boundaryLabelFrameInset * 2); + const units = textUnits(boundary.label); + const fitted = units > 0 + ? (availableWidth - 10) / (units * 0.6) + : layout.boundaryLabelFontPreferred; + const preferredFontSize = Math.max(layout.boundaryLabelFontPreferred, minimumFontSize); + const fontSize = Math.max( + minimumFontSize, + Math.min(preferredFontSize, fitted), + ); + const desiredWidth = boundaryLabelWidth(boundary.label, fontSize); + const height = Math.max(layout.boundaryLabelMaskHeight, Math.ceil(fontSize + 7)); + return { + x: boundary.x + layout.boundaryLabelFrameInset, + y: boundary.memberTop + - layout.boundaryLabelClearance + - height, + width: Math.min(availableWidth, desiredWidth), + height, + fontSize, + minimumFontSize, + baselineOffset: fontSize + 4, + availableWidth, + minimumWidth: boundaryLabelWidth(boundary.label, minimumFontSize), + }; +} + +function horizontalOverlap(left, right) { + return left.x < right.x + right.width && left.x + left.width > right.x; +} + +function layoutBoundaryTitles(rawBoundaries, minimumFontSize) { + const placedTitles = []; + const measured = new Map(); + const ordered = rawBoundaries + .map((boundary, index) => ({ boundary, index })) + .sort((left, right) => { + const areaDelta = left.boundary.width * left.boundary.height + - right.boundary.width * right.boundary.height; + return areaDelta || left.index - right.index; + }); + + for (const entry of ordered) { + const { index } = entry; + const boundary = expandBoundaryForReadableTitle(entry.boundary, minimumFontSize); + const title = measureBoundaryTitle(boundary, minimumFontSize); + let guard = 0; + while (guard < rawBoundaries.length + components.size + 1) { + guard += 1; + const blockers = [ + ...placedTitles, + ...components.values(), + ].filter((candidate) => horizontalOverlap(title, candidate) && rectsOverlap(title, candidate)); + if (!blockers.length) break; + title.y = blockers.reduce( + (min, blocker) => Math.min(min, blocker.y - layout.boundaryLabelRailGap - title.height), + Infinity, + ); + } + placedTitles.push(title); + measured.set(index, { boundary, title }); + } + + return rawBoundaries.map((_boundary, index) => { + const { boundary, title } = measured.get(index); + const bottom = boundary.y + boundary.height; + // Profile-less schema-v1 inputs keep their legacy boundary geometry. A + // quality profile opts into the stricter title-composition contract and + // may expand the frame to contain an adapted title rail. + const y = enforcesBoundaryTitleComposition + ? Math.min(boundary.y, title.y - layout.boundaryLabelFrameInset) + : boundary.y; + return { + ...boundary, + y, + height: bottom - y, + title, + }; + }); +} + +// ---- Routing state ---------------------------------------------------------- +// Initialized before the boundary-title work below: connection label rects are +// part of the derived canvas, so the title convergence must measure the same +// width the diagram actually renders into (a title sized for a narrower canvas +// would fall below the desktop-readability floor once labels grow it). Routing +// reads components, connections and the member-derived boundary frames (so an +// automatic route never borrows a frame border as its corridor), never the +// title-expanded frames or the viewBox. +const rawBoundaries = asArray(arch.boundaries).map(boundaryRect).filter(Boolean); +const { pathFor, connectionSides, connectionEndpointSide } = createRouter(components, arch.connections, { + distinctAutomaticPorts: true, + preferReadableRoutes: true, + frames: rawBoundaries.map((boundary) => ({ + ...boundary, + radius: boundary.kind === 'security-group' ? 8 : 12, + })), + labelRectFor: (conn, points, { routes, labels }) => (conn.label ? reservedLabelRect({ + label: { relation: conn, label: conn.label, ...connectionLabelBoxAt(conn, labelPoint(conn, points)) }, + points, + routes: routes.map((route, index) => ({ relationIndex: index, points: route })), + labels, + components: [...components.values()], + }) : null), +}); +function hasAutomaticRouteGeometry(connection) { + return !Array.isArray(connection?.via) + && (!connection?.route || connection.route === 'auto') + && connection?.channelX === undefined + && connection?.channelY === undefined; +} +// The auto canvas has to cover these rects; an authored viewBox is never +// resized to fit them — there the containment rule reports the clipping. +let connectionLabels = connectionLabelRects(); +// Unlabelled outer corridors are geometry too. Fitting only nodes and labels +// can clip a valid explicit via route while every browser overflow check passes. +const connectionGeometry = [ + ...connectionLabels, + ...asArray(arch.connections) + .filter((conn) => components.has(conn.from) && components.has(conn.to)) + .flatMap((conn) => pathFor(conn).points.map(([x, y]) => ({ x, y, width: 0, height: 0 }))), +]; + +function resolveBoundaryTitles() { + if (!enforcesBoundaryTitleComposition || rawBoundaries.length === 0) { + return { + boundaries: layoutBoundaryTitles(rawBoundaries, layout.boundaryLabelFontMinimum), + readabilityProblem: null, + }; + } + + const maximumIterations = 32; + let candidateBoundaries = rawBoundaries; + for (let iteration = 0; iteration < maximumIterations; iteration += 1) { + const budgetViewBoxWidth = resolvedViewBoxWidth(candidateBoundaries); + const minimumFontSize = Math.max( + layout.boundaryLabelFontMinimum, + minimumReadableSourceTextPx(budgetViewBoxWidth) + 1e-6, + ); + const nextBoundaries = layoutBoundaryTitles(rawBoundaries, minimumFontSize); + const finalViewBoxWidth = resolvedViewBoxWidth(nextBoundaries); + const finalMinimumFontSize = Math.max( + layout.boundaryLabelFontMinimum, + minimumReadableSourceTextPx(finalViewBoxWidth), + ); + if (minimumFontSize >= finalMinimumFontSize) { + return { boundaries: nextBoundaries, readabilityProblem: null }; + } + candidateBoundaries = nextBoundaries; + } + + const finalViewBoxWidth = resolvedViewBoxWidth(candidateBoundaries); + return { + boundaries: candidateBoundaries, + readabilityProblem: `[composition/desktop-readability] Boundary title layout did not converge after ${maximumIterations} iterations for the final ${finalViewBoxWidth}px viewBox — shorten boundary labels, provide a wider authored viewBox, or move wrapped components closer to the left edge.`, + }; +} + +const resolvedBoundaryTitles = resolveBoundaryTitles(); +const boundaries = resolvedBoundaryTitles.boundaries; +const compositionFrames = boundaries.map((boundary, index) => ({ + ...boundary, + id: boundary.id || index, + kind: boundary.kind || 'boundary', + radius: boundary.kind === 'security-group' ? 8 : 12, +})); + +function componentContext(component) { + const scopes = boundaries + .filter((boundary) => asArray(boundary.wraps).includes(component.id)) + .sort((a, b) => (b.width * b.height) - (a.width * a.height)) + .map((boundary) => boundary.label); + return scopes.length ? scopes.join(' › ') : i18nText(arch.meta.locale, 'node.context.architecture'); +} + +// ---- Auto viewBox: fit all geometry + the measured resolved legend ---------- +// Connection labels are diagram content, so an auto canvas that stopped at the +// component/boundary bbox would clip them; the label rects join the fit here +// and in the title convergence above, which sizes fonts for this same width. +const viewBox = arch.meta?.viewBox || autoViewBoxFor(boundaries, connectionGeometry); +const legendY = () => viewBox[1] - 16; + +// Fit titles and canvas from the original geometry first. Fallback labels must +// fit inside that canvas, so moving a label cannot trigger title reflow or a +// canvas/label feedback loop. Keep standard and every authored label control. +if (arch.meta?.quality_profile === 'showcase') { + connectionLabels = placeAutomaticLabels({ + keepFallbackNearRoute: true, + labels: connectionLabels, + routes: asArray(arch.connections).flatMap((conn, relationIndex) => ( + components.has(conn.from) && components.has(conn.to) + ? [{ relationIndex, points: pathFor(conn).points }] : [] + )), + components: [...components.values()], + titles: boundaries.map(boundary => boundary.title), + viewBox, + // Leave the resolved legend band available; moving a label must not hide + // an otherwise visible legend. Existing labels keep their placement. + placementBottom: architectureLegendEntries.length + ? legendY() - 32 - legendFootprint(architectureLegendEntries, { width: viewBox[0] - layout.margin * 2 }).extraHeight + : viewBox[1], + }); + for (const rect of connectionLabels) resolvedLabelPoints.set(rect.relation, [rect.lx, rect.ly]); +} + +// ---- Validation: mechanical correctness, never layout taste ----------------- +function validateArchitecture() { + const problems = []; + const diagnostics = []; + if (resolvedBoundaryTitles.readabilityProblem) { + problems.push(resolvedBoundaryTitles.readabilityProblem); + } + const requiresNestedBoundaryMembership = arch.meta?.engineering_profile === 'deployment-ownership'; + if (components.size !== asArray(arch.components).length) problems.push('Component ids must be unique.'); + if (grid) { + validateGridPlacement(arch, grid, problems); + } else { + for (const c of asArray(arch.components)) { + if (!Array.isArray(c.pos) || c.pos.length !== 2) { + problems.push(`Component "${c.id}" must include pos [x, y] when layout.mode is omitted (free placement).`); + } + } + } + + for (const c of components.values()) { + if (!isFinitePoint(c.x, c.y, c.width, c.height)) { + problems.push(`Component "${c.id}" has non-finite pos/size — pos and size must be [number, number].`); + continue; + } + if (c.width <= 0 || c.height <= 0) { + problems.push(`Component "${c.id}" has invalid size ${c.width}x${c.height} — width and height must be greater than 0.`); + continue; + } + if (c.x < 0 || c.y < 0 || c.x + c.width > viewBox[0] || c.y + c.height > viewBox[1]) { + problems.push(`Component "${c.id}" falls outside the viewBox ${viewBox[0]}x${viewBox[1]} — adjust pos/size or set a larger meta.viewBox.`); + } + const estLabelW = textUnits(c.label) * 6.6; + if (estLabelW > c.width + 8) { + problems.push(`Label "${c.label}" (~${Math.round(estLabelW)}px) is wider than component "${c.id}" (${c.width}px) — shorten the label or widen size.`); + } + const brandRailProblem = brandTopRailProblem(c, c.width, 8, 'Component'); + if (brandRailProblem) problems.push(brandRailProblem); + // sublabel and tag render as single unwrapped elements; shrink-to-fit + // handles the ordinary case, this rejects what it cannot rescue. + const availableTextW = availableNodeTextWidth(c.width); + for (const [field, value, minimum] of [ + ['Sublabel', c.sublabel, componentTextFit.sublabelMinimum], + ['Tag', c.tag, componentTextFit.tagMinimum], + ]) { + if (!value) continue; + const minimumW = minimumNodeTextWidth(value, minimum); + if (minimumW > availableTextW) { + problems.push(`${field} "${value}" needs ~${Math.ceil(minimumW)}px at the ${minimum}px legible minimum, but component "${c.id}" provides ${availableTextW}px — shorten the ${field.toLowerCase()} or widen size.`); + } + } + } + + // Component overlap — the highest-traffic hand-placement failure mode. + const list = [...components.values()]; + for (let i = 0; i < list.length; i += 1) { + for (let j = i + 1; j < list.length; j += 1) { + if (rectsOverlap(list[i], list[j], 8)) { + problems.push(`Components "${list[i].id}" and "${list[j].id}" are less than 8px apart — move one or shrink its size.\n${suggestComponentSeparation(list[i], list[j], 8)}`); + } + } + } + + // Boundaries: every wrapped id must exist; the computed box must stay in view. + for (const boundary of asArray(arch.boundaries)) { + for (const id of asArray(boundary.wraps)) { + if (!components.has(id)) problems.push(`Boundary "${boundary.label}" wraps unknown component "${id}".`); + } + } + const viewBoxRect = { x: 0, y: 0, width: viewBox[0], height: viewBox[1] }; + for (const boundary of boundaries) { + if (!enforcesBoundaryTitleComposition) continue; + if (boundary.title.minimumWidth > boundary.title.availableWidth) { + problems.push( + `Boundary label "${boundary.label}" needs ~${Math.ceil(boundary.title.minimumWidth)}px to fit at the ` + + `${Number(boundary.title.minimumFontSize.toFixed(2))}px desktop-readable source minimum, but its frame provides ${Math.floor(boundary.title.availableWidth)}px — ` + + 'shorten the boundary label, increase pad, or widen the wrapped component layout.', + ); + } + if (!rectContains(boundary, boundary.title)) { + problems.push( + `Boundary label "${boundary.label}" extends outside its final frame — shorten the label or increase boundary pad.`, + ); + } + if (!rectContains(viewBoxRect, boundary.title)) { + problems.push( + `Boundary label "${boundary.label}" extends outside the viewBox — move wrapped components away from the canvas edge, shorten the label, or increase the viewBox.`, + ); + } + for (const component of components.values()) { + if (!rectsOverlap(boundary.title, component)) continue; + problems.push( + `Boundary label "${boundary.label}" overlaps component "${component.id}" — move the component, increase boundary title space, or shorten the label.`, + ); + } + } + for (let leftIndex = 0; leftIndex < boundaries.length; leftIndex += 1) { + const left = boundaries[leftIndex]; + const leftMembers = new Set(asArray(left.wraps)); + for (let rightIndex = leftIndex + 1; rightIndex < boundaries.length; rightIndex += 1) { + const right = boundaries[rightIndex]; + if (enforcesBoundaryTitleComposition && rectsOverlap(left.title, right.title)) { + problems.push( + `Boundary labels "${left.label}" and "${right.label}" overlap — shorten a label or increase boundary title space.`, + ); + } + // Ordinary architecture boundaries are sets, not an implied ownership + // tree: orthogonal scopes such as runtime and compliance may share some + // components while each contains others. The opt-in deployment profile + // does promise hierarchical region/private-scope membership, so only it + // receives the stricter membership-to-frame containment contract. + if (!requiresNestedBoundaryMembership) continue; + const rightMembers = new Set(asArray(right.wraps)); + const shared = [...leftMembers].filter((id) => rightMembers.has(id)); + const leftNested = [...leftMembers].every((id) => rightMembers.has(id)); + const rightNested = [...rightMembers].every((id) => leftMembers.has(id)); + if (shared.length && !leftNested && !rightNested) { + const leftOnly = [...leftMembers].filter((id) => !rightMembers.has(id)); + const rightOnly = [...rightMembers].filter((id) => !leftMembers.has(id)); + problems.push( + `Boundary "${left.label}" crosses boundary "${right.label}" because their memberships partially overlap ` + + `(shared: ${shared.map((id) => `"${id}"`).join(', ')}; ` + + `only in "${left.label}": ${leftOnly.map((id) => `"${id}"`).join(', ')}; ` + + `only in "${right.label}": ${rightOnly.map((id) => `"${id}"`).join(', ')}) — ` + + 'keep one boundary fully nested by removing outside members, or split the boundary.', + ); + continue; + } + + if (!rectsOverlap(left, right)) continue; + const leftContainsRight = rectContains(left, right); + const rightContainsLeft = rectContains(right, left); + if (!leftContainsRight && !rightContainsLeft) { + problems.push( + `Boundary "${left.label}" and boundary "${right.label}" final frames partially overlap — ` + + 'adjust wraps, pad, or component positions so the frames are disjoint or one fully contains the other.', + ); + continue; + } + + if (!shared.length) { + problems.push( + `Boundary "${left.label}" and boundary "${right.label}" final frames overlap even though their memberships are disjoint — ` + + 'adjust pad or component positions so the frames are disjoint, or make wraps express the intended nesting.', + ); + continue; + } + + const containmentMatchesMembership = (leftNested && rightContainsLeft) + || (rightNested && leftContainsRight); + if (!containmentMatchesMembership) { + problems.push( + `Boundary "${left.label}" and boundary "${right.label}" final frame containment contradicts their wraps membership — ` + + 'reduce the inner boundary pad, move its components, or correct wraps so geometry and nesting agree.', + ); + } + } + } + for (const b of boundaries) { + if (b.x < 0 || b.y < 0 || b.x + b.width > viewBox[0] || b.y + b.height > viewBox[1]) { + const overflow = { + left: Math.max(0, -b.x), + top: Math.max(0, -b.y), + right: Math.max(0, b.x + b.width - viewBox[0]), + bottom: Math.max(0, b.y + b.height - viewBox[1]), + }; + const sides = Object.entries(overflow).filter(([, pixels]) => pixels > 0) + .map(([side, pixels]) => `${side} by ${Math.ceil(pixels)}px`).join(', '); + const supportedFixes = []; + if (overflow.left || overflow.top) { + supportedFixes.push(`move the wrapped components right by at least ${Math.ceil(overflow.left)}px and down by at least ${Math.ceil(overflow.top)}px, then revalidate connected routes and the opposite canvas sides; enlarging meta.viewBox cannot fix left/top overflow`); + } + if (overflow.right || overflow.bottom) { + supportedFixes.push(`increase meta.viewBox to at least [${Math.ceil(Math.max(viewBox[0], b.x + b.width))}, ${Math.ceil(Math.max(viewBox[1], b.y + b.height))}] for right/bottom overflow, or move the wrapped components inward; revalidate desktop readability`); + } + const message = `Boundary "${b.label}" extends outside the viewBox (${sides}) — preserve wraps membership and repair the measured canvas side.`; + diagnostics.push({ + code: 'layout/boundary-out-of-bounds', + severity: 'error', + message, + subject: { diagramType: 'architecture', boundary: { kind: b.kind, label: b.label, wraps: b.wraps } }, + evidence: { + bounds: { x: b.x, y: b.y, width: b.width, height: b.height }, + viewBox: [...viewBox], + overflow, + members: asArray(b.wraps).map((id) => components.get(id)).filter(Boolean).map(componentBox), + }, + supportedFixes, + }); + problems.push(message); + } + } + + for (const conn of asArray(arch.connections)) { + if (!components.has(conn.from)) problems.push(`Connection "${conn.label || conn.from}" references unknown source "${conn.from}".`); + if (!components.has(conn.to)) problems.push(`Connection "${conn.label || conn.to}" references unknown target "${conn.to}".`); + if (components.has(conn.from) && components.has(conn.to)) { + const routed = pathFor(conn); + const outsidePoints = routed.points.filter(([x, y]) => x < 0 || y < 0 || x > viewBox[0] || y > viewBox[1]); + if (arch.meta?.quality_profile === 'showcase' && outsidePoints.length) { + const message = `Connection "${conn.id || `${conn.from}->${conn.to}`}" extends outside the viewBox — move the measured outside route points inward or enlarge an authored viewBox for right/bottom overflow.`; + diagnostics.push({ + code: 'layout/route-out-of-bounds', severity: 'error', message, + subject: { diagramType: 'architecture', collection: 'connections', index: asArray(arch.connections).indexOf(conn), ...(conn.id ? { id: conn.id } : {}), from: conn.from, to: conn.to }, + evidence: { viewBox: [...viewBox], outsidePoints, points: routed.points }, + supportedFixes: ['move negative route coordinates inside the canvas; for right/bottom overflow, enlarge meta.viewBox or reroute inward; preserve endpoints, direction and labels, then revalidate'], + }); + problems.push(message); + } + const [start, end] = [routed.points[0], routed.points[routed.points.length - 1]]; + const distance = Math.hypot(end[0] - start[0], end[1] - start[1]); + if (distance < 24 && conn.from === conn.to) { + // "Move the components apart" cannot be executed for a self-loop; the + // ports sit on one component and the sides decide how far apart. + const message = `Self-loop "${conn.id || conn.label || conn.from}" on component "${conn.from}" has its two ports only ${Math.round(distance)}px apart (minimum 24px) — remove fromSide/toSide so the renderer can choose the loop's sides, or set fromSide and toSide to different sides.`; + diagnostics.push({ + code: 'layout/self-loop-ports', severity: 'error', message, + subject: { diagramType: 'architecture', collection: 'connections', index: asArray(arch.connections).indexOf(conn), ...(conn.id ? { id: conn.id } : {}), from: conn.from, to: conn.to }, + evidence: { distancePx: Math.round(distance), minimumPx: 24, fromSide: connectionEndpointSide(conn, 'source'), toSide: connectionEndpointSide(conn, 'target'), points: routed.points }, + supportedFixes: ['remove fromSide/toSide from the self-loop', 'set fromSide and toSide to different sides of the component'], + }); + problems.push(message); + } else if (distance < 24) { + problems.push(`Connection "${conn.label || `${conn.from}->${conn.to}`}" is too short (${Math.round(distance)}px; minimum 24px) — place its components farther apart.`); + } + } + } + + problems.push(...cleanEndpointSideProblems({ + relations: arch.connections, + endpointIds: new Set(components.keys()), + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + fromSideFor: (conn) => connectionEndpointSide(conn, 'source'), + toSideFor: (conn) => connectionEndpointSide(conn, 'target'), + routeHint: 'keep automatic routing so the renderer can use a side-aware bridge, or set truthful fromSide/toSide with perpendicular via segments', + })); + problems.push(...cleanFlowProblems({ + relations: arch.connections, + obstacles: components.values(), + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + obstacleKind: 'component', + routeHint: 'adjust fromSide/toSide, set route/via, or move the component' + })); + problems.push(...cleanCrossingProblems({ + relations: arch.connections, + endpointIds: new Set(components.keys()), + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + // Automatic architecture routes render with an opaque crossover halo. + // That makes a proper X visually unambiguous while explicit authored + // crossings remain a blocking composition error. + crossingResolved: (left, right) => ( + hasAutomaticRouteGeometry(left) && hasAutomaticRouteGeometry(right) + ), + routeHint: 'adjust route/via or fromSide/toSide so the connections use separate corridors' + })); + problems.push(...cleanAmbiguousCorridorProblems({ + relations: arch.connections, + endpointIds: new Set(components.keys()), + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + includeSharedEndpoints: (left, right) => hasAutomaticRouteGeometry(left) && !left.labelAt + && hasAutomaticRouteGeometry(right) && !right.labelAt, + routeHint: 'adjust route/via or fromSide/toSide so distinct connections do not visually merge' + })); + if ((process.env.ARCHIFY_QUALITY_PROFILE || arch.meta?.quality_profile) === 'showcase') { + const collisions = collectArrowheadCollisions({ + routedRelations: asArray(arch.connections) + .filter((conn) => components.has(conn.from) && components.has(conn.to) && hasAutomaticRouteGeometry(conn) && !conn.labelAt) + .map((relation) => ({ relation, points: pathFor(relation).points })), + }); + for (const hit of collisions) { + const left = hit.left.relation; + const right = hit.right.relation; + const message = `[composition/arrowhead-collision] automatic connections "${left.id || left.from}" and "${right.id || right.from}" into "${left.to}" have arrowheads ${hit.distance}px apart (minimum ${hit.minimum}px) — enlarge or reposition the destination, or choose separate toSide ports.`; + problems.push(message); + diagnostics.push({ + code: 'composition/arrowhead-collision', severity: 'error', message, + subject: { diagramType: 'architecture', collection: 'connections', id: left.id, from: left.from, to: left.to }, + evidence: { otherId: right.id, distancePx: hit.distance, minimumPx: hit.minimum, endpoints: [hit.left.tip, hit.right.tip] }, + supportedFixes: ['enlarge or reposition the destination', 'choose separate toSide ports'], + }); + } + } + problems.push(...cleanBorderRunProblems({ + relations: arch.connections, + endpointIds: new Set(components.keys()), + frames: compositionFrames, + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + routeHint: 'adjust route/via or fromSide/toSide so the connection crosses the boundary perpendicularly instead of following its border' + })); + problems.push(...cleanRouteRhythmProblems({ + relations: arch.connections, + endpointIds: new Set(components.keys()), + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + routeHint: 'move route/via points into a wider corridor or move the component so every turn has room to read' + })); + problems.push(...cleanRouteDetourProblems({ + relations: arch.connections, + obstacles: components.values(), + contentRects: [...components.values(), ...boundaries], + endpointIds: new Set(components.keys()), + pathFor, + fromSideFor: (conn) => connectionEndpointSide(conn, 'source'), + toSideFor: (conn) => connectionEndpointSide(conn, 'target'), + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + })); + + // Connection labels must not land on top of components. + const labelRects = connectionLabels; + for (const rect of labelRects) { + const blockedComponents = [...components.values()].filter(c => rectsOverlap(rect, c, -2)); + const labelPinned = ['labelAt', 'labelDx', 'labelDy', 'labelSegment'].some(key => rect.relation[key] !== undefined); + const points = pathFor(rect.relation).points; + const shortHorizontalGap = points.length === 2 && Math.abs(points[0][1] - points[1][1]) < 0.0001 + ? Math.abs(points[1][0] - points[0][0]) : null; + const requiredGap = Math.ceil(rect.width + 16); + if (arch.meta?.quality_profile === 'showcase' && !labelPinned && blockedComponents.length + && shortHorizontalGap != null && shortHorizontalGap < requiredGap) { + const message = `Label "${rect.label}" has only ${Math.round(shortHorizontalGap)}px between "${rect.relation.from}" and "${rect.relation.to}"; it needs at least ${requiredGap}px to stay beside its route — increase that clear gap or place the connected nodes on another readable row, preserving the label.`; + problems.push(message); + diagnostics.push({ + code: 'composition/label-gap', severity: 'error', message, + subject: { diagramType: 'architecture', collection: 'connections', id: rect.relation.id, from: rect.relation.from, to: rect.relation.to }, + evidence: { clearGapPx: shortHorizontalGap, minimumGapPx: requiredGap, labelWidthPx: rect.width, obstacles: blockedComponents.map(c => c.id) }, + supportedFixes: [`increase the clear gap between the connected nodes to at least ${requiredGap}px`, 'reposition the connected nodes together while preserving the full relationship label'], + }); + } else { + for (const c of blockedComponents) { + problems.push(`Label "${rect.label}" overlaps component "${c.id}" — adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, c, 'component', viewBox, components.values())}`); + } + } + if (enforcesBoundaryTitleComposition) { + for (const boundary of boundaries) { + if (!rectsOverlap(boundary.title, rect)) continue; + problems.push( + `Boundary label "${boundary.label}" overlaps connection label "${rect.label}" — move the boundary title rail by adjusting wrapped component positions, or move the connection label with labelAt/labelDx/labelDy/labelSegment.`, + ); + } + } + } + problems.push(...cleanLabelRouteClearanceProblems({ + relations: arch.connections, + labels: labelRects, + endpointIds: new Set(components.keys()), + pathFor, + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + })); + // See collectLabelCanvasOverflow in shared/geometry.mjs. An auto canvas now + // covers these rects, so this reports authored viewBoxes and the origin side, + // which growth cannot reach. + problems.push(...cleanLabelCanvasContainmentProblems({ + labels: labelRects, + viewBox, + diagramType: 'architecture', + relationCollection: 'connections', + profile: arch.meta?.quality_profile, + })); + + if (problems.length) { + throwDiagnosticProblems('Architecture layout validation failed', problems, { + subject: { diagramType: 'architecture' }, + diagnostics, + }); + } +} + +function buildLayoutReport() { + const labels = connectionLabels.map((rect) => ({ + text: rect.label, + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: 14, + labelAt: [Math.round(rect.lx), Math.round(rect.ly)], + })); + return { + ok: true, + diagram_type: 'architecture', + layout: grid ? { mode: 'grid', ...grid } : { mode: 'free' }, + viewBox, + components: [...components.values()].map(componentBox), + boundaries: boundaries.map(boundaryBox), + connections: asArray(arch.connections) + .filter((conn) => components.has(conn.from) && components.has(conn.to)) + .map((conn) => { + const routed = pathFor(conn); + const labelAt = conn.label ? resolvedLabelPoints.get(conn) || labelPoint(conn, routed.points) : null; + return connectionPath(conn, routed, labelAt); + }), + labels, + }; +} + +// ---- Rendering --------------------------------------------------------------- +function renderBoundaryFrame(b, index) { + const cls = b.kind === 'security-group' ? 'c-security-group' : 'c-region'; + const rx = b.kind === 'security-group' ? 8 : 12; + return ` `; +} + +function renderBoundaryLabel(b, index) { + const labelCls = b.kind === 'security-group' ? 't-security' : 't-cloud'; + return ` + + ${esc(b.label)} + `; +} + +function renderConnectionPath(conn, index) { + const [cls, marker] = arrowClassMap[conn.variant || 'default'] || arrowClassMap.default; + const routed = pathFor(conn); + const strokeWidth = conn.width || (conn.variant === 'emphasis' ? 1.8 : 1.5); + const automaticRoute = hasAutomaticRouteGeometry(conn); + const underlay = automaticRoute + ? ` \n` + : ''; + const crossover = automaticRoute + ? ` data-composition-crossover="halo"${conn.labelAt ? '' : ' data-composition-independent="true"'}` : ''; + const edge = ` `; + if (!automaticRoute) return edge; + // The wrapper is presentation-only: viewer state remains on the one semantic + // edge, while CSS can keep its preceding mask underlay at the same opacity. + return ` \n${underlay}${edge.replace(/^ /, ' ')}\n `; +} + +function renderConnectionLabel(conn, index) { + const box = connectionLabelBox(conn); + if (!box) return ''; + return ` + + ${esc(conn.label)} + `; +} + +function renderComponent(c) { + const fill = componentFill[c.type] || 'c-external'; + const accent = componentText[c.type] || 't-muted'; + const cx = c.cx; + const hasSub = c.sublabel != null && c.sublabel !== ''; + const labelY = hasSub ? c.y + c.height / 2 - 2 : c.y + c.height / 2 + 4; + const sub = hasSub + ? `\n ${esc(c.sublabel)}` + : ''; + const tag = c.tag + ? `\n ${esc(c.tag)}` + : ''; + const brand = renderBrandMark(c, { x: c.x + c.width - 22, y: c.y + 6 }); + const labelFontSize = fittedNodeFontSize(c.label, brandLabelFitWidth(c, c.width), 11, 8); + const passport = { kind: c.type, sublabel: c.sublabel, tag: c.tag, context: componentContext(c), ...brandMetadataFor(c) }; + return ` + ${focusNodeTitle(c.label, passport)} + + + ${renderSemanticSigil(c.type, { icon: c.icon, x: c.x + 6, y: c.y + 6 })}${brand ? `\n ${brand}` : ''} + ${esc(c.label)}${sub}${tag} + `; +} + +function renderLegend() { + const entries = architectureLegendEntries; + const relationshipObstacles = relationshipLegendObstacles(arch.connections, { + pointsFor: (connection) => pathFor(connection).points, + labelRectFor: connectionLabelBox, + }); + let contentBottom = 0; + for (const component of components.values()) { + contentBottom = Math.max(contentBottom, component.y + component.height); + } + for (const boundary of boundaries) { + contentBottom = Math.max(contentBottom, boundary.y + boundary.height); + } + return renderResolvedLegend({ + entries, + locale: arch.meta.locale, + layout: { + x: layout.margin, + baselineY: legendY(), + width: viewBox[0] - layout.margin * 2, + minTitleY: contentBottom + 8, + obstacles: relationshipObstacles, + unfit: arch.meta?.legend === undefined ? 'hide' : 'error', + diagramType: 'architecture', + }, + renderSwatch: (entry) => ``, + }); +} + +function renderSvg() { + // An automatic architecture canvas is compiler-measured geometry. Let the + // Reader spend the real desktop height budget on it, including when an + // outer route makes the canvas taller than the ordinary wide-diagram + // threshold. Authored viewBoxes keep their geometry and existing Reader width policy; + // their declared height may use readable document scrolling without reflow. + const readerFit = arch.meta?.viewBox + ? ' data-diagram-type="architecture" data-reader-fit="authored-height"' + : ' data-reader-fit="intrinsic-height"'; + // A complete repository architecture is allowed to use normal page scroll; + // keep its common-desktop text at a comfortable reading size instead of + // shrinking a semantically rich graph to the universal emergency floor. + const readerMinimumText = arch.meta?.viewBox ? '' : ' data-reader-min-text="7.5"'; + const readerPrimaryText = arch.meta?.viewBox ? '' : ' data-reader-primary-text="14"'; + return ` +${svgAccessibleText(arch.meta, 'architecture')} +${renderDefinitions()} + + + + + +${boundaries.map(renderBoundaryFrame).join('\n\n')} + + +${asArray(arch.connections).map(renderConnectionPath).join('\n')} + + +${[...components.values()].map(renderComponent).join('\n\n')} + + +${asArray(arch.connections).map(renderConnectionLabel).join('\n')} + + +${boundaries.map(renderBoundaryLabel).join('\n\n')} + + +${renderLegend()} + `; +} + +if (layoutJsonMode) { + try { + validateArchitecture(); + } catch (error) { + // A rejected layout is still useful repair evidence. Input/implementation + // failures must retain their existing failure boundary, not partial geometry. + if (!error.archifyDiagnostics?.length) throw error; + console.log(JSON.stringify({ + ...buildLayoutReport(), + ...rendererFailure(error), + contract: 'archify-architecture-layout-v1', + }, null, 2)); + process.exitCode = 1; + } + if (!process.exitCode) console.log(JSON.stringify(buildLayoutReport(), null, 2)); +} else { + validateArchitecture(); + writeDiagram({ + outPath, + template, + diagramType: 'architecture', + meta: arch.meta, + svg: renderSvg(), + cards: arch.cards, + sourceEvidence, + }); +} diff --git a/vendor/archify/renderers/architecture/routing.mjs b/vendor/archify/renderers/architecture/routing.mjs new file mode 100644 index 0000000..f2de287 --- /dev/null +++ b/vendor/archify/renderers/architecture/routing.mjs @@ -0,0 +1,1157 @@ +// Internal architecture router shared by rendering and geometry inspection. +// Create a new router when measured boxes or connections change: port spreading +// is computed once and route results are cached for this scene. + +import { + segmentIntersectsRect, + anchor, + automaticPortSpread, + automaticPortRhythmBridge, + defaultFromSide, + defaultToSide, + chosenSide, + properSegmentIntersection, + routeHonorsEndpointSides, + normalizeRoutePoints, + rectsOverlap, + roundedPath, + collectBorderRuns, + collectRouteRhythmIssues, + frameBorderSegments, +} from '../shared/geometry.mjs'; +import { shortestOrthogonalGridRoute } from '../shared/route-quality.mjs'; + +/** + * Router bound to one set of measured component boxes. + * + * @param {Map} components measured boxes by id + * @param {Array} connections the connection list to spread ports across + * @param {object} [options] + * @param {Array<{x,y,width,height,radius?}>} [options.frames] structural frames + * (boundaries) whose borders an automatic route may cross but never follow + * @param {number} [options.interiorSegmentPx] showcase floor for interior segments + * @param {number} [options.microSegmentPx] floor for any segment + * @param {(conn: object, points: number[][], context: {routes: number[][][], labels: object[]}) => object|null} [options.labelRectFor] + * default label rect of a routed relationship given the routes and label + * rects resolved so far; later automatic routes keep clear of it so a dense + * fan-out does not leave the label nowhere to go + */ +export function createRouter(components, connections = [], { + frames = [], + interiorSegmentPx = 16, + microSegmentPx = 8, + labelRectFor = null, + distinctAutomaticPorts = false, + preferReadableRoutes = false, +} = {}) { + const frameBorders = frames.flatMap((frame) => frameBorderSegments(frame)); + const LABEL_CLEARANCE = 4; + // Labels of already-routed relationships, reserved while planning the rest. + let reservedLabels = []; + let honourReservedLabels = true; + let allowGridSearch = true; + + function routeClearsReservedLabels(conn, points) { + if (!honourReservedLabels) return true; + for (const entry of reservedLabels) { + if (entry.conn === conn) continue; + for (let index = 0; index < points.length - 1; index += 1) { + if (segmentIntersectsRect({ start: points[index], end: points[index + 1] }, entry.rect, LABEL_CLEARANCE)) { + return false; + } + } + } + return true; + } + + // The grid search adds its own 2px component clearance; pre-expand so a + // reserved label keeps the same 4px clearance the placement pass demands. + function reservedLabelObstacles(gridClearance) { + if (!honourReservedLabels) return []; + const grow = LABEL_CLEARANCE - gridClearance; + return reservedLabels.map(({ rect }) => ({ + x: rect.x - grow, + y: rect.y - grow, + width: rect.width + grow * 2, + height: rect.height + grow * 2, + })); + } + + // Automatic routes are held to the same composition floors the showcase + // gate enforces afterwards. Accepting a route here that the gate rejects + // only hands the author a hand-routing repair the planner could have made. + function routeMeetsCompositionFloors(points) { + if (collectRouteRhythmIssues({ routedRelations: [{ points }], interiorSegmentPx, microSegmentPx }).length) { + return false; + } + return !frames.length || collectBorderRuns({ routedRelations: [{ points }], frames }).length === 0; + } + const planningMetrics = { + routeCount: 0, + explicitRouteCount: 0, + automaticRouteCount: 0, + gridSearchCount: 0, + gridRoutedCount: 0, + gridCandidateNodeCount: 0, + gridUsableNodeCount: 0, + gridEdgeCount: 0, + gridVisitedNodeCount: 0, + avoidedSegmentCount: 0, + conflictFallbackCount: 0, + maximumGridSearchCount: 64, + gridBudgetExhaustedCount: 0, + crossoverRoutedCount: 0, + readabilityCandidateCount: 0, + readabilityImprovedCount: 0, + reciprocalCandidateCount: 0, + reciprocalImprovedCount: 0, + gridAttempts: [], + }; + + // ---- Connection routing ------------------------------------------------------ + function routeClearsComponents(conn, points, clearance = 2) { + const endpointIds = new Set([conn.from, conn.to]); + for (const component of components.values()) { + if (endpointIds.has(component.id)) continue; + for (let index = 0; index < points.length - 1; index += 1) { + if (segmentIntersectsRect({ start: points[index], end: points[index + 1] }, component, clearance)) { + return false; + } + } + } + return routeClearsReservedLabels(conn, points); + } + + function routeClearsEndpointComponents(points, from, to) { + const lastSegment = points.length - 2; + for (let index = 0; index <= lastSegment; index += 1) { + const segment = { start: points[index], end: points[index + 1] }; + if (index > 0 && segmentIntersectsRect(segment, from)) return false; + if (index < lastSegment && segmentIntersectsRect(segment, to)) return false; + } + return true; + } + + function relationshipsShareEndpoint(left, right) { + return left.from === right.from + || left.from === right.to + || left.to === right.from + || left.to === right.to; + } + + function collinearOverlapLength(leftStart, leftEnd, rightStart, rightEnd) { + const epsilon = 0.0001; + if (Math.abs(leftStart[0] - leftEnd[0]) <= epsilon + && Math.abs(rightStart[0] - rightEnd[0]) <= epsilon + && Math.abs(leftStart[0] - rightStart[0]) <= epsilon) { + return Math.max(0, + Math.min(Math.max(leftStart[1], leftEnd[1]), Math.max(rightStart[1], rightEnd[1])) + - Math.max(Math.min(leftStart[1], leftEnd[1]), Math.min(rightStart[1], rightEnd[1]))); + } + if (Math.abs(leftStart[1] - leftEnd[1]) <= epsilon + && Math.abs(rightStart[1] - rightEnd[1]) <= epsilon + && Math.abs(leftStart[1] - rightStart[1]) <= epsilon) { + return Math.max(0, + Math.min(Math.max(leftStart[0], leftEnd[0]), Math.max(rightStart[0], rightEnd[0])) + - Math.max(Math.min(leftStart[0], leftEnd[0]), Math.min(rightStart[0], rightEnd[0]))); + } + return 0; + } + + function orthogonalTouchOnResolvedInterior(start, end, resolvedStart, resolvedEnd) { + const epsilon = 0.0001; + const candidateHorizontal = Math.abs(start[1] - end[1]) <= epsilon; + const candidateVertical = Math.abs(start[0] - end[0]) <= epsilon; + const resolvedHorizontal = Math.abs(resolvedStart[1] - resolvedEnd[1]) <= epsilon; + const resolvedVertical = Math.abs(resolvedStart[0] - resolvedEnd[0]) <= epsilon; + if (candidateHorizontal && resolvedVertical) { + const x = resolvedStart[0]; + const y = start[1]; + return x >= Math.min(start[0], end[0]) - epsilon + && x <= Math.max(start[0], end[0]) + epsilon + && y > Math.min(resolvedStart[1], resolvedEnd[1]) + epsilon + && y < Math.max(resolvedStart[1], resolvedEnd[1]) - epsilon; + } + if (candidateVertical && resolvedHorizontal) { + const x = start[0]; + const y = resolvedStart[1]; + return y >= Math.min(start[1], end[1]) - epsilon + && y <= Math.max(start[1], end[1]) + epsilon + && x > Math.min(resolvedStart[0], resolvedEnd[0]) + epsilon + && x < Math.max(resolvedStart[0], resolvedEnd[0]) - epsilon; + } + return false; + } + + function unrelatedResolvedRoutes(conn, resolvedRoutes) { + return resolvedRoutes.filter((entry) => !relationshipsShareEndpoint(conn, entry.conn)); + } + + function routeConflictsWithResolved(conn, points, resolvedRoutes) { + const unrelated = unrelatedResolvedRoutes(conn, resolvedRoutes); + for (const entry of unrelated) { + for (let left = 0; left < points.length - 1; left += 1) { + for (let right = 0; right < entry.points.length - 1; right += 1) { + if (properSegmentIntersection( + points[left], + points[left + 1], + entry.points[right], + entry.points[right + 1], + )) return true; + if (orthogonalTouchOnResolvedInterior( + points[left], + points[left + 1], + entry.points[right], + entry.points[right + 1], + )) return true; + if (collinearOverlapLength( + points[left], + points[left + 1], + entry.points[right], + entry.points[right + 1], + ) >= 8) return true; + } + } + } + return false; + } + + function routeOverlapsResolved(conn, points, resolvedRoutes) { + // A common destination does not make two independently labelled routes a + // bus. Keep their corridors distinct too; explicit routes bypass planning. + for (const entry of resolvedRoutes) { + if (relationshipsShareEndpoint(conn, entry.conn) + && (!distinctAutomaticPorts || hasAuthoredRouteGeometry(entry.conn) || entry.conn.labelAt || conn.labelAt)) continue; + for (let left = 0; left < points.length - 1; left += 1) { + for (let right = 0; right < entry.points.length - 1; right += 1) { + if (collinearOverlapLength( + points[left], + points[left + 1], + entry.points[right], + entry.points[right + 1], + ) >= 8) return true; + } + } + } + return false; + } + + const OUTWARD_SIDE_VECTOR = { + left: [-1, 0], + right: [1, 0], + top: [0, -1], + bottom: [0, 1], + }; + + function outwardStub(point, side, distance = 24) { + const [dx, dy] = OUTWARD_SIDE_VECTOR[side] || [0, 0]; + return [point[0] + dx * distance, point[1] + dy * distance]; + } + + function collinearBacktrack(a, b, c) { + const first = [b[0] - a[0], b[1] - a[1]]; + const second = [c[0] - b[0], c[1] - b[1]]; + const cross = first[0] * second[1] - first[1] * second[0]; + const dot = first[0] * second[0] + first[1] * second[1]; + return Math.abs(cross) <= 0.0001 && dot < -0.0001; + } + + function sideAwareBridgeCandidates(start, end, fromSide, toSide) { + const startStub = outwardStub(start, fromSide); + const endStub = outwardStub(end, toSide); + const rawCandidates = []; + const minimumBridge = 16; + const verticalSides = new Set(['top', 'bottom']); + const horizontalSides = new Set(['left', 'right']); + + // Port spreading can leave parallel-side anchors only a few pixels apart. + // Route through a bounded outside channel so we keep both endpoint normals + // without introducing a tiny, noisy connector between the two stubs. + if (verticalSides.has(fromSide) && verticalSides.has(toSide) + && Math.abs(start[0] - end[0]) < minimumBridge) { + for (const channelX of [ + Math.max(start[0], end[0]) + minimumBridge, + Math.min(start[0], end[0]) - minimumBridge, + ]) { + rawCandidates.push([ + startStub, + [channelX, startStub[1]], + [channelX, endStub[1]], + endStub, + ]); + } + } + if (horizontalSides.has(fromSide) && horizontalSides.has(toSide) + && Math.abs(start[1] - end[1]) < minimumBridge) { + for (const channelY of [ + Math.max(start[1], end[1]) + minimumBridge, + Math.min(start[1], end[1]) - minimumBridge, + ]) { + rawCandidates.push([ + startStub, + [startStub[0], channelY], + [endStub[0], channelY], + endStub, + ]); + } + } + + rawCandidates.push( + [startStub, [endStub[0], startStub[1]], endStub], + [startStub, [startStub[0], endStub[1]], endStub], + ); + return rawCandidates.map((candidate) => normalizeRoutePoints([start, ...candidate, end])) + .filter((points) => points.length >= 2) + .filter((points) => !collinearBacktrack(points[0], points[1], points[2] || points[1])) + .filter((points) => !collinearBacktrack(points.at(-3) || points.at(-2), points.at(-2), points.at(-1))) + .filter((points) => routeHonorsEndpointSides(points, fromSide, toSide)) + .map((points) => points.slice(1, -1)); + } + + const AUTOMATIC_PORT_CORNER_GUTTER = 16; + const AUTOMATIC_PORT_ALIGNMENT_DELTA = 16; + + function portHasCornerClearance(rect, side, point) { + if (side === 'left' || side === 'right') { + const inset = Math.min(AUTOMATIC_PORT_CORNER_GUTTER, rect.height / 2); + return point[1] >= rect.y + inset && point[1] <= rect.y + rect.height - inset; + } + if (side === 'top' || side === 'bottom') { + const inset = Math.min(AUTOMATIC_PORT_CORNER_GUTTER, rect.width / 2); + return point[0] >= rect.x + inset && point[0] <= rect.x + rect.width - inset; + } + return false; + } + + function alignFacingPorts(conn, from, to, start, end, fromSide, toSide, ports) { + const hasExplicitGeometry = ( + conn.via + || (conn.route && conn.route !== 'auto') + || conn.channelX !== undefined + || conn.channelY !== undefined + || conn.labelAt + ); + const horizontallyFacing = ( + (fromSide === 'right' && toSide === 'left') + || (fromSide === 'left' && toSide === 'right') + ); + const verticallyFacing = ( + (fromSide === 'bottom' && toSide === 'top') + || (fromSide === 'top' && toSide === 'bottom') + ); + if (hasExplicitGeometry || (!horizontallyFacing && !verticallyFacing)) return { start, end }; + + const fromSpread = Boolean(ports?.from); + const toSpread = Boolean(ports?.to); + if (fromSpread && toSpread) return { start, end }; + const hasExplicitSides = ( + (conn.fromSide && conn.fromSide !== 'auto') + || (conn.toSide && conn.toSide !== 'auto') + ); + if (!fromSpread && !toSpread && hasExplicitSides) return { start, end }; + + const alignmentDelta = horizontallyFacing + ? Math.abs(start[1] - end[1]) + : Math.abs(start[0] - end[0]); + if (alignmentDelta >= AUTOMATIC_PORT_ALIGNMENT_DELTA) return { start, end }; + + // Keep the shared endpoint's distinct spread slot and move only the + // relationship's unshared endpoint onto that axis. With no spread endpoint, + // retain the existing least-movement choice between the two facing sides. + // If both endpoints are shared, preserve the outside bridge so no competing + // port is silently collapsed. + const alignEndToStart = horizontallyFacing + ? { start, end: [end[0], start[1]] } + : { start, end: [start[0], end[1]] }; + const alignStartToEnd = horizontallyFacing + ? { start: [start[0], end[1]], end } + : { start: [end[0], start[1]], end }; + const candidates = fromSpread + ? [alignEndToStart] + : toSpread + ? [alignStartToEnd] + : [alignEndToStart, alignStartToEnd]; + for (const candidate of candidates) { + const points = [candidate.start, candidate.end]; + if (portHasCornerClearance(from, fromSide, candidate.start) + && portHasCornerClearance(to, toSide, candidate.end) + && routeHonorsEndpointSides(points, fromSide, toSide) + && routeClearsEndpointComponents(points, from, to) + && routeClearsComponents(conn, points)) { + return candidate; + } + } + return { start, end }; + } + + // A reciprocal pair is repaired jointly after planning, so crossings that + // involve it are not final yet; keep the established first choice there. + const reciprocal = (conn) => connections.some((other) => other.from === conn.to && other.to === conn.from); + function siblingCrossings(conn, points, resolvedRoutes) { + if (reciprocal(conn)) return 0; + return crossingCount(conn, points, resolvedRoutes.filter((entry) => !reciprocal(entry.conn))); + } + + function routeVia(conn, from, to, start, end, fromSide, toSide, resolvedRoutes = []) { + if (conn.via) return conn.via; + switch (conn.route || 'auto') { + case 'straight': + return []; + case 'orthogonal-h': { + const midX = (start[0] + end[0]) / 2; + return [[midX, start[1]], [midX, end[1]]]; + } + case 'orthogonal-v': { + const midY = (start[1] + end[1]) / 2; + return [[start[0], midY], [end[0], midY]]; + } + case 'auto': + default: { + // Direct line unless the anchors are clearly orthogonal-friendly. + const deltaX = Math.abs(start[0] - end[0]); + const deltaY = Math.abs(start[1] - end[1]); + if (deltaX < 4 || deltaY < 4) { + const direct = [start, end]; + if (routeHonorsEndpointSides(direct, fromSide, toSide) + && routeClearsEndpointComponents(direct, from, to) + && routeClearsComponents(conn, direct) + && routeMeetsCompositionFloors(direct) + && !routeConflictsWithResolved(conn, direct, resolvedRoutes)) return []; + } + + const rhythmBridge = automaticPortRhythmBridge(start, end, fromSide, toSide, { + accept: (points) => ( + routeClearsEndpointComponents(points, from, to) + && routeClearsComponents(conn, points) + && routeMeetsCompositionFloors(points) + && !routeConflictsWithResolved(conn, points, resolvedRoutes) + ), + }); + if (rhythmBridge) return rhythmBridge.slice(1, -1); + + // Automatic port spreading can leave otherwise aligned endpoints only a + // few pixels apart. A midpoint route would split that tiny difference + // into two unreadable endpoint stubs, so take a bounded outside channel + // when both anchors sit on parallel component sides. + const minimumStub = 8; + const fromVerticalSide = start[1] === from.y || start[1] === from.y + from.height; + const toVerticalSide = end[1] === to.y || end[1] === to.y + to.height; + if (fromVerticalSide && toVerticalSide && deltaX < minimumStub * 2) { + const outsideChannels = [ + Math.max(start[0], end[0]) + minimumStub * 2, + Math.min(start[0], end[0]) - minimumStub * 2, + ]; + for (const channelX of outsideChannels) { + const candidate = [[channelX, start[1]], [channelX, end[1]]]; + const points = [start, ...candidate, end]; + if (routeHonorsEndpointSides(points, fromSide, toSide) + && routeClearsComponents(conn, points) + && routeMeetsCompositionFloors(points) + && !routeConflictsWithResolved(conn, points, resolvedRoutes)) return candidate; + } + } + + const fromHorizontalSide = start[0] === from.x || start[0] === from.x + from.width; + const toHorizontalSide = end[0] === to.x || end[0] === to.x + to.width; + if (fromHorizontalSide && toHorizontalSide && deltaY < minimumStub * 2) { + const outsideChannels = [ + Math.max(start[1], end[1]) + minimumStub * 2, + Math.min(start[1], end[1]) - minimumStub * 2, + ]; + for (const channelY of outsideChannels) { + const candidate = [[start[0], channelY], [end[0], channelY]]; + const points = [start, ...candidate, end]; + if (routeHonorsEndpointSides(points, fromSide, toSide) + && routeClearsComponents(conn, points) + && routeMeetsCompositionFloors(points) + && !routeConflictsWithResolved(conn, points, resolvedRoutes)) return candidate; + } + } + + const midX = (start[0] + end[0]) / 2; + const horizontalFirst = [[midX, start[1]], [midX, end[1]]]; + const midY = (start[1] + end[1]) / 2; + const verticalFirst = [[start[0], midY], [end[0], midY]]; + const candidates = [horizontalFirst, verticalFirst]; + const sideSafe = candidates.filter((candidate) => ( + routeHonorsEndpointSides([start, ...candidate, end], fromSide, toSide) + )); + const sideAware = sideAwareBridgeCandidates(start, end, fromSide, toSide); + const nearParallelPorts = ( + ((fromSide === 'top' || fromSide === 'bottom') + && (toSide === 'top' || toSide === 'bottom') + && deltaX < minimumStub * 2) + || ((fromSide === 'left' || fromSide === 'right') + && (toSide === 'left' || toSide === 'right') + && deltaY < minimumStub * 2) + ); + const ordered = [ + ...(nearParallelPorts ? sideAware : sideSafe), + ...(nearParallelPorts ? sideSafe : sideAware), + ]; + for (const candidate of ordered) { + const points = [start, ...candidate, end]; + if (routeClearsEndpointComponents(points, from, to) + && routeClearsComponents(conn, points) + && routeMeetsCompositionFloors(points) + && !routeConflictsWithResolved(conn, points, resolvedRoutes) + && (!distinctAutomaticPorts || (!routeOverlapsResolved(conn, points, resolvedRoutes) + && !siblingCrossings(conn, points, resolvedRoutes)))) return candidate; + } + // Siblings fanning out from one side all want the same midpoint + // channel. Step outward through the corridor to a free parallel + // channel before searching. + if (distinctAutomaticPorts) { + const channels = (from, to, mid) => { + const [low, high] = [Math.min(from, to) + 24, Math.max(from, to) - 24]; + const values = []; + for (let offset = 16; mid - offset >= low || mid + offset <= high; offset += 16) { + values.push(...[mid + offset, mid - offset].filter((value) => value >= low && value <= high)); + } + return values; + }; + for (const candidate of [ + ...channels(start[0], end[0], midX).map((x) => [[x, start[1]], [x, end[1]]]), + ...channels(start[1], end[1], midY).map((y) => [[start[0], y], [end[0], y]]), + ]) { + const points = [start, ...candidate, end]; + if (routeHonorsEndpointSides(points, fromSide, toSide) + && routeClearsEndpointComponents(points, from, to) + && routeClearsComponents(conn, points) + && routeMeetsCompositionFloors(points) + && !routeConflictsWithResolved(conn, points, resolvedRoutes) + && !routeOverlapsResolved(conn, points, resolvedRoutes) + && !siblingCrossings(conn, points, resolvedRoutes)) return candidate; + } + } + + // Two-bend doglegs are deliberately cheap, but a real architecture can + // place adjacent components on both of those corridors. Search a + // bounded obstacle grid before falling back to a route that the Clean + // Flow gate already knows violates the inferred endpoint directions. + // This keeps ordinary multi-bend avoidance renderer-owned instead of + // forcing the author to hand-place via points. + // Alternative endpoint pairs are cheap probes, not another grid-search + // budget. A legal short bridge often beats the first grid detour. + if (!allowGridSearch) return sideSafe[0] || sideAware[0] || horizontalFirst; + if (planningMetrics.gridSearchCount >= planningMetrics.maximumGridSearchCount) { + planningMetrics.gridBudgetExhaustedCount += 1; + planningMetrics.conflictFallbackCount += 1; + return sideSafe[0] || sideAware[0] || horizontalFirst; + } + // First-draft architecture graphs are not always planar at their + // authored node positions. The renderer gives automatic crossings a + // visible halo, so the grid owns opaque-node avoidance and rejects + // ambiguous shared corridors without forcing the model to hand-route + // a sprawling perimeter detour. Cheap candidates above still prefer a + // genuinely crossing-free path whenever one is available. + const avoidedSegments = resolvedRoutes + .filter((entry) => distinctAutomaticPorts && relationshipsShareEndpoint(conn, entry.conn) + && !hasAuthoredRouteGeometry(entry.conn) && !entry.conn.labelAt && !conn.labelAt) + .flatMap((entry) => entry.points.slice(1).map((end, index) => ({ + start: entry.points[index], end, + }))); + const gridMetrics = {}; + planningMetrics.gridSearchCount += 1; + planningMetrics.avoidedSegmentCount += avoidedSegments.length; + const searched = shortestOrthogonalGridRoute({ + start, + end, + points: [start, end], + obstacles: [...components.values(), ...reservedLabelObstacles(2)], + fromSide, + toSide, + clearance: 2, + maximumObstacleCount: 80, + endpointStubPx: 24, + maximumGridNodes: 4096, + avoidedSegments, + allowAvoidedCrossings: true, + minimumAvoidedOverlapPx: 8, + routeSeparationPx: 8, + minimumSegmentPx: interiorSegmentPx, + borderSegments: frameBorders, + bendPenaltyPx: 48, + metrics: gridMetrics, + }); + planningMetrics.gridCandidateNodeCount += gridMetrics.candidateNodeCount || 0; + planningMetrics.gridUsableNodeCount += gridMetrics.usableNodeCount || 0; + planningMetrics.gridEdgeCount += gridMetrics.graphEdgeCount || 0; + planningMetrics.gridVisitedNodeCount += gridMetrics.visitedNodeCount || 0; + const clearsEndpoints = searched + ? routeClearsEndpointComponents(searched.points, from, to) : false; + const clearsComponents = searched + ? routeClearsComponents(conn, searched.points) : false; + const clearsRelationships = searched + ? !routeConflictsWithResolved(conn, searched.points, resolvedRoutes) : false; + const clearsSharedCorridors = searched + ? !routeOverlapsResolved(conn, searched.points, resolvedRoutes) : false; + const meetsFloors = searched ? routeMeetsCompositionFloors(searched.points) : false; + const accepted = Boolean( + searched && clearsEndpoints && clearsComponents && clearsSharedCorridors && meetsFloors, + ); + planningMetrics.gridAttempts.push({ + relationship: conn.id || `${conn.from}->${conn.to}`, + fromSide, + toSide, + inputAvoidedSegmentCount: avoidedSegments.length, + ...gridMetrics, + accepted, + ...(!accepted && searched ? { + rejectedBy: [ + ...(!clearsEndpoints ? ['endpoint-components'] : []), + ...(!clearsComponents ? ['components'] : []), + ...(!clearsSharedCorridors ? ['shared-corridor'] : []), + ...(!meetsFloors ? ['composition-floors'] : []), + ], + candidatePoints: searched.points, + } : {}), + }); + if (accepted) { + planningMetrics.gridRoutedCount += 1; + if (!clearsRelationships) planningMetrics.crossoverRoutedCount += 1; + return searched.points.slice(1, -1); + } + + // Both bounded doglegs are blocked. Keep the best endpoint-safe route + // when one exists so the universal Clean Flow gate reports the actual + // obstacle; otherwise preserve the historical deterministic fallback + // and let the endpoint-direction gate explain the side mismatch. + planningMetrics.conflictFallbackCount += 1; + return sideSafe[0] || sideAware[0] || horizontalFirst; + } + } + } + + // A node's neighbours laid out as one row below (or above) it read as a + // fan-out: reach them all through the same vertical side. Center-based + // inference alone sends the outer ones sideways around their siblings. + const neighbourRects = new Map(); + for (const conn of connections) { + const from = components.get(conn.from); + const to = components.get(conn.to); + if (!from || !to || from === to) continue; + for (const [node, other] of [[from, to], [to, from]]) { + neighbourRects.set(node.id, [...(neighbourRects.get(node.id) || []), other]); + } + } + function rowFanOutSides(from, to) { + if (!preferReadableRoutes) return null; + const verticalSides = (node, other) => { + const below = other.y >= node.y + node.height; + if (!below && other.y + other.height > node.y) return null; + if (['top', 'bottom'].includes(defaultFromSide(node, other))) return null; + const rowSibling = (neighbourRects.get(node.id) || []).some((sibling) => sibling !== other + && Math.abs(sibling.cy - other.cy) < 1 + && defaultFromSide(node, sibling) === (below ? 'bottom' : 'top')); + // Only when a node of that row blocks a sideways route to one of the + // neighbours on this side; otherwise side exits stay clear and keep the + // vertical side free. Decide per side so a row never mixes both styles. + const blocked = (target) => { + const [gapStart, gapEnd] = target.cx < node.cx + ? [target.x + target.width, node.x] : [node.x + node.width, target.x]; + return [...components.values()].some((rect) => rect !== node && rect !== target + && rect.y < target.y + target.height && rect.y + rect.height > target.y + && rect.x < gapEnd && rect.x + rect.width > gapStart); + }; + const sameSide = (neighbourRects.get(node.id) || []).filter((sibling) => Math.abs(sibling.cy - other.cy) < 1 + && defaultFromSide(node, sibling) === defaultFromSide(node, other)); + return rowSibling && sameSide.some(blocked) ? (below ? 'bottom' : 'top') : null; + }; + const opposite = { top: 'bottom', bottom: 'top' }; + const fromSide = verticalSides(from, to); + if (fromSide) return { fromSide, toSide: opposite[fromSide] }; + const toSide = verticalSides(to, from); + return toSide ? { fromSide: opposite[toSide], toSide } : null; + } + + const pathCache = new Map(); + const selectedSides = new Map(); + const stroke = (relation) => relation.width || (relation.variant === 'emphasis' ? 1.8 : 1.5); + const markerSpacing = (left, right) => 3.5 * (stroke(left) + stroke(right)); + const portSpacing = (left, right) => Math.max(14, markerSpacing(left, right) + 3.5); + const automaticPorts = automaticPortSpread(connections, components, { + sideFor: (relation, endpoint) => rowFanOutSides(components.get(relation.from), components.get(relation.to)) + ?.[endpoint === 'source' ? 'fromSide' : 'toSide'], + // Preserve the established initial placement for ordinary markers; only + // widen groups whose arrowheads cannot fit the legacy 14px slots. + ...(distinctAutomaticPorts ? { spacingFor: (left, right) => + markerSpacing(left, right) > 14 ? portSpacing(left, right) : 14 } : {}), + }); + const incidentEndpoints = new Map(); + for (const conn of connections) { + if (!components.has(conn.from) || !components.has(conn.to)) continue; + for (const [field, sideField] of [['from', 'fromSide'], ['to', 'toSide']]) { + const entries = incidentEndpoints.get(conn[field]) || []; + entries.push({ conn, field, sideField }); + incidentEndpoints.set(conn[field], entries); + } + } + function inferredConnectionSides(conn) { + const from = components.get(conn.from); + const to = components.get(conn.to); + const fanOut = rowFanOutSides(from, to); + return { + fromSide: chosenSide(conn.fromSide, fanOut?.fromSide || defaultFromSide(from, to)), + toSide: chosenSide(conn.toSide, fanOut?.toSide || defaultToSide(from, to)), + }; + } + + function connectionSides(conn) { + if (!routesPlanned && !routesPlanning) planRoutes(); + return selectedSides.get(conn) || inferredConnectionSides(conn); + } + + function connectionEndpointSide(conn, endpoint) { + const field = endpoint === 'source' ? 'fromSide' : 'toSide'; + if (conn[field] && conn[field] !== 'auto') return conn[field]; + return connectionSides(conn)[field]; + } + + function hasAuthoredRouteGeometry(conn) { + return Boolean( + conn?.via + || (conn?.route && conn.route !== 'auto') + || conn?.channelX !== undefined + || conn?.channelY !== undefined + ); + } + + function hasAuthoredLabelPlacement(conn) { + return ['labelAt', 'labelDx', 'labelDy', 'labelSegment'] + .some((field) => conn?.[field] !== undefined); + } + + // A route can change sides after the initial port spread. Reserve slots on + // the final side as well: falling back to its midpoint can put an arrow + // between two existing slots, or directly on another incoming arrow. + function automaticEndpoint(conn, endpoint, rect, side, inferredSide) { + const initial = side === inferredSide ? automaticPorts.get(conn)?.[endpoint] : null; + const preferred = initial || anchor(rect, side); + if (hasAuthoredRouteGeometry(conn) || conn.labelAt) return { point: preferred, spread: Boolean(initial) }; + const axis = side === 'left' || side === 'right' ? 1 : 0; + const occupied = []; + for (const { conn: other, field, sideField } of incidentEndpoints.get(rect.id) || []) { + if (other === conn || hasAuthoredRouteGeometry(other) || other.labelAt) continue; + const routed = pathCache.get(other); + const sides = selectedSides.get(other) || inferredConnectionSides(other); + if (sides[sideField] !== side) continue; + const point = routed + ? (field === 'from' ? routed.points[0] : routed.points.at(-1)) + : automaticPorts.get(other)?.[field] || anchor(rect, side); + occupied.push({ value: point[axis], spacing: portSpacing(conn, other) }); + } + if (!occupied.length) return { point: preferred, spread: Boolean(initial) }; + const candidates = [preferred[axis], ...occupied.flatMap(({ value, spacing }) => [value - spacing, value + spacing])] + .sort((a, b) => Math.abs(a - preferred[axis]) - Math.abs(b - preferred[axis]) || a - b); + for (const value of candidates) { + const point = [...preferred]; + point[axis] = value; + if (portHasCornerClearance(rect, side, point) + && occupied.every((other) => Math.abs(value - other.value) >= other.spacing - 0.0001)) { + return { point, spread: true }; + } + } + return { point: preferred, spread: true, crowded: true }; + } + + function connectionGeometry(conn, sides = inferredConnectionSides(conn)) { + const from = components.get(conn.from); + const to = components.get(conn.to); + const inferred = inferredConnectionSides(conn); + const { fromSide, toSide } = sides; + const legacyPorts = fromSide === inferred.fromSide && toSide === inferred.toSide ? automaticPorts.get(conn) : null; + const source = distinctAutomaticPorts + ? automaticEndpoint(conn, 'from', from, fromSide, inferred.fromSide) + : { point: legacyPorts?.from || anchor(from, fromSide), spread: Boolean(legacyPorts?.from) }; + const target = distinctAutomaticPorts + ? automaticEndpoint(conn, 'to', to, toSide, inferred.toSide) + : { point: legacyPorts?.to || anchor(to, toSide), spread: Boolean(legacyPorts?.to) }; + const ports = { from: source.spread, to: target.spread }; + const { start, end } = alignFacingPorts( + conn, + from, + to, + source.point, + target.point, + fromSide, + toSide, + ports, + ); + return { from, to, start, end, fromSide, toSide, crowded: source.crowded || target.crowded }; + } + + function routedForGeometry(conn, resolvedRoutes, geometry) { + const { from, to, start, end, fromSide, toSide } = geometry; + const authoredPoints = [ + start, + ...routeVia(conn, from, to, start, end, fromSide, toSide, resolvedRoutes), + end, + ]; + // Explicit waypoints are author-owned geometry. Keep even a collinear + // waypoint: it can intentionally split a route at a semantic touch point, + // and preserving it is part of the backwards-compatible authoring contract. + // Automatic routes remain normalized so the renderer does not emit noisy + // duplicate turns or zero-length segments. + const points = hasAuthoredRouteGeometry(conn) + ? authoredPoints + : normalizeRoutePoints(authoredPoints); + return { d: roundedPath(points, 8), points }; + } + + function cachePath(conn, routed, sides) { + pathCache.set(conn, routed); + selectedSides.set(conn, { fromSide: sides.fromSide, toSide: sides.toSide }); + return routed; + } + + const SIDE_ORDER = ['right', 'bottom', 'left', 'top']; + function candidateSidePairs(conn) { + const inferred = inferredConnectionSides(conn); + const authoredFrom = conn.fromSide && conn.fromSide !== 'auto' ? conn.fromSide : null; + const authoredTo = conn.toSide && conn.toSide !== 'auto' ? conn.toSide : null; + const fromOptions = authoredFrom + ? [authoredFrom] + : [inferred.fromSide, ...SIDE_ORDER.filter((side) => side !== inferred.fromSide)]; + const toOptions = authoredTo + ? [authoredTo] + : [inferred.toSide, ...SIDE_ORDER.filter((side) => side !== inferred.toSide)]; + return fromOptions.flatMap((fromSide) => toOptions.map((toSide) => ({ + fromSide, + toSide, + deviationCount: Number(fromSide !== inferred.fromSide) + Number(toSide !== inferred.toSide), + }))).sort((left, right) => { + if (left.deviationCount !== right.deviationCount) { + return left.deviationCount - right.deviationCount; + } + const leftGeometry = connectionGeometry(conn, left); + const rightGeometry = connectionGeometry(conn, right); + const leftDistance = Math.abs(leftGeometry.end[0] - leftGeometry.start[0]) + + Math.abs(leftGeometry.end[1] - leftGeometry.start[1]); + const rightDistance = Math.abs(rightGeometry.end[0] - rightGeometry.start[0]) + + Math.abs(rightGeometry.end[1] - rightGeometry.start[1]); + return leftDistance - rightDistance; + }); + } + + function routeIsClear(conn, routed, geometry, resolvedRoutes) { + return !geometry.crowded && routed.points.length >= 2 + && routeHonorsEndpointSides(routed.points, geometry.fromSide, geometry.toSide) + && routeClearsEndpointComponents(routed.points, geometry.from, geometry.to) + && routeClearsComponents(conn, routed.points) + && routeMeetsCompositionFloors(routed.points) + && !routeOverlapsResolved(conn, routed.points, resolvedRoutes); + } + + function crossingCount(conn, points, resolvedRoutes) { + return resolvedRoutes.reduce((total, entry) => total + Number( + !(relationshipsShareEndpoint(conn, entry.conn) + && (!distinctAutomaticPorts || hasAuthoredRouteGeometry(entry.conn) || entry.conn.labelAt || conn.labelAt)) + && points.slice(1).some((end, index) => entry.points.slice(1).some((otherEnd, otherIndex) => + properSegmentIntersection(points[index], end, entry.points[otherIndex], otherEnd))), + ), 0); + } + + function readabilityCost(conn, routed, resolvedRoutes) { + const points = routed.points; + const length = points.slice(1).reduce((total, point, index) => total + + Math.abs(point[0] - points[index][0]) + Math.abs(point[1] - points[index][1]), 0); + // A crossover is a reading cost even with a halo. Keep it finite: avoiding + // one crossing must not justify an arbitrarily long perimeter excursion. + return length + Math.max(0, points.length - 2) * 48 + crossingCount(conn, points, resolvedRoutes) * 160; + } + + function computePath(conn, resolvedRoutes) { + if (hasAuthoredRouteGeometry(conn)) { + const sides = inferredConnectionSides(conn); + return cachePath(conn, routedForGeometry( + conn, + resolvedRoutes, + connectionGeometry(conn, sides), + ), sides); + } + + let firstFallback = null; + const clearRoute = () => { + let firstClear = null; + for (const sides of candidateSidePairs(conn)) { + const geometry = connectionGeometry(conn, sides); + const routed = routedForGeometry(conn, resolvedRoutes, geometry); + if (!firstFallback) firstFallback = { routed, sides }; + if (routeIsClear(conn, routed, geometry, resolvedRoutes)) { + firstClear = { routed, sides }; + break; + } + } + return firstClear; + }; + const routeLength = ({ routed }) => routed.points.slice(1) + .reduce((total, point, index) => total + Math.abs(point[0] - routed.points[index][0]) + Math.abs(point[1] - routed.points[index][1]), 0); + + // Reserved labels are a preference, not an obstacle. A route that has no + // corridor around them, or that would have to detour far around them, + // takes the plain route instead and the label placement pass moves the + // label; only a modest extra length is worth keeping a label in place. + honourReservedLabels = true; + let chosen = clearRoute(); + if (reservedLabels.length) { + honourReservedLabels = false; + if (!chosen) chosen = clearRoute(); + else { + const direct = Math.abs(chosen.routed.points.at(-1)[0] - chosen.routed.points[0][0]) + + Math.abs(chosen.routed.points.at(-1)[1] - chosen.routed.points[0][1]); + if (routeLength(chosen) > direct * 1.25 + 64) { + const plain = clearRoute(); + if (plain && routeLength(plain) * 1.25 + 64 < routeLength(chosen)) chosen = plain; + } + } + honourReservedLabels = true; + } + if (chosen) return cachePath(conn, chosen.routed, chosen.sides); + return cachePath(conn, firstFallback.routed, firstFallback.sides); + } + + let routesPlanned = false; + let routesPlanning = false; + function planRoutes() { + if (routesPlanned || routesPlanning) return; + routesPlanning = true; + const indexed = connections + .map((conn, index) => ({ conn, index })) + .filter(({ conn }) => components.has(conn.from) && components.has(conn.to)); + const explicit = indexed.filter(({ conn }) => hasAuthoredRouteGeometry(conn)); + const automatic = indexed.filter(({ conn }) => !hasAuthoredRouteGeometry(conn)) + .sort((left, right) => { + const leftFrom = components.get(left.conn.from); + const leftTo = components.get(left.conn.to); + const rightFrom = components.get(right.conn.from); + const rightTo = components.get(right.conn.to); + const leftDistance = Math.abs(leftTo.cx - leftFrom.cx) + Math.abs(leftTo.cy - leftFrom.cy); + const rightDistance = Math.abs(rightTo.cx - rightFrom.cx) + Math.abs(rightTo.cy - rightFrom.cy); + return leftDistance - rightDistance || left.index - right.index; + }); + const resolvedRoutes = []; + reservedLabels = []; + for (const { conn } of [...explicit, ...automatic]) { + const routed = computePath(conn, resolvedRoutes); + resolvedRoutes.push({ conn, points: routed.points }); + const rect = labelRectFor?.(conn, routed.points, { + routes: resolvedRoutes.map((entry) => entry.points), + labels: reservedLabels.map((entry) => entry.rect), + }); + if (rect) reservedLabels.push({ conn, rect }); + } + // Improve against the complete set of routes, not only earlier edges. + // A greedy side change during the initial pass can steal a later edge's + // corridor. This bounded sweep only changes one route at a time, keeping + // every other route and label as an obstacle/reference. + if (preferReadableRoutes) { + const scenePoints = resolvedRoutes.flatMap((entry) => entry.points); + for (const rect of [...components.values(), ...frames, ...reservedLabels.map((entry) => entry.rect)]) { + scenePoints.push([rect.x, rect.y], [rect.x + rect.width, rect.y + rect.height]); + } + const bounds = { + left: Math.min(...scenePoints.map(([x]) => x)), right: Math.max(...scenePoints.map(([x]) => x)), + top: Math.min(...scenePoints.map(([, y]) => y)), bottom: Math.max(...scenePoints.map(([, y]) => y)), + }; + const withinScene = ([x, y]) => x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom; + const jointlyImproved = new Set(); + function improveReciprocalPairs() { + // A one-route sweep cannot repair reciprocal facing edges whose initial + // spread groups differ. Try both direct lanes together after final side + // geometry is known, leaving every other route and authored port in place. + const paired = new Set(); + for (const first of resolvedRoutes) { + const conn = first.conn; + if (paired.has(conn) || hasAuthoredRouteGeometry(conn) || hasAuthoredLabelPlacement(conn) + || (conn.fromSide && conn.fromSide !== 'auto') + || (conn.toSide && conn.toSide !== 'auto')) continue; + const second = resolvedRoutes.find((entry) => entry !== first + && entry.conn.from === conn.to && entry.conn.to === conn.from + && !hasAuthoredRouteGeometry(entry.conn) && !hasAuthoredLabelPlacement(entry.conn) + && (!entry.conn.fromSide || entry.conn.fromSide === 'auto') + && (!entry.conn.toSide || entry.conn.toSide === 'auto')); + if (!second) continue; + paired.add(conn); + paired.add(second.conn); + const firstSides = inferredConnectionSides(conn); + const secondSides = inferredConnectionSides(second.conn); + const horizontal = firstSides.fromSide === 'right' && firstSides.toSide === 'left' + && secondSides.fromSide === 'left' && secondSides.toSide === 'right'; + const horizontalReverse = firstSides.fromSide === 'left' && firstSides.toSide === 'right' + && secondSides.fromSide === 'right' && secondSides.toSide === 'left'; + const vertical = firstSides.fromSide === 'bottom' && firstSides.toSide === 'top' + && secondSides.fromSide === 'top' && secondSides.toSide === 'bottom'; + const verticalReverse = firstSides.fromSide === 'top' && firstSides.toSide === 'bottom' + && secondSides.fromSide === 'bottom' && secondSides.toSide === 'top'; + if (!horizontal && !horizontalReverse && !vertical && !verticalReverse) continue; + if (first.points.length <= 2 && second.points.length <= 2) continue; + const axis = horizontal || horizontalReverse ? 1 : 0; + const others = resolvedRoutes.filter((entry) => entry !== first && entry !== second); + const firstGeometry = connectionGeometry(conn, firstSides); + const secondGeometry = connectionGeometry(second.conn, secondSides); + if (firstGeometry.crowded || secondGeometry.crowded) continue; + const previousLabels = reservedLabels; + let improved = false; + reservedLabels = reservedLabels.filter((entry) => entry.conn !== conn && entry.conn !== second.conn); + try { + const lanes = (geometry) => [...new Set([geometry.start[axis], geometry.end[axis]])]; + const direct = (geometry, lane) => { + const start = [...geometry.start]; + const end = [...geometry.end]; + start[axis] = lane; + end[axis] = lane; + return { points: [start, end], d: roundedPath([start, end], 8) }; + }; + const endpointSlotsClear = (candidateRoutes) => { + const entries = [...others, ...candidateRoutes]; + for (const candidate of candidateRoutes) { + const sides = candidate.conn === conn ? firstSides : secondSides; + for (const [componentId, side, point] of [ + [candidate.conn.from, sides.fromSide, candidate.points[0]], + [candidate.conn.to, sides.toSide, candidate.points.at(-1)], + ]) { + const coordinate = side === 'left' || side === 'right' ? 1 : 0; + for (const other of entries) { + if (other.conn === candidate.conn) continue; + const otherSides = other.conn === conn ? firstSides + : other.conn === second.conn ? secondSides : selectedSides.get(other.conn); + for (const [otherId, otherSide, otherPoint] of [ + [other.conn.from, otherSides.fromSide, other.points[0]], + [other.conn.to, otherSides.toSide, other.points.at(-1)], + ]) { + if (componentId === otherId && side === otherSide + && Math.abs(point[coordinate] - otherPoint[coordinate]) + < portSpacing(candidate.conn, other.conn) - 0.0001) return false; + } + } + } + } + return true; + }; + const labelClears = (rect, owner, entries, labels) => { + if (!rect) return !owner.label; + if (!withinScene([rect.x, rect.y]) + || !withinScene([rect.x + rect.width, rect.y + rect.height]) + || labels.some((other) => rectsOverlap(rect, other.rect, 2))) return false; + return entries.every((entry) => entry.conn === owner || entry.points.slice(1).every((end, index) => + !segmentIntersectsRect({ start: entry.points[index], end }, rect, LABEL_CLEARANCE))); + }; + let best = null; + let bestCost = readabilityCost(conn, first, [...others, second]) + + readabilityCost(second.conn, second, [...others, first]); + for (const firstLane of lanes(firstGeometry)) { + for (const secondLane of lanes(secondGeometry)) { + planningMetrics.reciprocalCandidateCount += 1; + const firstRoute = { conn, ...direct(firstGeometry, firstLane) }; + const secondRoute = { conn: second.conn, ...direct(secondGeometry, secondLane) }; + const candidates = [firstRoute, secondRoute]; + if (!candidates.every((entry) => entry.points.every(withinScene))) continue; + if (!endpointSlotsClear(candidates)) continue; + if (!candidates.every((entry, index) => { + const geometry = entry.conn === conn ? firstGeometry : secondGeometry; + if (!portHasCornerClearance(geometry.from, geometry.fromSide, entry.points[0]) + || !portHasCornerClearance(geometry.to, geometry.toSide, entry.points.at(-1))) return false; + return routeIsClear(entry.conn, entry, geometry, [...others, candidates[1 - index]]); + })) continue; + const firstRect = labelRectFor?.(conn, firstRoute.points, { + routes: [...others.map((entry) => entry.points), ...candidates.map((entry) => entry.points)], + labels: reservedLabels.map((entry) => entry.rect), + }); + if (!labelClears(firstRect, conn, [...others, secondRoute], reservedLabels)) continue; + const secondRect = labelRectFor?.(second.conn, secondRoute.points, { + routes: [...others.map((entry) => entry.points), ...candidates.map((entry) => entry.points)], + labels: [...reservedLabels.map((entry) => entry.rect), ...(firstRect ? [firstRect] : [])], + }); + if (!labelClears(secondRect, second.conn, [...others, firstRoute], [ + ...reservedLabels, ...(firstRect ? [{ conn, rect: firstRect }] : []), + ])) continue; + const cost = readabilityCost(conn, firstRoute, [...others, secondRoute]) + + readabilityCost(second.conn, secondRoute, [...others, firstRoute]); + if (cost < bestCost) { + best = { firstRoute, secondRoute, firstRect, secondRect }; + bestCost = cost; + } + } + } + if (!best) continue; + cachePath(conn, best.firstRoute, firstSides); + cachePath(second.conn, best.secondRoute, secondSides); + first.points = best.firstRoute.points; + second.points = best.secondRoute.points; + planningMetrics.reciprocalImprovedCount += 1; + jointlyImproved.add(conn); + jointlyImproved.add(second.conn); + improved = true; + reservedLabels = [ + ...reservedLabels, + ...(best.firstRect ? [{ conn, rect: best.firstRect }] : []), + ...(best.secondRect ? [{ conn: second.conn, rect: best.secondRect }] : []), + ]; + } finally { + if (!improved) reservedLabels = previousLabels; + } + } + } + allowGridSearch = false; + try { + improveReciprocalPairs(); + for (const entry of resolvedRoutes) { + const { conn } = entry; + if (jointlyImproved.has(conn) || hasAuthoredRouteGeometry(conn) || conn.labelAt) continue; + const others = resolvedRoutes.filter((other) => other !== entry); + // Preserve uncomplicated routes and their established inferred sides. + // Spend the comparison budget where the complete scene has a reading + // cost: a crossover or more than two bends. + if (entry.points.length <= 4 && crossingCount(conn, entry.points, others) === 0) continue; + let best = null; + let bestCost = readabilityCost(conn, { points: entry.points }, others); + for (const sides of candidateSidePairs(conn)) { + const geometry = connectionGeometry(conn, sides); + const routed = routedForGeometry(conn, others, geometry); + planningMetrics.readabilityCandidateCount += 1; + if (!routed.points.every(withinScene) || !routeIsClear(conn, routed, geometry, others)) continue; + const cost = readabilityCost(conn, routed, others); + if (cost < bestCost) { + const rect = labelRectFor?.(conn, routed.points, { + routes: [...others.map((other) => other.points), routed.points], + labels: reservedLabels.filter((label) => label.conn !== conn).map((label) => label.rect), + }); + // A shorter route must not expand the canvas and shrink every + // label at the default viewport. Include its reserved label too. + if (rect && (!withinScene([rect.x, rect.y]) || !withinScene([rect.x + rect.width, rect.y + rect.height]))) continue; + best = { routed, sides, rect }; + bestCost = cost; + } + } + if (!best) continue; + cachePath(conn, best.routed, best.sides); + entry.points = best.routed.points; + planningMetrics.readabilityImprovedCount += 1; + reservedLabels = reservedLabels.filter((label) => label.conn !== conn); + if (best.rect) reservedLabels.push({ conn, rect: best.rect }); + } + } finally { + allowGridSearch = true; + } + } + planningMetrics.routeCount = resolvedRoutes.length; + planningMetrics.explicitRouteCount = explicit.length; + planningMetrics.automaticRouteCount = automatic.length; + routesPlanning = false; + routesPlanned = true; + } + + function pathFor(conn) { + planRoutes(); + if (pathCache.has(conn)) return pathCache.get(conn); + return computePath(conn, []); + } + + function routingMetrics() { + planRoutes(); + return { ...planningMetrics }; + } + + return { pathFor, connectionSides, connectionEndpointSide, routingMetrics }; +} diff --git a/vendor/archify/renderers/dataflow/README.md b/vendor/archify/renderers/dataflow/README.md new file mode 100644 index 0000000..108a4b1 --- /dev/null +++ b/vendor/archify/renderers/dataflow/README.md @@ -0,0 +1,105 @@ +# Data Flow Renderer + +Render `diagram_type: "dataflow"` JSON files into the standard Archify HTML +template. + +```bash +node archify/renderers/dataflow/render-dataflow.mjs input.dataflow.json output.html +``` + +The renderer validates input against `archify/schemas/dataflow.schema.json` +with the bundled standalone validator. No dependency installation is required. + +If `output.html` is omitted, the renderer uses the required `meta.output` value +from the JSON file. + +## Input + +Data-flow JSON files must set: + +```json +{ + "schema_version": 1, + "diagram_type": "dataflow", + "meta": { + "title": "Product Analytics Data Flow", + "output": "product-analytics-dataflow.html", + "viewBox": [940, 720] + }, + "stages": [], + "nodes": [], + "flows": [], + "cards": [] +} +``` + +A complete worked example lives at +`archify/examples/product-analytics.dataflow.json`. + +The schema lives at: + +```text +archify/schemas/dataflow.schema.json +``` + +## Legend + +The default visual legend derives kinds from `flows[].variant` (omitting +`variant` means `default`) and adds `database` only when a database node exists. +Supported `meta.legend.entries` keys, in stable order, are `emphasis`, +`security`, `dashed`, `database`, and `default`. Flow variants remain +visual-only because Archify has no compiled edge-kind facts in this slice. A +present `database` entry is different: it comes from exact +`nodes[].type: "database"` facts, so it publishes the normal Semantic Legend +count, accessible name, and keyboard interaction. Forcing `database` visible +without a database node keeps it visual-only. + +## Layout budget + +| Constant | Value | +|----------|-------| +| viewBox | default `[940, 720]`; schema minimum `[360, 360]` | +| Stages (2–5) | centers at x = 100 + stage×215; stage band 168 wide, header at y 46 | +| Row tops (`row` 0–4) | y = 128, 242, 356, 470, 584 (plus `yOffset`) | +| Default node | 112×58 | +| Node area | x within `[24, width − 24]`; y within `[104, height − 74]` | +| Node spacing | ≥10px between any two nodes (checked across stages and rows) | +| Flow length | ≥34px between endpoints | +| Legend row | y = height − 36 | + +Route presets for flows: `straight`, `vertical-channel`, `bottom-channel`, +`top-channel`, explicit `via` points, or the default `auto` (midpoint elbow). + +## Design Rules + +- Use stages for data lifecycle boundaries: source, ingest, process, store, + consume. +- Place nodes by stage index and row index; do not hand-place raw SVG for the + common case. +- Use flow labels to name the data asset, not the transport primitive: + `clickstream`, `identity map`, `normalized facts`, `feature vectors`. +- Use `classification` for short sensitivity or governance context: + `PII touch`, `non-PII`, `approved only`, `batch`, `read-only`. +- Use `security` for PII, policy, consent, access-control, or restricted joins. +- Use `emphasis` for the primary data path and `dashed` for async or batch + derivations. +- Keep labels short enough to fit in narrow previews. + +Schema violations exit non-zero with path-prefixed messages annotated with the +element's id or label. The renderer additionally fails when it can detect +layout problems, including missing stages, duplicate node IDs, nodes outside +the readable diagram area, node overlap, labels colliding with nodes or other +labels, labels wider than their node, unknown flow endpoints, missing flow +labels, unreadably short flows, flows crossing unrelated nodes (2px Clean Flow +clearance), or stages that exceed the viewBox. Stage frames remain intentional +pass-through containers. Text width +is estimated CJK-aware: fullwidth glyphs count as two units. + +Set `meta.quality_profile` to `showcase` for polished delivery. Unrelated proper +X crossings then fail with `composition/proper-crossing`; default `standard` +keeps them as artifact-receipt warnings. Collinear stage corridors are outside +the proper-X rule, but a separate gate warns in `standard` and fails in +`showcase` when unrelated flows overlap for at least 8px. Shared semantic +endpoints, point touches, and shorter overlaps remain valid. Showcase also +rejects any route segment below 8px and any interior turn segment below 16px; +ordinary 8–15px endpoint stubs remain valid. diff --git a/vendor/archify/renderers/dataflow/render-dataflow.mjs b/vendor/archify/renderers/dataflow/render-dataflow.mjs new file mode 100644 index 0000000..8907988 --- /dev/null +++ b/vendor/archify/renderers/dataflow/render-dataflow.mjs @@ -0,0 +1,544 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs'; +import { animateAttr, focusEdgeAttrs, focusNodeAttrs, focusNodeTitle, loadDiagramWithBrandMarks, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs'; +import { throwDiagnosticProblems } from '../shared/diagnostics.mjs'; +import { resolveLegend, renderLegend as renderResolvedLegend } from '../shared/legend.mjs'; +import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth, nodeLabelLayout } from '../shared/text-fit.mjs'; +import { brandLabelFitWidth, brandMarkFor, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs'; +import { translateMessage as i18nText } from '../shared/i18n.mjs'; +import { + asArray, + isFinitePoint, + rectsOverlap, + cleanEndpointSideProblems, + cleanFlowProblems, + cleanCrossingProblems, + cleanAmbiguousCorridorProblems, + cleanBorderRunProblems, + cleanRouteRhythmProblems, + cleanLabelRouteClearanceProblems, + cleanLabelCanvasContainmentProblems, + suggestLabelObstacleFix, + suggestLabelPairFix, + anchor, + automaticPortSpread, + legacyDefaultFromSide as defaultFromSide, + legacyDefaultToSide as defaultToSide, + chosenSide, + polylinePath, + routePointsValue, + authoredStraightRouteAttrs, + labelPoint, + componentFill, + componentText, + arrowClassMap, + edgeLabelAccent +} from '../shared/geometry.mjs'; + +const nodeTextFit = { + sublabelPreferred: 7, + sublabelMinimum: 6, + tagPreferred: 7, + tagMinimum: 6, +}; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const { diagram: dataflow, template, outPath, sourceEvidence } = await loadDiagramWithBrandMarks({ + rendererDir: __dirname, + diagramType: 'dataflow', + defaultExample: 'product-analytics.dataflow.json' +}); + +const viewBox = dataflow.meta?.viewBox || [940, 720]; +const layout = { + stageY: 46, + stageH: 36, + stageBottomPad: 74, + leftX: 100, + colGap: 215, + stageW: 168, + nodeW: 112, + nodeH: 58, + rowYs: [128, 242, 356, 470, 584], + labelH: 16 +}; + +function flowLabelSize(flow) { + const longestLine = Math.max(textUnits(flow.label), textUnits(flow.classification || '')); + return { + width: Math.round(Math.max(34, longestLine * 4.9 + 12) * 10) / 10, + height: flow.classification ? 27 : layout.labelH, + }; +} + +function stageX(index) { + return layout.leftX + index * layout.colGap; +} + +function stageFrame(stage, index) { + return { + id: index, + label: stage.label, + kind: 'stage', + x: stageX(index) - layout.stageW / 2, + y: layout.stageY, + width: layout.stageW, + height: viewBox[1] - layout.stageY - layout.stageBottomPad, + radius: 10, + }; +} + +const compositionFrames = asArray(dataflow.stages).map(stageFrame); + +function measureNode(node) { + const width = node.width || layout.nodeW; + const height = node.height || layout.nodeH; + const cx = stageX(node.stage); + const y = layout.rowYs[node.row] + (node.yOffset || 0); + return { + ...node, + width, + height, + cx, + cy: y + height / 2, + x: cx - width / 2, + y + }; +} + +const nodes = new Map(asArray(dataflow.nodes).map((node) => [node.id, measureNode(node)])); +const nodeSteps = new Map(); +for (const [index, flow] of asArray(dataflow.flows).entries()) { + if (!nodeSteps.has(flow.from)) nodeSteps.set(flow.from, index); + if (!nodeSteps.has(flow.to)) nodeSteps.set(flow.to, index + 1); +} +for (const [index, node] of asArray(dataflow.nodes).entries()) { + if (!nodeSteps.has(node.id)) nodeSteps.set(node.id, index); +} + +function validateDataflow() { + const problems = []; + if (nodes.size !== asArray(dataflow.nodes).length) problems.push('Node ids must be unique.'); + + const stageCount = asArray(dataflow.stages).length; + for (const node of nodes.values()) { + if (typeof node.stage !== 'number' || node.stage < 0 || node.stage >= stageCount) { + problems.push(`Node "${node.id}" uses invalid stage ${node.stage} — valid stages are 0..${stageCount - 1}.`); + } + if (typeof node.row !== 'number' || node.row < 0 || node.row >= layout.rowYs.length) { + problems.push(`Node "${node.id}" uses invalid row ${node.row} — valid rows are 0..${layout.rowYs.length - 1}.`); + } + if (!isFinitePoint(node.x, node.y, node.cx, node.cy)) { + problems.push(`Node "${node.id}" produced non-finite coordinates — check stage, row, width, height, and yOffset are numbers.`); + continue; + } + if (node.x < 24 || node.x + node.width > viewBox[0] - 24) { + problems.push(`Node "${node.id}" exceeds the horizontal bounds of the viewBox — reduce node.width or increase meta.viewBox[0].`); + } + if (node.y < layout.stageY + layout.stageH + 22 || node.y + node.height > viewBox[1] - layout.stageBottomPad) { + problems.push(`Node "${node.id}" exceeds the readable diagram area — keep y between ${layout.stageY + layout.stageH + 22} and ${viewBox[1] - layout.stageBottomPad} (adjust row/yOffset or increase meta.viewBox[1]).`); + } + const estLabelW = textUnits(node.label) * 6.2; + if (estLabelW > node.width + 6) { + problems.push(`Label "${node.label}" (~${Math.round(estLabelW)}px) is wider than node "${node.id}" (${node.width}px) — shorten the label or increase node.width.`); + } + const brandRailProblem = brandTopRailProblem(node, node.width, 8); + if (brandRailProblem) problems.push(brandRailProblem); + // sublabel and tag render as single unwrapped elements; shrink-to-fit + // handles the ordinary case, this rejects what it cannot rescue. + const availableTextW = availableNodeTextWidth(node.width); + for (const [field, value, minimum] of [ + ['Sublabel', node.sublabel, nodeTextFit.sublabelMinimum], + ['Tag', node.tag, nodeTextFit.tagMinimum], + ]) { + if (!value) continue; + const minimumW = minimumNodeTextWidth(value, minimum); + if (minimumW > availableTextW) { + problems.push(`${field} "${value}" needs ~${Math.ceil(minimumW)}px at the ${minimum}px legible minimum, but node "${node.id}" provides ${availableTextW}px — shorten the ${field.toLowerCase()} or increase node.width.`); + } + } + } + + const nodeList = asArray(dataflow.nodes); + for (let i = 0; i < nodeList.length; i += 1) { + for (let j = i + 1; j < nodeList.length; j += 1) { + const a = nodes.get(nodeList[i].id); + const b = nodes.get(nodeList[j].id); + if (rectsOverlap(a, b, 10)) { + problems.push(`Nodes "${a.id}" and "${b.id}" are less than 10px apart — move one to another stage/row or adjust yOffset.`); + } + } + } + + for (const flow of asArray(dataflow.flows)) { + if (!nodes.has(flow.from)) problems.push(`Flow "${flow.label || flow.from}" references unknown source "${flow.from}".`); + if (!nodes.has(flow.to)) problems.push(`Flow "${flow.label || flow.to}" references unknown target "${flow.to}".`); + if (!flow.label) problems.push(`Flow "${flow.from}" -> "${flow.to}" must include a short data label.`); + if (nodes.has(flow.from) && nodes.has(flow.to)) { + const routed = pathFor(flow); + const [start, end] = [routed.points[0], routed.points[routed.points.length - 1]]; + const distance = Math.hypot(end[0] - start[0], end[1] - start[1]); + if (distance < 34) problems.push(`Flow "${flow.label}" is too short (${Math.round(distance)}px; minimum 34px) — route it through a channel or spread its nodes.`); + if (Array.isArray(flow.via)) { + for (let segmentIndex = 0; segmentIndex < routed.points.length - 1; segmentIndex += 1) { + const segmentStart = routed.points[segmentIndex]; + const segmentEnd = routed.points[segmentIndex + 1]; + const isDiagonal = Math.abs(segmentStart[0] - segmentEnd[0]) > 0.01 + && Math.abs(segmentStart[1] - segmentEnd[1]) > 0.01; + if (!isDiagonal) continue; + const viaIndex = Math.min(segmentIndex, flow.via.length - 1); + problems.push(`Flow "${flow.label}" has a diagonal segment from (${segmentStart.join(', ')}) to (${segmentEnd.join(', ')}) — align via[${viaIndex}] with its adjacent point by sharing the same x or y coordinate.`); + } + } + } + } + + problems.push(...cleanEndpointSideProblems({ + relations: dataflow.flows, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + fromSideFor: (flow) => flowSides(flow).fromSide, + toSideFor: (flow) => flowSides(flow).toSide, + routeHint: 'keep automatic routing, or choose fromSide/toSide and via points whose first and final segments cross node borders perpendicularly', + })); + problems.push(...cleanFlowProblems({ + relations: dataflow.flows, + obstacles: nodes.values(), + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + obstacleKind: 'node', + routeHint: 'adjust fromSide/toSide, set route/via or channelX/channelY, or move the node to another stage/row' + })); + problems.push(...cleanCrossingProblems({ + relations: dataflow.flows, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + profile: dataflow.meta?.quality_profile, + routeHint: 'adjust route/via or channelX/channelY so the flows use separate stage corridors' + })); + problems.push(...cleanAmbiguousCorridorProblems({ + relations: dataflow.flows, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + profile: dataflow.meta?.quality_profile, + routeHint: 'adjust route/via or channelX/channelY so unrelated flows do not visually merge' + })); + problems.push(...cleanBorderRunProblems({ + relations: dataflow.flows, + endpointIds: new Set(nodes.keys()), + frames: compositionFrames, + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + profile: dataflow.meta?.quality_profile, + routeHint: 'adjust route/via or channelX/channelY so the flow crosses the stage perpendicularly instead of following its border' + })); + problems.push(...cleanRouteRhythmProblems({ + relations: dataflow.flows, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + profile: dataflow.meta?.quality_profile, + routeHint: 'adjust route/via or channelX/channelY so each turn uses a clear inter-stage corridor' + })); + + const labelRects = []; + for (const [flowIndex, flow] of asArray(dataflow.flows).entries()) { + if (!flow.label || !nodes.has(flow.from) || !nodes.has(flow.to)) continue; + const [lx, ly] = labelPoint(flow, pathFor(flow).points); + const { width, height } = flowLabelSize(flow); + labelRects.push({ relation: flow, relationIndex: flowIndex, label: flow.label, x: lx - width / 2, y: ly - 11, width, height, lx, ly }); + } + for (const rect of labelRects) { + for (const node of nodes.values()) { + if (rectsOverlap(rect, node, -2)) { + problems.push(`Label "${rect.label}" overlaps node "${node.id}" — adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, node, 'node', viewBox, nodes.values())}`); + } + } + } + for (let i = 0; i < labelRects.length; i += 1) { + for (let j = i + 1; j < labelRects.length; j += 1) { + if (rectsOverlap(labelRects[i], labelRects[j], -2)) { + problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — adjust labelDx/labelDy.\n${suggestLabelPairFix(labelRects[i], labelRects[j])}`); + } + } + } + problems.push(...cleanLabelRouteClearanceProblems({ + relations: dataflow.flows, + labels: labelRects, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'dataflow', + relationCollection: 'flows', + profile: dataflow.meta?.quality_profile, + routeHint: 'adjust labelAt, labelDx, labelDy, or labelSegment; otherwise adjust the other flow route/via/channelX/channelY' + })); + problems.push(...cleanLabelCanvasContainmentProblems({ + labels: labelRects, + viewBox, + diagramType: 'dataflow', + relationCollection: 'flows', + profile: dataflow.meta?.quality_profile, + })); + + const lastStageX = stageX(asArray(dataflow.stages).length - 1); + if (lastStageX + layout.stageW / 2 > viewBox[0] - 24) { + problems.push(`Stages exceed viewBox width — set meta.viewBox[0] to at least ${Math.ceil(lastStageX + layout.stageW / 2 + 24)}.`); + } + + if (problems.length) { + throwDiagnosticProblems('Data-flow layout validation failed', problems, { + subject: { diagramType: 'dataflow' }, + }); + } +} + +function routeVia(flow, from, to, start, end) { + if (flow.via) return flow.via; + switch (flow.route || 'auto') { + case 'straight': + return []; + case 'vertical-channel': { + const x = flow.channelX ?? start[0] + (end[0] > start[0] ? 44 : -44); + return [[x, start[1]], [x, end[1]]]; + } + case 'bottom-channel': { + const y = flow.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 26; + return [[start[0], y], [end[0], y]]; + } + case 'top-channel': { + const y = flow.channelY ?? Math.min(from.y, to.y) - 24; + return [[start[0], y], [end[0], y]]; + } + case 'auto': + default: { + if (Math.abs(start[1] - end[1]) < 4) return []; + const midX = start[0] + (end[0] - start[0]) / 2; + return [[midX, start[1]], [midX, end[1]]]; + } + } +} + +const pathCache = new Map(); + +function flowSides(flow) { + const from = nodes.get(flow.from); + const to = nodes.get(flow.to); + return { + fromSide: chosenSide(flow.fromSide, defaultFromSide(from, to)), + toSide: chosenSide(flow.toSide, defaultToSide(from, to)), + }; +} + +const automaticPorts = automaticPortSpread(dataflow.flows, nodes, { + sideFor: (flow, endpoint) => flowSides(flow)[endpoint === 'source' ? 'fromSide' : 'toSide'], +}); + +function pathFor(flow) { + if (pathCache.has(flow)) return pathCache.get(flow); + const from = nodes.get(flow.from); + const to = nodes.get(flow.to); + const ports = automaticPorts.get(flow); + const { fromSide, toSide } = flowSides(flow); + const start = ports?.from || anchor(from, fromSide); + const end = ports?.to || anchor(to, toSide); + // Drop consecutive duplicate points so a purely vertical (or horizontal) + // auto-route never emits a zero-length final segment — SVG derives + // marker-end orientation from the last segment, and a degenerate segment + // leaves the arrowhead angle undefined (see #169). + const rawPoints = [start, ...routeVia(flow, from, to, start, end), end]; + const points = []; + for (const p of rawPoints) { + const prev = points.at(-1); + if (!prev || Math.abs(p[0] - prev[0]) > 0.0001 || Math.abs(p[1] - prev[1]) > 0.0001) { + points.push(p); + } + } + // Guard against an all-degenerate route (e.g. start === end): keep both + // endpoints so the path is still well-formed even if the marker is hidden. + if (points.length < 2) points.push(end); + const routed = { d: polylinePath(points), points }; + pathCache.set(flow, routed); + return routed; +} + +// Header measurement follows 276970789's #257, including the ordinal. +// Long titles wrap at the same legible floor instead of becoming invalid input. +function stageHeaderText(stage, index) { + return `${String(index + 1).padStart(2, '0')} / ${stage.label}`; +} + +function renderStageHeader(stage, index, cx) { + const text = stageHeaderText(stage, index); + const font = fittedNodeFontSize(text, layout.stageW, 9, 7); + const available = availableNodeTextWidth(layout.stageW); + const open = ``; + if (minimumNodeTextWidth(text, font) <= available) return `${open}${esc(text)}`; + const lines = []; + let line = ''; + // Prefer word boundaries; split oversized words and CJK by grapheme, without + // dropping whitespace or splitting a combining character/emoji sequence. + const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + for (const word of text.match(/\s+|\S+/gu) || []) { + if (line && minimumNodeTextWidth(line + word, font) > available) { + lines.push(line); line = ''; + } + for (const { segment } of segmenter.segment(word)) { + if (line && minimumNodeTextWidth(line + segment, font) > available) { + lines.push(line); line = ''; + } + line += segment; + } + } + if (line) lines.push(line); + // Explicit node geometry is authoritative. Do not turn an old horizontal + // overflow into a new collision with nodes when the header area is packed. + const firstNodeY = Math.min(...[...nodes.values()].filter(node => node.stage === index).map(node => node.y), + viewBox[1] - layout.stageBottomPad); + const lastLineBottom = layout.stageY + 22 + (lines.length - 1) * (font + 4) + font * 0.3; + if (lastLineBottom > firstNodeY - 4) return `${open}${esc(text)}`; + const content = lines.length === 1 ? esc(text) : lines.map((line, i) => + `${esc(line)}`).join(''); + return `${open}${content}`; +} + +function renderStage(stage, index) { + const frame = compositionFrames[index]; + const cx = stageX(index); + return ` + ${renderStageHeader(stage, index, cx)}`; +} + +function renderNode(node) { + const fill = componentFill[node.type] || 'c-external'; + const accent = componentText[node.type] || 't-muted'; + const hasSub = node.sublabel != null && node.sublabel !== ''; + const labelFontSize = fittedNodeFontSize(node.label, brandLabelFitWidth(node, node.width), 10, 8); + const sublabelFontSize = fittedNodeFontSize(node.sublabel, node.width, nodeTextFit.sublabelPreferred, nodeTextFit.sublabelMinimum); + const tagFontSize = fittedNodeFontSize(node.tag, node.width, nodeTextFit.tagPreferred, nodeTextFit.tagMinimum); + const textRows = [{ text: node.label, font: labelFontSize, y: 21 }]; + if (hasSub) textRows.push({ text: node.sublabel, font: sublabelFontSize, y: 37 }); + if (node.tag) textRows.push({ text: node.tag, font: tagFontSize, y: node.height - 11 }); + const labelLayout = nodeLabelLayout({ width: node.width, height: node.height, rows: textRows, + brand: Boolean(brandMarkFor(node)), source: Boolean(sourceEvidence?.nodes?.[node.id]?.length) }); + const sub = hasSub + ? `\n ${esc(node.sublabel)}` + : ''; + const tag = node.tag + ? `\n ${esc(node.tag)}` + : ''; + const stage = asArray(dataflow.stages)[node.stage]; + const context = stage + ? `${String(node.stage + 1).padStart(2, '0')} / ${stage.label}` + : i18nText(dataflow.meta.locale, 'node.context.dataflow'); + const brand = renderBrandMark(node, { x: node.x + node.width - 22, y: node.y + 6 }); + const passport = { kind: node.type, sublabel: node.sublabel, tag: node.tag, context, ...brandMetadataFor(node) }; + return ` + ${focusNodeTitle(node.label, passport)} + + + ${renderSemanticSigil(node.type, { icon: node.icon, x: node.x + 6, y: node.y + labelLayout.sigilY, size: labelLayout.sigilSize })}${brand ? `\n ${brand}` : ''} + ${esc(node.label)}${sub}${tag} + `; +} + +function renderFlowPath(flow, index) { + const [cls, marker] = arrowClassMap[flow.variant || 'default'] || arrowClassMap.default; + const routed = pathFor(flow); + const strokeWidth = flow.width || (flow.variant === 'emphasis' ? 1.8 : 1.4); + return ` `; +} + +function renderFlowLabel(flow, index) { + const routed = pathFor(flow); + const [lx, ly] = labelPoint(flow, routed.points); + const { width: labelW, height: labelH } = flowLabelSize(flow); + const classification = flow.classification + ? `\n ${esc(flow.classification)}` + : ''; + return ` + + ${esc(flow.label)}${classification} + `; +} + +const LEGEND_CATALOG = [ + { kind: 'emphasis', className: 'a-emphasis', marker: 'arrowhead-emphasis', strokeWidth: 1.8, swatchWidth: 34, swatchGap: 9, interactive: false }, + { kind: 'security', className: 'a-security', marker: 'arrowhead-security', swatchWidth: 34, swatchGap: 9, interactive: false }, + { kind: 'dashed', className: 'a-dashed', marker: 'arrowhead-dashed', swatchWidth: 34, swatchGap: 9, interactive: false }, + { kind: 'database' }, + { kind: 'default', className: 'a-default', marker: 'arrowhead', swatchWidth: 34, swatchGap: 9, interactive: false }, +].map((entry) => ({ + ...entry, + label: i18nText(dataflow.meta.locale, `legend.dataflow.${entry.kind}`), +})); + +function renderLegend() { + const presentKinds = new Set(asArray(dataflow.flows).map((flow) => flow.variant || 'default')); + if ([...nodes.values()].some((node) => node.type === 'database')) presentKinds.add('database'); + const entries = resolveLegend(dataflow.meta?.legend, LEGEND_CATALOG, presentKinds); + return renderResolvedLegend({ + entries, + locale: dataflow.meta.locale, + layout: { + x: 40, + baselineY: viewBox[1] - 36, + width: viewBox[0] - 80, + minTitleY: viewBox[1] - 66, + unfit: dataflow.meta?.legend === undefined ? 'hide' : 'error', + diagramType: 'dataflow', + }, + renderSwatch: (entry) => entry.kind === 'database' + ? `` + : ``, + }); +} + +function renderSvg() { + // Same default-canvas contract as lifecycle: 940x720 is below the 1.55 wide + // ratio, so without intrinsic-height the desktop Reader can neither narrow + // nor scroll it and every default dataflow fails the browser gate. + const readerFit = dataflow.meta?.viewBox ? '' : ' data-reader-fit="intrinsic-height"'; + return ` +${svgAccessibleText(dataflow.meta, 'dataflow')} +${renderDefinitions()} + + + + + +${dataflow.stages.map(renderStage).join('\n\n')} + + +${asArray(dataflow.flows).map(renderFlowPath).join('\n')} + + +${[...nodes.values()].map(renderNode).join('\n\n')} + + +${asArray(dataflow.flows).map(renderFlowLabel).join('\n')} + + +${renderLegend()} + `; +} + +validateDataflow(); +writeDiagram({ + outPath, + template, + diagramType: 'dataflow', + meta: dataflow.meta, + svg: renderSvg(), + cards: dataflow.cards, + sourceEvidence, +}); diff --git a/vendor/archify/renderers/lifecycle/README.md b/vendor/archify/renderers/lifecycle/README.md new file mode 100644 index 0000000..8b8f4f8 --- /dev/null +++ b/vendor/archify/renderers/lifecycle/README.md @@ -0,0 +1,171 @@ +# Lifecycle Renderer + +Render `diagram_type: "lifecycle"` JSON files into the standard Archify HTML +template. + +```bash +node archify/renderers/lifecycle/render-lifecycle.mjs input.lifecycle.json output.html +``` + +The renderer validates input against `archify/schemas/lifecycle.schema.json` +with the bundled standalone validator. No dependency installation is required. + +If `output.html` is omitted, the renderer uses the required `meta.output` value +from the JSON file. + +## Input + +Lifecycle JSON files must set: + +```json +{ + "schema_version": 2, + "diagram_type": "lifecycle", + "meta": { + "title": "Deployment Release Lifecycle", + "output": "deployment-release-lifecycle.html" + }, + "lanes": [], + "states": [], + "transitions": [], + "cards": [] +} +``` + +`schema_version` is `1` or `2`; author new diagrams as `2`. Lane ids `main` +(required) and `terminal` are reserved in both versions. + +- **v2** renders one row per populated lane: `main` first, `terminal` last, + other lanes in `lanes[]` order, each titled in the left gutter. A complete + example lives at `archify/examples/deployment-release.lifecycle.json`. +- **v1** keeps the fixed three bands: `main` is the top phase band, + `terminal` the bottom outcome band, and every other lane shares the middle + event band, whose header joins their labels with ` + `. A complete example + lives at `archify/examples/agent-run.lifecycle.json`. + +The schema lives at: + +```text +archify/schemas/lifecycle.schema.json +``` + +## Legend and state marks + +State color follows `states[].type`: active and start are cyan, waiting amber, +decision purple, success green, failure rose, neutral and external slate. +Structure is drawn, not colored: every `start` state gets a UML initial marker +(a dot and arrow into its left side), and a state with no outgoing transition +gets a double border as a final state (in v1, a main state followed by +another main column is not final, because the implied rail continues). + +The default legend derives kinds from `states[].type`; the `start` entry shows +the initial marker, and a non-interactive `final` entry appears when a final +state exists. Supported `meta.legend.entries` keys, in stable order, are +`start`, `active`, `waiting`, `decision`, `success`, `failure`, `neutral`, and +`external`. Labels and visibility may be overridden through the shared legend +contract; only kinds backed by rendered states receive Semantic Legend +controls. + +State decorations share one top rail: the type sigil and `step` on the left, +the brand mark at the right corner, and the Viewer's runtime source badge just +left of the brand. Label layout reserves the badge's width whenever the state +has verified repository sources. + +## Layout budget (v2) + +| Item | Value | +|------|-------| +| Columns | `col` 0–4, one x grid shared by every row | +| Default state | 140×64 (text 11px label, 8px sublabel and tag) | +| Column gap | 64px, widened until a labelled same-row neighbour transition fits beside its line; all gaps shrink toward 44px when the canvas would exceed the desktop readability budget of the smallest state text | +| Row gap | at least 120px, opened further for the horizontal tracks its routes need | +| Canvas | sized from the rows, columns, and measured legend when `meta.viewBox` is omitted; an authored `viewBox` is honored and validated | + +Transitions without `via`, a channel, or a non-`auto` route use the v2 grid +router: neighbours in one row connect horizontally (a reciprocal pair runs as +two parallel lines), rows connect through the facing top/bottom sides with one +turn in a row gap, a state blocking a straight descent sends the route through +the empty corridor between columns, and an unlabelled route blocked in an +outer column loops around the outside of the grid. Each gap assigns tracks in +the order that minimizes crossings. There is no implied rail; a forward +transition between two `main` states without a `variant` renders as the +emphasized primary path. Showcase labels are ranked beside their line, then on +it, then outward past neighbouring parallels. + +Explicit `fromSide` / `toSide` values remain authoritative. Pins that match the +grid router's chosen sides keep its routes and adaptive row gaps. If an automatic +transition pins a different side, the scene uses the shared side-aware obstacle +planner, retaining the v2 state grid and shared port spreading. + +## Layout budget (v1) + +| Band | Lane id | Top y | Column centers | Default state | +|------|---------|-------|----------------|---------------| +| Phase | `main` (required) | 126 | `col` 0–4 → x = 94, 248, 402, 556, 710 | 118×62 | +| Event | any other id | 278 | `col` 0–2 → x = 402, 556, 710 | 126×58 | +| Outcome | `terminal` | 450 | `col` 0–2 → x = 402, 556, 710 | 118×58 | + +Event and terminal columns are intentionally offset from the main rail: +event/terminal `col: N` uses the same x coordinate as main `col: N + 2`. +For example, lower-band columns 0, 1, and 2 align beneath main columns 2, 3, +and 4 respectively. + +| Constant | Value | +|----------|-------| +| viewBox | default `[980, 660]`; schema minimum `[420, 566]` | +| State area | x within `[32, width − 32]`; state bottom at or above `height − 122` | +| State spacing | ≥10px between any two states — checked across lanes, because all event lanes share one band; separate same-band states with `col` or `yOffset` | +| Transition length | ≥32px between endpoints | +| Legend row | final baseline y = height − 36; extra measured rows wrap upward | + +The primary lifecycle rail runs along the phase band and extends to the +furthest occupied phase column. Route presets for transitions: `straight`, +`drop` (bend at `channelY`, defaulting to the vertical midpoint), +`bottom-channel`, `top-channel`, `right-channel`, `left-channel`, explicit +`via` points, or the default `auto`. Multi-segment transitions get rounded +corners; tune them with `cornerRadius` (default 10, `0` for sharp bends). + +Transition `label` and `note` are independently optional. A non-empty `note` +renders even when `label` is omitted or empty, using its existing secondary +text style on a single row and retaining the note's fine-detail visibility. +With both fields present, the note stays below the label. Notes participate in +automatic label placement, route-space reservation, and label collision checks; +the existing `labelAt`, `labelDx`, `labelDy`, and +`labelSegment` controls also position a note-only text block. + +## Design Rules + +- Treat lifecycle diagrams as a phase map, not a dense state-transition graph. +- Put the primary lifecycle on one horizontal row using the `main` lane; in v2, + author each step of it as a transition. +- In v2, place an interruption, recovery, or exit in the column of the state it + leaves so its transition drops straight down. +- Use `step` labels for ordered phases, such as `01`, `02`, and `03`. +- Use lower lanes only for interruptions, recovery, and terminal exits. +- Keep transition labels out of the main SVG unless the label is essential; + prefer node labels, tags, legend entries, and summary cards. +- Prefer axis-aligned lines and avoid crossings. Terminal exits should drop + vertically from their source event whenever possible. Explicit `straight` + routes remain supported; see the [authored routing contract](../../references/authoring-contract.md#executable-geometry-rules). +- Use `success` for completion, `failure` for failure/terminal exits, + `waiting` for pauses, and `decision` for quality gates. + +Schema violations exit non-zero with path-prefixed messages annotated with the +element's id or label. The renderer additionally fails when it can detect +layout problems, including a missing `main` lane, duplicate state IDs, unknown +lanes, unknown transition endpoints, states outside the lifecycle area, +overlapping states (including across lanes), labels colliding with states or +other labels, labels wider than their state, unreadably short transitions, or +transitions crossing unrelated states (2px Clean Flow clearance). Lifecycle +bands remain intentional pass-through containers. +Text width is estimated CJK-aware: fullwidth glyphs count as two units. + +Set `meta.quality_profile` to `showcase` for polished delivery. Unrelated proper +X crossings then fail with `composition/proper-crossing`; default `standard` +keeps them as artifact-receipt warnings. The final artifact check samples +rounded `Q` corners. Collinear corridors remain outside the proper-X rule, but +a separate gate warns in `standard` and fails in `showcase` when unrelated +transitions overlap for at least 8px. Shared semantic endpoints, point touches, +and shorter overlaps remain valid. Showcase also rejects any route segment +below 8px and any interior turn segment below 16px; ordinary 8–15px endpoint +stubs remain valid. diff --git a/vendor/archify/renderers/lifecycle/grid-routing.mjs b/vendor/archify/renderers/lifecycle/grid-routing.mjs new file mode 100644 index 0000000..788cabe --- /dev/null +++ b/vendor/archify/renderers/lifecycle/grid-routing.mjs @@ -0,0 +1,429 @@ +// Orthogonal router for schema_version 2 lifecycle diagrams. +// +// v2 states sit on a fixed grid: one row per lane and a shared column pitch, +// with empty gaps between rows and between columns. That structure lets +// every automatic transition use a small, predictable set of shapes instead +// of a general obstacle search: +// - neighbours in one row connect with a horizontal line; +// - other states in one row connect through the gap above (below for the +// first row); +// - states in different rows leave through the facing top/bottom side, +// turn once in a row gap, and enter the target's facing side; when a +// state blocks the straight descent, the route steps sideways through the +// empty corridor between two columns. +// Ports on each side are spread in the order of where their routes head, and +// the horizontal runs in each gap get their own tracks, ordered to minimize +// crossings. Reciprocal pairs therefore render as two parallel lines. + +const PORT_GUTTER = 16; +const PORT_SPACING = 30; +const SNAP_LIMIT = 16; +const TRACK_TOP_CLEARANCE = 18; +const TRACK_BOTTOM_CLEARANCE = 18; +const PREFERRED_TRACK_SPACING = 22; +const MAX_TRACK_SPACING = 24; +const CORRIDOR_SPACING = 10; +const EXHAUSTIVE_TRACK_LIMIT = 7; + +const opposite = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }; + +function permutations(items) { + if (items.length <= 1) return [items]; + return items.flatMap((item, index) => permutations([...items.slice(0, index), ...items.slice(index + 1)]) + .map((rest) => [item, ...rest])); +} + +export function createLifecycleGridRouter(states, transitions, { rowOf, columnXs }) { + const byRow = new Map(); + for (const state of states.values()) { + const row = rowOf(state); + if (!Number.isInteger(row)) continue; + if (!byRow.has(row)) byRow.set(row, []); + byRow.get(row).push(state); + } + const rows = [...byRow.keys()].sort((a, b) => a - b); + const rowTop = new Map(rows.map((row) => [row, Math.min(...byRow.get(row).map((s) => s.y))])); + const rowBottom = new Map(rows.map((row) => [row, Math.max(...byRow.get(row).map((s) => s.y + s.height))])); + const nextRow = (row) => rows.find((candidate) => candidate > row); + const previousRow = (row) => [...rows].reverse().find((candidate) => candidate < row); + + // Gap g sits below row g. The gap under the last row borders the legend, + // so it only receives tracks when nothing else is possible. + function gapBand(row) { + const below = nextRow(row); + const top = rowBottom.get(row) + TRACK_TOP_CLEARANCE; + const bottom = below === undefined ? rowBottom.get(row) + 40 : rowTop.get(below) - TRACK_BOTTOM_CLEARANCE; + return [top, Math.max(top, bottom)]; + } + + function blocksVertical(x, fromRow, toRow, exclude) { + const [low, high] = fromRow < toRow ? [fromRow, toRow] : [toRow, fromRow]; + return [...states.values()].some((state) => { + if (exclude.has(state.id)) return false; + const row = rowOf(state); + return row > low && row < high && x >= state.x - 6 && x <= state.x + state.width + 6; + }); + } + + function blocksHorizontal(from, to) { + const row = rowOf(from); + const [left, right] = from.cx < to.cx ? [from, to] : [to, from]; + return byRow.get(row).some((state) => state !== left && state !== right + && state.x < right.x && state.x + state.width > left.x + left.width + && state.y < Math.max(left.y + left.height, right.y + right.height) + && state.y + state.height > Math.min(left.y, right.y)); + } + + // The empty corridors between neighbouring columns. + const corridorXs = columnXs.slice(1).map((x, index) => (x + columnXs[index]) / 2); + const allStates = [...states.values()]; + const leftmostX = Math.min(...allStates.map((state) => state.cx)); + const rightmostX = Math.max(...allStates.map((state) => state.cx)); + const gridLeft = Math.min(...allStates.map((state) => state.x)); + const gridRight = Math.max(...allStates.map((state) => state.x + state.width)); + // The initial-state marker occupies a start state's left side. + function loopBlocked(from, to, side) { + if (side !== 'left') return false; + const [low, high] = [Math.min(rowOf(from), rowOf(to)), Math.max(rowOf(from), rowOf(to))]; + return allStates.some((state) => state.type === 'start' && Math.abs(state.cx - from.cx) < 1 + && rowOf(state) >= low && rowOf(state) <= high); + } + + // Plan: sides, the gap each horizontal run uses, and where each end heads. + const plans = new Map(); + for (const transition of transitions) { + const from = states.get(transition.from); + const to = states.get(transition.to); + if (!from || !to || from === to) continue; + const fromRow = rowOf(from); + const toRow = rowOf(to); + if (!Number.isInteger(fromRow) || !Number.isInteger(toRow)) continue; + const exclude = new Set([from.id, to.id]); + if (fromRow === toRow) { + if (!blocksHorizontal(from, to)) { + const fromSide = to.cx > from.cx ? 'right' : 'left'; + plans.set(transition, { kind: 'horizontal', from, to, fromSide, toSide: opposite[fromSide] }); + } else { + const gapRow = previousRow(fromRow); + const useAbove = gapRow !== undefined; + const side = useAbove ? 'top' : 'bottom'; + plans.set(transition, { + kind: 'channel', from, to, fromSide: side, toSide: side, + runs: [{ gap: useAbove ? gapRow : fromRow, legs: useAbove ? ['down', 'down'] : ['up', 'up'] }], + }); + } + continue; + } + const down = toRow > fromRow; + const fromSide = down ? 'bottom' : 'top'; + const toSide = down ? 'top' : 'bottom'; + const gapNearTarget = down ? previousRow(toRow) : toRow; + const gapNearSource = down ? fromRow : previousRow(fromRow); + const legs = down ? ['up', 'down'] : ['down', 'up']; + if (!blocksVertical(from.cx, fromRow, toRow, exclude) && Math.abs(from.cx - to.cx) < 1) { + plans.set(transition, { kind: 'vertical', from, to, fromSide, toSide, runs: [{ gap: gapNearTarget, legs }] }); + } else if (!blocksVertical(from.cx, fromRow, toRow, exclude)) { + plans.set(transition, { kind: 'channel', from, to, fromSide, toSide, runs: [{ gap: gapNearTarget, legs }] }); + } else if (!blocksVertical(to.cx, fromRow, toRow, exclude)) { + plans.set(transition, { kind: 'channel', from, to, fromSide, toSide, runs: [{ gap: gapNearSource, legs }] }); + } else if (!transition.label && !transition.note + && Math.abs(from.cx - to.cx) < 1 && (from.cx <= leftmostX || from.cx >= rightmostX) + && !loopBlocked(from, to, from.cx <= leftmostX ? 'left' : 'right')) { + // A blocked edge column loops around the outside of the grid, like a + // bracket, instead of weaving through the rows' interior corridors. + // The margin has no room for a label, so only unlabeled edges loop. + const side = from.cx <= leftmostX ? 'left' : 'right'; + plans.set(transition, { kind: 'loop', from, to, fromSide: side, toSide: side, side }); + } else { + const middle = (from.cx + to.cx) / 2; + const corridor = corridorXs + .filter((x) => !blocksVertical(x, fromRow, toRow, new Set())) + .sort((a, b) => Math.abs(a - middle) - Math.abs(b - middle))[0]; + plans.set(transition, corridor === undefined + ? { kind: 'channel', from, to, fromSide, toSide, runs: [{ gap: gapNearTarget, legs }] } + : { + kind: 'corridor', from, to, fromSide, toSide, corridor, + runs: [{ gap: gapNearSource, legs }, { gap: gapNearTarget, legs }], + }); + } + } + + // Corridor offsets: routes sharing one corridor run side by side. + const corridorUse = new Map(); + for (const plan of plans.values()) { + if (plan.kind !== 'corridor') continue; + const list = corridorUse.get(plan.corridor) || []; + list.push(plan); + corridorUse.set(plan.corridor, list); + } + for (const [x, list] of corridorUse) { + list.sort((a, b) => a.from.cx - b.from.cx || a.to.cx - b.to.cx); + list.forEach((plan, index) => { plan.corridorX = x + (index - (list.length - 1) / 2) * CORRIDOR_SPACING; }); + } + // Outer loops nest: a longer span sits further out so loops never cross. + for (const side of ['left', 'right']) { + const loops = [...plans.values()].filter((plan) => plan.kind === 'loop' && plan.side === side) + .sort((a, b) => Math.abs(rowOf(a.from) - rowOf(a.to)) - Math.abs(rowOf(b.from) - rowOf(b.to))); + loops.forEach((plan, index) => { + plan.loopX = side === 'left' ? gridLeft - 18 - index * CORRIDOR_SPACING : gridRight + 18 + index * CORRIDOR_SPACING; + }); + } + + // Where each end heads after leaving its side, used to order the ports. + function headingFor(plan, end) { + const self = end === 'source' ? plan.from : plan.to; + const other = end === 'source' ? plan.to : plan.from; + if (plan.kind === 'horizontal' || plan.kind === 'loop') return other.cy; + if (plan.kind === 'corridor') return plan.corridorX; + return other === self ? self.cx : other.cx; + } + + const sideEnds = new Map(); + for (const [transition, plan] of plans) { + for (const end of ['source', 'target']) { + const state = end === 'source' ? plan.from : plan.to; + const side = end === 'source' ? plan.fromSide : plan.toSide; + const key = `${state.id}:${side}`; + if (!sideEnds.has(key)) sideEnds.set(key, { state, side, ends: [] }); + // Movers in the positive direction (right/down) take the first slot so + // both ends of a reciprocal pair line up. + const positive = plan.kind === 'horizontal' + ? plan.to.cx > plan.from.cx + : rowOf(plan.to) > rowOf(plan.from) || (rowOf(plan.to) === rowOf(plan.from) && plan.to.cx > plan.from.cx); + sideEnds.get(key).ends.push({ transition, end, heading: headingFor(plan, end), positive }); + } + } + + const ports = new Map(); + for (const { state, side, ends } of sideEnds.values()) { + ends.sort((a, b) => a.heading - b.heading || Number(b.positive) - Number(a.positive)); + const horizontalSide = side === 'top' || side === 'bottom'; + const length = horizontalSide ? state.width : state.height; + const gutter = Math.min(PORT_GUTTER, length / 4); + const spacing = ends.length > 1 ? Math.min(PORT_SPACING, (length - gutter * 2) / (ends.length - 1)) : 0; + ends.forEach((entry, index) => { + const offset = (index - (ends.length - 1) / 2) * spacing; + const point = horizontalSide + ? [state.cx + offset, side === 'top' ? state.y : state.y + state.height] + : [side === 'left' ? state.x : state.x + state.width, state.cy + offset]; + const record = ports.get(entry.transition) || {}; + record[entry.end] = point; + ports.set(entry.transition, record); + }); + } + + // Straight connections whose spread ports landed a few px apart would + // otherwise need a jog shorter than a readable turn: move one end onto the + // other's line when that side still has room there. + function sideRange(state, side) { + return side === 'top' || side === 'bottom' + ? [state.x + PORT_GUTTER / 2, state.x + state.width - PORT_GUTTER / 2] + : [state.y + PORT_GUTTER / 2, state.y + state.height - PORT_GUTTER / 2]; + } + function portsOnSide(state, side, except) { + return (sideEnds.get(`${state.id}:${side}`)?.ends || []) + .filter((entry) => entry.transition !== except) + .map((entry) => ports.get(entry.transition)[entry.end]); + } + for (const [transition, plan] of plans) { + const record = ports.get(transition); + const axis = plan.kind === 'horizontal' ? 1 : 0; + const delta = Math.abs(record.source[axis] - record.target[axis]); + if (delta < 0.5 || delta >= SNAP_LIMIT || !(plan.kind === 'horizontal' || plan.kind === 'vertical' || plan.kind === 'channel')) continue; + for (const [end, fixed] of [['target', 'source'], ['source', 'target']]) { + const state = end === 'source' ? plan.from : plan.to; + const side = end === 'source' ? plan.fromSide : plan.toSide; + const value = record[fixed][axis]; + const [low, high] = sideRange(state, side); + const crowded = portsOnSide(state, side, transition).some((point) => Math.abs(point[axis] - value) < 10); + if (value >= low && value <= high && !crowded) { + record[end] = axis ? [record[end][0], value] : [value, record[end][1]]; + break; + } + } + } + + // Horizontal runs per gap, then tracks ordered to minimize crossings. + const runsByGap = new Map(); + for (const [transition, plan] of plans) { + if (plan.kind === 'horizontal' || plan.kind === 'loop') continue; + const { source, target } = ports.get(transition); + plan.runs.forEach((run, index) => { + const x1 = index === 0 ? source[0] : plan.corridorX; + const x2 = plan.kind === 'corridor' && index === 0 ? plan.corridorX : target[0]; + if (plan.kind !== 'corridor' && Math.abs(x1 - x2) < 0.5) return; + const entry = { transition, index, x1, x2, legs: run.legs }; + if (!runsByGap.has(run.gap)) runsByGap.set(run.gap, []); + runsByGap.get(run.gap).push(entry); + }); + } + + const trackY = new Map(); + const trackCounts = new Map(); + for (const [gap, runs] of runsByGap) { + const [top, bottom] = gapBand(gap); + // Greedy interval colouring keeps unrelated runs on shared tracks only + // when they do not overlap. + const sorted = [...runs].sort((a, b) => Math.min(a.x1, a.x2) - Math.min(b.x1, b.x2)); + const classes = []; + for (const run of sorted) { + const low = Math.min(run.x1, run.x2) - 8; + const high = Math.max(run.x1, run.x2) + 8; + let target = classes.find((members) => members.every((other) => ( + high < Math.min(other.x1, other.x2) - 8 || low > Math.max(other.x1, other.x2) + 8 + ))); + if (!target || runs.length <= EXHAUSTIVE_TRACK_LIMIT) { + target = []; + classes.push(target); + } + target.push(run); + } + const count = classes.length; + trackCounts.set(gap, count); + // The renderer sizes each gap for its track count; a fixed canvas that + // cannot grow compresses the tracks rather than leaving the gap. + const spacing = count > 1 ? Math.min(MAX_TRACK_SPACING, (bottom - top) / (count - 1)) : 0; + const center = (top + bottom) / 2; + const ys = classes.map((_, index) => center + (index - (count - 1) / 2) * spacing); + const crossings = (order) => { + const y = new Map(); + order.forEach((members, index) => members.forEach((run) => y.set(run, ys[index]))); + let total = 0; + for (const a of runs) { + for (const b of runs) { + if (a === b || a.transition === b.transition) continue; + const [low, high] = [Math.min(a.x1, a.x2), Math.max(a.x1, a.x2)]; + for (const [x, leg] of [[b.x1, b.legs[0]], [b.x2, b.legs[1]]]) { + // An up leg and a down leg on one x overlap when the down leg + // starts above where the up leg ends: that merges two routes. + for (const [ax, aLeg] of [[a.x1, a.legs[0]], [a.x2, a.legs[1]]]) { + if (Math.abs(ax - x) < 1 && aLeg === 'up' && leg === 'down' && y.get(b) < y.get(a)) total += 100; + } + if (x <= low + 0.5 || x >= high - 0.5) continue; + if ((leg === 'up' && y.get(a) < y.get(b)) || (leg === 'down' && y.get(a) > y.get(b))) total += 1; + } + } + } + return total; + }; + let best = classes; + let bestScore = crossings(best); + if (count <= EXHAUSTIVE_TRACK_LIMIT) { + for (const order of permutations(classes).slice(1)) { + const score = crossings(order); + if (score < bestScore) { + best = order; + bestScore = score; + } + } + } else { + // Too many tracks to enumerate: swap pairs while that still helps. + for (let improved = true; improved;) { + improved = false; + for (let i = 0; i < count && !improved; i += 1) { + for (let j = i + 1; j < count && !improved; j += 1) { + const order = [...best]; + [order[i], order[j]] = [order[j], order[i]]; + const score = crossings(order); + if (score < bestScore) { + best = order; + bestScore = score; + improved = true; + } + } + } + } + } + best.forEach((members, index) => members.forEach((run) => trackY.set(`${gap}:${run.index}:${transitions.indexOf(run.transition)}`, ys[index]))); + } + + const pathCache = new Map(); + function pointsFor(transition) { + const plan = plans.get(transition); + if (!plan) { + // Self transitions and unknown endpoints are rejected by validation; + // a degenerate stub keeps that diagnostic reachable. + const state = states.get(transition.from) || states.get(transition.to); + const point = state ? [state.x + state.width, state.cy] : [0, 0]; + return [point, point]; + } + const { source, target } = ports.get(transition); + if (plan.kind === 'horizontal') { + if (Math.abs(source[1] - target[1]) < 0.5) return [source, target]; + const x = (source[0] + target[0]) / 2; + return [source, [x, source[1]], [x, target[1]], target]; + } + if (plan.kind === 'loop') return [source, [plan.loopX, source[1]], [plan.loopX, target[1]], target]; + const trackFor = (index) => trackY.get(`${plan.runs[index].gap}:${index}:${transitions.indexOf(transition)}`); + if (plan.kind === 'corridor') { + const y1 = trackFor(0); + const y2 = trackFor(1); + return [source, [source[0], y1], [plan.corridorX, y1], [plan.corridorX, y2], [target[0], y2], target]; + } + if (Math.abs(source[0] - target[0]) < 0.5) return [source, target]; + const y = trackFor(0); + return [source, [source[0], y], [target[0], y], target]; + } + + // Ports of two routes can still line up across a gap so their vertical + // runs share one line. Nudge a turning route's end sideways on its side. + for (const transition of plans.keys()) pathCache.set(transition, pointsFor(transition)); + const verticals = (points) => points.slice(1).flatMap((end, index) => { + const start = points[index]; + if (Math.abs(start[0] - end[0]) >= 0.5 || Math.abs(start[1] - end[1]) < 0.5) return []; + return [{ x: start[0], low: Math.min(start[1], end[1]), high: Math.max(start[1], end[1]), index, last: index === points.length - 2 }]; + }); + for (let round = 0; round < 12; round += 1) { + const entries = [...pathCache.entries()]; + let conflict = null; + for (let i = 0; i < entries.length && !conflict; i += 1) { + for (let j = i + 1; j < entries.length && !conflict; j += 1) { + for (const left of verticals(entries[i][1])) { + const right = verticals(entries[j][1]).find((other) => Math.abs(other.x - left.x) < 1 + && Math.min(other.high, left.high) - Math.max(other.low, left.low) > 0.5); + if (right) { + conflict = [[entries[i][0], left], [entries[j][0], right]]; + break; + } + } + } + } + if (!conflict) break; + let moved = false; + for (const [transition, segment] of conflict) { + const plan = plans.get(transition); + const end = segment.index === 0 ? 'source' : segment.last ? 'target' : null; + if (!end || pathCache.get(transition).length < 4 || !['channel', 'corridor'].includes(plan.kind)) continue; + const state = end === 'source' ? plan.from : plan.to; + const side = end === 'source' ? plan.fromSide : plan.toSide; + const record = ports.get(transition); + const [low, high] = sideRange(state, side); + const x = [12, -12, 20, -20].map((delta) => record[end][0] + delta).find((candidate) => ( + candidate >= low && candidate <= high + && !portsOnSide(state, side, transition).some((point) => Math.abs(point[0] - candidate) < 10) + )); + if (x === undefined) continue; + record[end] = [x, record[end][1]]; + pathCache.set(transition, pointsFor(transition)); + moved = true; + break; + } + if (!moved) break; + } + + return { + // Height a gap below `row` needs for its tracks at a readable spacing. + gapHeight(row) { + const count = trackCounts.get(row) || 0; + return TRACK_TOP_CLEARANCE + TRACK_BOTTOM_CLEARANCE + Math.max(0, count - 1) * PREFERRED_TRACK_SPACING; + }, + connectionSides(transition) { + const plan = plans.get(transition); + return plan ? { fromSide: plan.fromSide, toSide: plan.toSide } : { fromSide: 'right', toSide: 'right' }; + }, + pathFor(transition) { + if (!pathCache.has(transition)) pathCache.set(transition, pointsFor(transition)); + return pathCache.get(transition); + }, + }; +} diff --git a/vendor/archify/renderers/lifecycle/render-lifecycle.mjs b/vendor/archify/renderers/lifecycle/render-lifecycle.mjs new file mode 100644 index 0000000..91904ad --- /dev/null +++ b/vendor/archify/renderers/lifecycle/render-lifecycle.mjs @@ -0,0 +1,938 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs'; +import { animateAttr, focusEdgeAttrs, focusNodeAttrs, focusNodeTitle, loadDiagramWithBrandMarks, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs'; +import { recordDiagnostic, throwDiagnosticProblems } from '../shared/diagnostics.mjs'; +import { createRouter } from '../architecture/routing.mjs'; +import { createLifecycleGridRouter } from './grid-routing.mjs'; +import { placeAutomaticLabels, reservedLabelRect } from '../architecture/labels.mjs'; +import { legendFootprint, resolveLegend, renderLegend as renderResolvedLegend } from '../shared/legend.mjs'; +import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth, nodeLabelLayout } from '../shared/text-fit.mjs'; +import { brandLabelFitWidth, brandMarkFor, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs'; +import { translateMessage as i18nText } from '../shared/i18n.mjs'; +import { DESKTOP_READER_DIAGRAM_WIDTH, MIN_PROJECTED_NODE_TEXT_PX } from '../shared/desktop-readability.mjs'; +import { + asArray, + isFinitePoint, + rectsOverlap, + cleanEndpointSideProblems, + cleanFlowProblems, + cleanCrossingProblems, + cleanAmbiguousCorridorProblems, + cleanBorderRunProblems, + cleanRouteRhythmProblems, + cleanLabelRouteClearanceProblems, + cleanLabelCanvasContainmentProblems, + suggestLabelObstacleFix, + suggestLabelPairFix, + anchor, + automaticPortSpread, + legacyDefaultFromSide as defaultFromSide, + legacyDefaultToSide as defaultToSide, + chosenSide, + roundedPath, + routePointsValue, + authoredStraightRouteAttrs, + labelPoint, + arrowClassMap, + edgeLabelAccent +} from '../shared/geometry.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const { diagram: lifecycle, template, outPath, sourceEvidence } = await loadDiagramWithBrandMarks({ + rendererDir: __dirname, + diagramType: 'lifecycle', + defaultExample: 'agent-run.lifecycle.json' +}); + +// v2 sets state text one step larger; its canvas width is then budgeted +// from the smallest fitted text so the desktop Reader keeps it legible. +const stateTextFit = lifecycle.schema_version === 2 ? { + labelPreferred: 11, + labelMinimum: 9, + sublabelPreferred: 8, + sublabelMinimum: 7, + tagPreferred: 8, + tagMinimum: 7, + step: 8, +} : { + labelPreferred: 10, + labelMinimum: 8, + sublabelPreferred: 7, + sublabelMinimum: 6, + tagPreferred: 7, + tagMinimum: 6, + step: 7, +}; + +// schema_version 2 replaces the three fixed bands (main/event/outcome, with +// non-main lanes sharing one band and lower columns offset by +2) with one +// row per populated lane on a shared 0..4 column grid. v1 keeps its exact +// state geometry; only presentation (colors, markers, legend, sigil side) +// is shared between versions. +const isV2 = lifecycle.schema_version === 2; +const authoredViewBox = lifecycle.meta?.viewBox; + +const layout = { + phaseY: 126, + eventY: 278, + outcomeY: 450, + phaseW: 118, + phaseH: 62, + eventW: 126, + eventH: 58, + outcomeW: 118, + outcomeH: 58, + phaseXs: [94, 248, 402, 556, 710], + eventXs: [402, 556, 710], + outcomeXs: [402, 556, 710] +}; + +const layoutV2 = { + stateW: 140, + stateH: 64, + marginX: 60, + minGap: 64, + floorGap: 44, + firstRowTop: 56, + rowPitch: 184, +}; + +function transitionLabelWidth(transition) { + const longestLine = Math.max(textUnits(transition.label), textUnits(transition.note || '')); + return Math.max(32, longestLine * 4.9 + 12); +} + +function stateFontSizes(state, width) { + return { + label: fittedNodeFontSize(state.label, brandLabelFitWidth(state, width), stateTextFit.labelPreferred, stateTextFit.labelMinimum), + sublabel: fittedNodeFontSize(state.sublabel, width, stateTextFit.sublabelPreferred, stateTextFit.sublabelMinimum), + tag: fittedNodeFontSize(state.tag, width, stateTextFit.tagPreferred, stateTextFit.tagMinimum), + }; +} + +// v2 column centers. A gap between two columns widens until the label of a +// same-row neighbour transition fits beside its line; the whole canvas then +// stays inside the desktop readability budget of its smallest state text. +const v2ColumnCenters = (() => { + if (!isV2) return []; + const authored = asArray(lifecycle.states); + const widths = [0, 1, 2, 3, 4].map((col) => Math.max( + layoutV2.stateW, ...authored.filter((state) => state.col === col).map((state) => state.width || 0), + )); + const cols = authored.map((state) => state.col).filter((col) => Number.isInteger(col) && col >= 0 && col <= 4); + const lastCol = cols.length ? Math.max(...cols) : 0; + const gaps = [0, 1, 2, 3].map(() => layoutV2.minGap); + const byId = new Map(authored.map((state) => [state.id, state])); + for (const transition of asArray(lifecycle.transitions)) { + const [from, to] = [byId.get(transition.from), byId.get(transition.to)]; + if (!(transition.label || transition.note) || !from || !to || from.lane !== to.lane + || !Number.isInteger(from.col) || !Number.isInteger(to.col) || Math.abs(from.col - to.col) !== 1) continue; + const gap = Math.min(from.col, to.col); + if (gap >= 0 && gap < 4) gaps[gap] = Math.max(gaps[gap], Math.ceil(transitionLabelWidth(transition) + 24)); + } + const smallestText = Math.min(stateTextFit.step, ...authored.flatMap((state) => ( + Object.values(stateFontSizes(state, state.width || layoutV2.stateW)) + ))); + const budget = Math.floor(DESKTOP_READER_DIAGRAM_WIDTH * smallestText / MIN_PROJECTED_NODE_TEXT_PX); + const fixed = layoutV2.marginX * 2 + widths.slice(0, lastCol + 1).reduce((sum, width) => sum + width, 0); + const used = gaps.slice(0, lastCol); + const excess = fixed + used.reduce((sum, gap) => sum + gap, 0) - budget; + const slack = used.reduce((sum, gap) => sum + gap - layoutV2.floorGap, 0); + if (excess > 0 && slack > 0) { + const ratio = Math.min(1, excess / slack); + for (let index = 0; index < used.length; index += 1) { + gaps[index] = Math.floor(gaps[index] - (gaps[index] - layoutV2.floorGap) * ratio); + } + } + const centers = []; + let x = layoutV2.marginX; + for (let col = 0; col <= 4; col += 1) { + centers.push(x + widths[col] / 2); + x += widths[col] + (gaps[col] ?? 0); + } + return centers; +})(); + +// Rows render in authored lane order with `main` first and `terminal` last; +// lanes without states get no row and no title. +function laneRowOrder() { + const lanes = asArray(lifecycle.lanes); + const populated = new Set(asArray(lifecycle.states).map((state) => state.lane)); + const ordered = []; + if (populated.has('main')) ordered.push('main'); + for (const lane of lanes) { + if (lane.id !== 'main' && lane.id !== 'terminal' && populated.has(lane.id)) ordered.push(lane.id); + } + if (populated.has('terminal')) ordered.push('terminal'); + return ordered; +} + +const v2RowTop = new Map(isV2 + ? laneRowOrder().map((laneId, index) => [laneId, layoutV2.firstRowTop + index * layoutV2.rowPitch]) + : []); + +const typeClass = { + start: 'c-frontend', + active: 'c-frontend', + waiting: 'c-cloud', + decision: 'c-database', + success: 'c-backend', + failure: 'c-security', + neutral: 'c-external', + external: 'c-external' +}; + +const textClass = { + start: 't-frontend', + active: 't-frontend', + waiting: 't-cloud', + decision: 't-database', + success: 't-backend', + failure: 't-security', + neutral: 't-muted', + external: 't-muted' +}; + +// Lane semantics are fixed: lane id "main" maps to the top phase band, lane id +// "terminal" maps to the bottom outcome band, and every other lane shares the +// middle event band (separated visually via yOffset). v1 only. +function bandFor(lane) { + if (lane === 'main') return 'phase'; + if (lane === 'terminal') return 'outcome'; + return 'event'; +} + +function measureState(state) { + let width; + let height; + let cx; + let y; + if (isV2) { + width = state.width || layoutV2.stateW; + height = state.height || layoutV2.stateH; + cx = v2ColumnCenters[state.col] ?? NaN; + y = (v2RowTop.get(state.lane) ?? NaN) + (state.yOffset || 0); + } else { + const isPhase = bandFor(state.lane) === 'phase'; + const isOutcome = bandFor(state.lane) === 'outcome'; + width = state.width || (isPhase ? layout.phaseW : isOutcome ? layout.outcomeW : layout.eventW); + height = state.height || (isPhase ? layout.phaseH : isOutcome ? layout.outcomeH : layout.eventH); + const xs = isPhase ? layout.phaseXs : isOutcome ? layout.outcomeXs : layout.eventXs; + cx = xs[state.col] ?? xs[xs.length - 1]; + y = ( + isPhase ? layout.phaseY : + isOutcome ? layout.outcomeY : + layout.eventY + ) + (state.yOffset || 0); + } + return { + ...state, + width, + height, + x: cx - width / 2, + y, + cx, + cy: y + height / 2 + }; +} + +const states = new Map(asArray(lifecycle.states).map((state) => [state.id, measureState(state)])); +const plannedTransitions = asArray(lifecycle.transitions).filter(plannerRouted); +const v2Rows = laneRowOrder(); +const v2RowOf = (state) => (v2Rows.includes(state.lane) ? v2Rows.indexOf(state.lane) : undefined); +let useGridRouter = isV2; +if (isV2) { + // Route once to learn how many horizontal tracks each row gap carries, + // then open every gap to fit them before the final routing pass. + const probe = createLifecycleGridRouter(states, plannedTransitions, { + rowOf: v2RowOf, columnXs: v2ColumnCenters, + }); + // Compatible pins keep the grid layout. A conflicting pin sends the whole + // scene to the side-aware planner so all edges still share port spreading + // and obstacle reservations. + useGridRouter = plannedTransitions.every(transition => { + const sides = probe.connectionSides(transition); + return ['fromSide', 'toSide'].every(key => !transition[key] || transition[key] === 'auto' || transition[key] === sides[key]); + }); + let top = layoutV2.firstRowTop; + v2Rows.forEach((laneId, index) => { + const rowHeight = Math.max(layoutV2.stateH, ...[...states.values()] + .filter((state) => state.lane === laneId) + .map((state) => state.y + state.height - v2RowTop.get(laneId))); + v2RowTop.set(laneId, top); + top += rowHeight + Math.max(layoutV2.rowPitch - layoutV2.stateH, useGridRouter ? probe.gapHeight(index) : 0); + }); + for (const state of asArray(lifecycle.states)) states.set(state.id, measureState(state)); +} +const laneLabels = new Map(asArray(lifecycle.lanes).map((lane) => [lane.id, lane.label])); +const authoredOutgoing = new Set(asArray(lifecycle.transitions).map((transition) => transition.from)); + +// A state with no authored outgoing transition is terminal in the UML sense. +// v1 main states rely on the implied phase rail, so a main state is only final +// at the furthest occupied main column; every other v1 lane is explicit. +function isFinal(state) { + if (authoredOutgoing.has(state.id)) return false; + if (isV2) return true; + if (bandFor(state.lane) !== 'phase') return true; + const mainCols = [...states.values()].filter((s) => bandFor(s.lane) === 'phase').map((s) => s.col); + return state.col === Math.max(...mainCols); +} + +const LEGEND_CATALOG = [ + 'start', + 'active', + 'waiting', + 'decision', + 'success', + 'failure', + 'neutral', + 'external', +].map((kind) => ({ + kind, + label: i18nText(lifecycle.meta.locale, `legend.lifecycle.${kind}`), + swatchWidth: kind === 'start' ? 26 : undefined, +})); + +// Entries exist before the canvas so an auto-sized v2 viewBox can reserve the +// measured legend rows below the last row instead of painting over them. +// The structural `final` entry explains the double border; it follows the +// kind entries and disappears with them, so a hidden legend stays empty. +function legendCatalog() { + const presentKinds = new Set([...states.values()].map((state) => state.type)); + const entries = resolveLegend(lifecycle.meta?.legend, LEGEND_CATALOG, presentKinds); + if (!entries.length || ![...states.values()].some(isFinal)) return entries; + return [...entries, { + kind: 'final', + label: i18nText(lifecycle.meta.locale, 'legend.lifecycle.final'), + interactive: false, + present: true, + swatchWidth: 16, + }]; +} + +const resolvedLegendEntries = legendCatalog(); + +let viewBox; +if (isV2 && !authoredViewBox) { + const finite = [...states.values()]; + const maxRight = Math.max(0, ...finite.map((state) => state.cx + state.width / 2).filter(Number.isFinite)); + const width = Math.max(640, Math.ceil(maxRight + layoutV2.marginX)); + const footprint = legendFootprint(resolvedLegendEntries, { width: width - 80 }); + const statesBottom = Math.max(0, ...finite.map((state) => state.y + state.height).filter(Number.isFinite)); + viewBox = [width, Math.ceil(statesBottom + footprint.extraHeight + 96)]; +} else { + viewBox = authoredViewBox || [980, 660]; +} + +const legendExtraHeight = legendFootprint(resolvedLegendEntries, { width: viewBox[0] - 80 }).extraHeight; + +function legendY() { + return viewBox[1] - 36; +} + +// Keep the authored state-placement contract independent from the measured +// legend's lower baseline. Moving legend chrome must not admit new state +// geometry into the reserved outcome/legend band. +function lifecycleAreaBottom() { + return isV2 ? viewBox[1] - legendExtraHeight - 96 : viewBox[1] - 122; +} +const stateSteps = new Map(); +for (const [index, transition] of asArray(lifecycle.transitions).entries()) { + if (!stateSteps.has(transition.from)) stateSteps.set(transition.from, index); + if (!stateSteps.has(transition.to)) stateSteps.set(transition.to, index + 1); +} +for (const [index, state] of asArray(lifecycle.states).entries()) { + if (!stateSteps.has(state.id)) stateSteps.set(state.id, index); +} + +function validateLifecycle() { + const problems = []; + if (states.size !== asArray(lifecycle.states).length) problems.push('State ids must be unique.'); + + // The three bands are fixed at y=112/264/436. Preserve the original + // outcome/legend reserve even though measured legend rows now sit lower. + // v2 derives its canvas from rendered rows, so the floor does not apply. + if (!isV2 && lifecycleAreaBottom() + 4 < 448) { + problems.push(`viewBox height ${viewBox[1]} is too short for the fixed band layout — set meta.viewBox[1] to at least 566.`); + } + + const laneIds = new Set(asArray(lifecycle.lanes).map((lane) => lane.id)); + if (laneIds.size !== asArray(lifecycle.lanes).length) problems.push('Lane ids must be unique.'); + if (!laneIds.has('main')) { + problems.push(isV2 + ? 'Lifecycle diagrams need a lane with id "main" (the first row). Lane ids "main" and "terminal" are reserved: "main" renders as the first row, "terminal" as the last, and every other lane in authored order.' + : 'Lifecycle diagrams need a lane with id "main" (the phase rail). Lane ids "main" and "terminal" are reserved: "main" maps to the top phase band, "terminal" to the bottom outcome band, and all other lanes share the middle event band.'); + } + + for (const state of states.values()) { + if (!laneIds.has(state.lane)) { + problems.push(`State "${state.id}" uses unknown lane "${state.lane}".`); + continue; + } + if (isV2) { + if (!Number.isInteger(state.col) || state.col < 0 || state.col > 4) { + problems.push(`State "${state.id}" uses invalid column ${state.col} — every lifecycle row has integer columns 0..4.`); + continue; + } + } else { + const band = bandFor(state.lane); + const maxCol = band === 'phase' + ? layout.phaseXs.length + : band === 'outcome' + ? layout.outcomeXs.length + : layout.eventXs.length; + if (!Number.isInteger(state.col) || state.col < 0 || state.col >= maxCol) { + problems.push(`State "${state.id}" uses invalid column ${state.col} — the ${band} band has integer columns 0..${maxCol - 1}.`); + continue; + } + } + if (!isFinitePoint(state.x, state.y, state.cx, state.cy)) { + problems.push(`State "${state.id}" produced non-finite coordinates — check col, width, height, and yOffset are numbers.`); + continue; + } + if (state.x < (isV2 ? 28 : 32) || state.x + state.width > viewBox[0] - (isV2 ? 28 : 32)) { + problems.push(`State "${state.id}" exceeds the horizontal bounds of the diagram — reduce state.width${isV2 ? ', lower its col,' : ''} or increase meta.viewBox[0].`); + } + if (state.y < (isV2 ? 44 : 64) || state.y + state.height > lifecycleAreaBottom()) { + problems.push(`State "${state.id}" exceeds the vertical lifecycle area — keep y between ${isV2 ? 44 : 64} and ${lifecycleAreaBottom()} (adjust yOffset or increase meta.viewBox[1]).`); + } + const estLabelW = textUnits(state.label) * 6.2; + if (estLabelW > state.width + 6) { + problems.push(`Label "${state.label}" (~${Math.round(estLabelW)}px) is wider than state "${state.id}" (${state.width}px) — shorten the label or increase state.width.`); + } + const brandRailProblem = brandTopRailProblem(state, state.width, 8, 'State'); + if (brandRailProblem) problems.push(brandRailProblem); + // sublabel and tag render as single unwrapped elements; shrink-to-fit + // handles the ordinary case, this rejects what it cannot rescue. + const availableTextW = availableNodeTextWidth(state.width); + for (const [field, value, minimum] of [ + ['Sublabel', state.sublabel, stateTextFit.sublabelMinimum], + ['Tag', state.tag, stateTextFit.tagMinimum], + ]) { + if (!value) continue; + const minimumW = minimumNodeTextWidth(value, minimum); + if (minimumW > availableTextW) { + problems.push(`${field} "${value}" needs ~${Math.ceil(minimumW)}px at the ${minimum}px legible minimum, but state "${state.id}" provides ${availableTextW}px — shorten the ${field.toLowerCase()} or increase state.width.`); + } + } + } + + // v1: all non-main/non-terminal lanes share the same y band, so the overlap + // check must run across lanes — not per-lane. v2 gives each lane its own + // row, so only same-row neighbours can collide, but the shared check still + // covers custom widths and yOffset nudges. + const allStates = [...states.values()]; + for (let i = 0; i < allStates.length; i += 1) { + for (let j = i + 1; j < allStates.length; j += 1) { + if (rectsOverlap(allStates[i], allStates[j], 10)) { + problems.push(`States "${allStates[i].id}" and "${allStates[j].id}" are less than 10px apart — move one to another col or separate them with yOffset${isV2 ? '.' : ' (lanes other than "main"/"terminal" share one band).'}`); + } + } + } + + for (const transition of asArray(lifecycle.transitions)) { + if (!states.has(transition.from)) problems.push(`Transition "${transition.label || transition.from}" references unknown source "${transition.from}".`); + if (!states.has(transition.to)) problems.push(`Transition "${transition.label || transition.to}" references unknown target "${transition.to}".`); + if (states.has(transition.from) && states.has(transition.to)) { + const routed = pathFor(transition); + const [start, end] = [routed.points[0], routed.points[routed.points.length - 1]]; + const distance = Math.hypot(end[0] - start[0], end[1] - start[1]); + if (distance < 32) problems.push(`Transition "${transition.label || `${transition.from}->${transition.to}`}" is too short (${Math.round(distance)}px; minimum 32px) — route it through a channel or drop its label.`); + } + } + + // Authored via points are authoritative in schema v1, including under a + // quality profile. Preserve and render them exactly: applying the endpoint + // gate would either reject an existing typed input or require silently + // falsifying its geometry. Automatic routes still receive the side gate. + problems.push(...cleanEndpointSideProblems({ + relations: lifecycle.transitions, + endpointIds: new Set(states.keys()), + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + fromSideFor: (transition) => transitionSides(transition).fromSide, + toSideFor: (transition) => transitionSides(transition).toSide, + shouldCheckRelation: (transition) => !Array.isArray(transition.via), + routeHint: 'keep automatic routing, or choose fromSide/toSide and via points whose first and final segments cross state borders perpendicularly', + })); + problems.push(...cleanFlowProblems({ + relations: lifecycle.transitions, + obstacles: states.values(), + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + obstacleKind: 'state', + routeHint: 'adjust fromSide/toSide, set route/via or channelX/channelY, or move the state with col/yOffset' + })); + problems.push(...cleanCrossingProblems({ + relations: lifecycle.transitions, + endpointIds: new Set(states.keys()), + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + profile: lifecycle.meta?.quality_profile, + // Planner routes render with the opaque crossover halo, like architecture. + crossingResolved: (left, right) => plannerRouted(left) && plannerRouted(right), + routeHint: 'adjust route/via or channelX/channelY so the transitions use separate lifecycle corridors' + })); + problems.push(...cleanAmbiguousCorridorProblems({ + relations: lifecycle.transitions, + endpointIds: new Set(states.keys()), + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + profile: lifecycle.meta?.quality_profile, + routeHint: 'adjust route/via or channelX/channelY so unrelated transitions do not visually merge' + })); + // Lifecycle bands are dashed reading guides, not closed containers. Keep the + // shared contract wired with an explicit empty frame set so future typed + // lifecycle containers cannot accidentally inherit presentation geometry. + problems.push(...cleanBorderRunProblems({ + relations: lifecycle.transitions, + endpointIds: new Set(states.keys()), + frames: [], + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + profile: lifecycle.meta?.quality_profile + })); + problems.push(...cleanRouteRhythmProblems({ + relations: lifecycle.transitions, + endpointIds: new Set(states.keys()), + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + profile: lifecycle.meta?.quality_profile, + routeHint: 'move route/via or channel coordinates so each lifecycle turn has a readable run-up' + })); + + const labelRects = transitionLabelRects(); + if (lifecycle.meta?.quality_profile === 'showcase') { + for (const rect of labelRects) { + for (const title of bandGeometry()) { + if (!rectsOverlap(rect, title)) continue; + const message = `Transition ${rect.relationIndex} label "${rect.label}" overlaps lifecycle band title "${title.label}" — move the label with labelAt/labelDx/labelDy/labelSegment or provide more space.`; + recordDiagnostic({ + code: 'composition/label-band-title-overlap', severity: 'error', message, + subject: { diagramType: 'lifecycle', collection: 'transitions', index: rect.relationIndex, from: rect.relation.from, to: rect.relation.to }, + evidence: { labelRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, bandTitle: title }, + supportedFixes: ['move the transition label with labelAt/labelDx/labelDy/labelSegment while preserving its text'], + }); + problems.push(message); + } + } + } + for (const rect of labelRects) { + for (const state of states.values()) { + if (rectsOverlap(rect, state, -2)) { + problems.push(`Label "${rect.label}" overlaps state "${state.id}" — adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, state, 'state', viewBox, states.values())}`); + } + } + } + for (let i = 0; i < labelRects.length; i += 1) { + for (let j = i + 1; j < labelRects.length; j += 1) { + if (rectsOverlap(labelRects[i], labelRects[j], -2)) { + problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — adjust labelDx/labelDy.\n${suggestLabelPairFix(labelRects[i], labelRects[j])}`); + } + } + } + problems.push(...cleanLabelRouteClearanceProblems({ + relations: lifecycle.transitions, + labels: labelRects, + endpointIds: new Set(states.keys()), + pathFor, + diagramType: 'lifecycle', + relationCollection: 'transitions', + profile: lifecycle.meta?.quality_profile, + })); + problems.push(...cleanLabelCanvasContainmentProblems({ + labels: labelRects, + viewBox, + diagramType: 'lifecycle', + relationCollection: 'transitions', + profile: lifecycle.meta?.quality_profile, + })); + + if (problems.length) { + throwDiagnosticProblems('Lifecycle layout validation failed', problems, { + subject: { diagramType: 'lifecycle' }, + }); + } +} + +function routeVia(transition, from, to, start, end, fromSide, toSide) { + if (transition.via) return transition.via; + switch (transition.route || 'auto') { + case 'straight': + return []; + case 'drop': { + const y = transition.channelY ?? (start[1] + end[1]) / 2; + return [[start[0], y], [end[0], y]]; + } + case 'bottom-channel': { + const y = transition.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 34; + return [[start[0], y], [end[0], y]]; + } + case 'top-channel': { + const y = transition.channelY ?? Math.min(from.y, to.y) - 28; + return [[start[0], y], [end[0], y]]; + } + case 'right-channel': { + const x = transition.channelX ?? Math.max(from.x + from.width, to.x + to.width) + 36; + return [[x, start[1]], [x, end[1]]]; + } + case 'left-channel': { + const x = transition.channelX ?? Math.min(from.x, to.x) - 36; + return [[x, start[1]], [x, end[1]]]; + } + case 'auto': + default: { + if (start[0] === end[0] || start[1] === end[1]) return []; + const fromVertical = fromSide === 'top' || fromSide === 'bottom'; + const toVertical = toSide === 'top' || toSide === 'bottom'; + if (fromVertical !== toVertical) { + return [fromVertical ? [start[0], end[1]] : [end[0], start[1]]]; + } + if (fromVertical) { + const y = transition.channelY ?? (start[1] + end[1]) / 2; + return [[start[0], y], [end[0], y]]; + } + const x = transition.channelX ?? (start[0] + end[0]) / 2; + return [[x, start[1]], [x, end[1]]]; + } + } +} + +const pathCache = new Map(); + +// A transition without via, channel, or a lifecycle route preset is routed by +// the obstacle-aware planner shared with architecture, so a first draft that +// leaves routing to the renderer does not cross unrelated states or produce +// micro jogs. Authored via/route/channel geometry keeps the lifecycle presets. +function plannerRouted(transition) { + return !transition.via + && (!transition.route || transition.route === 'auto') + && transition.channelX === undefined + && transition.channelY === undefined; +} + +// v2 states sit on a fixed row/column grid, so automatic transitions use the +// dedicated orthogonal grid router; v1 keeps the shared obstacle planner. +const planner = useGridRouter ? createLifecycleGridRouter(states, plannedTransitions, { + rowOf: v2RowOf, + columnXs: v2ColumnCenters, +}) : createRouter(states, plannedTransitions, { + labelRectFor: (transition, points, { routes, labels }) => ((transition.label || transition.note) ? reservedLabelRect({ + label: { relation: transition, label: transition.label || transition.note, ...transitionLabelBoxAt(transition, labelPoint(transition, points)) }, + points, + routes: routes.map((route, index) => ({ relationIndex: index, points: route })), + labels, + components: [...states.values()], + viewBox, + placementBottom: lifecycleAreaBottom(), + }) : null), +}); + +function transitionSides(transition) { + if (plannerRouted(transition)) return planner.connectionSides(transition); + const from = states.get(transition.from); + const to = states.get(transition.to); + return { + fromSide: chosenSide(transition.fromSide, defaultFromSide(from, to)), + toSide: chosenSide(transition.toSide, defaultToSide(from, to)), + }; +} + +const automaticPorts = automaticPortSpread( + asArray(lifecycle.transitions).filter((transition) => !plannerRouted(transition)), + states, + { sideFor: (transition, endpoint) => transitionSides(transition)[endpoint === 'source' ? 'fromSide' : 'toSide'] }, +); + +function pathFor(transition) { + if (pathCache.has(transition)) return pathCache.get(transition); + if (plannerRouted(transition)) { + let routed = planner.pathFor(transition); + if (useGridRouter) routed = { d: roundedPath(routed, transition.cornerRadius ?? 10), points: routed }; + pathCache.set(transition, routed); + return routed; + } + const from = states.get(transition.from); + const to = states.get(transition.to); + const ports = automaticPorts.get(transition); + const { fromSide, toSide } = transitionSides(transition); + const start = ports?.from || anchor(from, fromSide); + const end = ports?.to || anchor(to, toSide); + let via = routeVia(transition, from, to, start, end, fromSide, toSide); + if (ports && !via.length && Math.abs(start[0] - end[0]) >= 4 && Math.abs(start[1] - end[1]) >= 4) { + const midX = (start[0] + end[0]) / 2; + via = [[midX, start[1]], [midX, end[1]]]; + } + const points = [start, ...via, end]; + const routed = { + d: roundedPath(points, transition.cornerRadius ?? 10), + points + }; + pathCache.set(transition, routed); + return routed; +} + +const resolvedLabelPoints = new Map(); + +function transitionLabelBox(transition) { + return transitionLabelBoxAt( + transition, + resolvedLabelPoints.get(transition) || labelPoint(transition, pathFor(transition).points), + ); +} + +function transitionLabelBoxAt(transition, [lx, ly]) { + const width = transitionLabelWidth(transition); + const height = transition.label && transition.note ? 27 : 16; + return { x: lx - width / 2, y: ly - 11, width, height, lx, ly }; +} + +function transitionLabelRects() { + const rects = []; + for (const [relationIndex, transition] of asArray(lifecycle.transitions).entries()) { + if (!(transition.label || transition.note) || !states.has(transition.from) || !states.has(transition.to)) continue; + rects.push({ relation: transition, relationIndex, label: transition.label || transition.note, ...transitionLabelBox(transition) }); + } + return rects; +} + +// Showcase drafts leave label positions to the renderer too: move an unpinned +// label off other routes and states instead of reporting a clearance defect. +if (lifecycle.meta?.quality_profile === 'showcase') { + const placed = placeAutomaticLabels({ + labels: transitionLabelRects(), + routes: asArray(lifecycle.transitions).flatMap((transition, relationIndex) => ( + states.has(transition.from) && states.has(transition.to) + ? [{ relationIndex, points: pathFor(transition).points }] : [] + )), + components: [...states.values()], + titles: bandGeometry(), + viewBox, + placementBottom: lifecycleAreaBottom(), + keepFallbackNearRoute: isV2, + gridSweep: isV2, + }); + for (const rect of placed) resolvedLabelPoints.set(rect.relation, [rect.lx, rect.ly]); +} + +function bandGeometry() { + if (isV2) { + // One row header per rendered row, set vertically in the left gutter so + // routes entering a row never run through its title. + return laneRowOrder().map((laneId, index) => { + const cy = v2RowTop.get(laneId) + layoutV2.stateH / 2; + const label = laneLabels.get(laneId) || laneId; + const length = textUnits(label) * 6.2; + return { index, label, vertical: true, cx: 18, cy, x: 11, y: cy - length / 2, width: 14, height: length }; + }); + } + const lanes = asArray(lifecycle.lanes); + const mainLane = lanes.find((lane) => lane.id === 'main'); + const terminalLane = lanes.find((lane) => lane.id === 'terminal'); + const eventLanes = lanes.filter((lane) => lane.id !== 'main' && lane.id !== 'terminal'); + const populated = new Set([...states.values()].map((state) => bandFor(state.lane))); + return [ + mainLane?.label || 'Lifecycle phases', + eventLanes.length ? eventLanes.map((lane) => lane.label).join(' + ') : 'Interruptions + recovery', + terminalLane?.label || 'Outcomes' + ].map((title, index) => { + const baseline = [100, 252, 424][index]; + const label = `${String(index + 1).padStart(2, '0')} / ${title}`; + return { index, band: ['phase', 'event', 'outcome'][index], label, x: 72, y: baseline - 11, width: textUnits(label) * 6.2, height: 14, baseline }; + }).filter((band) => populated.has(band.band)); +} + +function renderBands() { + return bandGeometry().map((band) => (band.vertical + ? ` ${esc(band.label)}` + : ` + ${esc(band.label)}`)).join('\n'); +} + +function renderState(state) { + const fill = typeClass[state.type] || typeClass.neutral; + const accent = textClass[state.type] || 't-muted'; + const hasSub = state.sublabel != null && state.sublabel !== ''; + const { label: labelFontSize, sublabel: sublabelFontSize, tag: tagFontSize } = stateFontSizes(state, state.width); + const textRows = [{ text: state.label, font: labelFontSize, y: isV2 ? 23 : 21 }]; + if (hasSub) textRows.push({ text: state.sublabel, font: sublabelFontSize, y: isV2 ? 40 : 37 }); + if (state.tag) textRows.push({ text: state.tag, font: tagFontSize, y: state.height - (isV2 ? 12 : 11) }); + const hasBrand = Boolean(brandMarkFor(state)); + const hasSource = Boolean(sourceEvidence?.nodes?.[state.id]?.length); + const labelLayout = nodeLabelLayout({ width: state.width, height: state.height, rows: textRows, + brand: hasBrand, source: hasSource, side: 'left', step: state.step }); + const sub = hasSub + ? `\n ${esc(state.sublabel)}` + : ''; + const tag = state.tag + ? `\n ${esc(state.tag)}` + : ''; + const step = state.step + ? `\n ${esc(state.step)}` + : ''; + const brand = renderBrandMark(state, { x: state.x + state.width - 22, y: state.y + 6 }); + // UML pseudo-state markers: start states get an initial dot + arrow into + // the left border; states with no authored outgoing transition get a double + // border. Both are decorations, not focus/relationship edges. + const initialMarker = state.type === 'start' + ? `\n ` + : ''; + const finalBorder = isFinal(state) + ? `\n ` + : ''; + const passport = { + kind: state.type, + sublabel: state.sublabel, + tag: state.tag, + context: laneLabels.get(state.lane) || i18nText(lifecycle.meta.locale, 'node.context.lifecycle'), + ...brandMetadataFor(state), + }; + return ` + ${focusNodeTitle(state.label, passport)} + + ${finalBorder}${initialMarker} + ${renderSemanticSigil(state.type, { icon: state.icon, x: state.x + 6, y: state.y + labelLayout.sigilY, size: labelLayout.sigilSize })}${brand ? `\n ${brand}` : ''}${step} + ${esc(state.label)}${sub}${tag} + `; +} + +// v2 replaces the implied phase rail with explicit topology: a forward +// transition between two main-lane states renders as the emphasized primary +// path unless the author chose another variant. +function effectiveVariant(transition) { + if (transition.variant) return transition.variant; + if (isV2) { + const from = states.get(transition.from); + const to = states.get(transition.to); + if (from?.lane === 'main' && to?.lane === 'main' && to.col > from.col) return 'emphasis'; + } + return 'default'; +} + +function renderTransitionPath(transition, index) { + const variant = effectiveVariant(transition); + const [cls, marker] = arrowClassMap[variant] || arrowClassMap.default; + const routed = pathFor(transition); + const strokeWidth = transition.width || (variant === 'emphasis' ? (isV2 ? 1.6 : 2) : 1.1); + const automaticRoute = plannerRouted(transition); + const crossover = automaticRoute ? ' data-composition-crossover="halo"' : ''; + const edge = ` `; + if (!automaticRoute) return edge; + // Same presentation-only wrapper as architecture: the mask underlay lets two + // planner routes cross legibly while the viewer still sees one semantic edge. + const underlay = ` \n`; + return ` \n${underlay}${edge.replace(/^ /, ' ')}\n `; +} + +function renderTransitionLabel(transition, index) { + if (!(transition.label || transition.note)) return ''; + const { lx, ly, width: labelW, height: labelH } = transitionLabelBox(transition); + const label = transition.label + ? `\n ${esc(transition.label)}` + : ''; + const note = transition.note + ? `\n ${esc(transition.note)}` + : ''; + return ` + ${label}${note} + `; +} + +// UML initial pseudo-state: a filled dot and a short arrow ending at `tipX`. +function initialMarkerShape(dotX, tipX, y) { + return ``; +} + +function renderSwatch(entry) { + // `start` is structural, not a color: its swatch is the same initial + // pseudo-state marker drawn on the canvas. `final` is a non-interactive + // structural entry (a double-border rect) appended when a final state exists. + if (entry.kind === 'start') { + return initialMarkerShape(entry.x + 4.5, entry.x + 24, entry.baseline - 3.5); + } + if (entry.kind === 'final') { + return ``; + } + return ``; +} + +function renderLegend() { + return renderResolvedLegend({ + entries: resolvedLegendEntries, + locale: lifecycle.meta.locale, + layout: { + x: 40, + baselineY: legendY(), + width: viewBox[0] - 80, + minTitleY: lifecycleAreaBottom() + 8, + unfit: lifecycle.meta?.legend === undefined ? 'hide' : 'error', + diagramType: 'lifecycle', + }, + renderSwatch, + }); +} + +// v1 only: the implied emphasis line behind main states. v2 topology is fully +// explicit, so the rail would double the authored forward transitions. +function renderLifecycleRail() { + if (isV2) return ''; + const mainCols = [...states.values()] + .filter((state) => bandFor(state.lane) === 'phase') + .map((state) => state.col); + if (!mainCols.length) return ''; + const railEnd = layout.phaseXs[mainCols.reduce((max, col) => Math.max(max, col))] + 38; + return ` `; +} + +function renderSvg() { + // A renderer-sized canvas declares the intrinsic-height fit exactly like + // architecture: the default 980x660 band layout is below the 1.55 wide + // ratio, so without this the desktop Reader could neither narrow it nor + // scroll it and every default lifecycle failed the browser gate. + const readerFit = lifecycle.meta?.viewBox ? '' : ' data-reader-fit="intrinsic-height"'; + return ` +${svgAccessibleText(lifecycle.meta, 'lifecycle')} +${renderDefinitions()} + + + + + +${renderBands()} + + +${renderLifecycleRail()} + + +${asArray(lifecycle.transitions).map(renderTransitionPath).join('\n')} + + +${[...states.values()].map(renderState).join('\n\n')} + + +${asArray(lifecycle.transitions).map(renderTransitionLabel).join('\n')} + + +${renderLegend()} + `; +} + +validateLifecycle(); +writeDiagram({ + outPath, + template, + diagramType: 'lifecycle', + meta: lifecycle.meta, + svg: renderSvg(), + cards: lifecycle.cards, + sourceEvidence, +}); diff --git a/vendor/archify/renderers/sequence/README.md b/vendor/archify/renderers/sequence/README.md new file mode 100644 index 0000000..8a25f1e --- /dev/null +++ b/vendor/archify/renderers/sequence/README.md @@ -0,0 +1,129 @@ +# Sequence Renderer + +Render `diagram_type: "sequence"` JSON files into the standard Archify HTML +template. + +```bash +node archify/renderers/sequence/render-sequence.mjs input.sequence.json output.html +``` + +The renderer validates input against `archify/schemas/sequence.schema.json` +with the bundled standalone validator. No dependency installation is required. + +If `output.html` is omitted, the renderer uses the required `meta.output` value +from the JSON file. + +## Input + +Sequence JSON files must set: + +```json +{ + "schema_version": 1, + "diagram_type": "sequence", + "meta": { + "title": "Cache Miss Request Sequence", + "output": "cache-miss-request.html", + "viewBox": [920, 760] + }, + "participants": [], + "segments": [], + "messages": [], + "activations": [], + "cards": [] +} +``` + +The timeline scales with the viewBox height: a taller `meta.viewBox` buys more +message room, a shorter one shrinks the readable band instead of clipping. A +complete worked example lives at +`archify/examples/cache-miss-request.sequence.json`. + +The schema lives at: + +```text +archify/schemas/sequence.schema.json +``` + +## Legend + +The default visual legend derives kinds from `messages[].variant` (omitting +`variant` means `default`). Supported `meta.legend.entries` keys, in stable +order, are `emphasis`, `return`, `security`, `dashed`, and `default`. These are +visual message keys, not Semantic Lens controls; label/visibility overrides do +not create edge facts. + +The legend sits below all timeline content: the last message and its note, +activation bars, and segment frames, with a 12px gap. Without `meta.viewBox` +the canvas grows to keep that gap. With an authored `viewBox` that is too short, +`showcase` fails with the exact height to set, and `standard` hides the implicit +legend rather than drawing it over content. Lifelines stop above the legend. +Message labels use their line's color; gray default and return lines keep the +muted text color. + +## Layout budget + +| Constant | Value | +|----------|-------| +| viewBox | default `[920, 760]`, taller when late content needs legend room; schema minimum `[480, 480]` | +| Participant boxes | `fixed` (default): 86×54 at y 72; `spread`: viewBox-relative width from 86px up to 190px | +| Participant columns | `fixed`: centers at x = 62 + index×108; `spread`: columns distribute across the available viewBox width | +| Participant count | the last box must end at or before width − 40; layouts that cannot fit fail closed | +| Lifelines | from y 142 down to height − 65 (drawn to just above the legend); band must be ≥120px tall | +| Message `y` range | `[160, height − 83]` | +| Message spacing | ≥28px vertical between messages that share horizontal space | +| Arrow span | ≥60px horizontal between the two participants | +| Segments | y pixel ranges with `to > from`, inside `[72, lifeline bottom + 20]` | +| Legend | last row baseline at height − 54; extra rows wrap upward and stay 12px below the timeline content | + +`segments[].from/to` and `activations[].from/to` are y pixel coordinates, not +participant ids; activations also require `to > from`. + +### Column fit + +Sequence diagrams use `meta.column_fit: "fixed"` by default so existing +documents keep their historical coordinates. Use `"spread"` when a wide +viewBox would otherwise leave empty space on the right or when meaningful +participant labels do not fit the fixed 86px boxes. Spread derives box width +and column distance from the viewBox while preserving participant order, +lifelines, and message semantics. + +The artifact checker reports `composition.sequenceColumnSpace` from the rendered +participants, routes and text. A large unused right-hand region in a fixed layout +can produce an `inspect-sequence-width` recommendation in `finalize`; it is advice, +not a new warning or failure. See [Sequence width review](../../references/delivery-contract.md#sequence-width-review) +for the bounded authoring repair and explicit-fixed/legacy preservation rules. + +## Design Rules + +- Put participants across the top, ordered by the story the reader should + follow. +- Time moves downward. +- Use `emphasis` for the main request path. +- Use `security` for auth, consent, permission, and policy calls. +- Use `return` for quiet response messages. +- Use `dashed` for async trace, event, logging, and non-blocking work. +- Use segments as light background guides; keep segment labels short. +- Keep labels concise, but try `meta.column_fit: "spread"` before shortening a + meaningful participant label just to fit the fixed boxes. + +Schema violations exit non-zero with path-prefixed messages annotated with the +element's id or label. The renderer additionally fails when it can detect +layout problems, including missing participants, duplicate participant IDs, +participant labels wider than their box, unknown message endpoints, messages +outside the readable timeline, overly tight vertical spacing between messages +that overlap horizontally, invalid segment or activation ranges, or +participants that exceed the viewBox. The shared Clean Flow contract treats +participant headers as semantic boxes while explicitly allowing messages to +cross intermediate lifelines, activation bars, and segment frames. Text width is estimated CJK-aware: +fullwidth glyphs count as two units. + +Set `meta.quality_profile` to `showcase` for polished delivery. Unrelated proper +message X crossings then fail with `composition/proper-crossing`; default +`standard` keeps them as artifact-receipt warnings. Messages may still cross +intermediate lifelines. Collinear corridors remain outside the proper-X rule, +but a separate gate warns in `standard` and fails in `showcase` when unrelated +messages overlap for at least 8px. Shared semantic endpoints, point touches, +and shorter overlaps remain valid. Showcase also rejects any route segment +below 8px and any interior turn segment below 16px; ordinary 8–15px endpoint +stubs remain valid. diff --git a/vendor/archify/renderers/sequence/render-sequence.mjs b/vendor/archify/renderers/sequence/render-sequence.mjs new file mode 100644 index 0000000..808db62 --- /dev/null +++ b/vendor/archify/renderers/sequence/render-sequence.mjs @@ -0,0 +1,526 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs'; +import { animateAttr, focusEdgeAttrs, focusNodeAttrs, focusNodeTitle, loadDiagramWithBrandMarks, writeDiagram, svgAccessibleText, svgRootAttrs } from '../shared/cli.mjs'; +import { throwDiagnosticProblems } from '../shared/diagnostics.mjs'; +import { legendFootprint, measureLegend, resolveLegend, renderLegend as renderResolvedLegend } from '../shared/legend.mjs'; +import { componentFill, arrowClassMap, rectsOverlap, cleanFlowProblems, cleanCrossingProblems, cleanAmbiguousCorridorProblems, cleanBorderRunProblems, cleanRouteRhythmProblems, cleanLabelRouteClearanceProblems, cleanLabelCanvasContainmentProblems, routePointsValue, asArray, isFinitePoint, edgeLabelAccent } from '../shared/geometry.mjs'; +import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth } from '../shared/text-fit.mjs'; +import { brandLabelFitWidth, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs'; +import { translateMessage as i18nText } from '../shared/i18n.mjs'; + +const participantTextFit = { + sublabelPreferred: 7, + sublabelMinimum: 6, +}; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const { diagram: sequence, template, outPath, sourceEvidence } = await loadDiagramWithBrandMarks({ + rendererDir: __dirname, + diagramType: 'sequence', + defaultExample: 'cache-miss-request.sequence.json' +}); + +const LEGEND_CATALOG = [ + { kind: 'emphasis', className: 'a-emphasis', marker: 'arrowhead-emphasis', strokeWidth: 1.8 }, + { kind: 'return', className: 'a-default', marker: 'arrowhead', dash: '3,5' }, + { kind: 'security', className: 'a-security', marker: 'arrowhead-security' }, + { kind: 'dashed', className: 'a-dashed', marker: 'arrowhead-dashed' }, + { kind: 'default', className: 'a-default', marker: 'arrowhead' }, +].map((entry) => ({ + ...entry, + interactive: false, + swatchWidth: 34, + swatchGap: 9, + label: i18nText(sequence.meta.locale, `legend.sequence.${entry.kind}`), +})); + +function legendEntries() { + const presentKinds = new Set(asArray(sequence.messages).map((message) => message.variant || 'default')); + return resolveLegend(sequence.meta?.legend, LEGEND_CATALOG, presentKinds); +} + +// The legend sits below the timeline content: the last message and its note, +// activation bars, and segment frames. Its block starts LEGEND_CONTENT_GAP +// below that content; from the block top to the canvas bottom a one-row legend +// needs LEGEND_BLOCK_HEIGHT (title glyphs, row, and the 54px baseline inset). +const LEGEND_CONTENT_GAP = 12; +const LEGEND_BLOCK_HEIGHT = 86; +const contentBottom = Math.max( + 0, + ...asArray(sequence.messages).map((message) => message.y + (message.note ? 22 : 6)), + ...asArray(sequence.activations).map((activation) => activation.to), + ...asArray(sequence.segments).map((segment) => segment.to), +); +function legendRequiredHeight(width) { + const entries = legendEntries(); + if (!entries.length) return 0; + return Math.ceil(contentBottom + LEGEND_CONTENT_GAP + LEGEND_BLOCK_HEIGHT + + legendFootprint(entries, { width: width - 80 }).extraHeight); +} +// A renderer-sized canvas grows to keep the legend clear of late messages; +// an authored viewBox is honored and validated below. +const viewBox = sequence.meta?.viewBox || [920, Math.max(760, legendRequiredHeight(920))]; +// The timeline scales with viewBox height: a taller viewBox gains message room, +// a shorter one shrinks the readable band (validated below) instead of clipping. +// `column_fit: "spread"` widens the lanes with the viewBox instead of keeping +// the fixed 108px gap, so a wide canvas gains column distance and label room +// rather than dead space on the right. The default stays "fixed" so existing +// diagrams keep their coordinates. +const columnFit = sequence.meta?.column_fit === 'spread' ? 'spread' : 'fixed'; +const participantCount = Math.max(1, asArray(sequence.participants).length); +const sideMargin = 62; +const participantW = columnFit === 'spread' + ? Math.max(86, Math.min(190, Math.round((viewBox[0] - sideMargin * 2) / participantCount) - 24)) + : 86; +const colGap = columnFit === 'spread' && participantCount > 1 + ? Math.max(108, (viewBox[0] - 40 - sideMargin - participantW) / (participantCount - 1)) + : 108; + +// Showcase is the fast-authoring default; standard retains legacy label geometry. +const readableMessages = sequence.meta?.quality_profile === 'showcase'; +const messageFontSize = readableMessages ? 11 : 9; +const messageUnitWidth = readableMessages ? 6.6 : 5.2; +const layout = { + topY: 72, + participantW, + // Keep a separate top rail for the 11px semantic sigil and 16px brand mark. + // Literal labels retain their fitted font size and full authored wording. + participantH: 60, + participantLabelY: 36, + participantSublabelY: 50, + lifelineTop: 142, + lifelineBottom: viewBox[1] - 65, + legendY: viewBox[1] - 54, + leftX: columnFit === 'spread' ? sideMargin + participantW / 2 : sideMargin, + colGap, + labelH: readableMessages ? 18 : 16 +}; + +const participantBoxWidthNote = columnFit === 'spread' + ? `participant boxes are ${participantW}px for this viewBox width and ${participantCount} participants` + : `participant boxes are a fixed ${participantW}px unless meta.column_fit is "spread"`; + +const arrowClass = { + ...arrowClassMap, + return: ['a-default', 'arrowhead'] +}; + +function participantX(index) { + return layout.leftX + index * layout.colGap; +} + +const participants = new Map(asArray(sequence.participants).map((participant, index) => [ + participant.id, + { + ...participant, + index, + cx: participantX(index), + x: participantX(index) - layout.participantW / 2, + y: layout.topY, + width: layout.participantW, + height: layout.participantH, + cy: layout.topY + layout.participantH / 2 + } +])); + +function messageGeometry(message) { + const from = participants.get(message.from); + const to = participants.get(message.to); + if (!from || !to || typeof message.y !== 'number') return null; + const direction = to.cx > from.cx ? 1 : -1; + const start = from.cx + direction * 7; + const end = to.cx - direction * 7; + return { start, end, center: (start + end) / 2 }; +} + +function messageLabelBox(message, relationIndex = null) { + const geometry = messageGeometry(message); + if (!geometry) return null; + const width = Math.max(34, textUnits(message.label) * messageUnitWidth + 12); + return { + relation: message, + relationIndex, + label: message.label, + x: geometry.center - width / 2, + y: message.y - 20, + width, + height: layout.labelH, + }; +} + +function messageRouteBox(message) { + const geometry = messageGeometry(message); + if (!geometry) return null; + return { + x: Math.min(geometry.start, geometry.end), + y: message.y - 2, + width: Math.abs(geometry.end - geometry.start), + height: 4, + }; +} + +function segmentLabelBox(segment) { + const labelW = Math.max(42, textUnits(segment.label) * 5.2 + 14); + const occupied = asArray(sequence.messages) + .flatMap((message) => [messageLabelBox(message), messageRouteBox(message)]) + .filter(Boolean); + const label = { x: 56, y: segment.from - 22, width: labelW, height: 18 }; + for (let attempt = 0; attempt < 4; attempt += 1) { + if (!occupied.some((rect) => rectsOverlap(label, rect, 2))) break; + label.y -= 22; + } + return label; +} + +const compositionFrames = asArray(sequence.segments).map((segment, index) => ({ + id: index, + label: segment.label, + kind: 'segment', + x: 48, + y: segment.from, + width: viewBox[0] - 96, + height: segment.to - segment.from, + radius: 10, +})); + +function messagePath(message) { + return { + points: participants.has(message.from) && participants.has(message.to) + ? [[participants.get(message.from).cx, message.y], [participants.get(message.to).cx, message.y]] + : [] + }; +} + +function validateSequence() { + const problems = []; + if (participants.size !== asArray(sequence.participants).length) problems.push('Participant ids must be unique.'); + + if (layout.lifelineBottom - layout.lifelineTop < 120) { + problems.push(`viewBox height ${viewBox[1]} leaves under 120px of timeline — set meta.viewBox[1] to at least ${layout.lifelineTop + 120 + 65}.`); + } + + for (const participant of participants.values()) { + const estLabelW = textUnits(participant.label) * 6.8; + if (estLabelW > layout.participantW + 6) { + problems.push(`Label "${participant.label}" (~${Math.round(estLabelW)}px) is wider than the ${layout.participantW}px participant box — shorten it.`); + } + const brandRailProblem = brandTopRailProblem(participant, layout.participantW, 8, 'Participant'); + if (brandRailProblem) problems.push(brandRailProblem); + // sublabel renders as a single unwrapped ; shrink-to-fit handles the + // ordinary case, this rejects what it cannot rescue. + if (participant.sublabel) { + const availableTextW = availableNodeTextWidth(layout.participantW); + const minimumW = minimumNodeTextWidth(participant.sublabel, participantTextFit.sublabelMinimum); + if (minimumW > availableTextW) { + problems.push(`Sublabel "${participant.sublabel}" needs ~${Math.ceil(minimumW)}px at the ${participantTextFit.sublabelMinimum}px legible minimum, but participant "${participant.id}" provides ${availableTextW}px — shorten the sublabel (${participantBoxWidthNote}).`); + } + } + } + + for (const message of asArray(sequence.messages)) { + if (!participants.has(message.from)) problems.push(`Message "${message.label}" references unknown source "${message.from}".`); + if (!participants.has(message.to)) problems.push(`Message "${message.label}" references unknown target "${message.to}".`); + if (typeof message.y !== 'number') problems.push(`Message "${message.label}" must provide a numeric y.`); + if (message.y < layout.lifelineTop + 18 || message.y > layout.lifelineBottom - 18) { + problems.push(`Message "${message.label}" sits outside the readable timeline — keep y between ${layout.lifelineTop + 18} and ${layout.lifelineBottom - 18}.`); + } + if (participants.has(message.from) && participants.has(message.to)) { + const distance = Math.abs(participants.get(message.to).cx - participants.get(message.from).cx); + if (distance < 60) problems.push(`Message "${message.label}" spans ${Math.round(distance)}px (minimum 60px) — give its participants more column distance.`); + } + } + + // Participant headers are opaque nodes. Lifelines, activation bars, and + // segment bands remain intentional pass-through geometry and are excluded. + problems.push(...cleanFlowProblems({ + relations: sequence.messages, + obstacles: participants.values(), + pathFor: messagePath, + diagramType: 'sequence', + relationCollection: 'messages', + obstacleKind: 'participant header', + clearance: 0, + routeHint: 'move the message y below the participant headers or reorder participants' + })); + problems.push(...cleanCrossingProblems({ + relations: sequence.messages, + endpointIds: new Set(participants.keys()), + pathFor: messagePath, + diagramType: 'sequence', + relationCollection: 'messages', + profile: sequence.meta?.quality_profile, + routeHint: 'separate the message y values; lifeline crossings remain allowed' + })); + problems.push(...cleanAmbiguousCorridorProblems({ + relations: sequence.messages, + endpointIds: new Set(participants.keys()), + pathFor: messagePath, + diagramType: 'sequence', + relationCollection: 'messages', + profile: sequence.meta?.quality_profile, + routeHint: 'separate the message y values so unrelated messages do not visually merge' + })); + problems.push(...cleanBorderRunProblems({ + relations: sequence.messages, + endpointIds: new Set(participants.keys()), + frames: compositionFrames, + pathFor: messagePath, + diagramType: 'sequence', + relationCollection: 'messages', + profile: sequence.meta?.quality_profile, + routeHint: 'move the message y so it crosses a segment boundary perpendicularly or stays clearly inside the segment' + })); + problems.push(...cleanRouteRhythmProblems({ + relations: sequence.messages, + endpointIds: new Set(participants.keys()), + pathFor: messagePath, + diagramType: 'sequence', + relationCollection: 'messages', + profile: sequence.meta?.quality_profile, + routeHint: 'increase participant spacing or simplify message routing so every turn has room to read' + })); + + // Vertical crowding only matters when the arrows share horizontal space; + // disjoint arrows may legitimately run in parallel rows. + const placed = asArray(sequence.messages) + .filter((m) => participants.has(m.from) && participants.has(m.to)) + .map((m) => ({ + label: m.label, + y: m.y, + x1: Math.min(participants.get(m.from).cx, participants.get(m.to).cx), + x2: Math.max(participants.get(m.from).cx, participants.get(m.to).cx) + })) + .sort((a, b) => a.y - b.y); + for (let i = 0; i < placed.length; i += 1) { + for (let j = i + 1; j < placed.length && placed[j].y - placed[i].y < 28; j += 1) { + if (placed[i].x1 < placed[j].x2 && placed[j].x1 < placed[i].x2) { + problems.push(`Messages "${placed[i].label}" and "${placed[j].label}" are less than 28px apart and share horizontal space — spread their y values.`); + } + } + } + + // Label masks can extend well past the arrow span, so check the actual + // label rectangles too — tangent arrows with long labels still collide. + const labelRects = asArray(sequence.messages) + .map((m, messageIndex) => messageLabelBox(m, messageIndex)) + .filter(Boolean); + for (let i = 0; i < labelRects.length; i += 1) { + for (let j = i + 1; j < labelRects.length; j += 1) { + if (rectsOverlap(labelRects[i], labelRects[j], -2)) { + problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — spread their message y values or shorten the labels.`); + } + } + } + problems.push(...cleanLabelRouteClearanceProblems({ + relations: sequence.messages, + labels: labelRects, + endpointIds: new Set(participants.keys()), + pathFor: messagePath, + diagramType: 'sequence', + relationCollection: 'messages', + profile: sequence.meta?.quality_profile, + routeHint: 'spread the message y values, shorten the label, or reorder participants so the adjacent route stays visible' + })); + problems.push(...cleanLabelCanvasContainmentProblems({ + labels: labelRects, + viewBox, + diagramType: 'sequence', + relationCollection: 'messages', + profile: sequence.meta?.quality_profile, + routeHint: 'shorten the label, reorder participants, or enlarge meta.viewBox', + })); + + for (const segment of asArray(sequence.segments)) { + if (segment.to <= segment.from) { + problems.push(`Segment "${segment.label}" has invalid y range (from ${segment.from} to ${segment.to}) — "to" must be greater than "from".`); + } + if (segment.from < layout.topY || segment.to > layout.lifelineBottom + 20) { + problems.push(`Segment "${segment.label}" extends outside the canvas — keep its y range between ${layout.topY} and ${layout.lifelineBottom + 20}.`); + } + const labelBox = segmentLabelBox(segment); + const availableWidth = Math.max(0, viewBox[0] - 48 - labelBox.x); + if (labelBox.x + labelBox.width > viewBox[0] - 48) { + const requiredWidth = Math.ceil(labelBox.x + labelBox.width + 48); + problems.push(`Segment "${segment.label}" label (~${Math.round(labelBox.width)}px) exceeds the segment frame's available width (${availableWidth}px) — shorten the label or increase meta.viewBox[0] to at least ${requiredWidth}.`); + } + } + + for (const activation of asArray(sequence.activations)) { + if (!participants.has(activation.participant)) problems.push(`Activation references unknown participant "${activation.participant}".`); + if (activation.to <= activation.from) problems.push(`Activation for "${activation.participant}" has invalid time range — "to" must be greater than "from".`); + } + + const lastParticipant = asArray(sequence.participants)[asArray(sequence.participants).length - 1]; + if (lastParticipant && participants.get(lastParticipant.id).cx + layout.participantW / 2 > viewBox[0] - 40) { + const requiredWidth = Math.ceil(participants.get(lastParticipant.id).cx + layout.participantW / 2 + 40); + problems.push(`Participants exceed viewBox width — set meta.viewBox[0] to at least ${requiredWidth} or remove a participant.`); + } + + // Showcase must not silently drop the implicit legend because late content + // leaves no room for it; give the exact canvas height instead. + const legendHeight = legendRequiredHeight(viewBox[0]); + if (sequence.meta?.quality_profile === 'showcase' && sequence.meta?.legend === undefined + && legendHeight > viewBox[1] && !measureLegend(legendEntries(), legendLayout())) { + problems.push(`Sequence content ends at y=${contentBottom}, leaving no room for the legend below it — set meta.viewBox[1] to at least ${legendHeight} or omit meta.viewBox so the canvas grows.`); + } + + if (problems.length) { + throwDiagnosticProblems('Sequence layout validation failed', problems, { + subject: { diagramType: 'sequence' }, + }); + } +} + +function renderParticipant(participant) { + const fill = componentFill[participant.type] || 'c-external'; + const hasSub = participant.sublabel != null && participant.sublabel !== ''; + const sub = hasSub + ? `\n ${esc(participant.sublabel)}` + : ''; + const brand = renderBrandMark(participant, { x: participant.x + layout.participantW - 22, y: layout.topY + 6 }); + const labelFontSize = fittedNodeFontSize(participant.label, brandLabelFitWidth(participant, layout.participantW), 11, 8); + const passport = { + kind: participant.type, + sublabel: participant.sublabel, + context: i18nText(sequence.meta.locale, 'node.context.sequence'), + ...brandMetadataFor(participant), + }; + return ` + ${focusNodeTitle(participant.label, passport)} + + + ${renderSemanticSigil(participant.type, { icon: participant.icon, x: participant.x + 6, y: layout.topY + 6 })}${brand ? `\n ${brand}` : ''} + ${esc(participant.label)}${sub} + `; +} + +// Lifelines never enter the legend band. The legend is placed below all +// timeline content, so stopping above its title still reaches every message. +function lifelineEnd() { + const legend = measureLegend(legendEntries(), legendLayout()); + return legend?.titleY == null ? layout.lifelineBottom : Math.min(layout.lifelineBottom, legend.titleY - 22); +} + +function renderLifeline(participant, end) { + return ` `; +} + +function renderSegment(segment, index) { + return ` `; +} + +function renderSegmentLabel(segment, index) { + const label = segmentLabelBox(segment); + return ` + + ${esc(segment.label)} + `; +} + +function renderActivation(activation) { + const participant = participants.get(activation.participant); + const fill = componentFill[activation.type] || componentFill[participant.type] || 'c-external'; + const x = participant.cx - 5; + const height = activation.to - activation.from; + return ` + `; +} + +function messageLabel(message, x1, x2) { + const box = messageLabelBox(message); + const center = box ? box.x + box.width / 2 : (x1 + x2) / 2; + const y = message.y - 10; + const labelW = box?.width || Math.max(34, textUnits(message.label) * messageUnitWidth + 12); + // A colored line gets a label in the same color, as in the legend swatches. + // Gray lines (default and return) keep the readable muted text color. + const accent = ['emphasis', 'security', 'dashed'].includes(message.variant) ? edgeLabelAccent(message.variant) : 't-muted'; + return ` + + ${esc(message.label)} + `; +} + +function renderMessage(message, index) { + const { start, end } = messageGeometry(message); + const [cls, marker] = arrowClass[message.variant || 'default'] || arrowClass.default; + const strokeWidth = message.variant === 'emphasis' ? 1.8 : 1.4; + const dash = message.variant === 'return' ? ' stroke-dasharray="3,5"' : ''; + const note = message.note + ? `\n ${esc(message.note)}` + : ''; + return ` + +${messageLabel(message, start, end)}${note} + `; +} + + +function legendLayout() { + return { + x: 40, + baselineY: layout.legendY, + width: viewBox[0] - 80, + // The same content-based budget as legendRequiredHeight(): a wrapped + // legend may use any rows it needs as long as it stays below the content. + minTitleY: Math.max(layout.lifelineTop, contentBottom + LEGEND_CONTENT_GAP), + unfit: sequence.meta?.legend === undefined ? 'hide' : 'error', + diagramType: 'sequence', + }; +} + +function renderLegend() { + return renderResolvedLegend({ + entries: legendEntries(), + locale: sequence.meta.locale, + layout: legendLayout(), + renderSwatch: (entry) => ``, + }); +} + +function renderSvg() { + const participantList = [...participants.values()]; + // Same default-canvas contract as lifecycle: 920x760 is below the 1.55 wide + // ratio, so without intrinsic-height the desktop Reader can neither narrow + // nor scroll it and every default sequence fails the browser gate. + const readerFit = sequence.meta?.viewBox ? '' : ' data-reader-fit="intrinsic-height"'; + return ` +${svgAccessibleText(sequence.meta, 'sequence')} +${renderDefinitions()} + + + + + +${asArray(sequence.segments).map(renderSegment).join('\n\n')} + + +${participantList.map((participant) => renderLifeline(participant, lifelineEnd())).join('\n')} + + +${asArray(sequence.activations).map(renderActivation).join('\n')} + + +${asArray(sequence.messages).map(renderMessage).join('\n\n')} + + +${asArray(sequence.segments).map(renderSegmentLabel).join('\n')} + + +${participantList.map(renderParticipant).join('\n\n')} + + +${renderLegend()} + `; +} + +validateSequence(); +writeDiagram({ + outPath, + template, + diagramType: 'sequence', + meta: sequence.meta, + svg: renderSvg(), + cards: sequence.cards, + sourceEvidence, +}); diff --git a/vendor/archify/renderers/shared/atomic-output.mjs b/vendor/archify/renderers/shared/atomic-output.mjs new file mode 100644 index 0000000..8895d70 --- /dev/null +++ b/vendor/archify/renderers/shared/atomic-output.mjs @@ -0,0 +1,1982 @@ +import { createHash, randomBytes } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { canonicalFuturePath } from './output-path.mjs'; + +function relation(status, code, details = {}) { + return { status, reason: { code, ...details } }; +} + +function filesystemFailure(code, error, details = {}) { + return relation('unknown', code, { + ...details, + ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}), + ...(typeof error?.message === 'string' ? { message: error.message } : {}), + }); +} + +function entryType(metadata) { + if (metadata.isFile()) return 'file'; + if (metadata.isSymbolicLink()) return 'symbolic-link'; + if (metadata.isDirectory()) return 'directory'; + if (metadata.isFIFO()) return 'fifo'; + if (metadata.isSocket()) return 'socket'; + if (metadata.isBlockDevice()) return 'block-device'; + if (metadata.isCharacterDevice()) return 'character-device'; + return 'other'; +} + +// Windows lstat can fall back to directory enumeration when a file handle +// cannot be opened, and that fallback reports a synthetic link count of one. +// Treat handle-backed fstat as authoritative and fail closed when it cannot be +// reconciled with the no-follow path snapshots on either side of the open. +function captureRegularFileHandle(filePath, initial, subject) { + const pathKey = subject === 'target' + ? 'commitPath' + : subject === 'requested-entry' + ? 'requestedPath' + : 'candidatePath'; + if (initial.ino === 0n) { + return relation('unknown', `${subject}-identity-unavailable`, { [pathKey]: filePath }); + } + let descriptor; + let handle; + let current; + let closeError; + try { + const noFollow = process.platform === 'win32' ? 0 : (fs.constants.O_NOFOLLOW || 0); + const nonBlock = fs.constants.O_NONBLOCK || 0; + descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow | nonBlock); + handle = fs.fstatSync(descriptor, { bigint: true }); + current = fs.lstatSync(filePath, { bigint: true }); + } catch (error) { + return filesystemFailure(`${subject}-handle-inspection-failed`, error, { + [pathKey]: filePath, + }); + } finally { + if (descriptor !== undefined) { + try { + fs.closeSync(descriptor); + } catch (error) { + closeError = error; + } + } + } + if (closeError) { + return filesystemFailure(`${subject}-handle-close-failed`, closeError, { + [pathKey]: filePath, + }); + } + if (!handle.isFile() || !current.isFile() + || handle.ino === 0n || current.ino === 0n + || handle.dev !== initial.dev || handle.ino !== initial.ino + || current.dev !== handle.dev || current.ino !== handle.ino) { + return relation('unknown', `${subject}-changed-during-inspection`, { + [pathKey]: filePath, + }); + } + if (handle.nlink === 0n) { + return relation('unknown', `${subject}-link-count-unavailable`, { + [pathKey]: filePath, + }); + } + if (handle.nlink !== 1n) { + return relation('unsupported', `${subject}-hardlinked`, { + [pathKey]: filePath, + links: handle.nlink.toString(), + }); + } + return { status: 'captured', metadata: handle }; +} + +const regularFileBindings = new WeakMap(); +const regularFileBindingGroups = new Map(); +const digestChunkBytes = 64 * 1024; +const removalCleanupSignal = new Int32Array(new SharedArrayBuffer(4)); +const removalCleanupAttempts = 10; +const publicationRecoveryRecordName = 'publication-recovery-v1.json'; +const publicationRecoveryRecordVersion = 1; +const publicationRecoveryRecordMaxBytes = 16 * 1024; + +export function removeEmptyDirectoryWithRetry(directory, { retry = true } = {}) { + const attempts = retry ? removalCleanupAttempts : 1; + let failure; + for (let attempt = 0; attempt < attempts; attempt += 1) { + try { + fs.rmdirSync(directory); + return; + } catch (error) { + failure = error; + // SMB can acknowledge unlink before a following directory removal sees + // the deleted entry disappear. Retrying rmdir is claimant-safe: a real + // entry keeps the directory non-empty and is never removed recursively. + if (error?.code !== 'ENOTEMPTY' || attempt === attempts - 1) break; + Atomics.wait( + removalCleanupSignal, + 0, + 0, + Math.min(5 * (2 ** attempt), 250), + ); + } + } + throw failure; +} + +function validExpectedLinks(value) { + return Number.isSafeInteger(value) && value > 0; +} + +function validateBindingExpectations(options) { + const hasSha256 = options.expectedSha256 !== undefined; + const hasBytes = options.expectedBytes !== undefined; + const expectedIdentity = options.expectedIdentity; + if (hasSha256 !== hasBytes + || (hasSha256 && !/^[a-f\d]{64}$/i.test(options.expectedSha256)) + || (hasBytes && (!Number.isSafeInteger(options.expectedBytes) || options.expectedBytes < 0)) + || (options.expectedMode !== undefined + && (!Number.isSafeInteger(options.expectedMode) + || options.expectedMode < 0 + || options.expectedMode > 0o777)) + || (expectedIdentity !== undefined + && (typeof expectedIdentity?.device !== 'bigint' + || typeof expectedIdentity?.inode !== 'bigint' + || expectedIdentity.inode === 0n)) + || typeof options.includeContent !== 'boolean' + || !validExpectedLinks(options.expectedLinks)) { + return relation('unknown', 'invalid-regular-file-binding-expectation'); + } + return null; +} + +function descriptorDigest(descriptor, expectedSize, subject, filePath, includeContent) { + if (expectedSize < 0n || expectedSize > BigInt(Number.MAX_SAFE_INTEGER)) { + return relation('unknown', `${subject}-size-unavailable`, { filePath }); + } + const expectedBytes = Number(expectedSize); + const digest = createHash('sha256'); + let capturedContent; + let chunk; + try { + capturedContent = includeContent ? Buffer.allocUnsafe(expectedBytes) : null; + chunk = capturedContent + || Buffer.allocUnsafe(Math.min(digestChunkBytes, Math.max(1, expectedBytes))); + } catch (error) { + return filesystemFailure(`${subject}-content-inspection-failed`, error, { filePath }); + } + let bytes = 0; + try { + while (bytes < expectedBytes) { + const offset = includeContent ? bytes : 0; + const read = fs.readSync( + descriptor, + chunk, + offset, + Math.min(digestChunkBytes, chunk.byteLength - offset, expectedBytes - bytes), + bytes, + ); + if (read === 0) break; + digest.update(chunk.subarray(offset, offset + read)); + bytes += read; + } + } catch (error) { + return filesystemFailure(`${subject}-content-inspection-failed`, error, { filePath }); + } + return { + status: 'inspected', + sha256: digest.digest('hex'), + bytes, + ...(includeContent ? { buffer: capturedContent.subarray(0, bytes) } : {}), + }; +} + +function sameHandleState(left, right) { + return left.dev === right.dev + && left.ino === right.ino + && left.nlink === right.nlink + && left.mode === right.mode + && left.size === right.size + && left.mtimeNs === right.mtimeNs + && left.ctimeNs === right.ctimeNs; +} + +function inspectRegularFileDescriptor({ + descriptor, + filePath, + identity, + subject, + expectedLinks, + phase, + includeContent = false, +}) { + let beforeHandle; + let beforePath; + let afterHandle; + let afterPath; + let content; + try { + beforeHandle = fs.fstatSync(descriptor, { bigint: true }); + beforePath = fs.lstatSync(filePath, { bigint: true }); + } catch (error) { + return filesystemFailure(`${subject}-handle-${phase}-failed`, error, { filePath }); + } + if (!beforeHandle.isFile() || !beforePath.isFile()) { + return relation('unsupported', `${subject}-not-regular-file`, { + filePath, + entryType: entryType(beforePath), + }); + } + if (beforeHandle.ino === 0n || beforePath.ino === 0n) { + return relation('unknown', `${subject}-identity-unavailable`, { filePath }); + } + if (beforeHandle.dev !== identity.device + || beforeHandle.ino !== identity.inode + || beforePath.dev !== beforeHandle.dev + || beforePath.ino !== beforeHandle.ino) { + return relation(phase === 'inspection' ? 'unknown' : 'different', + `${subject}-${phase === 'inspection' ? 'changed-during-inspection' : 'identity-changed'}`, + { filePath }); + } + if (beforeHandle.nlink === 0n) { + return relation('unknown', `${subject}-link-count-unavailable`, { filePath }); + } + if (beforeHandle.nlink !== BigInt(expectedLinks)) { + if (beforeHandle.nlink > BigInt(expectedLinks)) { + return relation('unsupported', `${subject}-hardlinked`, { + filePath, + links: beforeHandle.nlink.toString(), + }); + } + return relation('different', `${subject}-link-count-changed`, { + filePath, + expectedLinks, + currentLinks: beforeHandle.nlink.toString(), + }); + } + content = descriptorDigest( + descriptor, + beforeHandle.size, + subject, + filePath, + includeContent, + ); + if (content.status !== 'inspected') return content; + try { + afterHandle = fs.fstatSync(descriptor, { bigint: true }); + afterPath = fs.lstatSync(filePath, { bigint: true }); + } catch (error) { + return filesystemFailure(`${subject}-handle-${phase}-failed`, error, { filePath }); + } + if (!afterHandle.isFile() || !afterPath.isFile() + || afterHandle.ino === 0n || afterPath.ino === 0n + || afterHandle.dev !== identity.device || afterHandle.ino !== identity.inode + || afterPath.dev !== afterHandle.dev || afterPath.ino !== afterHandle.ino) { + return relation(phase === 'inspection' ? 'unknown' : 'different', + `${subject}-${phase === 'inspection' ? 'changed-during-inspection' : 'identity-changed'}`, + { filePath }); + } + if (!sameHandleState(beforeHandle, afterHandle)) { + return relation('unknown', `${subject}-changed-during-${phase}`, { filePath }); + } + if (content.bytes !== Number(afterHandle.size)) { + return relation('unknown', `${subject}-changed-during-${phase}`, { filePath }); + } + return { status: 'inspected', metadata: afterHandle, content }; +} + +function closeCapturedDescriptor(descriptor, subject, filePath) { + try { + fs.closeSync(descriptor); + return null; + } catch (error) { + return filesystemFailure(`${subject}-handle-close-failed`, error, { filePath }); + } +} + +/** + * Capture a regular file through an open descriptor. The opaque binding keeps + * the descriptor alive so callers can prove that the same inode, mode and + * bytes are still named by either the original path or a deliberate hard-link + * publication path. Callers must release every successfully captured binding. + */ +export function captureRegularFileBinding(filePath, { + subject = 'file', + expectedSha256, + expectedBytes, + expectedMode, + expectedIdentity, + expectedLinks = 1, + includeContent = false, +} = {}) { + const options = { + expectedSha256, + expectedBytes, + expectedMode, + expectedIdentity, + expectedLinks, + includeContent, + }; + const invalid = validateBindingExpectations(options); + if (invalid) return invalid; + const resolvedPath = path.resolve(filePath); + let initial; + try { + initial = fs.lstatSync(resolvedPath, { bigint: true }); + } catch (error) { + return filesystemFailure(`${subject}-inspection-failed`, error, { filePath: resolvedPath }); + } + const type = entryType(initial); + if (type !== 'file') { + return relation('unsupported', `${subject}-not-regular-file`, { + filePath: resolvedPath, + entryType: type, + }); + } + if (initial.ino === 0n) { + return relation('unknown', `${subject}-identity-unavailable`, { filePath: resolvedPath }); + } + + let descriptor; + try { + const noFollow = process.platform === 'win32' ? 0 : (fs.constants.O_NOFOLLOW || 0); + const nonBlock = fs.constants.O_NONBLOCK || 0; + descriptor = fs.openSync(resolvedPath, fs.constants.O_RDONLY | noFollow | nonBlock); + } catch (error) { + return filesystemFailure(`${subject}-handle-inspection-failed`, error, { filePath: resolvedPath }); + } + const identity = { device: initial.dev, inode: initial.ino }; + if (expectedIdentity !== undefined + && (identity.device !== expectedIdentity.device + || identity.inode !== expectedIdentity.inode)) { + const closeFailure = closeCapturedDescriptor(descriptor, subject, resolvedPath); + return closeFailure || relation('different', `${subject}-identity-changed`, { + filePath: resolvedPath, + }); + } + const inspected = inspectRegularFileDescriptor({ + descriptor, + filePath: resolvedPath, + identity, + subject, + expectedLinks, + phase: 'inspection', + includeContent, + }); + if (inspected.status !== 'inspected') { + const closeFailure = closeCapturedDescriptor(descriptor, subject, resolvedPath); + return closeFailure || inspected; + } + const mode = Number(inspected.metadata.mode & 0o777n); + if (expectedMode !== undefined && mode !== expectedMode) { + const closeFailure = closeCapturedDescriptor(descriptor, subject, resolvedPath); + return closeFailure || relation('different', `${subject}-mode-changed`, { + filePath: resolvedPath, + expectedMode, + currentMode: mode, + }); + } + if (expectedSha256 !== undefined + && (inspected.content.sha256.toLowerCase() !== expectedSha256.toLowerCase() + || inspected.content.bytes !== expectedBytes)) { + const closeFailure = closeCapturedDescriptor(descriptor, subject, resolvedPath); + return closeFailure || relation('different', `${subject}-content-changed`, { + filePath: resolvedPath, + expectedSha256, + currentSha256: inspected.content.sha256, + expectedBytes, + currentBytes: inspected.content.bytes, + }); + } + + const identityKey = `${identity.device}:${identity.inode}`; + let cleanupGroup = regularFileBindingGroups.get(identityKey); + if (!cleanupGroup) { + cleanupGroup = { identityKey, bindings: 0, deferredCleanupDirectories: new Set() }; + regularFileBindingGroups.set(identityKey, cleanupGroup); + } + cleanupGroup.bindings += 1; + const binding = Object.freeze({}); + regularFileBindings.set(binding, { + descriptor, + filePath: resolvedPath, + subject, + expectedLinks, + cleanupGroup, + identity, + mode, + sha256: inspected.content.sha256, + bytes: inspected.content.bytes, + }); + return { + status: 'captured', + binding, + identity: { ...identity, mode, links: inspected.metadata.nlink }, + content: { + sha256: inspected.content.sha256, + bytes: inspected.content.bytes, + ...(includeContent ? { buffer: inspected.content.buffer } : {}), + }, + mode, + }; +} + +/** Verify a captured descriptor against its original or an alternate linked path. */ +export function verifyRegularFileBinding(binding, { filePath, expectedLinks } = {}) { + const captured = regularFileBindings.get(binding); + if (!captured) return relation('unknown', 'regular-file-binding-unavailable'); + const links = expectedLinks ?? captured.expectedLinks; + if (!validExpectedLinks(links)) { + return relation('unknown', 'invalid-regular-file-binding-expectation'); + } + const verificationPath = path.resolve(filePath ?? captured.filePath); + const inspected = inspectRegularFileDescriptor({ + descriptor: captured.descriptor, + filePath: verificationPath, + identity: captured.identity, + subject: captured.subject, + expectedLinks: links, + phase: 'verification', + }); + if (inspected.status !== 'inspected') return inspected; + const mode = Number(inspected.metadata.mode & 0o777n); + if (mode !== captured.mode) { + return relation('different', `${captured.subject}-mode-changed`, { + filePath: verificationPath, + previousMode: captured.mode, + currentMode: mode, + }); + } + if (inspected.content.sha256 !== captured.sha256 + || inspected.content.bytes !== captured.bytes) { + return relation('different', `${captured.subject}-content-changed`, { + filePath: verificationPath, + previousSha256: captured.sha256, + currentSha256: inspected.content.sha256, + previousBytes: captured.bytes, + currentBytes: inspected.content.bytes, + }); + } + return relation('match', `${captured.subject}-binding-match`, { + filePath: verificationPath, + links, + }); +} + +/** Close and invalidate a captured regular-file binding. */ +export function releaseRegularFileBinding(binding) { + const captured = regularFileBindings.get(binding); + if (!captured) return relation('unknown', 'regular-file-binding-unavailable'); + regularFileBindings.delete(binding); + const failure = closeCapturedDescriptor( + captured.descriptor, + captured.subject, + captured.filePath, + ); + const { cleanupGroup } = captured; + cleanupGroup.bindings -= 1; + let cleanupFailure; + if (cleanupGroup.bindings === 0) { + regularFileBindingGroups.delete(cleanupGroup.identityKey); + // A staging registry and its publication transaction can bind the same + // inode independently. SMB may keep a retired name visible until every + // descriptor closes, so cleanup belongs to the inode's last binding. + for (const directory of cleanupGroup.deferredCleanupDirectories) { + const cleanup = removeEmptyQuarantine(directory, { retry: true }); + cleanupFailure ||= cleanup; + } + } + if (failure || cleanupFailure) return failure || cleanupFailure; + return relation('released', `${captured.subject}-binding-released`, { + filePath: captured.filePath, + }); +} + +function createRemovalQuarantine(parentPath) { + for (let attempt = 0; attempt < 8; attempt += 1) { + const directory = path.join( + parentPath, + `.archify-remove-${randomBytes(16).toString('hex')}`, + ); + try { + fs.mkdirSync(directory, { mode: 0o700 }); + return { status: 'created', directory }; + } catch (error) { + if (error?.code !== 'EEXIST') { + return filesystemFailure('removal-quarantine-create-failed', error, { + parentPath, + }); + } + } + } + return relation('unknown', 'removal-quarantine-name-exhausted', { parentPath }); +} + +function removeEmptyQuarantine(directory, { retry = false } = {}) { + let failure; + try { + removeEmptyDirectoryWithRetry(directory, { retry }); + return null; + } catch (error) { + failure = error; + } + return filesystemFailure('removal-quarantine-cleanup-failed', failure, { + recoveryDirectory: directory, + }); +} + +function deferEmptyQuarantineCleanup(binding, cleanup) { + if (cleanup?.reason?.code !== 'removal-quarantine-cleanup-failed' + || cleanup.reason.systemCode !== 'ENOTEMPTY') return false; + const captured = regularFileBindings.get(binding); + if (!captured) return false; + captured.cleanupGroup.deferredCleanupDirectories.add(cleanup.reason.recoveryDirectory); + return true; +} + +function removalRecovery(subject, code, filePath, quarantineDirectory, quarantineFile, error) { + return { + status: 'recovery-required', + reason: { + code: `${subject}-${code}`, + filePath, + ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}), + ...(error?.message ? { message: error.message } : {}), + }, + recoveryDirectory: quarantineDirectory, + recoveryFile: quarantineFile, + target: filePath, + }; +} + +function decimal(value) { + return value.toString(); +} + +function parseDecimal(value) { + if (typeof value !== 'string' || !/^(?:0|[1-9]\d*)$/.test(value)) return null; + try { + return BigInt(value); + } catch { + return null; + } +} + +function parseSafeInteger(value) { + return Number.isSafeInteger(value) && value >= 0 ? value : null; +} + +function serializeRequestedEntry(entry) { + if (entry?.kind === 'absent') return { kind: 'absent' }; + if (entry?.kind !== 'existing' || typeof entry.entryType !== 'string' + || typeof entry.device !== 'bigint' || typeof entry.inode !== 'bigint') { + return null; + } + return { + kind: 'existing', + entryType: entry.entryType, + device: decimal(entry.device), + inode: decimal(entry.inode), + ...(entry.entryType === 'symbolic-link' ? { + changedAtNs: decimal(entry.changedAtNs), + createdAtNs: decimal(entry.createdAtNs), + } : {}), + }; +} + +function createPublicationRecoveryRecord(directory, snapshot, previous) { + const requestedEntry = serializeRequestedEntry(snapshot?.requestedEntry); + const slot = snapshot?.slot; + if (!requestedEntry || !slot || typeof snapshot?.requestedPath !== 'string' + || typeof snapshot?.requestedEntryPolicy !== 'string' + || typeof slot.commitPath !== 'string' || typeof slot.parentPath !== 'string' + || typeof slot.name !== 'string' || typeof slot.parentDevice !== 'bigint' + || typeof slot.parentInode !== 'bigint' || !previous?.identity + || !previous?.content || !Number.isSafeInteger(previous.mode)) { + return relation('unknown', 'publication-recovery-record-input-invalid'); + } + const recordPath = path.join(directory, publicationRecoveryRecordName); + let directoryMetadata; + let parentMetadata; + try { + directoryMetadata = fs.lstatSync(directory, { bigint: true }); + parentMetadata = fs.statSync(path.dirname(directory), { bigint: true }); + } catch (error) { + return filesystemFailure('publication-recovery-directory-inspection-failed', error, { + recoveryDirectory: directory, + }); + } + if (!directoryMetadata.isDirectory() || directoryMetadata.ino === 0n + || !parentMetadata.isDirectory() + || parentMetadata.dev !== slot.parentDevice || parentMetadata.ino !== slot.parentInode + || !/^\.archify-remove-[a-f\d]{32}$/.test(path.basename(directory))) { + return relation('unknown', 'publication-recovery-directory-invalid', { + recoveryDirectory: directory, + }); + } + const record = { + version: publicationRecoveryRecordVersion, + kind: 'archify-retired-output-recovery', + backup: { + name: 'previous', + sha256: previous.content.sha256, + bytes: previous.content.bytes, + mode: previous.mode, + device: decimal(previous.identity.device), + inode: decimal(previous.identity.inode), + }, + recovery: { + name: path.basename(directory), + device: decimal(directoryMetadata.dev), + inode: decimal(directoryMetadata.ino), + }, + target: { + requestedPath: snapshot.requestedPath, + requestedEntryPolicy: snapshot.requestedEntryPolicy, + requestedEntry, + commitPath: slot.commitPath, + parentPath: slot.parentPath, + parentDevice: decimal(slot.parentDevice), + parentInode: decimal(slot.parentInode), + name: slot.name, + }, + }; + const bytes = Buffer.from(`${JSON.stringify(record)}\n`); + let descriptor; + let writeFailure; + try { + descriptor = fs.openSync( + recordPath, + fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, + 0o600, + ); + let offset = 0; + while (offset < bytes.byteLength) { + const wrote = fs.writeSync(descriptor, bytes, offset, bytes.byteLength - offset); + if (wrote <= 0) throw Object.assign(new Error('short recovery record write'), { code: 'EIO' }); + offset += wrote; + } + fs.fsyncSync(descriptor); + } catch (error) { + writeFailure = error; + } finally { + if (descriptor !== undefined) { + try { + fs.closeSync(descriptor); + } catch (error) { + writeFailure ||= error; + } + } + } + if (writeFailure) { + return filesystemFailure('publication-recovery-record-write-failed', writeFailure, { + recoveryDirectory: directory, + recoveryFile: recordPath, + }); + } + // POSIX requires the parent directory entry to be synced separately. Do not + // claim a kill-recovery record is durable where the platform cannot do that. + if (process.platform !== 'win32') { + let directoryDescriptor; + let directoryFailure; + try { + directoryDescriptor = fs.openSync(directory, fs.constants.O_RDONLY); + fs.fsyncSync(directoryDescriptor); + } catch (error) { + directoryFailure = error; + } finally { + if (directoryDescriptor !== undefined) { + try { + fs.closeSync(directoryDescriptor); + } catch (error) { + directoryFailure ||= error; + } + } + } + if (directoryFailure) { + return filesystemFailure('publication-recovery-record-directory-sync-failed', directoryFailure, { + recoveryDirectory: directory, + recoveryFile: recordPath, + }); + } + } + const captured = captureRegularFileBinding(recordPath, { + subject: 'publication-recovery-record', + expectedSha256: createHash('sha256').update(bytes).digest('hex'), + expectedBytes: bytes.byteLength, + // Windows reports a synthetic POSIX mode whose read bits cannot represent + // the private ACL established by the exclusive create above. + expectedMode: process.platform === 'win32' ? undefined : 0o600, + expectedLinks: 1, + }); + if (captured.status !== 'captured') { + return { + ...captured, + recoveryDirectory: directory, + recoveryFile: recordPath, + }; + } + return { + status: 'created', + recordPath, + binding: captured.binding, + }; +} + +function discardPublicationRecoveryRecord(record) { + if (!record?.binding || typeof record.recordPath !== 'string') return null; + let removed; + let released; + try { + removed = quarantineRemoveRegularFileBinding(record.binding, record.recordPath, { + subject: 'publication-recovery-record', + expectedLinks: 1, + }); + } finally { + released = releaseRegularFileBinding(record.binding); + } + if (released.status !== 'released') return released; + return removed.status === 'removed' ? null : removed; +} + +function releasePublicationRecoveryRecord(record) { + if (!record?.binding) return null; + const released = releaseRegularFileBinding(record.binding); + return released.status === 'released' ? null : released; +} + +/** + * Remove a public regular-file name without a verify-to-unlink race. The + * public entry is first moved into an unpredictable private directory and is + * deleted only after the caller's still-open binding proves that the moved + * inode, mode and bytes are the owned entry. If the move captured a successor, + * restore it with a no-clobber hard link or retain it as recovery material. + * The caller retains ownership of `binding` and must release it separately. + */ +export function quarantineRemoveRegularFileBinding(binding, filePath, { + subject = 'public-entry', + expectedLinks = 1, +} = {}) { + if (!validExpectedLinks(expectedLinks)) { + return relation('unknown', 'invalid-regular-file-binding-expectation'); + } + const resolvedPath = path.resolve(filePath); + const beforeMove = verifyRegularFileBinding(binding, { + filePath: resolvedPath, + expectedLinks, + }); + if (beforeMove.status !== 'match') return beforeMove; + + const quarantine = createRemovalQuarantine(path.dirname(resolvedPath)); + if (quarantine.status !== 'created') return quarantine; + const quarantineFile = path.join(quarantine.directory, path.basename(resolvedPath)); + const removeEmpty = () => removeEmptyQuarantine(quarantine.directory); + const removeEmptyAfterFileRemoval = () => { + const cleanup = removeEmpty(); + return cleanup && deferEmptyQuarantineCleanup(binding, cleanup) ? null : cleanup; + }; + + try { + fs.renameSync(resolvedPath, quarantineFile); + } catch (error) { + const cleanup = removeEmpty(); + return cleanup || filesystemFailure(`${subject}-quarantine-move-failed`, error, { + filePath: resolvedPath, + }); + } + + const moved = verifyRegularFileBinding(binding, { + filePath: quarantineFile, + expectedLinks, + }); + if (moved.status === 'match') { + try { + fs.unlinkSync(quarantineFile); + } catch (error) { + return removalRecovery( + subject, + 'quarantine-cleanup-failed', + resolvedPath, + quarantine.directory, + quarantineFile, + error, + ); + } + const cleanup = removeEmptyAfterFileRemoval(); + return cleanup || relation('removed', `${subject}-removed`, { filePath: resolvedPath }); + } + + let displaced; + try { + displaced = fs.lstatSync(quarantineFile, { bigint: true }); + } catch (error) { + return removalRecovery( + subject, + 'replacement-inspection-failed', + resolvedPath, + quarantine.directory, + quarantineFile, + error, + ); + } + if (displaced.ino === 0n || displaced.nlink === 0n) { + return removalRecovery( + subject, + 'replacement-identity-unavailable', + resolvedPath, + quarantine.directory, + quarantineFile, + ); + } + try { + fs.linkSync(quarantineFile, resolvedPath); + } catch (error) { + return removalRecovery( + subject, + error?.code === 'EEXIST' ? 'replacement-restore-blocked' : 'replacement-restore-failed', + resolvedPath, + quarantine.directory, + quarantineFile, + error, + ); + } + + let restored; + let retained; + try { + restored = fs.lstatSync(resolvedPath, { bigint: true }); + retained = fs.lstatSync(quarantineFile, { bigint: true }); + } catch (error) { + return removalRecovery( + subject, + 'replacement-restore-verification-failed', + resolvedPath, + quarantine.directory, + quarantineFile, + error, + ); + } + if (restored.dev !== displaced.dev || restored.ino !== displaced.ino + || retained.dev !== displaced.dev || retained.ino !== displaced.ino + || restored.nlink !== displaced.nlink + 1n + || retained.nlink !== displaced.nlink + 1n) { + return removalRecovery( + subject, + 'replacement-restore-identity-changed', + resolvedPath, + quarantine.directory, + quarantineFile, + ); + } + try { + fs.unlinkSync(quarantineFile); + } catch (error) { + return removalRecovery( + subject, + 'replacement-quarantine-cleanup-failed', + resolvedPath, + quarantine.directory, + quarantineFile, + error, + ); + } + try { + restored = fs.lstatSync(resolvedPath, { bigint: true }); + } catch (error) { + const cleanup = removeEmptyAfterFileRemoval(); + return cleanup || filesystemFailure( + `${subject}-replacement-final-verification-failed`, + error, + { filePath: resolvedPath }, + ); + } + if (restored.dev !== displaced.dev || restored.ino !== displaced.ino + || restored.nlink !== displaced.nlink) { + const cleanup = removeEmptyAfterFileRemoval(); + return cleanup || relation('different', `${subject}-replacement-final-identity-changed`, { + filePath: resolvedPath, + }); + } + const cleanup = removeEmptyAfterFileRemoval(); + return cleanup || relation('preserved', `${subject}-replacement-restored`, { + filePath: resolvedPath, + priorState: moved.reason, + }); +} + +/** + * Create a no-clobber hard-link backup of a bound public file, then remove the + * public name through the quarantine protocol above. The backup is reported as + * verified only after both names have been reconciled with the open binding. + */ +export function backupPublicRegularFileBinding(binding, filePath, backupPath, { + subject = 'public-entry', +} = {}) { + const resolvedPath = path.resolve(filePath); + const resolvedBackup = path.resolve(backupPath); + const beforeLink = verifyRegularFileBinding(binding, { + filePath: resolvedPath, + expectedLinks: 1, + }); + if (beforeLink.status !== 'match') { + return { ...beforeLink, backupCreated: false, backupVerified: false }; + } + try { + fs.linkSync(resolvedPath, resolvedBackup); + } catch (error) { + return { + ...filesystemFailure(`${subject}-backup-link-failed`, error, { + filePath: resolvedPath, + backupPath: resolvedBackup, + }), + backupCreated: false, + backupVerified: false, + }; + } + for (const linkedPath of [resolvedPath, resolvedBackup]) { + const linked = verifyRegularFileBinding(binding, { + filePath: linkedPath, + expectedLinks: 2, + }); + if (linked.status !== 'match') { + return { + status: 'recovery-required', + reason: { + code: `${subject}-backup-binding-mismatch`, + filePath: resolvedPath, + backupPath: resolvedBackup, + linkedState: linked.reason, + }, + recoveryDirectory: path.dirname(resolvedBackup), + recoveryFile: resolvedBackup, + target: resolvedPath, + backupCreated: true, + backupVerified: false, + }; + } + } + const removed = quarantineRemoveRegularFileBinding(binding, resolvedPath, { + subject, + expectedLinks: 2, + }); + if (removed.status !== 'removed') { + return { + ...removed, + backupPath: resolvedBackup, + backupCreated: true, + backupVerified: true, + }; + } + const finalized = verifyRegularFileBinding(binding, { + filePath: resolvedBackup, + expectedLinks: 1, + }); + if (finalized.status !== 'match') { + return { + status: 'recovery-required', + reason: { + code: `${subject}-backup-finalization-failed`, + filePath: resolvedPath, + backupPath: resolvedBackup, + backupState: finalized.reason, + }, + recoveryDirectory: path.dirname(resolvedBackup), + recoveryFile: resolvedBackup, + target: resolvedPath, + backupCreated: true, + backupVerified: false, + }; + } + return { + status: 'backed-up', + reason: { + code: `${subject}-backed-up`, + filePath: resolvedPath, + backupPath: resolvedBackup, + }, + backupPath: resolvedBackup, + backupCreated: true, + backupVerified: true, + }; +} + +/** + * Remove a hard-link name that this process just introduced while preserving + * the source claimant. This is the rollback path when a staged pathname was + * swapped after its expected binding was verified but before `linkSync` used + * that pathname. + */ +export function quarantineRemoveLinkedRegularFileAlias(sourcePath, aliasPath, { + subject = 'linked-claimant', +} = {}) { + const source = path.resolve(sourcePath); + const alias = path.resolve(aliasPath); + const captured = captureRegularFileBinding(source, { + subject, + expectedLinks: 2, + }); + if (captured.status !== 'captured') return captured; + let result; + let released; + try { + const aliasMatches = verifyRegularFileBinding(captured.binding, { + filePath: alias, + expectedLinks: 2, + }); + if (aliasMatches.status !== 'match') { + result = aliasMatches; + } else { + const removal = quarantineRemoveRegularFileBinding(captured.binding, alias, { + subject, + expectedLinks: 2, + }); + if (removal.status !== 'removed') { + result = removal; + } else { + const sourceRemains = verifyRegularFileBinding(captured.binding, { + filePath: source, + expectedLinks: 1, + }); + result = sourceRemains.status === 'match' ? removal : sourceRemains; + } + } + } finally { + released = releaseRegularFileBinding(captured.binding); + } + if (released.status !== 'released') { + return { + ...released, + removalStatus: result?.status, + ...(result?.recoveryDirectory ? { + recoveryDirectory: result.recoveryDirectory, + recoveryFile: result.recoveryFile, + target: result.target, + } : {}), + }; + } + return result; +} + +function removePublishedBindingName(binding, filePath, subject) { + for (const expectedLinks of [2, 1]) { + const matches = verifyRegularFileBinding(binding, { filePath, expectedLinks }); + if (matches.status === 'match') { + return quarantineRemoveRegularFileBinding(binding, filePath, { + subject, + expectedLinks, + }); + } + } + return relation('preserved', `${subject}-identity-changed`, { filePath }); +} + +function restorePublicationBackup(binding, backupPath, commitPath, subject) { + const backupMatches = verifyRegularFileBinding(binding, { + filePath: backupPath, + expectedLinks: 1, + }); + if (backupMatches.status !== 'match') return backupMatches; + try { + fs.linkSync(backupPath, commitPath); + } catch (error) { + return filesystemFailure( + error?.code === 'EEXIST' + ? `${subject}-restore-blocked` + : `${subject}-restore-failed`, + error, + { backupPath, commitPath }, + ); + } + for (const linkedPath of [backupPath, commitPath]) { + const linked = verifyRegularFileBinding(binding, { + filePath: linkedPath, + expectedLinks: 2, + }); + if (linked.status !== 'match') return linked; + } + const retired = quarantineRemoveRegularFileBinding(binding, backupPath, { + subject: `${subject}-backup`, + expectedLinks: 2, + }); + if (retired.status !== 'removed') return retired; + return verifyRegularFileBinding(binding, { + filePath: commitPath, + expectedLinks: 1, + }); +} + +/** + * Publish a captured candidate without ever replacing an unverified public + * entry. Existing output is retained through a bound private backup; the new + * name is created with a no-clobber hard link while the candidate descriptor + * remains open. Both the staged and public names are then verified through + * that descriptor before the staged name is retired via quarantine. + * Replacing an existing entry is recoverable but not crash-atomic: the public + * name is absent between retiring the verified previous name and linking the + * candidate. Portable Node.js exposes no replacement compare-and-swap that + * also preserves a late claimant. + * + * The caller owns `binding` and must release it after this function returns. + */ +export function publishRegularFileBinding(binding, candidatePath, snapshot, { + subject = 'candidate', +} = {}) { + const candidate = path.resolve(candidatePath); + const commitPath = snapshot?.slot?.commitPath; + if (typeof commitPath !== 'string') { + return relation('unknown', `${subject}-publication-snapshot-unavailable`); + } + + const beforePublish = verifyAtomicOutput(snapshot); + if (beforePublish.status !== 'match') return beforePublish; + const candidateMatches = verifyRegularFileBinding(binding, { + filePath: candidate, + expectedLinks: 1, + }); + if (candidateMatches.status !== 'match') return candidateMatches; + + let previousBinding; + let recoveryDirectory; + let backupPath; + let recoveryRecord; + let previousBackedUp = false; + const finishPrevious = () => { + if (!previousBinding) return null; + const released = releaseRegularFileBinding(previousBinding); + previousBinding = undefined; + return released.status === 'released' ? null : released; + }; + const cleanupRecoveryDirectory = () => { + if (!recoveryDirectory) return null; + const cleanup = removeEmptyQuarantine(recoveryDirectory); + if (!cleanup) recoveryDirectory = undefined; + return cleanup; + }; + const restorePrevious = () => { + if (!previousBinding || !previousBackedUp) return null; + return restorePublicationBackup( + previousBinding, + backupPath, + commitPath, + 'previous-output', + ); + }; + const rollback = (failure) => { + let publicRemoval = removePublishedBindingName( + binding, + commitPath, + `${subject}-publication`, + ); + if (publicRemoval.status === 'preserved') { + const claimantRemoval = quarantineRemoveLinkedRegularFileAlias( + candidate, + commitPath, + { subject: `${subject}-publication-claimant` }, + ); + if (claimantRemoval.status === 'removed') publicRemoval = claimantRemoval; + } + const restored = restorePrevious(); + const recordCleanup = restored?.status === 'match' + ? discardPublicationRecoveryRecord(recoveryRecord) + : releasePublicationRecoveryRecord(recoveryRecord); + recoveryRecord = undefined; + const released = finishPrevious(); + const directoryCleanup = cleanupRecoveryDirectory(); + return { + ...failure, + ...(publicRemoval.status !== 'removed' && publicRemoval.status !== 'absent' + ? { publicState: publicRemoval.reason } : {}), + ...(restored && restored.status !== 'match' ? { restoreState: restored.reason } : {}), + ...(recordCleanup ? { recoveryRecordState: recordCleanup.reason } : {}), + ...(released ? { releaseState: released.reason } : {}), + ...(directoryCleanup ? { + recoveryDirectory: directoryCleanup.reason?.recoveryDirectory || recoveryDirectory, + } : {}), + ...((restored && restored.status !== 'match') || recordCleanup || directoryCleanup + ? { recoveryFile: backupPath } : {}), + }; + }; + + if (snapshot.target.kind === 'file') { + const previous = captureRegularFileBinding(commitPath, { + subject: 'previous-output', + expectedIdentity: { + device: snapshot.target.device, + inode: snapshot.target.inode, + }, + expectedMode: snapshot.target.mode, + expectedLinks: 1, + }); + if (previous.status !== 'captured') return previous; + previousBinding = previous.binding; + const quarantine = createRemovalQuarantine(path.dirname(commitPath)); + if (quarantine.status !== 'created') { + const released = finishPrevious(); + return released || quarantine; + } + recoveryDirectory = quarantine.directory; + backupPath = path.join(recoveryDirectory, 'previous'); + const recovery = createPublicationRecoveryRecord(recoveryDirectory, snapshot, previous); + if (recovery.status !== 'created') { + const released = finishPrevious(); + const directoryCleanup = cleanupRecoveryDirectory(); + return { + ...recovery, + ...(recoveryDirectory ? { recoveryDirectory } : {}), + ...(released ? { releaseState: released.reason } : {}), + ...(directoryCleanup ? { directoryCleanupState: directoryCleanup.reason } : {}), + }; + } + recoveryRecord = recovery; + const backup = backupPublicRegularFileBinding( + previousBinding, + commitPath, + backupPath, + { subject: 'previous-output' }, + ); + previousBackedUp = backup.status === 'backed-up'; + if (!previousBackedUp) { + const recordCleanup = backup.backupCreated + ? releasePublicationRecoveryRecord(recoveryRecord) + : discardPublicationRecoveryRecord(recoveryRecord); + recoveryRecord = undefined; + const released = finishPrevious(); + const directoryCleanup = backup.backupCreated ? null : cleanupRecoveryDirectory(); + return { + ...backup, + ...(recordCleanup ? { recoveryRecordState: recordCleanup.reason } : {}), + ...(released ? { releaseState: released.reason } : {}), + ...(directoryCleanup ? { directoryCleanupState: directoryCleanup.reason } : {}), + }; + } + } + + try { + fs.linkSync(candidate, commitPath); + } catch (error) { + return rollback(filesystemFailure( + error?.code === 'EEXIST' + ? 'target-created-during-commit' + : `${subject}-publication-link-failed`, + error, + { candidatePath: candidate, commitPath }, + )); + } + + const published = verifyRegularFileBinding(binding, { + filePath: commitPath, + expectedLinks: 2, + }); + const staged = verifyRegularFileBinding(binding, { + filePath: candidate, + expectedLinks: 2, + }); + if (published.status !== 'match' || staged.status !== 'match') { + return rollback(relation('different', `${subject}-identity-changed`, { + candidatePath: candidate, + commitPath, + publishedState: published.reason, + stagedState: staged.reason, + })); + } + + const retired = quarantineRemoveRegularFileBinding(binding, candidate, { + subject: `${subject}-staging`, + expectedLinks: 2, + }); + if (retired.status !== 'removed' && retired.status !== 'preserved') { + return rollback(retired); + } + const final = verifyRegularFileBinding(binding, { + filePath: commitPath, + expectedLinks: 1, + }); + if (final.status !== 'match') return rollback(final); + + let cleanupWarning = retired.status === 'preserved' ? retired : undefined; + if (previousBinding) { + const backupRemoval = quarantineRemoveRegularFileBinding( + previousBinding, + backupPath, + { subject: 'previous-output-backup', expectedLinks: 1 }, + ); + if (backupRemoval.status !== 'removed') cleanupWarning = backupRemoval; + const recordCleanup = backupRemoval.status === 'removed' + ? discardPublicationRecoveryRecord(recoveryRecord) + : releasePublicationRecoveryRecord(recoveryRecord); + recoveryRecord = undefined; + cleanupWarning ||= recordCleanup; + } + const released = finishPrevious(); + const directoryCleanup = cleanupRecoveryDirectory(); + cleanupWarning ||= released || directoryCleanup; + const recoveryFile = cleanupWarning?.recoveryFile + || (retired.status === 'preserved' ? candidate : undefined) + || (recoveryDirectory ? backupPath : undefined); + return { + status: cleanupWarning ? 'committed-with-warning' : 'committed', + reason: { + code: `${subject}-published`, + candidatePath: candidate, + commitPath, + ...(retired.status === 'preserved' ? { stagedSuccessorPreserved: true } : {}), + }, + ...(cleanupWarning ? { + cleanupState: cleanupWarning.reason, + ...(recoveryDirectory ? { recoveryDirectory } : {}), + ...(recoveryFile ? { recoveryFile } : {}), + } : {}), + }; +} + +/** Remove only the directory entry that still names a caller-owned inode. */ +export function removeOwnedRegularFile(filePath, identity, { subject = 'candidate' } = {}) { + const resolvedPath = path.resolve(filePath); + let expectedLinks = 1; + let captured = captureRegularFileBinding(resolvedPath, { + subject, + expectedIdentity: identity, + expectedLinks, + }); + const reportedLinks = Number(captured.reason?.links); + if (captured.status === 'unsupported' + && captured.reason?.code === `${subject}-hardlinked` + && Number.isSafeInteger(reportedLinks) + && reportedLinks > 1) { + expectedLinks = reportedLinks; + captured = captureRegularFileBinding(resolvedPath, { + subject, + expectedIdentity: identity, + expectedLinks, + }); + } + if (captured.status !== 'captured') { + if (captured.reason?.systemCode === 'ENOENT') { + return relation('absent', `${subject}-already-absent`, { filePath: resolvedPath }); + } + if (captured.status === 'different' || captured.status === 'unsupported') { + return relation('preserved', `${subject}-identity-changed`, { + filePath: resolvedPath, + priorState: captured.reason, + }); + } + return captured; + } + + let removal; + let released; + try { + removal = quarantineRemoveRegularFileBinding(captured.binding, resolvedPath, { + subject, + expectedLinks, + }); + } finally { + released = releaseRegularFileBinding(captured.binding); + } + if (released.status !== 'released') { + return { + ...released, + removalStatus: removal?.status, + ...(removal?.recoveryDirectory ? { + recoveryDirectory: removal.recoveryDirectory, + recoveryFile: removal.recoveryFile, + target: removal.target, + } : {}), + }; + } + return removal; +} + +function captureWriteSlot(requestedPath) { + let commitPath; + try { + commitPath = canonicalFuturePath(requestedPath); + } catch (error) { + const diagnostic = error?.archifyDiagnostics?.[0]; + return relation('unknown', 'canonicalization-failed', { + ...(diagnostic?.code ? { diagnosticCode: diagnostic.code } : {}), + ...(diagnostic?.evidence?.relation ? { relation: diagnostic.evidence.relation } : {}), + }); + } + + const parentPath = path.dirname(commitPath); + let parent; + try { + parent = fs.statSync(parentPath, { bigint: true }); + } catch (error) { + return filesystemFailure('parent-inspection-failed', error, { commitPath, parentPath }); + } + if (!parent.isDirectory()) { + return relation('unknown', 'parent-not-directory', { commitPath, parentPath }); + } + if (parent.ino === 0n) { + return relation('unknown', 'parent-identity-unavailable', { commitPath, parentPath }); + } + return { + status: 'captured', + slot: { + commitPath, + parentPath, + name: path.basename(commitPath), + parentDevice: parent.dev, + parentInode: parent.ino, + }, + }; +} + +function captureTarget(commitPath) { + let metadata; + try { + // The canonical commit path names the directory entry that rename will + // replace. Never follow a link introduced after canonicalization. + metadata = fs.lstatSync(commitPath, { bigint: true }); + } catch (error) { + if (error.code === 'ENOENT') { + return { status: 'captured', target: { kind: 'absent' }, mode: null }; + } + return filesystemFailure('target-inspection-failed', error, { commitPath }); + } + + const type = entryType(metadata); + if (type !== 'file') { + return relation('unsupported', 'target-not-regular-file', { commitPath, entryType: type }); + } + const inspected = captureRegularFileHandle(commitPath, metadata, 'target'); + if (inspected.status !== 'captured') return inspected; + metadata = inspected.metadata; + return { + status: 'captured', + target: { + kind: 'file', + device: metadata.dev, + inode: metadata.ino, + mode: Number(metadata.mode & 0o777n), + }, + mode: Number(metadata.mode & 0o777n), + }; +} + +function captureRequestedEntry(requestedPath, policy) { + let metadata; + try { + metadata = fs.lstatSync(requestedPath, { bigint: true }); + } catch (error) { + if (error.code === 'ENOENT') { + return { status: 'captured', entry: { kind: 'absent' } }; + } + return filesystemFailure('requested-entry-inspection-failed', error, { requestedPath }); + } + const type = entryType(metadata); + if (policy === 'regular-or-absent' && type !== 'file') { + return relation('unsupported', type === 'symbolic-link' + ? 'requested-entry-symbolic-link' + : 'requested-entry-not-regular-file', { + requestedPath, + entryType: type, + }); + } + if (metadata.ino === 0n) { + return relation('unknown', 'requested-entry-identity-unavailable', { requestedPath }); + } + if (policy === 'regular-or-absent') { + const inspected = captureRegularFileHandle(requestedPath, metadata, 'requested-entry'); + if (inspected.status !== 'captured') return inspected; + metadata = inspected.metadata; + } + return { + status: 'captured', + entry: { + kind: 'existing', + entryType: type, + device: metadata.dev, + inode: metadata.ino, + // An unlinked symlink's inode can be reused immediately. Its target can + // stay the same while the requested directory entry is a new object. + // Reading the link only changes atime, so these no-follow timestamps + // distinguish that replacement without tracking target-file writes. + ...(type === 'symbolic-link' ? { + changedAtNs: metadata.ctimeNs, + createdAtNs: metadata.birthtimeNs, + } : {}), + }, + }; +} + +function requestedEntryMatches(left, right) { + if (left.kind !== right.kind) return false; + return left.kind === 'absent' + || (left.entryType === right.entryType + && left.device === right.device + && left.inode === right.inode + && (left.entryType !== 'symbolic-link' + || (left.changedAtNs === right.changedAtNs + && left.createdAtNs === right.createdAtNs))); +} + +/** + * Capture the physical directory-entry slot and target identity used by a + * verified publication. Hard links are intentionally unsupported: replacing + * one name cannot safely update its unknown sibling names. + * `regular-or-absent` additionally rejects requested-entry symlinks and special + * nodes for sidecars whose public name itself must never be followed. + */ +export function captureAtomicOutput(outputPath, { requestedEntryPolicy = 'followable-alias' } = {}) { + if (!['followable-alias', 'regular-or-absent'].includes(requestedEntryPolicy)) { + return relation('unknown', 'invalid-requested-entry-policy', { requestedEntryPolicy }); + } + const requestedPath = path.resolve(outputPath); + const requestedEntry = captureRequestedEntry(requestedPath, requestedEntryPolicy); + if (requestedEntry.status !== 'captured') return requestedEntry; + const slot = captureWriteSlot(requestedPath); + if (slot.status !== 'captured') return slot; + const target = captureTarget(slot.slot.commitPath); + if (target.status !== 'captured') return target; + const confirmedRequestedEntry = captureRequestedEntry(requestedPath, requestedEntryPolicy); + if (confirmedRequestedEntry.status !== 'captured') return confirmedRequestedEntry; + if (!requestedEntryMatches(confirmedRequestedEntry.entry, requestedEntry.entry)) { + return relation('unknown', 'requested-entry-changed-during-inspection', { requestedPath }); + } + return { + status: 'captured', + commitPath: slot.slot.commitPath, + mode: target.mode, + snapshot: { + requestedPath, + requestedEntryPolicy, + requestedEntry: requestedEntry.entry, + slot: slot.slot, + target: target.target, + }, + }; +} + +/** Re-resolve and verify the exact write slot plus its absent/file identity. */ +export function verifyAtomicOutput(snapshot) { + const currentRequestedEntry = captureRequestedEntry( + snapshot.requestedPath, + snapshot.requestedEntryPolicy || 'followable-alias', + ); + if (currentRequestedEntry.status !== 'captured') return currentRequestedEntry; + if (!requestedEntryMatches(currentRequestedEntry.entry, snapshot.requestedEntry)) { + return relation('different', 'requested-entry-changed', { + requestedPath: snapshot.requestedPath, + previousKind: snapshot.requestedEntry.kind === 'absent' + ? 'absent' + : snapshot.requestedEntry.entryType, + currentKind: currentRequestedEntry.entry.kind === 'absent' + ? 'absent' + : currentRequestedEntry.entry.entryType, + }); + } + const currentSlot = captureWriteSlot(snapshot.requestedPath); + if (currentSlot.status !== 'captured') return currentSlot; + const previousSlot = snapshot.slot; + if (currentSlot.slot.parentDevice !== previousSlot.parentDevice + || currentSlot.slot.parentInode !== previousSlot.parentInode + || currentSlot.slot.name !== previousSlot.name) { + return relation('different', 'write-slot-changed', { + previousCommitPath: previousSlot.commitPath, + currentCommitPath: currentSlot.slot.commitPath, + }); + } + + const currentTarget = captureTarget(previousSlot.commitPath); + if (currentTarget.status !== 'captured') return currentTarget; + const previousTarget = snapshot.target; + if (currentTarget.target.kind !== previousTarget.kind) { + return relation('different', 'target-existence-changed', { + commitPath: previousSlot.commitPath, + previousKind: previousTarget.kind, + currentKind: currentTarget.target.kind, + }); + } + if (previousTarget.kind === 'file' + && (currentTarget.target.device !== previousTarget.device + || currentTarget.target.inode !== previousTarget.inode)) { + return relation('different', 'target-identity-changed', { + commitPath: previousSlot.commitPath, + }); + } + if (previousTarget.kind === 'file' + && currentTarget.target.mode !== previousTarget.mode) { + return relation('different', 'target-mode-changed', { + commitPath: previousSlot.commitPath, + previousMode: previousTarget.mode, + currentMode: currentTarget.target.mode, + }); + } + return relation('match', 'atomic-output-match', { commitPath: previousSlot.commitPath }); +} + +function parsePublicationRecoveryRecord(buffer) { + if (!Buffer.isBuffer(buffer) || buffer.byteLength > publicationRecoveryRecordMaxBytes) { + return relation('unknown', 'publication-recovery-record-too-large'); + } + let parsed; + try { + parsed = JSON.parse(buffer.toString('utf8')); + } catch (error) { + return filesystemFailure('publication-recovery-record-invalid-json', error); + } + const backup = parsed?.backup; + const recovery = parsed?.recovery; + const target = parsed?.target; + const requestedEntry = target?.requestedEntry; + const validEntry = requestedEntry?.kind === 'absent' + || (requestedEntry?.kind === 'existing' + && typeof requestedEntry.entryType === 'string' + && parseDecimal(requestedEntry.device) !== null + && parseDecimal(requestedEntry.inode) !== null + && (requestedEntry.entryType !== 'symbolic-link' + || (parseDecimal(requestedEntry.changedAtNs) !== null + && parseDecimal(requestedEntry.createdAtNs) !== null))); + const valid = parsed?.version === publicationRecoveryRecordVersion + && parsed?.kind === 'archify-retired-output-recovery' + && backup?.name === 'previous' + && typeof backup.sha256 === 'string' && /^[a-f\d]{64}$/i.test(backup.sha256) + && parseSafeInteger(backup.bytes) !== null + && Number.isSafeInteger(backup.mode) && backup.mode >= 0 && backup.mode <= 0o777 + && parseDecimal(backup.device) !== null && parseDecimal(backup.inode) !== null + && typeof recovery?.name === 'string' && /^\.archify-remove-[a-f\d]{32}$/.test(recovery.name) + && parseDecimal(recovery.device) !== null + && parseDecimal(recovery.inode) !== null + && typeof target?.requestedPath === 'string' && path.isAbsolute(target.requestedPath) + && typeof target.requestedEntryPolicy === 'string' + && ['followable-alias', 'regular-or-absent'].includes(target.requestedEntryPolicy) + && validEntry + && typeof target.commitPath === 'string' && path.isAbsolute(target.commitPath) + && typeof target.parentPath === 'string' && path.isAbsolute(target.parentPath) + // path-contract-allow: lexical-capability -- A record must exactly decompose the captured canonical target spelling. + && typeof target.name === 'string' && target.name === path.basename(target.commitPath) + // path-contract-allow: lexical-capability -- A record must exactly decompose the captured canonical target spelling. + && target.parentPath === path.dirname(target.commitPath) + && parseDecimal(target.parentDevice) !== null && parseDecimal(target.parentInode) !== null; + if (!valid) return relation('unknown', 'publication-recovery-record-invalid'); + return { + status: 'parsed', + record: { + backup: { + ...backup, + device: parseDecimal(backup.device), + inode: parseDecimal(backup.inode), + }, + recovery: { + ...recovery, + device: parseDecimal(recovery.device), + inode: parseDecimal(recovery.inode), + }, + target: { + ...target, + parentDevice: parseDecimal(target.parentDevice), + parentInode: parseDecimal(target.parentInode), + requestedEntry: requestedEntry.kind === 'absent' ? requestedEntry : { + ...requestedEntry, + device: parseDecimal(requestedEntry.device), + inode: parseDecimal(requestedEntry.inode), + ...(requestedEntry.entryType === 'symbolic-link' ? { + changedAtNs: parseDecimal(requestedEntry.changedAtNs), + createdAtNs: parseDecimal(requestedEntry.createdAtNs), + } : {}), + }, + }, + }, + }; +} + +function verifyPublicationRecoveryDirectory(directory, metadata, record) { + let parentMetadata; + try { + parentMetadata = fs.statSync(path.dirname(directory), { bigint: true }); + } catch (error) { + return filesystemFailure('publication-recovery-parent-inspection-failed', error, { + recoveryDirectory: directory, + }); + } + // path-contract-allow: lexical-capability -- The generated private-directory component is an exact record-bound capability. + if (path.basename(directory) !== record.recovery.name + || !parentMetadata.isDirectory() + || parentMetadata.dev !== record.target.parentDevice + || parentMetadata.ino !== record.target.parentInode) { + return relation('different', 'publication-recovery-directory-not-target-child', { + recoveryDirectory: directory, + expectedParentPath: record.target.parentPath, + }); + } + if (metadata.dev !== record.recovery.device || metadata.ino !== record.recovery.inode) { + return relation('different', 'publication-recovery-directory-identity-changed', { + recoveryDirectory: directory, + }); + } + return relation('match', 'publication-recovery-directory-match', { + recoveryDirectory: directory, + }); +} + +function verifyRetiredPublicationSlot(record) { + const { target } = record; + const slot = captureWriteSlot(target.requestedPath); + if (slot.status !== 'captured') return slot; + // path-contract-allow: lexical-capability -- The recaptured canonical commit spelling must exactly match the recorded capability. + const commitPathChanged = slot.slot.commitPath !== target.commitPath; + // path-contract-allow: lexical-capability -- The recaptured canonical parent spelling must exactly match the recorded capability. + const parentPathChanged = slot.slot.parentPath !== target.parentPath; + if (commitPathChanged + || parentPathChanged + || slot.slot.name !== target.name + || slot.slot.parentDevice !== target.parentDevice + || slot.slot.parentInode !== target.parentInode) { + return relation('different', 'publication-recovery-slot-changed', { + requestedPath: target.requestedPath, + commitPath: target.commitPath, + }); + } + try { + fs.lstatSync(target.commitPath, { bigint: true }); + return relation('preserved', 'publication-recovery-target-present', { + commitPath: target.commitPath, + }); + } catch (error) { + if (error?.code !== 'ENOENT') { + return filesystemFailure('publication-recovery-target-inspection-failed', error, { + commitPath: target.commitPath, + }); + } + } + let requested; + try { + requested = fs.lstatSync(target.requestedPath, { bigint: true }); + } catch (error) { + if (error?.code !== 'ENOENT') { + return filesystemFailure('publication-recovery-requested-entry-inspection-failed', error, { + requestedPath: target.requestedPath, + }); + } + } + if (target.requestedEntry.kind === 'existing' + && target.requestedEntry.entryType === 'symbolic-link') { + if (!requested?.isSymbolicLink() + || requested.dev !== target.requestedEntry.device + || requested.ino !== target.requestedEntry.inode + || requested.ctimeNs !== target.requestedEntry.changedAtNs + || requested.birthtimeNs !== target.requestedEntry.createdAtNs) { + return relation('different', 'publication-recovery-requested-alias-changed', { + requestedPath: target.requestedPath, + }); + } + } else if (requested) { + return relation('different', 'publication-recovery-requested-entry-reclaimed', { + requestedPath: target.requestedPath, + }); + } + return relation('match', 'publication-recovery-slot-match', { + commitPath: target.commitPath, + }); +} + +function releaseRecoveryBindings(result, recordBinding, backupBinding) { + const failures = []; + if (backupBinding) { + const released = releaseRegularFileBinding(backupBinding); + if (released.status !== 'released') failures.push(released.reason); + } + if (recordBinding) { + const released = releaseRegularFileBinding(recordBinding); + if (released.status !== 'released') failures.push(released.reason); + } + if (failures.length === 0) return result; + return { + ...result, + status: 'recovery-required', + releaseState: failures, + }; +} + +/** + * Explicitly restore one retired output after a process crash. This is not a + * directory scanner: callers must name the private recovery directory they + * intend to recover. A record is only a claim; the restored byte stream must + * still match its recorded inode, mode, SHA-256 and size, the original parent + * slot and aliases must still resolve identically, and the public name must be + * absent. `linkSync` is deliberately no-clobber, so a later claimant wins. + */ +export function recoverRetiredPublication(recoveryDirectory) { + const directory = path.resolve(recoveryDirectory); + let directoryMetadata; + try { + directoryMetadata = fs.lstatSync(directory, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT') { + return relation('absent', 'publication-recovery-already-complete', { recoveryDirectory: directory }); + } + return filesystemFailure('publication-recovery-directory-inspection-failed', error, { + recoveryDirectory: directory, + }); + } + if (!directoryMetadata.isDirectory()) { + return relation('unsupported', 'publication-recovery-directory-not-directory', { + recoveryDirectory: directory, + entryType: entryType(directoryMetadata), + }); + } + if (process.platform !== 'win32' && Number(directoryMetadata.mode & 0o077n) !== 0) { + return relation('unsupported', 'publication-recovery-directory-not-private', { + recoveryDirectory: directory, + }); + } + const recordPath = path.join(directory, publicationRecoveryRecordName); + let recordMetadata; + try { + recordMetadata = fs.lstatSync(recordPath, { bigint: true }); + } catch (error) { + return filesystemFailure('publication-recovery-record-inspection-failed', error, { + recoveryDirectory: directory, + recoveryFile: recordPath, + }); + } + if (!recordMetadata.isFile() || recordMetadata.nlink !== 1n + || recordMetadata.size > BigInt(publicationRecoveryRecordMaxBytes)) { + return relation('unsupported', 'publication-recovery-record-not-private-regular-file', { + recoveryDirectory: directory, + recoveryFile: recordPath, + }); + } + const capturedRecord = captureRegularFileBinding(recordPath, { + subject: 'publication-recovery-record', + expectedLinks: 1, + includeContent: true, + }); + if (capturedRecord.status !== 'captured') return capturedRecord; + const parsed = parsePublicationRecoveryRecord(capturedRecord.content.buffer); + if (parsed.status !== 'parsed') { + return releaseRecoveryBindings(parsed, capturedRecord.binding); + } + const recoveryDirectoryState = verifyPublicationRecoveryDirectory( + directory, + directoryMetadata, + parsed.record, + ); + if (recoveryDirectoryState.status !== 'match') { + return releaseRecoveryBindings({ + ...recoveryDirectoryState, + recoveryDirectory: directory, + recoveryFile: recordPath, + }, capturedRecord.binding); + } + const slot = verifyRetiredPublicationSlot(parsed.record); + if (slot.status !== 'match') { + return releaseRecoveryBindings({ + ...slot, + recoveryDirectory: directory, + recoveryFile: recordPath, + }, capturedRecord.binding); + } + const backupPath = path.join(directory, parsed.record.backup.name); + const capturedBackup = captureRegularFileBinding(backupPath, { + subject: 'publication-recovery-backup', + expectedSha256: parsed.record.backup.sha256, + expectedBytes: parsed.record.backup.bytes, + expectedMode: parsed.record.backup.mode, + expectedIdentity: { + device: parsed.record.backup.device, + inode: parsed.record.backup.inode, + }, + expectedLinks: 1, + }); + if (capturedBackup.status !== 'captured') { + return releaseRecoveryBindings({ + ...capturedBackup, + recoveryDirectory: directory, + recoveryFile: backupPath, + }, capturedRecord.binding); + } + // This second check narrows the remaining path-based link window. It cannot + // make `linkSync` descriptor-bound, but it avoids linking after an ordinary + // observed parent, alias, target, or backup change. + const beforeLinkSlot = verifyRetiredPublicationSlot(parsed.record); + const beforeLinkBackup = verifyRegularFileBinding(capturedBackup.binding, { + filePath: backupPath, + expectedLinks: 1, + }); + if (beforeLinkSlot.status !== 'match' || beforeLinkBackup.status !== 'match') { + return releaseRecoveryBindings({ + status: beforeLinkSlot.status === 'match' ? beforeLinkBackup.status : beforeLinkSlot.status, + reason: { + code: 'publication-recovery-pre-link-verification-failed', + slotState: beforeLinkSlot.reason, + backupState: beforeLinkBackup.reason, + }, + recoveryDirectory: directory, + recoveryFile: backupPath, + }, capturedRecord.binding, capturedBackup.binding); + } + try { + fs.linkSync(backupPath, parsed.record.target.commitPath); + } catch (error) { + return releaseRecoveryBindings(relation( + error?.code === 'EEXIST' ? 'preserved' : 'unknown', + error?.code === 'EEXIST' + ? 'publication-recovery-target-created' + : 'publication-recovery-link-failed', + { + recoveryDirectory: directory, + recoveryFile: backupPath, + commitPath: parsed.record.target.commitPath, + ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}), + }, + ), capturedRecord.binding, capturedBackup.binding); + } + const publicState = verifyRegularFileBinding(capturedBackup.binding, { + filePath: parsed.record.target.commitPath, + expectedLinks: 2, + }); + const backupState = verifyRegularFileBinding(capturedBackup.binding, { + filePath: backupPath, + expectedLinks: 2, + }); + if (publicState.status !== 'match' || backupState.status !== 'match') { + // We may have introduced an alias to a pathname that was swapped after the + // pre-link checks. Remove only that alias when it is still a verified + // two-link pair; otherwise preserve every uncertain name for inspection. + const linkedAliasCleanup = quarantineRemoveLinkedRegularFileAlias( + backupPath, + parsed.record.target.commitPath, + { subject: 'publication-recovery-unverified-link' }, + ); + return releaseRecoveryBindings(relation('recovery-required', 'publication-recovery-link-verification-failed', { + recoveryDirectory: directory, + recoveryFile: backupPath, + commitPath: parsed.record.target.commitPath, + publicState: publicState.reason, + backupState: backupState.reason, + linkedAliasCleanup: linkedAliasCleanup.reason, + }), capturedRecord.binding, capturedBackup.binding); + } + const backupRemoval = quarantineRemoveRegularFileBinding(capturedBackup.binding, backupPath, { + subject: 'publication-recovery-backup', + expectedLinks: 2, + }); + if (backupRemoval.status !== 'removed') { + return releaseRecoveryBindings({ + ...backupRemoval, + status: 'recovered-with-warning', + recoveryDirectory: directory, + recoveryFile: backupPath, + target: parsed.record.target.commitPath, + }, capturedRecord.binding, capturedBackup.binding); + } + const recordCleanup = discardPublicationRecoveryRecord({ + recordPath, + binding: capturedRecord.binding, + }); + const directoryCleanup = removeEmptyQuarantine(directory); + const result = { + status: recordCleanup || directoryCleanup ? 'recovered-with-warning' : 'recovered', + reason: { + code: 'publication-recovery-restored', + recoveryDirectory: directory, + commitPath: parsed.record.target.commitPath, + }, + ...(recordCleanup ? { recoveryRecordState: recordCleanup.reason } : {}), + ...(directoryCleanup ? { directoryCleanupState: directoryCleanup.reason } : {}), + }; + return releaseRecoveryBindings(result, undefined, capturedBackup.binding); +} diff --git a/vendor/archify/renderers/shared/brand-marks.mjs b/vendor/archify/renderers/shared/brand-marks.mjs new file mode 100644 index 0000000..5eb520e --- /dev/null +++ b/vendor/archify/renderers/shared/brand-marks.mjs @@ -0,0 +1,670 @@ +import { createHash } from 'node:crypto'; +import { lookup } from 'node:dns/promises'; +import http from 'node:http'; +import https from 'node:https'; +import net from 'node:net'; +import { BRAND_MARKS } from './generated-brand-marks.mjs'; +import { throwDiagnosticError } from './diagnostics.mjs'; +import { esc, textUnits } from './utils.mjs'; + +const COLLECTIONS = Object.freeze({ + architecture: 'components', + workflow: 'nodes', + sequence: 'participants', + dataflow: 'nodes', + lifecycle: 'states', +}); +const MARK_BY_LOOKUP = new Map(); +const MARK_BY_DOMAIN = new Map(); +const RESOLVED_BY_NODE = new WeakMap(); +const RESOLVED_MARK = Symbol('archify.brandMark'); +const MAX_HTML_BYTES = 256 * 1024; +const MAX_IMAGE_BYTES = 1024 * 1024; +const MAX_CAPTURE_CONCURRENCY = 3; +const DEFAULT_CAPTURE_TIMEOUT_MS = 8000; +const USER_AGENT = 'Archify/2.15 brand-preview'; + +function lookupForms(value) { + const raw = String(value ?? '').trim().toLocaleLowerCase('en-US'); + if (!raw) return []; + const dashed = raw.replace(/[\s_]+/g, '-'); + const compact = raw.replace(/[\s_.-]+/g, ''); + return [...new Set([raw, dashed, compact])]; +} + +for (const mark of BRAND_MARKS) { + for (const value of [mark.id, mark.title, ...mark.aliases]) { + for (const form of lookupForms(value)) { + if (!MARK_BY_LOOKUP.has(form)) MARK_BY_LOOKUP.set(form, mark); + } + } + for (const domain of mark.domains) MARK_BY_DOMAIN.set(domain, mark); +} + +function asUrl(value) { + try { + const url = new URL(String(value)); + return ['https:', 'http:'].includes(url.protocol) ? url : null; + } catch { + return null; + } +} + +function domainMark(hostname) { + const host = hostname.toLocaleLowerCase('en-US').replace(/\.$/, ''); + const candidates = [...MARK_BY_DOMAIN.entries()] + .filter(([domain]) => host === domain || host.endsWith(`.${domain}`)) + .sort(([left], [right]) => right.length - left.length); + return candidates[0]?.[1] || null; +} + +export function findBrandMark(value) { + const url = asUrl(value); + if (url) return domainMark(url.hostname); + for (const form of lookupForms(value)) { + const mark = MARK_BY_LOOKUP.get(form); + if (mark) return mark; + } + return null; +} + +export function listBrandMarks(query = '') { + const needle = String(query).trim().toLocaleLowerCase('en-US'); + return BRAND_MARKS.filter((mark) => { + if (!needle) return true; + return [mark.id, mark.title, mark.category, ...mark.aliases, ...mark.domains] + .some((value) => String(value).toLocaleLowerCase('en-US').includes(needle)); + }).map(({ path, ...mark }) => mark); +} + +function ipv4Private(address) { + const parts = address.split('.').map(Number); + if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true; + const [a, b, c] = parts; + return a === 0 || a === 10 || a === 127 || a >= 224 + || (a === 100 && b >= 64 && b <= 127) + || (a === 169 && b === 254) + || (a === 172 && b >= 16 && b <= 31) + || (a === 192 && b === 0 && (c === 0 || c === 2)) + || (a === 192 && b === 88 && c === 99) + || (a === 192 && b === 168) + || (a === 198 && (b === 18 || b === 19)) + || (a === 198 && b === 51 && c === 100) + || (a === 203 && b === 0 && c === 113); +} + +function ipv6Private(address) { + const normalized = address.toLocaleLowerCase('en-US').split('%')[0]; + if (normalized === '::' || normalized === '::1') return true; + if (normalized.startsWith('fc') || normalized.startsWith('fd') || normalized.startsWith('ff') || /^fe[89ab]/.test(normalized)) return true; + if (normalized.startsWith('64:ff9b:') || normalized.startsWith('100:') + || normalized.startsWith('2001:db8:') || normalized.startsWith('2002:')) return true; + const mappedDotted = normalized.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/); + if (mappedDotted) return ipv4Private(mappedDotted[1]); + const mappedHex = normalized.match(/::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); + if (mappedHex) { + const high = Number.parseInt(mappedHex[1], 16); + const low = Number.parseInt(mappedHex[2], 16); + return ipv4Private(`${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}`); + } + const compatibleHex = normalized.match(/^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); + if (compatibleHex) { + const high = Number.parseInt(compatibleHex[1], 16); + const low = Number.parseInt(compatibleHex[2], 16); + return ipv4Private(`${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}`); + } + return false; +} + +export function isPrivateBrandAddress(address) { + const family = net.isIP(address); + return family === 4 ? ipv4Private(address) : (family === 6 ? ipv6Private(address) : true); +} + +function validateUrlShape(url, allowPrivate = process.env.ARCHIFY_BRAND_ALLOW_PRIVATE === '1') { + if (!['https:', 'http:'].includes(url.protocol)) throw new Error('only HTTP(S) brand links are supported'); + if (url.username || url.password) throw new Error('brand links cannot contain credentials'); + const expectedPort = url.protocol === 'https:' ? '443' : '80'; + if (!allowPrivate && url.port && url.port !== expectedPort) { + throw new Error('brand links must use a standard web port'); + } + const host = url.hostname.toLocaleLowerCase('en-US').replace(/\.$/, '').replace(/^\[|\]$/g, ''); + if (!allowPrivate && (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local'))) { + throw new Error('private brand links are not fetched'); + } + return host; +} + +function beforeDeadline(promise, deadline) { + const remaining = deadline - Date.now(); + if (remaining <= 0) return Promise.reject(new Error('brand capture timed out')); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('brand capture timed out')), remaining); + timer.unref?.(); + promise.then( + (value) => { clearTimeout(timer); resolve(value); }, + (error) => { clearTimeout(timer); reject(error); }, + ); + }); +} + +async function resolveRequestTarget(url, deadline) { + const allowPrivate = process.env.ARCHIFY_BRAND_ALLOW_PRIVATE === '1'; + const host = validateUrlShape(url, allowPrivate); + const directFamily = net.isIP(host); + const addresses = directFamily + ? [{ address: host, family: directFamily }] + : await beforeDeadline(lookup(host, { all: true, verbatim: true }), deadline); + if (!addresses.length || (!allowPrivate && addresses.some(({ address }) => isPrivateBrandAddress(address)))) { + throw new Error('private brand links are not fetched'); + } + return addresses[0]; +} + +function timeoutSignal(milliseconds) { + if (typeof AbortSignal.timeout === 'function') return AbortSignal.timeout(milliseconds); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), milliseconds); + timer.unref?.(); + return controller.signal; +} + +function captureTimeoutMilliseconds() { + const configured = Number(process.env.ARCHIFY_BRAND_CAPTURE_TIMEOUT_MS); + if (!Number.isFinite(configured)) return DEFAULT_CAPTURE_TIMEOUT_MS; + return Math.max(100, Math.min(30000, Math.round(configured))); +} + +function requestPinned(url, accept, target, deadline) { + return new Promise((resolve, reject) => { + const transport = url.protocol === 'https:' ? https : http; + const request = transport.request(url, { + method: 'GET', + signal: timeoutSignal(Math.max(1, Math.min(4500, deadline - Date.now()))), + headers: { accept, 'accept-encoding': 'identity', 'user-agent': USER_AGENT }, + // Reuse the exact public address that passed validation. This closes the + // DNS-rebinding gap between checking a hostname and opening its socket. + lookup(_hostname, options, callback) { + if (options?.all) callback(null, [target]); + else callback(null, target.address, target.family); + }, + }, (response) => { + const status = response.statusCode || 0; + resolve({ + status, + ok: status >= 200 && status < 300, + headers: { + get(name) { + const value = response.headers[String(name).toLocaleLowerCase('en-US')]; + return Array.isArray(value) ? value.join(', ') : (value ?? null); + }, + }, + body: response, + }); + }); + request.on('error', reject); + request.end(); + }); +} + +async function checkedFetch(input, accept, deadline) { + let current = new URL(input); + for (let redirects = 0; redirects <= 3; redirects += 1) { + if (Date.now() >= deadline) throw new Error('brand capture timed out'); + const target = await resolveRequestTarget(current, deadline); + const response = await requestPinned(current, accept, target, deadline); + if ([301, 302, 303, 307, 308].includes(response.status)) { + const location = response.headers.get('location'); + response.body.resume(); + if (!location || redirects === 3) throw new Error('brand link redirected too many times'); + current = new URL(location, current); + continue; + } + if (!response.ok) { + response.body.resume(); + throw new Error(`brand link returned HTTP ${response.status}`); + } + // Raw HTTP responses are not decompressed. Check successful bodies before + // HTML discovery or image validation so byte limits and digests stay valid. + const contentEncoding = (response.headers.get('content-encoding') || '').trim().toLowerCase(); + if (contentEncoding && contentEncoding !== 'identity') { + response.body.destroy(); + throw new Error(`unsupported brand content encoding ${contentEncoding}`); + } + return { response, finalUrl: current }; + } + throw new Error('brand link redirected too many times'); +} + +async function readLimited(response, maximum) { + const declared = Number(response.headers.get('content-length')); + if (Number.isFinite(declared) && declared > maximum) { + response.body?.destroy?.(); + throw new Error('brand asset is too large'); + } + if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { + const chunks = []; + let total = 0; + for await (const value of response.body) { + total += value.byteLength; + if (total > maximum) { + response.body.destroy?.(); + throw new Error('brand asset is too large'); + } + chunks.push(Buffer.from(value)); + } + return Buffer.concat(chunks, total); + } + if (!response.body?.getReader) { + const buffer = Buffer.from(await response.arrayBuffer()); + if (buffer.length > maximum) throw new Error('brand asset is too large'); + return buffer; + } + const reader = response.body.getReader(); + const chunks = []; + let total = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maximum) { + await reader.cancel(); + throw new Error('brand asset is too large'); + } + chunks.push(Buffer.from(value)); + } + return Buffer.concat(chunks, total); +} + +// Read only the bounded head, independent of network chunk boundaries. Scan +// bytes once so many tiny chunks cannot cause repeated concatenation/rescanning. +// This is a boundary scanner, not a DOM parser: comments, quoted attributes and +// raw-text elements must not turn a literal into an early stop. +async function readHtmlHead(response, maximum) { + const chunks = response.body && typeof response.body[Symbol.asyncIterator] === 'function' + ? response.body : [await readLimited(response, maximum)]; + const buffer = Buffer.alloc(maximum); + let total = 0; + let tagStart = -1; + let quote = 0; + let comment = false; + let rawClosing = ''; + let matched = 0; + for await (const value of chunks) { + const chunk = Buffer.from(value); + const length = Math.min(chunk.length, maximum - total); + chunk.copy(buffer, total, 0, length); + for (let offset = 0; offset < length; offset++) { + const byte = chunk[offset]; + const position = total + offset; + if (comment) { + if (byte === 0x3e && buffer[position - 1] === 0x2d && buffer[position - 2] === 0x2d) comment = false; + continue; + } + if (rawClosing) { + const lower = byte >= 65 && byte <= 90 ? byte + 32 : byte; + if (matched === rawClosing.length && [9, 10, 12, 13, 32, 47, 62].includes(byte)) { + tagStart = position - matched; + rawClosing = ''; + matched = 0; + } else { + matched = lower === rawClosing.charCodeAt(matched) ? matched + 1 : (byte === 0x3c ? 1 : 0); + continue; + } + } + if (tagStart < 0) { + if (byte === 0x3c) tagStart = position; + continue; + } + if (position === tagStart + 1 && !((byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || [33, 47, 63].includes(byte))) { + tagStart = byte === 0x3c ? position : -1; + continue; + } + if (position === tagStart + 3 && buffer[tagStart + 1] === 0x21 && buffer[tagStart + 2] === 0x2d && byte === 0x2d) { + comment = true; + tagStart = -1; + continue; + } + if (quote) { + if (byte === quote) quote = 0; + continue; + } + if (byte === 0x22 || byte === 0x27) { + quote = byte; + continue; + } + if (byte === 0x3e) { + const tag = buffer.toString('utf8', tagStart, position + 1); + if (/^<\/head[\t\n\f\r ]*>$/i.test(tag)) { + response.body?.destroy?.(); + return buffer.toString('utf8', 0, position + 1); + } + const raw = /^<(script|style|title|textarea|xmp|iframe|noembed|noframes)(?=[\t\n\f\r />])/i.exec(tag); + if (raw) rawClosing = ` length) { + response.body?.destroy?.(); + throw new Error('brand asset is too large'); + } + } + return buffer.toString('utf8', 0, total); +} + +function attribute(tag, name) { + const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i')); + return match ? (match[1] ?? match[2] ?? match[3] ?? '') : ''; +} + +// HTML numeric references in the C1 range use the legacy Windows-1252 mapping. +// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state +const HTML_C1_REFERENCES = [ + 0x20ac, 0x81, 0x201a, 0x192, 0x201e, 0x2026, 0x2020, 0x2021, + 0x2c6, 0x2030, 0x160, 0x2039, 0x152, 0x8d, 0x17d, 0x8f, + 0x90, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, + 0x2dc, 0x2122, 0x161, 0x203a, 0x153, 0x9d, 0x17e, 0x178, +]; +const BASIC_HTML_REFERENCES = { amp: '&', quot: '"', apos: "'", lt: '<', gt: '>' }; + +function decodeIconHref(value) { + // Decode only after extracting the attribute, in one pass. Leave percent + // escapes to URL parsing and do not reinterpret decoded quotes as markup. + return value.replace(/&#(?:[xX]([0-9a-fA-F]+)|([0-9]+));?|&(amp|AMP|quot|QUOT|lt|LT|gt|GT)(?:;|(?![A-Za-z0-9=]))|&(apos);/g, + (_match, hex, decimal, named, apostrophe) => { + if (named || apostrophe) return BASIC_HTML_REFERENCES[(named || apostrophe).toLowerCase()]; + let point = Number.parseInt(hex || decimal, hex ? 16 : 10); + if (point === 0 || point > 0x10ffff || (point >= 0xd800 && point <= 0xdfff)) return '\uFFFD'; + if (point >= 0x80 && point <= 0x9f) point = HTML_C1_REFERENCES[point - 0x80]; + return String.fromCodePoint(point); + }); +} + +function iconCandidates(html, pageUrl) { + const candidates = []; + for (const match of html.matchAll(/]*>/gi)) { + const tag = match[0]; + const rel = attribute(tag, 'rel').toLocaleLowerCase('en-US').split(/\s+/); + if (!rel.some((value) => value === 'icon' || value === 'apple-touch-icon' || value === 'mask-icon')) continue; + const href = decodeIconHref(attribute(tag, 'href')); + if (!href) continue; + try { + const url = new URL(href, pageUrl); + if (!['https:', 'http:'].includes(url.protocol)) continue; + const type = attribute(tag, 'type').toLocaleLowerCase('en-US'); + const sizes = attribute(tag, 'sizes'); + const area = [...sizes.matchAll(/(\d+)x(\d+)/gi)] + .reduce((best, size) => Math.max(best, Number(size[1]) * Number(size[2])), 0); + const score = (type.includes('svg') || /\.svg(?:$|[?#])/i.test(url.href) ? 1000000 : 0) + + (rel.includes('apple-touch-icon') ? 500000 : 0) + + area; + candidates.push({ url, score }); + } catch { + // A malformed icon candidate is ignored; the deterministic fallback remains available. + } + } + candidates.sort((left, right) => right.score - left.score); + const fallback = new URL('/favicon.ico', pageUrl); + const unique = new Map(candidates.map((candidate) => [candidate.url.href, candidate])); + unique.delete(fallback.href); + return [...unique.values()].slice(0, 5).concat({ url: fallback, score: -1 }); +} + +async function imageData(response) { + const contentType = (response.headers.get('content-type') || '').split(';')[0].trim().toLocaleLowerCase('en-US'); + const allowed = new Set([ + 'image/png', + 'image/jpeg', + 'image/webp', + 'image/x-icon', + 'image/vnd.microsoft.icon', + ]); + if (!allowed.has(contentType)) { + response.body?.destroy?.(); + throw new Error(`unsupported brand image type ${contentType || 'unknown'}`); + } + const buffer = await readLimited(response, MAX_IMAGE_BYTES); + const signatureMatches = contentType === 'image/png' + ? buffer.length >= 45 + && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) + && buffer.readUInt32BE(8) === 13 + && buffer.toString('ascii', 12, 16) === 'IHDR' + && buffer.readUInt32BE(16) > 0 + && buffer.readUInt32BE(20) > 0 + && buffer.toString('ascii', buffer.length - 8, buffer.length - 4) === 'IEND' + : (contentType === 'image/jpeg' + ? buffer.length >= 20 + && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff + && buffer.at(-2) === 0xff && buffer.at(-1) === 0xd9 + : (contentType === 'image/webp' + ? buffer.length >= 16 + && buffer.toString('ascii', 0, 4) === 'RIFF' + && buffer.toString('ascii', 8, 12) === 'WEBP' + && buffer.readUInt32LE(4) + 8 <= buffer.length + : buffer.length >= 22 + && buffer[0] === 0 && buffer[1] === 0 && buffer[2] === 1 && buffer[3] === 0 + && buffer.readUInt16LE(4) > 0 + && 6 + buffer.readUInt16LE(4) * 16 <= buffer.length)); + if (!signatureMatches) throw new Error(`brand asset bytes do not match ${contentType}`); + return { + dataUrl: `data:${contentType};base64,${buffer.toString('base64')}`, + sha256: createHash('sha256').update(buffer).digest('hex'), + contentType, + }; +} + +async function captureRemoteBrand(value, deadline = Date.now() + captureTimeoutMilliseconds()) { + const sourceUrl = new URL(value); + const fallback = (reason) => ({ + id: sourceUrl.hostname, + title: sourceUrl.hostname, + category: 'link', + kind: 'fallback', + status: 'unavailable', + sourceUrl: sourceUrl.href, + reason, + }); + try { + const page = await checkedFetch(sourceUrl, 'text/html,application/xhtml+xml,image/*;q=0.8', deadline); + const pageType = (page.response.headers.get('content-type') || '').toLocaleLowerCase('en-US'); + if (pageType.startsWith('image/')) { + const image = await imageData(page.response); + return { + id: sourceUrl.hostname, + title: sourceUrl.hostname, + category: 'link', + kind: 'remote', + status: 'captured', + sourceUrl: sourceUrl.href, + resolvedUrl: page.finalUrl.href, + ...image, + }; + } + if (!pageType.includes('text/html') && !pageType.includes('application/xhtml+xml')) { + page.response.body?.destroy?.(); + return fallback('linked page is not HTML'); + } + const html = await readHtmlHead(page.response, MAX_HTML_BYTES); + const iconErrors = []; + for (const candidate of iconCandidates(html, page.finalUrl)) { + try { + const fetched = await checkedFetch(candidate.url, 'image/*', deadline); + const image = await imageData(fetched.response); + return { + id: sourceUrl.hostname, + title: sourceUrl.hostname, + category: 'link', + kind: 'remote', + status: 'captured', + sourceUrl: sourceUrl.href, + resolvedUrl: fetched.finalUrl.href, + ...image, + }; + } catch (error) { + iconErrors.push(error); + // Try the next declared favicon before using the generic link mark. + } + } + const usefulError = iconErrors.find((error) => /unsupported brand (?:image type|content encoding)/i.test(error?.message)) + || iconErrors.at(-1); + return fallback(usefulError?.message || 'no usable site icon was found'); + } catch (error) { + return fallback(error.message); + } +} + +export async function captureBrandReference(value) { + const url = asUrl(value); + if (!url) throw new Error('brand capture requires one HTTP(S) URL'); + validateUrlShape(url); + const preset = findBrandMark(url.href); + if (preset) return { brand: preset.id, resolved: { ...preset, kind: 'preset', status: 'preset' } }; + const resolved = await captureRemoteBrand(url.href); + if (resolved.status !== 'captured' || !resolved.sha256) { + throw new Error(`brand capture failed: ${resolved.reason || 'no usable site icon was found'}`); + } + return { + brand: { url: url.href, sha256: resolved.sha256 }, + resolved, + }; +} + +function remoteBrand(value, cache, deadline) { + const key = new URL(value).href; + if (!cache.has(key)) cache.set(key, captureRemoteBrand(key, deadline)); + return cache.get(key); +} + +function suggestions(value) { + const needle = lookupForms(value)[0] || ''; + return BRAND_MARKS.map((mark) => ({ + id: mark.id, + score: lookupForms(mark.id).some((form) => form.includes(needle) || needle.includes(form)) ? 0 : 1, + })).sort((left, right) => left.score - right.score || left.id.localeCompare(right.id)) + .slice(0, 5) + .map((entry) => entry.id); +} + +async function mapConcurrent(values, limit, visit) { + let cursor = 0; + const workers = Array.from({ length: Math.min(limit, values.length) }, async () => { + while (cursor < values.length) { + const index = cursor; + cursor += 1; + await visit(values[index], index); + } + }); + await Promise.all(workers); +} + +export async function prepareDiagramBrandMarks(diagramType, diagram) { + const collection = COLLECTIONS[diagramType]; + const nodes = collection && Array.isArray(diagram[collection]) ? diagram[collection] : []; + const unknown = []; + const remoteByUrl = new Map(); + const deadline = Date.now() + captureTimeoutMilliseconds(); + await mapConcurrent(nodes, MAX_CAPTURE_CONCURRENCY, async (node, index) => { + if (!node.brand) return; + if (typeof node.brand === 'object') { + const url = asUrl(node.brand.url); + const resolved = url ? await remoteBrand(url.href, remoteByUrl, deadline) : null; + if (!resolved || resolved.status !== 'captured') { + unknown.push(`/${collection}/${index}/brand could not reproduce the pinned capture: ${resolved?.reason || 'invalid URL'}`); + return; + } + if (resolved.sha256 !== node.brand.sha256) { + unknown.push(`/${collection}/${index}/brand digest changed: expected ${node.brand.sha256}, received ${resolved.sha256}`); + return; + } + node[RESOLVED_MARK] = resolved; + RESOLVED_BY_NODE.set(node, resolved); + return; + } + const preset = findBrandMark(node.brand); + if (preset) { + const resolved = { ...preset, kind: 'preset', status: 'preset', sourceUrl: preset.provenance.source }; + node[RESOLVED_MARK] = resolved; + RESOLVED_BY_NODE.set(node, resolved); + return; + } + const url = asUrl(node.brand); + if (url) { + unknown.push(`/${collection}/${index}/brand ${JSON.stringify(node.brand)} is an unpinned URL; capture it first with \`archify brands capture ${url.href} --json\``); + return; + } + unknown.push(`/${collection}/${index}/brand ${JSON.stringify(node.brand)} is not a built-in brand; closest IDs: ${suggestions(node.brand).join(', ')}`); + }); + if (unknown.length) { + throwDiagnosticError(`Brand mark validation failed:\n- ${unknown.join('\n- ')}`, unknown.map((message) => ({ + code: message.includes('is an unpinned URL') ? 'brand/unpinned-url' + : (message.includes('digest changed') ? 'brand/digest-mismatch' + : (message.includes('could not reproduce') ? 'brand/capture-unavailable' : 'brand/unknown')), + severity: 'error', + message, + subject: { diagramType, collection }, + evidence: {}, + supportedFixes: message.includes('is an unpinned URL') + ? ['run `archify brands capture --json` and author the returned digest-pinned brand object'] + : ['choose an ID from `archify brands`', 'run `archify brands capture --json` for an unknown official site'], + }))); + } +} + +export function brandMarkFor(node) { + return node?.[RESOLVED_MARK] || RESOLVED_BY_NODE.get(node) || null; +} + +export function brandMetadataFor(node) { + const mark = brandMarkFor(node); + return mark ? { + brand: mark.title, + brandId: mark.id, + brandStatus: mark.status, + brandSource: mark.sourceUrl, + } : {}; +} + +export function brandLabelFitWidth(node, width) { + return brandMarkFor(node) ? Math.max(1, width - 48) : width; +} + +export function brandTopRailProblem(node, width, minimumFontSize, subject = 'Node') { + if (!brandMarkFor(node)) return null; + const available = width - 48; + const required = textUnits(node.label) * minimumFontSize * 0.6; + if (available >= required) return null; + return `${subject} "${node.id}" brand top rail leaves ${Math.max(0, available)}px for its label, but ` + + `"${node.label}" needs ~${Math.ceil(required)}px at the ${minimumFontSize}px legible minimum — widen the node or shorten the label.`; +} + +function markAttrs(mark) { + return [ + `data-brand-mark="${esc(mark.id)}"`, + `data-brand-title="${esc(mark.title)}"`, + `data-brand-status="${esc(mark.status)}"`, + mark.sourceUrl ? `data-brand-source="${esc(mark.sourceUrl)}"` : '', + mark.sha256 ? `data-brand-sha256="${esc(mark.sha256)}"` : '', + ].filter(Boolean).join(' '); +} + +export function renderBrandMark(node, { x, y, size = 16 } = {}) { + const mark = brandMarkFor(node); + if (!mark) return ''; + const inset = 3; + let content; + if (mark.kind === 'preset') { + const scale = (size - inset * 2) / mark.viewBox; + content = ``; + } else if (mark.kind === 'remote') { + content = ``; + } else { + const scale = size / 20; + content = ``; + } + return ``; +} diff --git a/vendor/archify/renderers/shared/cli.mjs b/vendor/archify/renderers/shared/cli.mjs new file mode 100644 index 0000000..bc4a5e3 --- /dev/null +++ b/vendor/archify/renderers/shared/cli.mjs @@ -0,0 +1,451 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { applyTemplate, renderCards, esc } from './utils.mjs'; +import { validateSchema } from './validator.mjs'; +import { verifyRepositoryEvidence } from './repository-evidence.mjs'; +import { installRendererDiagnosticBoundary, throwDiagnosticError, throwDiagnosticProblems, recordDiagnostic } from './diagnostics.mjs'; +import { validateEngineeringProfile } from './engineering-profiles.mjs'; +import { + resolveOutputPath, + validateAuthoredOutputPath, +} from './output-path.mjs'; +import { + captureAtomicOutput, + captureRegularFileBinding, + publishRegularFileBinding, + releaseRegularFileBinding, + removeOwnedRegularFile, + verifyAtomicOutput, +} from './atomic-output.mjs'; +import { resolveLocale, translateMessage, registerLocale, SUPPORTED_LOCALES } from './i18n.mjs'; +import { prepareDiagramBrandMarks } from './brand-marks.mjs'; + +const outputPathGuards = new Map(); +let renderCandidateSequence = 0; + +// meta.locale is renderer-owned Viewer UI, not authored content. +// en and zh-CN ship as built-in catalogs. +// Any other tag needs meta.translations (validated against the English +// message-key set, layered over English per-key so partial/invalid entries +// never break rendering) or it falls back to the English Viewer chrome — +// the same "omit locale, disclose the fallback" contract as before, just +// resolved from data instead of a hard-coded enum. See i18n.mjs. +function applyLocaleTranslations(diagramType, diagram) { + const locale = diagram.meta?.locale; + if (!locale) return; + const translations = diagram.meta?.translations; + if (translations && Object.keys(translations).length) { + const report = registerLocale(locale, translations); + if (report.missingKeys.length || report.unknownKeys.length || report.placeholderMismatches.length) { + recordDiagnostic({ + code: 'i18n/translation-coverage', + severity: 'warning', + message: `meta.translations for locale ${JSON.stringify(locale)} covers ${report.coveredKeys}/${report.totalKeys} renderer-owned messages (${Math.round(report.coverage * 100)}%); uncovered keys fall back to English.`, + subject: { diagramType, path: '/meta/translations' }, + evidence: { + missingKeys: report.missingKeys.slice(0, 10), + missingKeysTotal: report.missingKeys.length, + unknownKeys: report.unknownKeys.slice(0, 10), + unknownKeysTotal: report.unknownKeys.length, + placeholderMismatches: report.placeholderMismatches.slice(0, 10), + placeholderMismatchesTotal: report.placeholderMismatches.length, + }, + supportedFixes: ['Add the missing keys to meta.translations.', 'Match each translation\'s {placeholders} to the English source string.'], + }); + // Coverage is a fact about this render, not just a diagnostic-mode + // artifact: print it to stderr unconditionally so `render`/`deliver`/ + // `validate` disclose the fallback even without ARCHIFY_DIAGNOSTIC_FORMAT. + console.warn(`archify: meta.translations for locale ${JSON.stringify(locale)} covers ${report.coveredKeys}/${report.totalKeys} renderer-owned messages (${Math.round(report.coverage * 100)}%); uncovered keys fall back to English.`); + } + } else if (!SUPPORTED_LOCALES.includes(locale)) { + recordDiagnostic({ + code: 'i18n/locale-fallback', + severity: 'warning', + message: `meta.locale ${JSON.stringify(locale)} has no built-in catalog and no meta.translations; the Viewer chrome and fall back to English.`, + subject: { diagramType, path: '/meta/locale' }, + supportedFixes: ['Supply meta.translations for this locale.', `Use a built-in locale: ${SUPPORTED_LOCALES.join(', ')}.`], + }); + console.warn(`archify: meta.locale ${JSON.stringify(locale)} has no built-in catalog and no meta.translations; the Viewer chrome and fall back to English.`); + } +} + +// Common CLI head: node render-.mjs [input.json] [output.html] +// Keep this synchronous because callers also use it to establish the guarded +// output path before testing a last-moment filesystem alias change. +export function loadDiagram({ rendererDir, diagramType, defaultExample, argv = process.argv }) { + // Compilers also import this module for SVG helpers. Only CLI execution + // should install a process-level handler, before reading or validating input. + installRendererDiagnosticBoundary(); + const skillRoot = path.resolve(rendererDir, '../..'); + const inputPath = path.resolve(argv[2] || path.join(skillRoot, 'examples', defaultExample)); + let input; + try { + input = fs.readFileSync(inputPath, 'utf8'); + } catch (error) { + if (!isFilesystemError(error)) throw error; + const message = `Input could not be read: ${error.message}`; + throwDiagnosticError(message, [{ + code: 'input/read', message, + subject: { input: inputPath }, + evidence: { systemCode: error.code, reason: error.message }, + supportedFixes: ['provide one readable JSON input file'], + }]); + } + let diagram; + try { + diagram = JSON.parse(input); + } catch (error) { + if (!(error instanceof SyntaxError)) throw error; + const message = `Input JSON could not be parsed: ${error.message}`; + throwDiagnosticError(message, [{ + code: 'input/json-parse', message, + subject: { input: inputPath }, + evidence: { reason: error.message }, + supportedFixes: ['repair the JSON syntax and run validation again'], + }]); + } + const authoredOutput = diagram?.meta?.output; + if (authoredOutput !== undefined) validateAuthoredOutputPath(authoredOutput); + validateSchema(diagramType, diagram); + applyLocaleTranslations(diagramType, diagram); + validateCrossCollectionContracts(diagramType, diagram); + validateEngineeringProfile(diagramType, diagram); + const sourceEvidence = verifyRepositoryEvidence(diagramType, diagram, process.env.ARCHIFY_REPO_ROOT); + const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8'); + const outputRequest = { + requestedOutput: argv[3], + authoredOutput: diagram.meta?.output, + defaultOutput: `${diagramType}.html`, + inputPaths: [inputPath], + cwd: process.cwd(), + }; + let outPath; + try { + ({ outputPath: outPath } = resolveOutputPath(outputRequest)); + } catch (error) { + throwOutputError(error, path.resolve(outputRequest.requestedOutput || outputRequest.authoredOutput || outputRequest.defaultOutput)); + } + outputPathGuards.set(outPath, outputRequest); + return { diagram, template, outPath, sourceEvidence }; +} + +// Brand URL capture is the only asynchronous authoring step. Typed renderers +// opt into it through this wrapper without changing loadDiagram's long-lived +// synchronous safety contract. +export async function loadDiagramWithBrandMarks(options) { + const loaded = loadDiagram(options); + await prepareDiagramBrandMarks(options.diagramType, loaded.diagram); + return loaded; +} + +const START_TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']); + +function isFilesystemError(error) { + return typeof error?.code === 'string' + && typeof error?.syscall === 'string' + && typeof error?.errno === 'number'; +} + +function throwOutputError(error, output) { + if (error?.archifyDiagnostics || !isFilesystemError(error)) throw error; + const message = `Output could not be written: ${error.message}`; + throwDiagnosticError(message, [{ + code: 'output/write', message, + subject: { output }, + evidence: { systemCode: error.code, reason: error.message }, + supportedFixes: ['choose a writable HTML file path and ensure its parent directories can be created'], + }]); +} + +function throwAtomicOutputFailure(result, output) { + const reason = result.reason || { code: 'unclassified' }; + const changed = result.status === 'different'; + const candidate = reason.code.startsWith('candidate-'); + const nonRegular = ['target-not-regular-file', 'candidate-not-regular-file'].includes(reason.code); + const hardlinked = ['target-hardlinked', 'candidate-hardlinked'].includes(reason.code); + const message = changed + ? 'Output target changed while the rendered artifact was being prepared.' + : nonRegular + ? candidate + ? 'Temporary output candidate is no longer a regular file.' + : 'Output already exists and is not a regular file.' + : hardlinked + ? candidate + ? 'Temporary output candidate has multiple hard-link names.' + : 'Output already exists through multiple hard-link names.' + : 'Output target stability could not be determined safely before commit.'; + throwDiagnosticError(message, [{ + code: changed + ? 'output/target-changed' + : nonRegular + ? 'output/target-not-regular-file' + : hardlinked + ? 'output/target-hardlinked' + : 'output/target-indeterminate', + message, + subject: { output }, + evidence: { relation: reason }, + supportedFixes: [hardlinked + ? 'choose a non-hardlinked output path; atomic replacement cannot update every hard-link name' + : 'retry after other processes stop replacing or redirecting the output path'], + }]); +} + +function stageRenderedHtml(outputPath, html, mode) { + for (let attempt = 0; attempt < 100; attempt += 1) { + renderCandidateSequence += 1; + const candidatePath = path.join( + path.dirname(outputPath), + `.archify-render-${process.pid}-${Date.now().toString(36)}-${renderCandidateSequence}.tmp`, + ); + let descriptor; + let identity; + try { + const noFollow = process.platform === 'win32' ? 0 : (fs.constants.O_NOFOLLOW || 0); + descriptor = fs.openSync( + candidatePath, + fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, + mode ?? 0o666, + ); + let metadata; + try { + metadata = fs.fstatSync(descriptor, { bigint: true }); + } catch (error) { + // A transient first inspection failure must not strand the exclusive + // candidate. A successful retry binds cleanup to the still-open file; + // if both inspections fail, preserving the unknown entry is safer. + try { + const retry = fs.fstatSync(descriptor, { bigint: true }); + if (retry.isFile() && retry.ino !== 0n) { + identity = { device: retry.dev, inode: retry.ino }; + } + } catch {} + throw error; + } + if (!metadata.isFile() || metadata.ino === 0n) { + throw new Error('Temporary render candidate identity could not be verified safely.'); + } + identity = { device: metadata.dev, inode: metadata.ino }; + fs.writeFileSync(descriptor, html); + // Creation modes are filtered through the process umask. An atomic + // replacement must retain the exact permissions of an existing target, + // while a brand-new target should keep normal umask behavior. + if (mode !== null) fs.fchmodSync(descriptor, mode); + fs.closeSync(descriptor); + descriptor = undefined; + return { candidatePath, identity }; + } catch (error) { + if (descriptor !== undefined) { + try { fs.closeSync(descriptor); } catch {} + } + if (error.code === 'EEXIST') continue; + if (identity) { + const cleanup = removeOwnedRegularFile(candidatePath, identity); + if (!['removed', 'absent', 'preserved'].includes(cleanup.status)) { + const cleanupError = new Error(`${error.message}; temporary render candidate cleanup also failed.`); + cleanupError.cause = error; + throw cleanupError; + } + } + throw error; + } + } + const error = new Error(`Could not reserve a temporary render candidate beside "${outputPath}".`); + error.code = 'EEXIST'; + error.errno = -17; + error.syscall = 'open'; + throw error; +} + +// Common CLI tail: fill the template and write the standalone HTML file. +export function writeDiagram({ outPath, template, diagramType, meta, svg, cards, sourceEvidence = null }) { + if (!START_TYPES.has(diagramType)) throw new Error(`writeDiagram: unknown diagram type ${JSON.stringify(diagramType)}`); + const outputGuard = outputPathGuards.get(outPath); + const html = applyTemplate(template, { + title: meta.title, + subtitle: meta.subtitle, + svg, + cards: renderCards(cards), + locale: meta.locale, + visualPreset: meta.visual_preset || 'classic', + sourceEvidence, + }); + let candidatePath; + let candidateIdentity; + let candidateBinding; + try { + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + const outputCapture = captureAtomicOutput(outPath); + if (outputCapture.status !== 'captured') throwAtomicOutputFailure(outputCapture, outPath); + const beforeStage = verifyAtomicOutput(outputCapture.snapshot); + if (beforeStage.status !== 'match') throwAtomicOutputFailure(beforeStage, outPath); + ({ candidatePath, identity: candidateIdentity } = stageRenderedHtml( + outputCapture.commitPath, + html, + outputCapture.mode, + )); + + // The renderer may spend substantial time building HTML after loadDiagram + // establishes the guard. Re-run it after staging so a last-moment alias + // cannot redirect the commit onto an input file. + if (outputGuard) resolveOutputPath(outputGuard); + const candidateCapture = captureRegularFileBinding(candidatePath, { + subject: 'candidate', + expectedSha256: createHash('sha256').update(html).digest('hex'), + expectedBytes: Buffer.byteLength(html), + expectedIdentity: candidateIdentity, + ...(outputCapture.mode === null ? {} : { expectedMode: outputCapture.mode }), + }); + if (candidateCapture.status !== 'captured') throwAtomicOutputFailure(candidateCapture, outPath); + candidateBinding = candidateCapture.binding; + const beforeCommit = verifyAtomicOutput(outputCapture.snapshot); + if (beforeCommit.status !== 'match') throwAtomicOutputFailure(beforeCommit, outPath); + const publication = publishRegularFileBinding( + candidateBinding, + candidatePath, + outputCapture.snapshot, + { subject: 'candidate' }, + ); + if (!['committed', 'committed-with-warning'].includes(publication.status)) { + throwAtomicOutputFailure(publication, outPath); + } + const releasedCandidate = releaseRegularFileBinding(candidateBinding); + candidateBinding = undefined; + if (releasedCandidate.status !== 'released') throwAtomicOutputFailure(releasedCandidate, outPath); + candidatePath = undefined; + candidateIdentity = undefined; + } catch (error) { + throwOutputError(error, outPath); + } finally { + outputPathGuards.delete(outPath); + if (candidateBinding) releaseRegularFileBinding(candidateBinding); + if (candidatePath && candidateIdentity) { + const cleanup = removeOwnedRegularFile(candidatePath, candidateIdentity); + if (!['removed', 'absent', 'preserved'].includes(cleanup.status)) { + throwAtomicOutputFailure(cleanup, outPath); + } + } + } + console.log(outPath); +} + +const SEMANTIC_COLLECTIONS = { + architecture: 'components', + workflow: 'nodes', + sequence: 'participants', + dataflow: 'nodes', + lifecycle: 'states', +}; + +const RELATIONSHIP_COLLECTIONS = { + architecture: 'connections', + workflow: 'edges', + sequence: 'messages', + dataflow: 'flows', + lifecycle: 'transitions', +}; + +// Relationship IDs are optional for backwards compatibility, but once an +// author supplies one it becomes the durable identity used by viewer links. +// Keep uniqueness enforcement in the shared zero-install path so every typed +// renderer fails the same way even when development dependencies are absent. +export function validateRelationshipIds(diagramType, diagram) { + const collection = RELATIONSHIP_COLLECTIONS[diagramType]; + const relationships = collection && Array.isArray(diagram[collection]) ? diagram[collection] : []; + const seen = new Set(); + const problems = []; + + relationships.forEach((relationship, index) => { + if (relationship.id === undefined || relationship.id === null || relationship.id === '') return; + if (seen.has(relationship.id)) { + problems.push(`/${collection}/${index}/id duplicates relationship id ${JSON.stringify(relationship.id)}`); + } + seen.add(relationship.id); + }); + + if (problems.length) { + throwDiagnosticProblems('Relationship identity validation failed', problems, { + code: 'relationship/duplicate-id', + subject: { diagramType, collection }, + }); + } +} + +// Share relationship-ID semantic checks between the loader +// and workflow compiler without performing filesystem operations (see #429). +export function validateCrossCollectionContracts(diagramType, diagram) { + validateRelationshipIds(diagramType, diagram); +} + +// Accessible name for the generated diagram SVG. +export function svgRootAttrs(meta, explicitQualityProfile) { + const animation = meta.animation === 'trace' ? ' data-animation="trace"' : ''; + const preset = ` data-preset="${esc(meta.visual_preset || 'classic')}"`; + const engineeringProfile = meta.engineering_profile + ? ` data-engineering-profile="${esc(meta.engineering_profile)}"` + : ''; + const requestedProfile = explicitQualityProfile || process.env.ARCHIFY_QUALITY_PROFILE || meta.quality_profile; + const qualityProfile = requestedProfile === 'showcase' ? 'showcase' : 'standard'; + const advisory = requestedProfile ? '' : ' data-quality-gates="advisory"'; + return `role="img" lang="${esc(resolveLocale(meta.locale))}" aria-labelledby="archify-diagram-title archify-diagram-description"${animation}${preset}${engineeringProfile} data-quality-profile="${esc(qualityProfile)}"${advisory}`; +} + +// Keep the accessible name inside the SVG so it survives standalone SVG +// export and embedding. The fixed IDs are deterministic because an Archify +// artifact intentionally contains one primary diagram SVG. +export function svgAccessibleText(meta, kind) { + const description = meta.subtitle || translateMessage(meta.locale, `diagram.description.${kind}`); + return ` ${esc(meta.title)}\n ${esc(description)}`; +} + +export function animateAttr(meta, kind, step) { + if (meta.animation !== 'trace') return ''; + // Ambient trace must finish inside the fixed six-second WebM capture. The + // cap affects visual delay only; authored order and semantic identity stay + // untouched in the JSON, DOM, and relationship contracts. + const safeStep = Number.isFinite(step) && step >= 0 ? Math.min(12, Math.floor(step)) : 0; + return ` data-animate="${kind}" style="--step:${safeStep}"`; +} + +// Stable semantic hooks for the standalone HTML explorer. IDs already pass +// the schema's conservative identifier pattern; escape again at the markup +// boundary so these helpers remain safe if that contract expands later. +export function focusNodeAttrs(id, label, metadata = {}, locale) { + const optional = [ + ['data-node-kind', metadata.kind], + ['data-node-sublabel', metadata.sublabel], + ['data-node-tag', metadata.tag], + ['data-node-context', metadata.context], + ['data-node-brand', metadata.brand], + ['data-node-brand-id', metadata.brandId], + ['data-node-brand-status', metadata.brandStatus], + ['data-node-brand-source', metadata.brandSource], + ].filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '') + .map(([name, value]) => ` ${name}="${esc(String(value))}"`) + .join(''); + const detail = [metadata.sublabel, metadata.context, metadata.brand] + .filter((value) => value !== undefined && value !== null && String(value).trim() !== '') + .join(', '); + const aria = detail + ? translateMessage(locale, 'node.focus.detail', { label, detail }) + : translateMessage(locale, 'node.focus', { label }); + return `id="node-${esc(id)}" data-node-id="${esc(id)}" data-node-label="${esc(label)}" tabindex="0" role="button" aria-label="${esc(aria)}" aria-pressed="false"${optional}`; +} + +// Native SVG titles preserve a compact details-on-demand fallback when the +// canonical SVG is embedded inline outside the full Archify viewer. +export function focusNodeTitle(label, metadata = {}) { + const parts = [label, metadata.sublabel, metadata.context, metadata.tag, metadata.brand] + .filter((value) => value !== undefined && value !== null && String(value).trim() !== ''); + return `${esc(parts.join(' · '))}`; +} + +export function focusEdgeAttrs(from, to, label, key, id) { + const named = label ? ` data-edge-label="${esc(label)}"` : ''; + const keyed = key !== undefined && key !== null ? ` data-edge-key="${esc(String(key))}"` : ''; + const identified = id !== undefined && id !== null && String(id).trim() !== '' + ? ` data-edge-id="${esc(String(id))}"` + : ''; + return `data-edge-from="${esc(from)}" data-edge-to="${esc(to)}"${named}${keyed}${identified}`; +} diff --git a/vendor/archify/renderers/shared/desktop-readability.mjs b/vendor/archify/renderers/shared/desktop-readability.mjs new file mode 100644 index 0000000..c3292c6 --- /dev/null +++ b/vendor/archify/renderers/shared/desktop-readability.mjs @@ -0,0 +1,132 @@ +export const DESKTOP_READABILITY_VIEWPORT = Object.freeze({ width: 1440, height: 900 }); +export const DESKTOP_READER_MIN_WIDTH = 960; +export const DESKTOP_READER_HORIZONTAL_CHROME = 30; +export const DESKTOP_READER_DIAGRAM_WIDTH = DESKTOP_READER_MIN_WIDTH - DESKTOP_READER_HORIZONTAL_CHROME; +export const MIN_PROJECTED_NODE_TEXT_PX = 6; +export const DECLARED_WIDE_READER_CONTRACT = 'declared-wide-v1'; +export const DECLARED_WIDE_READER_RATIO = 1.55; +export const DECLARED_WIDE_READER_MAX_WIDTH = 1920; +export const DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX = 64; +export const DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX = 30; + +export function projectedNodeTextPx(sourceFontPx, viewBoxWidth, diagramWidth = DESKTOP_READER_DIAGRAM_WIDTH) { + if (![sourceFontPx, viewBoxWidth, diagramWidth].every(Number.isFinite) || viewBoxWidth <= 0 || diagramWidth <= 0) { + return Number.NaN; + } + return sourceFontPx * Math.min(1, diagramWidth / viewBoxWidth); +} + +export function minimumReadableSourceTextPx( + viewBoxWidth, + diagramWidth = DESKTOP_READER_DIAGRAM_WIDTH, + minimumProjectedPx = MIN_PROJECTED_NODE_TEXT_PX, +) { + if (![viewBoxWidth, diagramWidth, minimumProjectedPx].every(Number.isFinite) + || viewBoxWidth <= 0 + || diagramWidth <= 0 + || minimumProjectedPx <= 0) { + return Number.NaN; + } + return minimumProjectedPx / Math.min(1, diagramWidth / viewBoxWidth); +} + +// This is deliberately separate from the legacy 930px projection. Architecture +// boundary convergence depends on that legacy default, while only a recognized +// v2 wide Reader may use this declared-width proof. +export function declaredWideReadabilityBudget({ + viewBoxWidth, + viewBoxHeight, + minimumSourceTextPx, + requestedMinimumTextPx, + viewportWidth = DESKTOP_READABILITY_VIEWPORT.width, + bodyHorizontalPx = DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX, + diagramHorizontalPx = DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX, + minimumReaderWidth = DESKTOP_READER_MIN_WIDTH, + maximumReaderWidth = DECLARED_WIDE_READER_MAX_WIDTH, +} = {}) { + const values = [ + viewBoxWidth, viewBoxHeight, minimumSourceTextPx, requestedMinimumTextPx, + viewportWidth, bodyHorizontalPx, diagramHorizontalPx, minimumReaderWidth, maximumReaderWidth, + ]; + if (!values.every(Number.isFinite) || viewBoxWidth <= 0 || viewBoxHeight <= 0 + || minimumSourceTextPx <= 0 || requestedMinimumTextPx <= 0 || viewportWidth <= 0 + || bodyHorizontalPx < 0 || diagramHorizontalPx < 0 || minimumReaderWidth <= 0 + || maximumReaderWidth < minimumReaderWidth || viewBoxWidth / viewBoxHeight < DECLARED_WIDE_READER_RATIO) { + return null; + } + const requestedTargetPx = Math.max(MIN_PROJECTED_NODE_TEXT_PX, requestedMinimumTextPx); + const requestedScale = Math.min(1, requestedTargetPx / minimumSourceTextPx); + const desiredReaderWidth = Math.max(minimumReaderWidth, viewBoxWidth * requestedScale + diagramHorizontalPx); + const viewportCap = Math.max(0, viewportWidth - bodyHorizontalPx); + const cap = Math.min(maximumReaderWidth, viewportCap); + const actualReaderWidth = Math.min(desiredReaderWidth, cap); + const guaranteedSvgWidth = Math.max(0, actualReaderWidth - diagramHorizontalPx); + const projectedMinimumTextPx = projectedNodeTextPx(minimumSourceTextPx, viewBoxWidth, guaranteedSvgWidth); + const limit = actualReaderWidth < desiredReaderWidth + ? (viewportCap <= maximumReaderWidth ? 'viewport-cap' : 'reader-cap') + : 'source-size'; + return { + requestedTargetPx, + requestedScale, + desiredReaderWidth, + viewportCap, + maximumReaderWidth, + actualReaderWidth, + guaranteedSvgWidth, + projectedMinimumTextPx, + hardFloorPx: MIN_PROJECTED_NODE_TEXT_PX, + hardFloorMet: projectedMinimumTextPx >= MIN_PROJECTED_NODE_TEXT_PX, + requestedTargetMet: projectedMinimumTextPx >= requestedMinimumTextPx, + limit, + }; +} + +// Vertical chrome that always stacks with the SVG at the 1440x900 desktop +// viewport, measured from the delivered Viewer with the shortest one-line +// header and no cards: body padding 12, header 39, diagram padding/border 75. +// Cards are excluded so the prediction stays a lower bound. +export const DESKTOP_FIXED_VERTICAL_CHROME_PX = Object.freeze({ body: 12, header: 39, diagram: 75 }); + +// A canvas the Reader can neither narrow (viewBox ratio below the wide +// threshold) nor scroll readably (no intrinsic-height fit) renders at the full +// reader width, so its page height is a function of the viewBox alone. Returns +// null when the Reader has a way to fit the page; otherwise the certain +// overflow at 1440x900 before any cards are counted. +export function predictedFixedWidthOverflow({ + viewBoxWidth, + viewBoxHeight, + readerFit, + diagramType, + viewport = DESKTOP_READABILITY_VIEWPORT, + bodyHorizontalPx = DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX, + diagramHorizontalPx = DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX, + chrome = DESKTOP_FIXED_VERTICAL_CHROME_PX, +} = {}) { + if (![viewBoxWidth, viewBoxHeight].every(Number.isFinite) || viewBoxWidth <= 0 || viewBoxHeight <= 0) return null; + const ratio = viewBoxWidth / viewBoxHeight; + if (readerFit === 'intrinsic-height' + || (readerFit === 'authored-height' && diagramType === 'architecture') + || ratio >= DECLARED_WIDE_READER_RATIO) return null; + const svgWidthPx = viewport.width - bodyHorizontalPx - diagramHorizontalPx; + const svgHeightPx = Math.round(svgWidthPx * viewBoxHeight / viewBoxWidth); + const fixedChromePx = chrome.body + chrome.header + chrome.diagram; + const pageHeightPx = svgHeightPx + fixedChromePx; + if (pageHeightPx <= viewport.height) return null; + return { + viewportWidth: viewport.width, + viewportHeight: viewport.height, + ratio: Math.round(ratio * 100) / 100, + wideRatio: DECLARED_WIDE_READER_RATIO, + svgWidthPx, + svgHeightPx, + fixedChromePx, + pageHeightPx, + overflowPx: pageHeightPx - viewport.height, + }; +} + +export function describeFixedWidthOverflow(issue) { + const maximumViewBoxHeight = Math.floor(issue.viewBoxWidth / issue.wideRatio); + const wideViewBoxWidth = Math.ceil(issue.viewBoxHeight * issue.wideRatio); + return `Preserve every node, relationship, and label. This ${issue.viewBoxWidth}x${issue.viewBoxHeight} canvas (ratio ${issue.ratio}) declares no intrinsic-height fit and is below the ${issue.wideRatio} wide ratio, so the desktop Reader can neither narrow it nor accept vertical scroll: it renders ${issue.svgHeightPx}px tall at the full ${issue.svgWidthPx}px width and the page reaches ${issue.pageHeightPx}px before cards against ${issue.viewportHeight}px, a certain visual-check failure. Either compact vertical spacing so meta.viewBox height is at most ${maximumViewBoxHeight} at this width, or spread content sideways so the width is at least ${wideViewBoxWidth} at this height; for architecture, omitting meta.viewBox lets the renderer size the canvas and declare the fit.`; +} diff --git a/vendor/archify/renderers/shared/diagnostics.mjs b/vendor/archify/renderers/shared/diagnostics.mjs new file mode 100644 index 0000000..c725c62 --- /dev/null +++ b/vendor/archify/renderers/shared/diagnostics.mjs @@ -0,0 +1,173 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const DIAGNOSTIC_MODE = process.env.ARCHIFY_DIAGNOSTIC_FORMAT === 'json'; +const recorded = []; +const recordedMessages = new Set(); +const boundaryKey = Symbol.for('archify.renderer-diagnostic-boundary'); +let recordingSuppressionDepth = 0; + +function plainObject(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; + return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)); +} + +function normalizedDiagnostic(diagnostic) { + const message = String(diagnostic?.message || 'Archify could not classify this failure.').trim(); + return { + code: String(diagnostic?.code || 'internal/unclassified'), + severity: diagnostic?.severity === 'warning' ? 'warning' : 'error', + message, + subject: plainObject(diagnostic?.subject), + evidence: plainObject(diagnostic?.evidence), + supportedFixes: Array.isArray(diagnostic?.supportedFixes) + ? [...new Set(diagnostic.supportedFixes.map((fix) => String(fix).trim()).filter(Boolean))] + : [], + ...(Array.isArray(diagnostic?.suppresses) ? { + suppresses: [...new Set(diagnostic.suppresses.map((code) => String(code).trim()).filter(Boolean))], + } : {}), + }; +} + +export function recordDiagnostic(diagnostic) { + if (!DIAGNOSTIC_MODE || recordingSuppressionDepth > 0) return; + const normalized = normalizedDiagnostic(diagnostic); + if (recordedMessages.has(normalized.message)) return; + recordedMessages.add(normalized.message); + recorded.push(normalized); +} + +export function withDiagnosticRecordingSuppressed(callback) { + recordingSuppressionDepth += 1; + try { + return callback(); + } finally { + recordingSuppressionDepth -= 1; + } +} + +export function throwDiagnosticError(message, diagnostics) { + for (const diagnostic of diagnostics || []) recordDiagnostic(diagnostic); + const error = new Error(message); + error.archifyDiagnostics = (diagnostics || []).map(normalizedDiagnostic); + throw error; +} + +export function throwDiagnosticProblems(prefix, problems, { code = 'layout/constraint', subject = {}, diagnostics: details = [] } = {}) { + const messages = (problems || []).map((problem) => String(problem)); + const byMessage = new Map(details.map((entry) => [entry.message, entry])); + const diagnostics = messages.map((message) => normalizedDiagnostic(byMessage.get(message) || { + code, + severity: 'error', + message, + subject, + evidence: {}, + supportedFixes: [], + })); + throwDiagnosticError(`${prefix}:\n- ${messages.join('\n- ')}`, diagnostics); +} + +function fallbackDiagnostic(error) { + const input = process.argv[2] ? path.resolve(process.argv[2]) : undefined; + return normalizedDiagnostic({ + code: 'internal/unclassified', + severity: 'error', + message: error?.message || 'Renderer failed without a diagnostic.', + subject: { input }, + evidence: { errorName: error?.name || 'Error' }, + supportedFixes: [], + }); +} +export function rendererFailure(error) { + const attached = Array.isArray(error?.archifyDiagnostics) + ? error.archifyDiagnostics.map(normalizedDiagnostic) + : []; + // Earlier diagnostics do not classify a later, unrelated implementation error. + const diagnostics = attached.length + ? (recorded.length ? recorded : attached) + : [fallbackDiagnostic(error)]; + return { + schemaVersion: 1, + ok: false, + source: 'renderer', + error: error?.message || 'Renderer failed without a diagnostic.', + diagnostics, + }; +} + +// Match the public CLI's text format without making its standalone doctor +// bootstrap depend on this renderer runtime being present. +function formatDiagnostics(error, diagnostics = []) { + if (!diagnostics.length) return error; + return [ + error, + ...diagnostics.map((entry) => { + const fix = entry.supportedFixes?.length ? ` Fix: ${entry.supportedFixes.join('; ')}.` : ''; + return `[${entry.code}] ${entry.message}${fix}`; + }), + ].join('\n'); +} + +const readerSignal = new Int32Array(new SharedArrayBuffer(4)); + +function waitForReader() { + // Sleep instead of spinning on EAGAIN. A retry budget looks like a safeguard + // and behaves like a truncation gate: a spinning loop burns thousands of + // attempts in a few milliseconds, so a reader that is merely slow to start + // exhausts it and loses the tail of the receipt. Waiting costs nothing while + // the reader catches up, and a reader that goes away raises EPIPE, which the + // caller already treats as a real write failure. + Atomics.wait(readerSignal, 0, 0, 1); +} + +export function installRendererDiagnosticBoundary() { + if (globalThis[boundaryKey]) return; + globalThis[boundaryKey] = true; + if (!DIAGNOSTIC_MODE) { + process.once('uncaughtException', (error) => { + // Only errors classified at their operation boundary are author-facing. + // Preserve Node's debugging information for unexpected implementation errors. + if (!error?.archifyDiagnostics?.length) { + // The once-listener is already removed. Rethrow outside the exception + // handler so Node retains its normal stack and exit code (not code 7). + process.nextTick(() => { throw error; }); + return; + } + const payload = `${formatDiagnostics(error.message, error.archifyDiagnostics)}\n`; + process.stderr.once('error', () => process.exit(1)); + process.stderr.write(payload, () => process.exit(1)); + }); + return; + } + process.on('uncaughtException', (error) => { + const payload = `${JSON.stringify(rendererFailure(error))}\n`; + try { + // stderr may be a pipe. fs.writeSync performs a PARTIAL write once the + // payload exceeds the OS pipe buffer (8KB on macOS) and returns the byte + // count actually written. Ignoring that return value silently truncated + // large diagnostic payloads mid-JSON, so the parent CLI's JSON.parse + // failed and the fail-closed boundary reported internal/unclassified + // instead of the diagnostics we had already computed. Loop until drained. + // A full pipe also makes writeSync throw EAGAIN; wait for the reader + // rather than treat it as a stream failure, otherwise the tail is + // dropped just the same. + const buffer = Buffer.from(payload, 'utf8'); + let written = 0; + while (written < buffer.length) { + try { + written += fs.writeSync(process.stderr.fd, buffer, written, buffer.length - written); + } catch (writeError) { + if (writeError?.code === 'EAGAIN') { + waitForReader(); + continue; + } + throw writeError; + } + } + } catch { + // The renderer is already failing. Avoid replacing its real error with a + // secondary stream failure; the parent CLI still has the exit status. + } + process.exit(1); + }); +} diff --git a/vendor/archify/renderers/shared/engineering-profiles.mjs b/vendor/archify/renderers/shared/engineering-profiles.mjs new file mode 100644 index 0000000..76e3166 --- /dev/null +++ b/vendor/archify/renderers/shared/engineering-profiles.mjs @@ -0,0 +1,157 @@ +import { throwDiagnosticError } from './diagnostics.mjs'; + +const DEPLOYMENT_PROFILE = 'deployment-ownership'; +const DEPLOYMENT_BOUNDARY_KINDS = new Set(['region', 'security-group']); +const PRIVATE_STATE_TYPES = new Set(['database']); + +function subject(collection, index, item = {}) { + return { + diagramType: 'architecture', + profile: DEPLOYMENT_PROFILE, + collection, + index, + ...(item.id ? { id: item.id } : {}), + }; +} + +function membership(boundaries, componentId, kind) { + return boundaries + .map((boundary, index) => ({ boundary, index })) + .filter(({ boundary }) => boundary.kind === kind && boundary.wraps.includes(componentId)); +} + +export function deploymentOwnershipDiagnostics(diagram) { + const components = Array.isArray(diagram.components) ? diagram.components : []; + const boundaries = (Array.isArray(diagram.boundaries) ? diagram.boundaries : []) + .map((boundary) => ({ ...boundary, wraps: Array.isArray(boundary.wraps) ? boundary.wraps : [] })); + const connections = Array.isArray(diagram.connections) ? diagram.connections : []; + const diagnostics = []; + + for (const kind of DEPLOYMENT_BOUNDARY_KINDS) { + const count = boundaries.filter((boundary) => boundary.kind === kind).length; + if (count > 0) continue; + diagnostics.push({ + code: 'engineering/deployment-boundary-kind', + severity: 'error', + message: `Deployment ownership requires at least one ${kind} boundary.`, + subject: subject('boundaries', -1), + evidence: { requiredKind: kind, found: count }, + supportedFixes: [`add one ${kind} boundary with an explicit wraps list`], + }); + } + + components.forEach((component, index) => { + if (component.type === 'external') return; + if (typeof component.tag !== 'string' || component.tag.trim() === '') { + diagnostics.push({ + code: 'engineering/deployment-owner-missing', + severity: 'error', + message: `Deployment component ${JSON.stringify(component.id)} does not name its owner in tag.`, + subject: subject('components', index, component), + evidence: { componentType: component.type, ownerField: 'tag' }, + supportedFixes: [`set /components/${index}/tag to the responsible team or owner`], + }); + } + + const regions = membership(boundaries, component.id, 'region'); + if (regions.length === 0) { + diagnostics.push({ + code: 'engineering/deployment-region-scope', + severity: 'error', + message: `Deployment component ${JSON.stringify(component.id)} is not assigned to a region boundary.`, + subject: subject('components', index, component), + evidence: { componentType: component.type, regionMemberships: 0 }, + supportedFixes: ['add the component id to the real region boundary wraps list'], + }); + } else if (regions.length > 1) { + diagnostics.push({ + code: 'engineering/deployment-region-ambiguous', + severity: 'error', + message: `Deployment component ${JSON.stringify(component.id)} belongs to more than one region boundary.`, + subject: subject('components', index, component), + evidence: { + componentType: component.type, + regions: regions.map(({ boundary, index: boundaryIndex }) => ({ boundaryIndex, label: boundary.label })), + }, + supportedFixes: ['keep the component id in exactly one real region boundary wraps list'], + }); + } + + if (PRIVATE_STATE_TYPES.has(component.type)) { + const privateScopes = membership(boundaries, component.id, 'security-group'); + if (privateScopes.length === 0) { + diagnostics.push({ + code: 'engineering/deployment-private-state', + severity: 'error', + message: `Stateful component ${JSON.stringify(component.id)} is not assigned to a private security-group boundary.`, + subject: subject('components', index, component), + evidence: { componentType: component.type, privateMemberships: 0 }, + supportedFixes: ['add the component id to the real private security-group boundary wraps list'], + }); + } + } + }); + + boundaries.forEach((boundary, index) => { + if (boundary.kind !== 'security-group') return; + const members = boundary.wraps.map((id) => ({ + id, + regions: membership(boundaries, id, 'region').map(({ boundary: region, index: boundaryIndex }) => ({ + boundaryIndex, + label: region.label, + })), + })); + const regionIndexes = new Set(members.flatMap((member) => member.regions.map((region) => region.boundaryIndex))); + const consistent = members.length > 0 + && members.every((member) => member.regions.length === 1) + && regionIndexes.size === 1; + if (consistent) return; + diagnostics.push({ + code: 'engineering/deployment-private-region-consistency', + severity: 'error', + message: `Private boundary ${JSON.stringify(boundary.label)} must contain components from exactly one shared region.`, + subject: subject('boundaries', index, boundary), + evidence: { boundaryKind: boundary.kind, members }, + supportedFixes: ['assign every private-boundary component to exactly one shared region boundary'], + }); + }); + + connections.forEach((connection, index) => { + const crossedBoundaries = boundaries + .map((boundary, boundaryIndex) => ({ + boundaryIndex, + kind: boundary.kind, + label: boundary.label, + fromInside: boundary.wraps.includes(connection.from), + toInside: boundary.wraps.includes(connection.to), + })) + .filter((boundary) => DEPLOYMENT_BOUNDARY_KINDS.has(boundary.kind) && boundary.fromInside !== boundary.toInside); + if (crossedBoundaries.length === 0 || (typeof connection.label === 'string' && connection.label.trim() !== '')) return; + diagnostics.push({ + code: 'engineering/deployment-crossing-mechanism', + severity: 'error', + message: `Cross-boundary connection ${JSON.stringify(connection.id || `${connection.from}->${connection.to}`)} does not name its mechanism.`, + subject: subject('connections', index, connection), + evidence: { + from: connection.from, + to: connection.to, + crossedBoundaries: crossedBoundaries.map(({ boundaryIndex, kind, label }) => ({ boundaryIndex, kind, label })), + }, + supportedFixes: [`set /connections/${index}/label to the real cross-boundary mechanism`], + }); + }); + + return diagnostics; +} + +export function validateEngineeringProfile(diagramType, diagram) { + const profile = diagram.meta?.engineering_profile; + if (!profile) return; + if (diagramType !== 'architecture' || profile !== DEPLOYMENT_PROFILE) return; + const diagnostics = deploymentOwnershipDiagnostics(diagram); + if (!diagnostics.length) return; + throwDiagnosticError( + `Engineering profile ${JSON.stringify(profile)} failed:\n${diagnostics.map((entry) => `- ${entry.message}`).join('\n')}`, + diagnostics, + ); +} diff --git a/vendor/archify/renderers/shared/generated-brand-marks.mjs b/vendor/archify/renderers/shared/generated-brand-marks.mjs new file mode 100644 index 0000000..69dbb7c --- /dev/null +++ b/vendor/archify/renderers/shared/generated-brand-marks.mjs @@ -0,0 +1,2003 @@ +// Generated by scripts/generate-brand-marks.mjs from brand-marks/catalog.json. +// Simple Icons 16.28.0. Do not edit by hand. +export const BRAND_MARKS = Object.freeze([ + { + "id": "airtable", + "title": "Airtable", + "category": "collaboration", + "aliases": [], + "domains": [ + "airtable.com" + ], + "viewBox": 24, + "hex": "18BFFF", + "path": "M11.992 1.966c-.434 0-.87.086-1.28.257L1.779 5.917c-.503.208-.49.908.012 1.116l8.982 3.558a3.266 3.266 0 0 0 2.454 0l8.982-3.558c.503-.196.503-.908.012-1.116l-8.957-3.694a3.255 3.255 0 0 0-1.272-.257zM23.4 8.056a.589.589 0 0 0-.222.045l-10.012 3.877a.612.612 0 0 0-.38.564v8.896a.6.6 0 0 0 .821.552L23.62 18.1a.583.583 0 0 0 .38-.551V8.653a.6.6 0 0 0-.6-.596zM.676 8.095a.644.644 0 0 0-.48.19C.086 8.396 0 8.53 0 8.69v8.355c0 .442.515.737.908.54l6.27-3.006.307-.147 2.969-1.436c.466-.22.43-.908-.061-1.092L.883 8.138a.57.57 0 0 0-.207-.044z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://airtable.com/newsroom" + } + }, + { + "id": "alibaba-cloud", + "title": "Alibaba Cloud", + "category": "cloud", + "aliases": [ + "aliyun" + ], + "domains": [ + "alibabacloud.com", + "aliyun.com" + ], + "viewBox": 24, + "hex": "FF6A00", + "path": "M3.996 4.517h5.291L8.01 6.324 4.153 7.506a1.668 1.668 0 0 0-1.165 1.601v5.786a1.668 1.668 0 0 0 1.165 1.6l3.857 1.183 1.277 1.807H3.996A3.996 3.996 0 0 1 0 15.487V8.513a3.996 3.996 0 0 1 3.996-3.996m16.008 0h-5.291l1.277 1.807 3.857 1.182c.715.227 1.17.889 1.165 1.601v5.786a1.668 1.668 0 0 1-1.165 1.6l-3.857 1.183-1.277 1.807h5.291A3.996 3.996 0 0 0 24 15.487V8.513a3.996 3.996 0 0 0-3.996-3.996m-4.007 8.345H8.002v-1.804h7.995Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.alibabagroup.com/en/ir/reports" + } + }, + { + "id": "angular", + "title": "Angular", + "category": "framework", + "aliases": [], + "domains": [ + "angular.dev" + ], + "viewBox": 24, + "hex": "0F0F11", + "path": "M16.712 17.711H7.288l-1.204 2.916L12 24l5.916-3.373-1.204-2.916ZM14.692 0l7.832 16.855.814-12.856L14.692 0ZM9.308 0 .662 3.999l.814 12.856L9.308 0Zm-.405 13.93h6.198L12 6.396 8.903 13.93Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://angular.dev/press-kit", + "guidelines": "https://angular.dev/press-kit", + "license": { + "type": "CC-BY-4.0", + "url": "https://spdx.org/licenses/CC-BY-4.0" + } + } + }, + { + "id": "ansible", + "title": "Ansible", + "category": "engineering", + "aliases": [], + "domains": [ + "ansible.com" + ], + "viewBox": 24, + "hex": "EE0000", + "path": "M10.617 11.473l4.686 3.695-3.102-7.662zM12 0C5.371 0 0 5.371 0 12s5.371 12 12 12 12-5.371 12-12S18.629 0 12 0zm5.797 17.305c-.011.471-.403.842-.875.83-.236 0-.416-.09-.664-.293l-6.19-5-2.079 5.203H6.191L11.438 5.44c.124-.314.427-.52.764-.506.326-.014.63.189.742.506l4.774 11.494c.045.111.08.234.08.348-.001.009-.001.009-.001.023z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.ansible.com/logos" + } + }, + { + "id": "anthropic", + "title": "Anthropic", + "category": "ai", + "aliases": [], + "domains": [ + "anthropic.com" + ], + "viewBox": 24, + "hex": "191919", + "path": "M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.anthropic.com" + } + }, + { + "id": "apache-airflow", + "title": "Apache Airflow", + "category": "data", + "aliases": [ + "airflow" + ], + "domains": [ + "airflow.apache.org" + ], + "viewBox": 24, + "hex": "017CEE", + "path": "M17.195 16.822l4.002-4.102C23.55 10.308 23.934 5.154 24 .43a.396.396 0 0 0-.246-.373.392.392 0 0 0-.437.09l-6.495 6.658-4.102-4.003C10.309.45 5.154.066.43 0H.423a.397.397 0 0 0-.277.683l6.658 6.494-4.003 4.103C.45 13.692.065 18.846 0 23.57a.398.398 0 0 0 .683.282l6.494-6.657 3.934 3.837.17.165c2.41 2.353 7.565 2.737 12.288 2.803h.006a.397.397 0 0 0 .277-.683l-6.657-6.495zm-.409-9.476c.04.115.05.24.031.344-.17.96-1.593 2.538-4.304 3.87a.597.597 0 0 0-.08-.079c1.432-3.155 1.828-5.61 1.175-7.322l3.058 2.984.12.203zm-.131 9.44a.73.73 0 0 1-.347.031c-.96-.171-2.537-1.594-3.87-4.307a.656.656 0 0 0 .08-.078l-.001.001c3.155 1.432 5.61 1.83 7.324 1.174l-2.969 3.043M23.568.392a.05.05 0 0 1 .052-.011c.018.006.03.024.029.043-.065 4.655-.437 9.726-2.703 12.05-1.53 1.565-4.326 1.419-8.283-.377.006-.037.021-.07.02-.108 0-.044-.017-.082-.026-.123 2.83-1.39 4.315-3.037 4.506-4.115.057-.322-.009-.542-.102-.688l6.507-6.67V.392zM.393.43A.045.045 0 0 1 .382.38C.39.36.403.343.425.35c4.655.065 9.727.438 12.05 2.703l.002.002c1.56 1.527 1.415 4.323-.379 8.28-.033-.005-.062-.02-.097-.02h-.008c-.045.001-.084.019-.126.027-1.39-2.83-3.037-4.314-4.115-4.506-.323-.057-.542.01-.688.103L.393.43zm11.94 11.563a.331.331 0 0 1-.327.335H12a.332.332 0 0 1-.004-.661c.172.016.333.144.335.326h.002zm-5.12 4.661a.722.722 0 0 1-.03-.345c.17-.96 1.595-2.54 4.309-3.873.013.016.019.035.033.05.013.012.03.017.044.028-1.434 3.158-1.83 5.613-1.177 7.326l-3.041-2.967m-.006-9.659a.735.735 0 0 1 .345-.031c.961.17 2.54 1.594 3.871 4.306a.597.597 0 0 0-.079.08c-2.167-.983-4.007-1.484-5.498-1.484-.68 0-1.289.103-1.825.308L7.128 7.35M.43 23.607c-.018.018-.038.015-.052.01-.019-.007-.028-.021-.028-.043.065-4.654.437-9.725 2.703-12.049 1.527-1.565 4.325-1.419 8.286.378-.006.035-.02.067-.02.104 0 .043.018.083.026.124-2.831 1.391-4.317 3.04-4.51 4.117-.057.322.01.542.103.688L.43 23.607zm23.144.042c-4.655-.065-9.726-.437-12.05-2.703l-.005-.006c-1.56-1.526-1.412-4.322.383-8.279.033.005.064.02.098.02h.009c.043 0 .08-.018.122-.027 1.39 2.832 3.036 4.317 4.115 4.51.083.014.16.021.23.021a.776.776 0 0 0 .45-.133l6.68 6.516c.02.02.016.04.01.052a.042.042 0 0 1-.042.029z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://apache.org/logos", + "guidelines": "https://www.apache.org/foundation/marks", + "license": { + "type": "Apache-2.0", + "url": "https://spdx.org/licenses/Apache-2.0" + } + } + }, + { + "id": "apache-kafka", + "title": "Apache Kafka", + "category": "data", + "aliases": [ + "kafka" + ], + "domains": [ + "kafka.apache.org" + ], + "viewBox": 24, + "hex": "231F20", + "path": "M9.71 2.136a1.43 1.43 0 0 0-2.047 0h-.007a1.48 1.48 0 0 0-.421 1.042c0 .41.161.777.422 1.039l.007.007c.257.264.616.426 1.019.426.404 0 .766-.162 1.027-.426l.003-.007c.261-.262.421-.629.421-1.039 0-.408-.159-.777-.421-1.042H9.71zM8.683 22.295c.404 0 .766-.167 1.027-.429l.003-.008c.261-.261.421-.631.421-1.036 0-.41-.159-.778-.421-1.044H9.71a1.42 1.42 0 0 0-1.027-.432 1.4 1.4 0 0 0-1.02.432h-.007c-.26.266-.422.634-.422 1.044 0 .406.161.775.422 1.036l.007.008c.258.262.617.429 1.02.429zm7.89-4.462c.359-.096.683-.33.882-.684l.027-.052a1.47 1.47 0 0 0 .114-1.067 1.454 1.454 0 0 0-.675-.896l-.021-.014a1.425 1.425 0 0 0-1.078-.132c-.36.091-.684.335-.881.686-.2.349-.241.75-.146 1.119.099.363.33.691.675.896h.002c.346.203.737.239 1.101.144zm-6.405-7.342a2.083 2.083 0 0 0-1.485-.627c-.58 0-1.103.242-1.482.627-.378.385-.612.916-.612 1.507s.233 1.124.612 1.514a2.08 2.08 0 0 0 2.967 0c.379-.39.612-.923.612-1.514s-.233-1.122-.612-1.507zm-.835-2.51c.843.141 1.6.552 2.178 1.144h.004c.092.093.182.196.265.299l1.446-.851a3.176 3.176 0 0 1-.047-1.808 3.149 3.149 0 0 1 1.456-1.926l.025-.016a3.062 3.062 0 0 1 2.345-.306c.77.21 1.465.721 1.898 1.482v.002c.431.757.518 1.626.313 2.408a3.145 3.145 0 0 1-1.456 1.928l-.198.118h-.02a3.095 3.095 0 0 1-2.154.201 3.127 3.127 0 0 1-1.514-.944l-1.444.848a4.162 4.162 0 0 1 0 2.879l1.444.846c.413-.47.939-.789 1.514-.944a3.041 3.041 0 0 1 2.371.319l.048.023v.002a3.17 3.17 0 0 1 1.408 1.906 3.215 3.215 0 0 1-.313 2.405l-.026.053-.003-.005a3.147 3.147 0 0 1-1.867 1.436 3.096 3.096 0 0 1-2.371-.318v-.006a3.156 3.156 0 0 1-1.456-1.927 3.175 3.175 0 0 1 .047-1.805l-1.446-.848a3.905 3.905 0 0 1-.265.294l-.004.005a3.938 3.938 0 0 1-2.178 1.138v1.699a3.09 3.09 0 0 1 1.56.862l.002.004c.565.572.914 1.368.914 2.243 0 .873-.35 1.664-.914 2.239l-.002.009a3.1 3.1 0 0 1-2.21.931 3.1 3.1 0 0 1-2.206-.93h-.002v-.009a3.186 3.186 0 0 1-.916-2.239c0-.875.35-1.672.916-2.243v-.004h.002a3.1 3.1 0 0 1 1.558-.862v-1.699a3.926 3.926 0 0 1-2.176-1.138l-.006-.005a4.098 4.098 0 0 1-1.173-2.874c0-1.122.452-2.136 1.173-2.872h.006a3.947 3.947 0 0 1 2.176-1.144V6.289a3.137 3.137 0 0 1-1.558-.864h-.002v-.004a3.192 3.192 0 0 1-.916-2.243c0-.871.35-1.669.916-2.243l.002-.002A3.084 3.084 0 0 1 8.683 0c.861 0 1.641.355 2.21.932v.002h.002c.565.574.914 1.372.914 2.243 0 .876-.35 1.667-.914 2.243l-.002.005a3.142 3.142 0 0 1-1.56.864v1.692zm8.121-1.129l-.012-.019a1.452 1.452 0 0 0-.87-.668 1.43 1.43 0 0 0-1.103.146h.002c-.347.2-.58.529-.677.896-.095.365-.054.768.146 1.119l.007.009c.2.347.519.579.874.673.357.103.755.059 1.098-.144l.019-.009a1.47 1.47 0 0 0 .657-.885 1.493 1.493 0 0 0-.141-1.118", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://apache.org/logos", + "guidelines": "https://www.apache.org/foundation/marks", + "license": { + "type": "Apache-2.0", + "url": "https://spdx.org/licenses/Apache-2.0" + } + } + }, + { + "id": "argo", + "title": "Argo", + "category": "engineering", + "aliases": [ + "argocd", + "argo-cd" + ], + "domains": [ + "argoproj.github.io" + ], + "viewBox": 24, + "hex": "EF7B4D", + "path": "M12.581 0c.436.037.871.1 1.299.186 1.679.383 3.121 1.213 4.382 2.365 1.161 1.06 1.917 2.372 2.335 3.881.089.321.216.56.586.624.205.035.238.245.239.43.003.646.002 1.294.002 1.94l-.002 1.21c-.001.356-.116.479-.466.474-.211-.003-.293.119-.344.291-.146.489-.33.966-.552 1.426-.818 1.682-2.084 2.938-3.688 3.87-.077.045-.155.088-.233.131-.252.137-.258.146-.155.415.114.299.358.529.664.625.269.096.553.134.827.21a.672.672 0 0 1 .236.094c-.066.082-.156.067-.231.082-.36.073-.713.184-1.086.17a1.275 1.275 0 0 1-.438-.064c-.114-.045-.152-.006-.176.109a5.354 5.354 0 0 0-.084.92c-.015.617-.071 1.23-.112 1.844-.042.598-.018.651.558.842.281.094.563.187.842.286.069.024.15.038.192.117-.04.057-.098.035-.146.035-.493.003-.985.005-1.478.001-.524-.005-.806-.282-.845-.803-.055-.762-.12-1.524-.182-2.286a.947.947 0 0 0-.026-.12c-.079.455-.065.879-.084 1.298-.023.528-.008 1.057-.007 1.584 0 .27.086.388.335.483.359.135.711.295 1.114.262.141-.012.276.062.402.129.032.017.073.033.069.073-.004.043-.049.047-.084.045-.657-.019-1.317.065-1.972-.028-.323-.046-.533-.236-.631-.552-.094-.303-.114-.617-.137-.93-.046-.626-.078-1.253-.116-1.88a.222.222 0 0 0-.061-.171.282.282 0 0 0-.031.193c-.002.956-.002 1.911-.001 2.866 0 .388.123.575.494.708.481.172.976.298 1.47.423.11.028.225.047.242.192h-1.852c-.051-.01-.103-.022-.155-.03-.701-.1-1.001-.372-1.143-1.042l-.067-.331-.226-1.103c-.069.12-.118.25-.144.386-.083.399-.151.802-.243 1.2-.113.493-.444.763-.932.857l-.33.063H8.558c.057-.171.216-.185.355-.221.476-.127.96-.223 1.417-.409a.603.603 0 0 0 .397-.521c.058-.435.002-.865-.013-1.296a1.528 1.528 0 0 0-.078-.315.405.405 0 0 0-.071.207c-.026.296-.049.591-.075.886-.038.432-.273.716-.679.81a1.702 1.702 0 0 1-.37.045c-.557.003-1.115-.001-1.673-.005-.048 0-.109.019-.148-.065.178-.103.377-.168.582-.187a5.67 5.67 0 0 0 .939-.193c.42-.114.522-.249.512-.687-.023-.931-.091-1.86-.069-2.791.004-.184.001-.368.001-.551a2.387 2.387 0 0 0-.05.385 40.299 40.299 0 0 1-.186 2.623c-.052.513-.296.748-.804.805-.446.051-.889.002-1.332-.02-.108-.006-.234.012-.339-.064.043-.066.106-.07.16-.087.362-.115.725-.224 1.086-.344.246-.081.35-.235.355-.492a2.241 2.241 0 0 0-.003-.232 45.315 45.315 0 0 1-.105-2.149 5.487 5.487 0 0 0-.035-.478c-.024-.188-.131-.287-.295-.258-.505.092-.99-.006-1.473-.139-.059-.016-.134-.007-.178-.088a.986.986 0 0 1 .285-.09c.255-.052.507-.121.753-.208.312-.112.564-.347.695-.651.089-.203.056-.317-.112-.398-1.418-.683-2.512-1.73-3.391-3.017a8.152 8.152 0 0 1-1.123-2.447c-.067-.246-.156-.3-.383-.26-.306.053-.401.006-.535-.273v-3.49c.144-.303.205-.341.534-.329.235.01.247-.004.309-.242.396-1.508 1.082-2.861 2.171-3.988C6.9 1.42 8.523.631 10.34.203c.456-.108.922-.15 1.387-.203h.854Zm7.974 8.948a7.34 7.34 0 0 0-.048-.938 8.353 8.353 0 0 0-.099-.65c-.598-2.964-2.344-5.02-5.051-6.268-1.553-.715-3.21-.835-4.878-.511-3.248.633-5.396 2.583-6.539 5.652-.436 1.173-.495 2.406-.37 3.65.087.935.339 1.846.745 2.694.585 1.213 1.444 2.207 2.477 3.058.343.286.719.528 1.121.719.235.111.247.105.245-.146.006-.16.003-.32-.009-.48-.125-1.02-.142-2.045-.169-3.069a.392.392 0 0 0-.184-.353c-.385-.268-.713-.592-.921-1.019-.474-.97-.372-2.361.813-3.215.136-.097.217-.19.198-.373a1.724 1.724 0 0 1 .031-.442c.177-1.187.748-2.138 1.722-2.84.68-.492 1.442-.772 2.286-.782.483-.007.953.11 1.414.244 1.609.467 2.846 2.07 2.845 3.697a.64.64 0 0 0 .268.565c.463.371.821.83.943 1.426.22 1.077-.083 1.982-.979 2.634-.266.194-.347.406-.333.698.002.047 0 .095-.002.142l-.062 1.439c-.025.586-.138 1.165-.117 1.754.008.223.006.226.201.128a7.46 7.46 0 0 0 2.393-1.903c1.32-1.577 2.074-3.372 2.059-5.511ZM9.117 12.102c1.489.021 2.443-1.578 1.716-2.879a1.937 1.937 0 0 0-1.699-.991c-1.094-.004-1.954.822-1.958 1.881-.005 1.148.813 1.985 1.941 1.989Zm5.794 0c1.101.002 1.935-.823 1.935-1.917 0-1.091-.846-1.949-1.92-1.947-1.064.003-1.94.866-1.943 1.915-.003 1.105.831 1.948 1.928 1.949Zm-1.472 1.937c-.208.128-.407.277-.63.384-.536.257-1.063.257-1.579-.048-.158-.094-.308-.201-.464-.298-.047-.028-.092-.103-.15-.062-.044.03-.01.1-.001.151.037.179.064.362.082.544.027.565.293.992.742 1.31a.984.984 0 0 0 .791.186c.565-.119 1.025-.614 1.124-1.218.043-.266.005-.544.109-.803a.133.133 0 0 0-.024-.146Zm-8.78-4.92c-.012-1.102.143-2.055.54-2.961.633-1.443 1.642-2.553 2.98-3.374a.378.378 0 0 1 .459.067c.06.06.036.118.01.178a1.09 1.09 0 0 1-.48.51c-1.079.639-1.829 1.571-2.357 2.688a6.325 6.325 0 0 0-.618 2.986c.055 1.309.439 2.516 1.213 3.588.088.104.148.23.173.365.01.08.059.168-.031.228a.312.312 0 0 1-.288.041.502.502 0 0 1-.234-.185c-.72-.979-1.193-2.056-1.331-3.273-.036-.326-.004-.653-.036-.858ZM8.94 2.34a.373.373 0 0 1 .378-.382c.211.001.409.226.416.473.004.138-.309.39-.476.386-.189-.005-.318-.2-.318-.477Zm-.465 7.48a.609.609 0 0 1 .586-.631c.38-.003.671.271.675.633.004.356-.27.622-.639.621-.38-.002-.621-.241-.622-.623Zm6.496.623c-.381-.002-.625-.255-.621-.646a.635.635 0 0 1 .596-.613.656.656 0 0 1 .669.643c.001.354-.275.618-.644.616Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/cncf/artwork/blob/c2e619cdf85e8bac090ceca7c0834c5cfedf9426/projects/argo/icon/black/argo-icon-black.svg" + } + }, + { + "id": "asana", + "title": "Asana", + "category": "collaboration", + "aliases": [], + "domains": [ + "asana.com" + ], + "viewBox": 24, + "hex": "F06A6A", + "path": "M18.78 12.653c-2.882 0-5.22 2.336-5.22 5.22s2.338 5.22 5.22 5.22 5.22-2.34 5.22-5.22-2.336-5.22-5.22-5.22zm-13.56 0c-2.88 0-5.22 2.337-5.22 5.22s2.338 5.22 5.22 5.22 5.22-2.338 5.22-5.22-2.336-5.22-5.22-5.22zm12-6.525c0 2.883-2.337 5.22-5.22 5.22-2.882 0-5.22-2.337-5.22-5.22 0-2.88 2.338-5.22 5.22-5.22 2.883 0 5.22 2.34 5.22 5.22z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://asana.com/brand", + "guidelines": "https://asana.com/brand" + } + }, + { + "id": "bitbucket", + "title": "Bitbucket", + "category": "engineering", + "aliases": [], + "domains": [ + "bitbucket.org" + ], + "viewBox": 24, + "hex": "0052CC", + "path": "M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://atlassian.design/resources/logo-library", + "guidelines": "https://atlassian.design/foundations/logos" + } + }, + { + "id": "circleci", + "title": "CircleCI", + "category": "engineering", + "aliases": [ + "circle-ci" + ], + "domains": [ + "circleci.com" + ], + "viewBox": 24, + "hex": "343434", + "path": "M8.963 12c0-1.584 1.284-2.855 2.855-2.855 1.572 0 2.856 1.284 2.856 2.855 0 1.572-1.284 2.856-2.856 2.856-1.57 0-2.855-1.284-2.855-2.856zm2.855-12C6.215 0 1.522 3.84.19 9.025c-.01.036-.01.07-.01.12 0 .313.252.576.575.576H5.59c.23 0 .433-.13.517-.333.997-2.16 3.18-3.672 5.712-3.672 3.466 0 6.286 2.82 6.286 6.287 0 3.47-2.82 6.29-6.29 6.29-2.53 0-4.714-1.5-5.71-3.673-.097-.19-.29-.336-.517-.336H.755c-.312 0-.575.253-.575.576 0 .037.014.072.014.12C1.514 20.16 6.214 24 11.818 24c6.624 0 12-5.375 12-12 0-6.623-5.376-12-12-12z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://circleci.com/press" + } + }, + { + "id": "claude", + "title": "Claude", + "category": "ai", + "aliases": [ + "claude-ai" + ], + "domains": [ + "claude.ai" + ], + "viewBox": 24, + "hex": "D97757", + "path": "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://claude.ai" + } + }, + { + "id": "clickhouse", + "title": "ClickHouse", + "category": "data", + "aliases": [], + "domains": [ + "clickhouse.com" + ], + "viewBox": 24, + "hex": "FFCC01", + "path": "M21.333 10H24v4h-2.667ZM16 1.335h2.667v21.33H16Zm-5.333 0h2.666v21.33h-2.666ZM0 22.665V1.335h2.667v21.33zm5.333-21.33H8v21.33H5.333Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/ClickHouse/ClickHouse/blob/12bd453a43819176d25ecf247033f6cb1af54beb/website/images/logo-clickhouse.svg" + } + }, + { + "id": "cloudflare", + "title": "Cloudflare", + "category": "cloud", + "aliases": [], + "domains": [ + "cloudflare.com" + ], + "viewBox": 24, + "hex": "F38020", + "path": "M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.cloudflare.com/logo/", + "guidelines": "https://www.cloudflare.com/trademark/" + } + }, + { + "id": "cloudinary", + "title": "Cloudinary", + "category": "cloud", + "aliases": [], + "domains": [ + "cloudinary.com" + ], + "viewBox": 24, + "hex": "3448C5", + "path": "M24 14.8598c0 2.1729-1.3757 3.974-3.5903 4.6996l-.0995.0318V17.989c1.3777-.5805 2.1869-1.7275 2.1869-3.1291-.0072-2-1.6087-3.6288-3.6082-3.6699h-.5964l-.1432-.5686c-.7025-2.8996-3.2886-4.9489-6.2721-4.97a6.3915 6.3915 0 0 0-5.811 3.664l-.1828.3757-.4175.0437a4.4311 4.4311 0 0 0-3.3052 2.088c-1.2803 2.0856-.6274 4.8143 1.4583 6.0947v1.6897h-.01l-.149-.0675a5.9402 5.9402 0 0 1-3.3658-4.3494c-.5787-3.2291 1.57-6.3161 4.7991-6.8948a7.8766 7.8766 0 0 1 6.9839-4.149c3.4724.025 6.535 2.28 7.5901 5.5883 2.5789.3366 4.5138 2.5245 4.5327 5.1251zm-15.3176-1.322h.5647a.0656.0656 0 0 0 .0457-.1113L7.084 11.2158l-.0007-.0007a.0656.0656 0 0 0-.0927.0007L4.78 13.4265a.0656.0656 0 0 0 .0477.1113h.5566a.0656.0656 0 0 1 .0657.0656v5.0574c0 .6588.534 1.1928 1.1928 1.1928H9.247a.0656.0656 0 0 0 .0457-.1113l-.33-.33a1.1928 1.1928 0 0 1-.348-.839v-4.97a.0676.0676 0 0 1 .0676-.0655zm9.769 2.5466h.5667a.0655.0655 0 0 0 .0457-.1133l-2.2107-2.2087-.0015-.0015a.0636.0636 0 0 0-.0899.0015L14.551 15.971a.0657.0657 0 0 0 .0457.1133h.5567a.0656.0656 0 0 1 .0656.0656v2.5108c0 .6588.534 1.1928 1.1928 1.1928h2.6063a.0655.0655 0 0 0 .0457-.1113l-.33-.33a1.1928 1.1928 0 0 1-.348-.839V16.15a.0656.0656 0 0 1 .0657-.0656zm-4.8844-1.2743h.5646a.0656.0656 0 0 0 .0477-.1114l-2.2107-2.2027-.0006-.0006a.0656.0656 0 0 0-.0928.0006l-2.2087 2.2068a.0656.0656 0 0 0 .0457.1113h.5626a.0676.0676 0 0 1 .0657.0676v3.7791c0 .6588.534 1.1928 1.1928 1.1928h2.5983a.0656.0656 0 0 0 .0477-.1113l-.332-.33a1.193 1.193 0 0 1-.346-.839v-3.6956c0-.0366.0291-.0665.0657-.0676z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://cloudinary.com" + } + }, + { + "id": "databricks", + "title": "Databricks", + "category": "data", + "aliases": [], + "domains": [ + "databricks.com" + ], + "viewBox": 24, + "hex": "FF3621", + "path": "M.95 14.184L12 20.403l9.919-5.55v2.21L12 22.662l-10.484-5.96-.565.308v.77L12 24l11.05-6.218v-4.317l-.515-.309L12 19.118l-9.867-5.653v-2.21L12 16.805l11.05-6.218V6.32l-.515-.308L12 11.974 2.647 6.681 12 1.388l7.76 4.368.668-.411v-.566L12 0 .95 6.27v.72L12 13.207l9.919-5.55v2.26L12 15.52 1.516 9.56l-.565.308Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.databricks.com", + "guidelines": "https://brand.databricks.com/Styleguide/Guide/" + } + }, + { + "id": "datadog", + "title": "Datadog", + "category": "engineering", + "aliases": [], + "domains": [ + "datadoghq.com" + ], + "viewBox": 24, + "hex": "632CA6", + "path": "M19.57 17.04l-1.997-1.316-1.665 2.782-1.937-.567-1.706 2.604.087.82 9.274-1.71-.538-5.794zm-8.649-2.498l1.488-.204c.241.108.409.15.697.223.45.117.97.23 1.741-.16.18-.088.553-.43.704-.625l6.096-1.106.622 7.527-10.444 1.882zm11.325-2.712l-.602.115L20.488 0 .789 2.285l2.427 19.693 2.306-.334c-.184-.263-.471-.581-.96-.989-.68-.564-.44-1.522-.039-2.127.53-1.022 3.26-2.322 3.106-3.956-.056-.594-.15-1.368-.702-1.898-.02.22.017.432.017.432s-.227-.289-.34-.683c-.112-.15-.2-.199-.319-.4-.085.233-.073.503-.073.503s-.186-.437-.216-.807c-.11.166-.137.48-.137.48s-.241-.69-.186-1.062c-.11-.323-.436-.965-.343-2.424.6.421 1.924.321 2.44-.439.171-.251.288-.939-.086-2.293-.24-.868-.835-2.16-1.066-2.651l-.028.02c.122.395.374 1.223.47 1.625.293 1.218.372 1.642.234 2.204-.116.488-.397.808-1.107 1.165-.71.358-1.653-.514-1.713-.562-.69-.55-1.224-1.447-1.284-1.883-.062-.477.275-.763.445-1.153-.243.07-.514.192-.514.192s.323-.334.722-.624c.165-.109.262-.178.436-.323a9.762 9.762 0 0 0-.456.003s.42-.227.855-.392c-.318-.014-.623-.003-.623-.003s.937-.419 1.678-.727c.509-.208 1.006-.147 1.286.257.367.53.752.817 1.569.996.501-.223.653-.337 1.284-.509.554-.61.99-.688.99-.688s-.216.198-.274.51c.314-.249.66-.455.66-.455s-.134.164-.259.426l.03.043c.366-.22.797-.394.797-.394s-.123.156-.268.358c.277-.002.838.012 1.056.037 1.285.028 1.552-1.374 2.045-1.55.618-.22.894-.353 1.947.68.903.888 1.609 2.477 1.259 2.833-.294.295-.874-.115-1.516-.916a3.466 3.466 0 0 1-.716-1.562 1.533 1.533 0 0 0-.497-.85s.23.51.23.96c0 .246.03 1.165.424 1.68-.039.076-.057.374-.1.43-.458-.554-1.443-.95-1.604-1.067.544.445 1.793 1.468 2.273 2.449.453.927.186 1.777.416 1.997.065.063.976 1.197 1.15 1.767.306.994.019 2.038-.381 2.685l-1.117.174c-.163-.045-.273-.068-.42-.153.08-.143.241-.5.243-.572l-.063-.111c-.348.492-.93.97-1.414 1.245-.633.359-1.363.304-1.838.156-1.348-.415-2.623-1.327-2.93-1.566 0 0-.01.191.048.234.34.383 1.119 1.077 1.872 1.56l-1.605.177.759 5.908c-.337.048-.39.071-.757.124-.325-1.147-.946-1.895-1.624-2.332-.599-.384-1.424-.47-2.214-.314l-.05.059a2.851 2.851 0 0 1 1.863.444c.654.413 1.181 1.481 1.375 2.124.248.822.42 1.7-.248 2.632-.476.662-1.864 1.028-2.986.237.3.481.705.876 1.25.95.809.11 1.577-.03 2.106-.574.452-.464.69-1.434.628-2.456l.714-.104.258 1.834 11.827-1.424zM15.05 6.848c-.034.075-.085.125-.007.37l.004.014.013.032.032.073c.14.287.295.558.552.696.067-.011.136-.019.207-.023.242-.01.395.028.492.08.009-.048.01-.119.005-.222-.018-.364.072-.982-.626-1.308-.264-.122-.634-.084-.757.068a.302.302 0 0 1 .058.013c.186.066.06.13.027.207m1.958 3.392c-.092-.05-.52-.03-.821.005-.574.068-1.193.267-1.328.372-.247.191-.135.523.047.66.511.382.96.638 1.432.575.29-.038.546-.497.728-.914.124-.288.124-.598-.058-.698m-5.077-2.942c.162-.154-.805-.355-1.556.156-.554.378-.571 1.187-.041 1.646.053.046.096.078.137.104a4.77 4.77 0 0 1 1.396-.412c.113-.125.243-.345.21-.745-.044-.542-.455-.456-.146-.749", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.datadoghq.com/about/resources", + "guidelines": "https://www.datadoghq.com/about/resources/" + } + }, + { + "id": "deepseek", + "title": "DeepSeek", + "category": "ai", + "aliases": [], + "domains": [ + "deepseek.com" + ], + "viewBox": 24, + "hex": "5786FE", + "path": "M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.deepseek.com" + } + }, + { + "id": "digitalocean", + "title": "DigitalOcean", + "category": "cloud", + "aliases": [ + "digital-ocean" + ], + "domains": [ + "digitalocean.com" + ], + "viewBox": 24, + "hex": "0080FF", + "path": "M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.digitalocean.com/press/", + "guidelines": "https://www.digitalocean.com/press/" + } + }, + { + "id": "discord", + "title": "Discord", + "category": "collaboration", + "aliases": [], + "domains": [ + "discord.com" + ], + "viewBox": 24, + "hex": "5865F2", + "path": "M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://discord.com/branding", + "guidelines": "https://discord.com/branding" + } + }, + { + "id": "django", + "title": "Django", + "category": "framework", + "aliases": [], + "domains": [ + "djangoproject.com" + ], + "viewBox": 24, + "hex": "092E20", + "path": "M11.146 0h3.924v18.166c-2.013.382-3.491.535-5.096.535-4.791 0-7.288-2.166-7.288-6.32 0-4.002 2.65-6.6 6.753-6.6.637 0 1.121.05 1.707.203zm0 9.143a3.894 3.894 0 00-1.325-.204c-1.988 0-3.134 1.223-3.134 3.365 0 2.09 1.096 3.236 3.109 3.236.433 0 .79-.025 1.35-.102V9.142zM21.314 6.06v9.098c0 3.134-.229 4.638-.917 5.937-.637 1.249-1.478 2.039-3.211 2.905l-3.644-1.733c1.733-.815 2.574-1.53 3.109-2.625.561-1.121.739-2.421.739-5.835V6.059h3.924zM17.39.021h3.924v4.026H17.39z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.djangoproject.com/community/logos/" + } + }, + { + "id": "docker", + "title": "Docker", + "category": "engineering", + "aliases": [], + "domains": [ + "docker.com" + ], + "viewBox": 24, + "hex": "2496ED", + "path": "M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.docker.com/company/newsroom/media-resources" + } + }, + { + "id": "dotnet", + "title": ".NET", + "category": "framework", + "aliases": [ + ".net" + ], + "domains": [ + "dotnet.microsoft.com" + ], + "viewBox": 24, + "hex": "512BD4", + "path": "M24 8.77h-2.468v7.565h-1.425V8.77h-2.462V7.53H24zm-6.852 7.565h-4.821V7.53h4.63v1.24h-3.205v2.494h2.953v1.234h-2.953v2.604h3.396zm-6.708 0H8.882L4.78 9.863a2.896 2.896 0 0 1-.258-.51h-.036c.032.189.048.592.048 1.21v5.772H3.157V7.53h1.659l3.965 6.32c.167.261.275.442.323.54h.024c-.04-.233-.06-.629-.06-1.185V7.529h1.372zm-8.703-.693a.868.829 0 0 1-.869.829.868.829 0 0 1-.868-.83.868.829 0 0 1 .868-.828.868.829 0 0 1 .869.829Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/dotnet/brand/blob/c7d0f51b8ec59531332d05fb27a5b758a7a3d689/logo/dotnet-logo.svg", + "guidelines": "https://github.com/dotnet/brand/blob/c7d0f51b8ec59531332d05fb27a5b758a7a3d689/dotnet-styleGuide-2024.pdf", + "license": { + "type": "CC0-1.0", + "url": "https://spdx.org/licenses/CC0-1.0" + } + } + }, + { + "id": "elasticsearch", + "title": "Elasticsearch", + "category": "data", + "aliases": [ + "elastic" + ], + "domains": [ + "elastic.co" + ], + "viewBox": 24, + "hex": "005571", + "path": "M13.394 0C8.683 0 4.609 2.716 2.644 6.667h15.641a4.77 4.77 0 0 0 3.073-1.11c.446-.375.864-.785 1.247-1.243l.001-.002A11.974 11.974 0 0 0 13.394 0zM1.804 8.889a12.009 12.009 0 0 0 0 6.222h14.7a3.111 3.111 0 1 0 0-6.222zm.84 8.444C4.61 21.283 8.684 24 13.395 24c3.701 0 7.011-1.677 9.212-4.312l-.001-.002a9.958 9.958 0 0 0-1.247-1.243 4.77 4.77 0 0 0-3.073-1.11z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.elastic.co/brand" + } + }, + { + "id": "facebook", + "title": "Facebook", + "category": "channel", + "aliases": [], + "domains": [ + "facebook.com" + ], + "viewBox": 24, + "hex": "0866FF", + "path": "M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://about.meta.com/brand/resources/facebook/logo", + "guidelines": "https://about.meta.com/brand/resources/facebook/logo" + } + }, + { + "id": "fastapi", + "title": "FastAPI", + "category": "framework", + "aliases": [], + "domains": [ + "fastapi.tiangolo.com" + ], + "viewBox": 24, + "hex": "009688", + "path": "M12 .0387C5.3729.0384.0003 5.3931 0 11.9988c-.001 6.6066 5.372 11.9628 12 11.9625 6.628.0003 12.001-5.3559 12-11.9625-.0003-6.6057-5.3729-11.9604-12-11.96m-.829 5.4153h7.55l-7.5805 5.3284h5.1828L5.279 18.5436q2.9466-6.5444 5.892-13.0896", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/tiangolo/fastapi/blob/ffb4f77a11f83132b521ba0aac6c95792c19e797/docs/en/docs/img/icon-white.svg" + } + }, + { + "id": "figma", + "title": "Figma", + "category": "collaboration", + "aliases": [], + "domains": [ + "figma.com" + ], + "viewBox": 24, + "hex": "F24E1E", + "path": "M15.852 8.981h-4.588V0h4.588c2.476 0 4.49 2.014 4.49 4.49s-2.014 4.491-4.49 4.491zM12.735 7.51h3.117c1.665 0 3.019-1.355 3.019-3.019s-1.355-3.019-3.019-3.019h-3.117V7.51zm0 1.471H8.148c-2.476 0-4.49-2.014-4.49-4.49S5.672 0 8.148 0h4.588v8.981zm-4.587-7.51c-1.665 0-3.019 1.355-3.019 3.019s1.354 3.02 3.019 3.02h3.117V1.471H8.148zm4.587 15.019H8.148c-2.476 0-4.49-2.014-4.49-4.49s2.014-4.49 4.49-4.49h4.588v8.98zM8.148 8.981c-1.665 0-3.019 1.355-3.019 3.019s1.355 3.019 3.019 3.019h3.117V8.981H8.148zM8.172 24c-2.489 0-4.515-2.014-4.515-4.49s2.014-4.49 4.49-4.49h4.588v4.441c0 2.503-2.047 4.539-4.563 4.539zm-.024-7.51a3.023 3.023 0 0 0-3.019 3.019c0 1.665 1.365 3.019 3.044 3.019 1.705 0 3.093-1.376 3.093-3.068v-2.97H8.148zm7.704 0h-.098c-2.476 0-4.49-2.014-4.49-4.49s2.014-4.49 4.49-4.49h.098c2.476 0 4.49 2.014 4.49 4.49s-2.014 4.49-4.49 4.49zm-.097-7.509c-1.665 0-3.019 1.355-3.019 3.019s1.355 3.019 3.019 3.019h.098c1.665 0 3.019-1.355 3.019-3.019s-1.355-3.019-3.019-3.019h-.098z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.figma.com/using-the-figma-brand/", + "guidelines": "https://www.figma.com/using-the-figma-brand/" + } + }, + { + "id": "firebase", + "title": "Firebase", + "category": "cloud", + "aliases": [], + "domains": [ + "firebase.google.com" + ], + "viewBox": 24, + "hex": "DD2C00", + "path": "M19.455 8.369c-.538-.748-1.778-2.285-3.681-4.569-.826-.991-1.535-1.832-1.884-2.245a146 146 0 0 0-.488-.576l-.207-.245-.113-.133-.022-.032-.01-.005L12.57 0l-.609.488c-1.555 1.246-2.828 2.851-3.681 4.64-.523 1.064-.864 2.105-1.043 3.176-.047.241-.088.489-.121.738-.209-.017-.421-.028-.632-.033-.018-.001-.035-.002-.059-.003a7.46 7.46 0 0 0-2.28.274l-.317.089-.163.286c-.765 1.342-1.198 2.869-1.252 4.416-.07 2.01.477 3.954 1.583 5.625 1.082 1.633 2.61 2.882 4.42 3.611l.236.095.071.025.003-.001a9.59 9.59 0 0 0 2.941.568q.171.006.342.006c1.273 0 2.513-.249 3.69-.742l.008.004.313-.145a9.63 9.63 0 0 0 3.927-3.335c1.01-1.49 1.577-3.234 1.641-5.042.075-2.161-.643-4.304-2.133-6.371m-7.083 6.695c.328 1.244.264 2.44-.191 3.558-1.135-1.12-1.967-2.352-2.475-3.665-.543-1.404-.87-2.74-.974-3.975.48.157.922.366 1.315.622 1.132.737 1.914 1.902 2.325 3.461zm.207 6.022c.482.368.99.712 1.513 1.028-.771.21-1.565.302-2.369.273a8 8 0 0 1-.373-.022c.458-.394.869-.823 1.228-1.279zm1.347-6.431c-.516-1.957-1.527-3.437-3.002-4.398-.647-.421-1.385-.741-2.194-.95.011-.134.026-.268.043-.4.014-.113.03-.216.046-.313.133-.689.332-1.37.589-2.025.099-.25.206-.499.321-.74l.004-.008c.177-.358.376-.719.61-1.105l.092-.152-.003-.001c.544-.851 1.197-1.627 1.942-2.311l.288.341c.672.796 1.304 1.548 1.878 2.237 1.291 1.549 2.966 3.583 3.612 4.48 1.277 1.771 1.893 3.579 1.83 5.375-.049 1.395-.461 2.755-1.195 3.933-.694 1.116-1.661 2.05-2.8 2.708-.636-.318-1.559-.839-2.539-1.599.79-1.575.952-3.28.479-5.072zm-2.575 5.397c-.725.939-1.587 1.55-2.09 1.856-.081-.029-.163-.06-.243-.093l-.065-.026c-1.49-.616-2.747-1.656-3.635-3.01-.907-1.384-1.356-2.993-1.298-4.653.041-1.19.338-2.327.882-3.379.316-.07.638-.114.96-.131l.084-.002c.162-.003.324-.003.478 0 .227.011.454.035.677.07.073 1.513.445 3.145 1.105 4.852.637 1.644 1.694 3.162 3.144 4.515z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://firebase.google.com/brand-guidelines", + "guidelines": "https://firebase.google.com/brand-guidelines" + } + }, + { + "id": "flask", + "title": "Flask", + "category": "framework", + "aliases": [], + "domains": [ + "palletsprojects.com" + ], + "viewBox": 24, + "hex": "3BABC3", + "path": "M10.773 2.878c-.013 1.434.322 4.624.445 5.734l-8.558 3.83c-.56-.959-.98-2.304-1.237-3.38l-.06.027c-.205.09-.406.053-.494-.088l-.011-.018-.82-1.506c-.058-.105-.05-.252.024-.392a.78.78 0 0 1 .358-.331l9.824-4.207c.146-.064.299-.063.4.004.106.062.127.128.13.327Zm.68 7c.523 1.97.675 2.412.832 2.818l-7.263 3.7a19.35 19.35 0 0 1-1.81-2.83l8.24-3.689Zm12.432 8.786h.003c.283.402-.047.657-.153.698l-.947.37c.037.125.035.319-.217.414l-.736.287c-.229.09-.398-.059-.42-.2l-.025-.125c-4.427 1.784-7.94 1.685-10.696.647-1.981-.745-3.576-1.983-4.846-3.379l6.948-3.54c.721 1.431 1.586 2.454 2.509 3.178 2.086 1.638 4.415 1.712 5.793 1.563l-.047-.233c-.015-.077.007-.135.086-.165l.734-.288a.302.302 0 0 1 .342.086l.748-.288a.306.306 0 0 1 .341.086l.583.89Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/pallets/flask/blob/85c5d93cbd049c4bd0679c36fd1ddcae8c37b642/docs/_static/flask-icon.svg" + } + }, + { + "id": "fly-io", + "title": "Fly.io", + "category": "cloud", + "aliases": [ + "fly.io" + ], + "domains": [ + "fly.io" + ], + "viewBox": 24, + "hex": "24175B", + "path": "M11.987 0c-2.45-.01-5.002.925-6.541 2.897-1.17 1.502-1.664 3.474-1.49 5.356.29 2.112 1.476 3.96 2.676 5.672a41.5 41.5 0 0 0 4.216 4.831c-1.063.832-1.943 2.286-1.357 3.644.821 2.32 4.665 2.05 5.122-.372.39-1.288-.694-2.533-1.428-3.309 2.388-2.431 4.706-5.036 6.17-8.145.595-1.32.902-2.802.614-4.24-.28-2.341-1.823-4.473-3.967-5.46C14.76.266 13.364.016 11.987 0m-.236 1.577v15.534C9.881 13.483 7.724 9.266 8.73 5.069c.35-1.539 1.253-3.309 3.02-3.492m1.996.04c1.534.357 3.031 1.096 3.906 2.48 1.3 1.93 1.318 4.55.1 6.521-1.268 2.395-3.06 4.463-4.916 6.415 1.472-2.974 3.074-6.106 3.182-9.5-.043-2.08-.438-4.612-2.272-5.916M11.97 20.103c.848.342 1.597 1.983.153 2.173-.664.15-1.367-.599-.995-1.222.213-.355.488-.73.842-.95", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://fly.io/docs/about/brand", + "guidelines": "https://fly.io/docs/about/brand" + } + }, + { + "id": "github", + "title": "GitHub", + "category": "engineering", + "aliases": [], + "domains": [ + "github.com" + ], + "viewBox": 24, + "hex": "181717", + "path": "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/logos", + "guidelines": "https://github.com/logos" + } + }, + { + "id": "github-actions", + "title": "GitHub Actions", + "category": "engineering", + "aliases": [], + "domains": [], + "viewBox": 24, + "hex": "2088FF", + "path": "M10.984 13.836a.5.5 0 0 1-.353-.146l-.745-.743a.5.5 0 1 1 .706-.708l.392.391 1.181-1.18a.5.5 0 0 1 .708.707l-1.535 1.533a.504.504 0 0 1-.354.146zm9.353-.147l1.534-1.532a.5.5 0 0 0-.707-.707l-1.181 1.18-.392-.391a.5.5 0 1 0-.706.708l.746.743a.497.497 0 0 0 .706-.001zM4.527 7.452l2.557-1.585A1 1 0 0 0 7.09 4.17L4.533 2.56A1 1 0 0 0 3 3.406v3.196a1.001 1.001 0 0 0 1.527.85zm2.03-2.436L4 6.602V3.406l2.557 1.61zM24 12.5c0 1.93-1.57 3.5-3.5 3.5a3.503 3.503 0 0 1-3.46-3h-2.08a3.503 3.503 0 0 1-3.46 3 3.502 3.502 0 0 1-3.46-3h-.558c-.972 0-1.85-.399-2.482-1.042V17c0 1.654 1.346 3 3 3h.04c.244-1.693 1.7-3 3.46-3 1.93 0 3.5 1.57 3.5 3.5S13.43 24 11.5 24a3.502 3.502 0 0 1-3.46-3H8c-2.206 0-4-1.794-4-4V9.899A5.008 5.008 0 0 1 0 5c0-2.757 2.243-5 5-5s5 2.243 5 5a5.005 5.005 0 0 1-4.952 4.998A2.482 2.482 0 0 0 7.482 12h.558c.244-1.693 1.7-3 3.46-3a3.502 3.502 0 0 1 3.46 3h2.08a3.503 3.503 0 0 1 3.46-3c1.93 0 3.5 1.57 3.5 3.5zm-15 8c0 1.378 1.122 2.5 2.5 2.5s2.5-1.122 2.5-2.5-1.122-2.5-2.5-2.5S9 19.122 9 20.5zM5 9c2.206 0 4-1.794 4-4S7.206 1 5 1 1 2.794 1 5s1.794 4 4 4zm9 3.5c0-1.378-1.122-2.5-2.5-2.5S9 11.122 9 12.5s1.122 2.5 2.5 2.5 2.5-1.122 2.5-2.5zm9 0c0-1.378-1.122-2.5-2.5-2.5S18 11.122 18 12.5s1.122 2.5 2.5 2.5 2.5-1.122 2.5-2.5zm-13 8a.5.5 0 1 0 1 0 .5.5 0 0 0-1 0zm2 0a.5.5 0 1 0 1 0 .5.5 0 0 0-1 0zm12 0c0 1.93-1.57 3.5-3.5 3.5a3.503 3.503 0 0 1-3.46-3.002c-.007.001-.013.005-.021.005l-.506.017h-.017a.5.5 0 0 1-.016-.999l.506-.017c.018-.002.035.006.052.007A3.503 3.503 0 0 1 20.5 17c1.93 0 3.5 1.57 3.5 3.5zm-1 0c0-1.378-1.122-2.5-2.5-2.5S18 19.122 18 20.5s1.122 2.5 2.5 2.5 2.5-1.122 2.5-2.5z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/features/actions" + } + }, + { + "id": "gitlab", + "title": "GitLab", + "category": "engineering", + "aliases": [], + "domains": [ + "gitlab.com" + ], + "viewBox": 24, + "hex": "FC6D26", + "path": "m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://about.gitlab.com/press/press-kit/", + "guidelines": "https://about.gitlab.com/handbook/marketing/corporate-marketing/brand-activation/trademark-guidelines/" + } + }, + { + "id": "go", + "title": "Go", + "category": "language", + "aliases": [ + "golang" + ], + "domains": [ + "go.dev" + ], + "viewBox": 24, + "hex": "00ADD8", + "path": "M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://blog.golang.org/go-brand", + "guidelines": "https://blog.golang.org/go-brand" + } + }, + { + "id": "google-cloud", + "title": "Google Cloud", + "category": "cloud", + "aliases": [ + "gcp", + "googlecloud" + ], + "domains": [ + "cloud.google.com" + ], + "viewBox": 24, + "hex": "4285F4", + "path": "M12.19 2.38a9.344 9.344 0 0 0-9.234 6.893c.053-.02-.055.013 0 0-3.875 2.551-3.922 8.11-.247 10.941l.006-.007-.007.03a6.717 6.717 0 0 0 4.077 1.356h5.173l.03.03h5.192c6.687.053 9.376-8.605 3.835-12.35a9.365 9.365 0 0 0-2.821-4.552l-.043.043.006-.05A9.344 9.344 0 0 0 12.19 2.38zm-.358 4.146c1.244-.04 2.518.368 3.486 1.15a5.186 5.186 0 0 1 1.862 4.078v.518c3.53-.07 3.53 5.262 0 5.193h-5.193l-.008.009v-.04H6.785a2.59 2.59 0 0 1-1.067-.23h.001a2.597 2.597 0 1 1 3.437-3.437l3.013-3.012A6.747 6.747 0 0 0 8.11 8.24c.018-.01.04-.026.054-.023a5.186 5.186 0 0 1 3.67-1.69z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://cloud.google.com" + } + }, + { + "id": "google-gemini", + "title": "Google Gemini", + "category": "ai", + "aliases": [ + "gemini" + ], + "domains": [ + "gemini.google.com" + ], + "viewBox": 24, + "hex": "8E75B2", + "path": "M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://gemini.google.com" + } + }, + { + "id": "grafana", + "title": "Grafana", + "category": "engineering", + "aliases": [], + "domains": [ + "grafana.com" + ], + "viewBox": 24, + "hex": "F46800", + "path": "M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://grafana.com" + } + }, + { + "id": "helm", + "title": "Helm", + "category": "engineering", + "aliases": [], + "domains": [ + "helm.sh" + ], + "viewBox": 24, + "hex": "0F1689", + "path": "M12.337 0c-.475 0-.861 1.016-.861 2.269 0 .527.069 1.011.183 1.396a8.514 8.514 0 0 0-3.961 1.22 5.229 5.229 0 0 0-.595-1.093c-.606-.866-1.34-1.436-1.79-1.43a.381.381 0 0 0-.217.066c-.39.273-.123 1.326.596 2.353.267.381.559.705.84.948a8.683 8.683 0 0 0-1.528 1.716h1.734a7.179 7.179 0 0 1 5.381-2.421 7.18 7.18 0 0 1 5.382 2.42h1.733a8.687 8.687 0 0 0-1.32-1.53c.35-.249.735-.643 1.078-1.133.719-1.027.986-2.08.596-2.353a.382.382 0 0 0-.217-.065c-.45-.007-1.184.563-1.79 1.43a4.897 4.897 0 0 0-.676 1.325 8.52 8.52 0 0 0-3.899-1.42c.12-.39.193-.887.193-1.429 0-1.253-.386-2.269-.862-2.269zM1.624 9.443v5.162h1.358v-1.968h1.64v1.968h1.357V9.443H4.62v1.838H2.98V9.443zm5.912 0v5.162h3.21v-1.108H8.893v-.95h1.64v-1.142h-1.64v-.84h1.853V9.443zm4.698 0v5.162h3.218v-1.362h-1.86v-3.8zm4.706 0v5.162h1.364v-2.643l1.357 1.225 1.35-1.232v2.65h1.365V9.443h-.614l-2.1 1.914-2.109-1.914zm-11.82 7.28a8.688 8.688 0 0 0 1.412 1.548 5.206 5.206 0 0 0-.841.948c-.719 1.027-.985 2.08-.596 2.353.39.273 1.289-.338 2.007-1.364a5.23 5.23 0 0 0 .595-1.092 8.514 8.514 0 0 0 3.961 1.219 5.01 5.01 0 0 0-.183 1.396c0 1.253.386 2.269.861 2.269.476 0 .862-1.016.862-2.269 0-.542-.072-1.04-.193-1.43a8.52 8.52 0 0 0 3.9-1.42c.121.4.352.865.675 1.327.719 1.026 1.617 1.637 2.007 1.364.39-.273.123-1.326-.596-2.353-.343-.49-.727-.885-1.077-1.135a8.69 8.69 0 0 0 1.202-1.36h-1.771a7.174 7.174 0 0 1-5.227 2.252 7.174 7.174 0 0 1-5.226-2.252z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://helm.sh" + } + }, + { + "id": "hubspot", + "title": "HubSpot", + "category": "business", + "aliases": [], + "domains": [ + "hubspot.com" + ], + "viewBox": 24, + "hex": "FF7A59", + "path": "M18.164 7.93V5.084a2.198 2.198 0 001.267-1.978v-.067A2.2 2.2 0 0017.238.845h-.067a2.2 2.2 0 00-2.193 2.193v.067a2.196 2.196 0 001.252 1.973l.013.006v2.852a6.22 6.22 0 00-2.969 1.31l.012-.01-7.828-6.095A2.497 2.497 0 104.3 4.656l-.012.006 7.697 5.991a6.176 6.176 0 00-1.038 3.446c0 1.343.425 2.588 1.147 3.607l-.013-.02-2.342 2.343a1.968 1.968 0 00-.58-.095h-.002a2.033 2.033 0 102.033 2.033 1.978 1.978 0 00-.1-.595l.005.014 2.317-2.317a6.247 6.247 0 104.782-11.134l-.036-.005zm-.964 9.378a3.206 3.206 0 113.215-3.207v.002a3.206 3.206 0 01-3.207 3.207z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.hubspot.com/style-guide", + "guidelines": "https://www.hubspot.com/style-guide" + } + }, + { + "id": "hugging-face", + "title": "Hugging Face", + "category": "ai", + "aliases": [ + "huggingface" + ], + "domains": [ + "huggingface.co" + ], + "viewBox": 24, + "hex": "FFD21E", + "path": "M12.025 1.13c-5.77 0-10.449 4.647-10.449 10.378 0 1.112.178 2.181.503 3.185.064-.222.203-.444.416-.577a.96.96 0 0 1 .524-.15c.293 0 .584.124.84.284.278.173.48.408.71.694.226.282.458.611.684.951v-.014c.017-.324.106-.622.264-.874s.403-.487.762-.543c.3-.047.596.06.787.203s.31.313.4.467c.15.257.212.468.233.542.01.026.653 1.552 1.657 2.54.616.605 1.01 1.223 1.082 1.912.055.537-.096 1.059-.38 1.572.637.121 1.294.187 1.967.187.657 0 1.298-.063 1.921-.178-.287-.517-.44-1.041-.384-1.581.07-.69.465-1.307 1.081-1.913 1.004-.987 1.647-2.513 1.657-2.539.021-.074.083-.285.233-.542.09-.154.208-.323.4-.467a1.08 1.08 0 0 1 .787-.203c.359.056.604.29.762.543s.247.55.265.874v.015c.225-.34.457-.67.683-.952.23-.286.432-.52.71-.694.257-.16.547-.284.84-.285a.97.97 0 0 1 .524.151c.228.143.373.388.43.625l.006.04a10.3 10.3 0 0 0 .534-3.273c0-5.731-4.678-10.378-10.449-10.378M8.327 6.583a1.5 1.5 0 0 1 .713.174 1.487 1.487 0 0 1 .617 2.013c-.183.343-.762-.214-1.102-.094-.38.134-.532.914-.917.71a1.487 1.487 0 0 1 .69-2.803m7.486 0a1.487 1.487 0 0 1 .689 2.803c-.385.204-.536-.576-.916-.71-.34-.12-.92.437-1.103.094a1.487 1.487 0 0 1 .617-2.013 1.5 1.5 0 0 1 .713-.174m-10.68 1.55a.96.96 0 1 1 0 1.921.96.96 0 0 1 0-1.92m13.838 0a.96.96 0 1 1 0 1.92.96.96 0 0 1 0-1.92M8.489 11.458c.588.01 1.965 1.157 3.572 1.164 1.607-.007 2.984-1.155 3.572-1.164.196-.003.305.12.305.454 0 .886-.424 2.328-1.563 3.202-.22-.756-1.396-1.366-1.63-1.32q-.011.001-.02.006l-.044.026-.01.008-.03.024q-.018.017-.035.036l-.032.04a1 1 0 0 0-.058.09l-.014.025q-.049.088-.11.19a1 1 0 0 1-.083.116 1.2 1.2 0 0 1-.173.18q-.035.029-.075.058a1.3 1.3 0 0 1-.251-.243 1 1 0 0 1-.076-.107c-.124-.193-.177-.363-.337-.444-.034-.016-.104-.008-.2.022q-.094.03-.216.087-.06.028-.125.063l-.13.074q-.067.04-.136.086a3 3 0 0 0-.135.096 3 3 0 0 0-.26.219 2 2 0 0 0-.12.121 2 2 0 0 0-.106.128l-.002.002a2 2 0 0 0-.09.132l-.001.001a1.2 1.2 0 0 0-.105.212q-.013.036-.024.073c-1.139-.875-1.563-2.317-1.563-3.203 0-.334.109-.457.305-.454m.836 10.354c.824-1.19.766-2.082-.365-3.194-1.13-1.112-1.789-2.738-1.789-2.738s-.246-.945-.806-.858-.97 1.499.202 2.362c1.173.864-.233 1.45-.685.64-.45-.812-1.683-2.896-2.322-3.295s-1.089-.175-.938.647 2.822 2.813 2.562 3.244-1.176-.506-1.176-.506-2.866-2.567-3.49-1.898.473 1.23 2.037 2.16c1.564.932 1.686 1.178 1.464 1.53s-3.675-2.511-4-1.297c-.323 1.214 3.524 1.567 3.287 2.405-.238.839-2.71-1.587-3.216-.642-.506.946 3.49 2.056 3.522 2.064 1.29.33 4.568 1.028 5.713-.624m5.349 0c-.824-1.19-.766-2.082.365-3.194 1.13-1.112 1.789-2.738 1.789-2.738s.246-.945.806-.858.97 1.499-.202 2.362c-1.173.864.233 1.45.685.64.451-.812 1.683-2.896 2.322-3.295s1.089-.175.938.647-2.822 2.813-2.562 3.244 1.176-.506 1.176-.506 2.866-2.567 3.49-1.898-.473 1.23-2.037 2.16c-1.564.932-1.686 1.178-1.464 1.53s3.675-2.511 4-1.297c.323 1.214-3.524 1.567-3.287 2.405.238.839 2.71-1.587 3.216-.642.506.946-3.49 2.056-3.522 2.064-1.29.33-4.568 1.028-5.713-.624", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://huggingface.co/brand", + "guidelines": "https://huggingface.co/brand" + } + }, + { + "id": "influxdb", + "title": "InfluxDB", + "category": "data", + "aliases": [ + "influx-db" + ], + "domains": [ + "influxdata.com" + ], + "viewBox": 24, + "hex": "22ADF6", + "path": "M23.778 14.482l-2.287-9.959c-.13-.545-.624-1.09-1.169-1.248L9.87.051C9.74 0 9.584 0 9.426 0c-.443 0-.909.18-1.222.443L.716 7.412C.3 7.776.092 8.504.222 9.024l2.445 10.662c.13.545.624 1.092 1.169 1.248l9.775 3.015c.13.051.285.051.443.051.443 0 .91-.18 1.223-.443l8.007-7.435c.418-.39.624-1.092.494-1.64zM10.962 2.417l7.175 2.21c.285.08.285.21 0 .286l-3.77.858c-.285.08-.674-.05-.883-.26l-2.626-2.834c-.235-.232-.184-.336.104-.26zm4.47 12.872c.079.286-.105.444-.39.365l-7.748-2.392c-.285-.079-.338-.313-.13-.52l5.93-5.514c.209-.209.443-.13.52.156zM2.667 8.267l6.293-5.85c.21-.209.545-.18.754.025L12.86 5.85c.209.21.18.545-.026.754l-6.293 5.85c-.21.21-.545.181-.754-.025L2.64 9.024a.536.536 0 01.026-.757zm1.536 9.284L2.54 10.244c-.08-.285.05-.34.234-.13L5.4 12.949c.209.209.285.624.209.909L4.462 17.55c-.079.285-.208.285-.26 0zm9.202 4.264l-8.217-2.522a.547.547 0 01-.364-.675l1.378-4.421a.547.547 0 01.675-.365l8.216 2.522c.285.079.443.39.364.675L14.08 21.45a.553.553 0 01-.674.365zm7.279-5.98L15.2 20.93c-.209.209-.31.13-.234-.155l1.144-3.694c.079-.285.39-.573.674-.624l3.77-.858c.288-.076.339.054.13.234zm.598-1.09l-4.523 1.039a.534.534 0 01-.65-.39l-1.922-8.372a.534.534 0 01.39-.65L19.1 5.335a.534.534 0 01.649.39l1.923 8.371c.079.31-.102.596-.39.65Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://influxdata.github.io/branding/logo/downloads/", + "guidelines": "https://influxdata.github.io/branding/logo/usage/" + } + }, + { + "id": "instagram", + "title": "Instagram", + "category": "channel", + "aliases": [], + "domains": [ + "instagram.com" + ], + "viewBox": 24, + "hex": "FF0069", + "path": "M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://about.meta.com/brand/resources/instagram", + "guidelines": "https://about.meta.com/brand/resources/instagram" + } + }, + { + "id": "intercom", + "title": "Intercom", + "category": "business", + "aliases": [], + "domains": [ + "intercom.com" + ], + "viewBox": 24, + "hex": "6AFDEF", + "path": "M21 0H3C1.343 0 0 1.343 0 3v18c0 1.658 1.343 3 3 3h18c1.658 0 3-1.342 3-3V3c0-1.657-1.342-3-3-3zm-5.801 4.399c0-.44.36-.8.802-.8.44 0 .8.36.8.8v10.688c0 .442-.36.801-.8.801-.443 0-.802-.359-.802-.801V4.399zM11.2 3.994c0-.44.357-.799.8-.799s.8.359.8.799v11.602c0 .44-.357.8-.8.8s-.8-.36-.8-.8V3.994zm-4 .405c0-.44.359-.8.799-.8.443 0 .802.36.802.8v10.688c0 .442-.36.801-.802.801-.44 0-.799-.359-.799-.801V4.399zM3.199 6c0-.442.36-.8.802-.8.44 0 .799.358.799.8v7.195c0 .441-.359.8-.799.8-.443 0-.802-.36-.802-.8V6zM20.52 18.202c-.123.105-3.086 2.593-8.52 2.593-5.433 0-8.397-2.486-8.521-2.593-.335-.288-.375-.792-.086-1.128.285-.334.79-.375 1.125-.09.047.041 2.693 2.211 7.481 2.211 4.848 0 7.456-2.186 7.479-2.207.334-.289.839-.25 1.128.086.289.336.25.84-.086 1.128zm.281-5.007c0 .441-.36.8-.801.8-.441 0-.801-.36-.801-.8V6c0-.442.361-.8.801-.8.441 0 .801.357.801.8v7.195z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.intercom.com/press", + "guidelines": "https://www.intercom.com/press" + } + }, + { + "id": "javascript", + "title": "JavaScript", + "category": "language", + "aliases": [ + "js" + ], + "domains": [], + "viewBox": 24, + "hex": "F7DF1E", + "path": "M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/voodootikigod/logo.js/blob/1544bdeed6d618a6cfe4f0650d04ab8d9cfa76d9/js.svg", + "license": { + "type": "MIT", + "url": "https://spdx.org/licenses/MIT" + } + } + }, + { + "id": "jenkins", + "title": "Jenkins", + "category": "engineering", + "aliases": [], + "domains": [ + "jenkins.io" + ], + "viewBox": 24, + "hex": "D24939", + "path": "M2.872 24h-.975a3.866 3.866 0 01-.07-.197c-.215-.666-.594-1.49-.692-2.154-.146-.984.78-1.039 1.374-1.465.915-.66 1.635-1.025 2.627-1.62.295-.179 1.182-.624 1.281-.829.201-.408-.345-.982-.49-1.3-.225-.507-.345-.937-.376-1.435-.824-.13-1.455-.627-1.844-1.185-.63-.925-1.066-2.635-.525-3.936.045-.103.254-.305.285-.463.06-.308-.105-.72-.12-1.048-.06-1.692.284-3.15 1.425-3.66.463-1.84 2.113-2.453 3.673-3.367.58-.342 1.224-.562 1.89-.807 2.372-.877 6.027-.712 7.994.783.836.633 2.176 1.97 2.656 2.939 1.262 2.555 1.17 6.825.287 9.934-.12.421-.29 1.032-.533 1.533-.168.35-.689 1.05-.625 1.36.064.314 1.19 1.17 1.432 1.395.434.422 1.26.975 1.324 1.5.07.557-.248 1.336-.41 1.875-.217.721-.436 1.441-.654 2.131H2.87zm11.104-3.54c-.545-.3-1.361-.622-2.065-.757-.87-.164-.78 1.188-.75 1.994.03.643.36 1.316.51 1.744.076.197.09.41.256.449.3.068 1.29-.326 1.575-.479.6-.328 1.064-.844 1.574-1.189.016-.17.016-.34.03-.508a2.648 2.648 0 00-1.095-.277c.314-.15.75-.15 1.035-.332l.016-.193c-.496-.03-.69-.254-1.021-.436zm7.454 2.935a17.78 17.78 0 00.465-1.752c.06-.287.215-.918.178-1.176-.059-.459-.684-.799-1.004-1.086-.584-.525-.95-.975-1.56-1.469-.249.375-.78.615-.983.914 1.447-.689 1.71 2.625 1.141 3.69.09.329.391.45.514.735l-.086.166h1.29c.013 0 .03 0 .044.014zm-6.634-.012c-.05-.074-.1-.135-.15-.209l-.301.195h.45zm2.77 0c.008-.209.018-.404.03-.598-.53.029-.825-.48-1.196-.527-.324-.045-.6.361-1.02.195-.095.105-.183.227-.284.316.154.18.295.375.424.584h.815c.014-.164.135-.285.3-.285.165 0 .284.121.284.27h.66zm2.116 0c-.314-.479-.947-.898-1.68-.555l-.03.541h1.71zm-8.51 0l-.104-.344c-.225-.72-.36-1.26-.405-1.68-.914-.436-1.875-.87-2.654-1.426-.15-.105-1.109-1.35-1.23-1.305-1.739.676-3.359 1.86-4.814 2.984.256.557.48 1.141.69 1.74h8.505zm8.265-2.113c-.029-.512-.164-1.56-.48-1.74-.66-.39-1.846.78-2.34.943.045.15.135.271.15.48.285-.074.645-.029.898.092-.299.03-.629.03-.824.164-.074.195.016.48-.029.764.69.197 1.5.303 2.385.332.164-.227.225-.645.211-1.082zm-4.08-.36c-.044.375.046.51.12.943 1.26.391 1.034-1.74-.135-.959zM8.76 19.5c-.45.457 1.27 1.082 1.814 1.115 0-.29.165-.564.135-.77-.65-.118-1.502-.042-1.945-.347zm5.565.215c0 .043-.061.03-.068.064.58.451 1.014.545 1.802.51.354-.262.67-.563 1.043-.807-.855.074-1.931.607-2.774.23zm3.42-17.726c-1.606-.906-4.35-1.591-6.076-.731-1.38.692-3.27 1.84-3.899 3.292.6 1.402-.166 2.686-.226 4.109-.018.757.36 1.42.391 2.242-.2.338-.825.38-1.26.356-.146-.729-.4-1.549-1.155-1.63-1.064-.116-1.845.764-1.89 1.683-.06 1.08.833 2.864 2.085 2.745.488-.046.608-.54 1.139-.54.285.57-.445.75-.523 1.154-.016.105.06.511.104.705.233.944.744 2.16 1.245 2.88.635.9 1.884 1.051 3.229 1.141.24-.525 1.125-.48 1.706-.346-.691-.27-1.336-.945-1.875-1.529-.615-.676-1.23-1.41-1.261-2.28 1.155 1.604 2.1 3 4.2 3.704 1.59.525 3.45-.254 4.664-1.109.51-.359.811-.93 1.17-1.439 1.35-1.936 1.98-4.71 1.846-7.394-.06-1.111-.06-2.221-.436-2.955-.389-.781-1.695-1.471-2.475-.781-.15-.764.63-1.23 1.545-.96-.66-.854-1.336-1.858-2.266-2.384zM13.58 14.896c.615 1.544 2.724 1.363 4.505 1.323-.084.194-.256.435-.465.515-.57.232-2.145.408-2.937-.012-.506-.27-.824-.873-1.102-1.227-.137-.172-.795-.608-.012-.609zm.164-.87c.893.464 2.52.517 3.731.48.066.267.066.593.068.913-1.55.08-3.386-.304-3.794-1.395h-.005zm6.675-.586c-.473.9-1.145 1.897-2.539 1.928-.023-.284-.045-.735 0-.904 1.064-.103 1.727-.646 2.543-1.017zm-.649-.667c-1.02.66-2.154 1.375-3.824 1.21-.351-.31-.485-1-.14-1.458.181.313.06.885.57.97.944.165 2.038-.579 2.73-.84.42-.713-.046-.976-.42-1.433-.782-.93-1.83-2.1-1.802-3.51.314-.224.346.346.391.45.404.96 1.424 2.175 2.174 3 .18.21.48.39.51.524.092.39-.254.854-.209 1.11zm-13.439-.675c-.314-.184-.393-.99-.768-1.01-.535-.03-.438 1.05-.436 1.68-.37-.33-.435-1.365-.164-1.89-.308-.15-.445.164-.618.284.22-1.59 2.34-.734 1.99.96zM4.713 5.995c-.685.756-.54 2.174-.459 3.188 1.244-.785 2.898.06 2.883 1.394.595-.016.223-.744.115-1.215-.353-1.528.592-3.187.041-4.59-1.064.084-1.939.52-2.578 1.215zm9.12 1.113c.307.562.404 1.148.84 1.57.195.19.574.424.387.95-.045.121-.365.391-.551.45-.674.195-2.254.03-1.721-.81.563.015 1.314.36 1.732-.045-.314-.524-.885-1.53-.674-2.13zm6.198-.013h.068c.33.668.6 1.375 1.004 1.965-.27.628-2.053 1.19-2.023.057.39-.17 1.05-.035 1.395-.25-.193-.556-.48-1.006-.434-1.771zm-6.927-1.617c-1.422-.33-2.131.592-2.56 1.553-.384-.094-.231-.615-.135-.883.255-.701 1.28-1.633 2.119-1.506.359.057.848.386.576.834zM9.642 1.593c-1.56.44-3.56 1.574-4.2 2.974.495-.07.84-.321 1.33-.351.186-.016.428.074.641.015.424-.104.78-1.065 1.102-1.41.31-.345.685-.496.94-.81.167-.09.409-.074.42-.33-.073-.075-.15-.135-.232-.105v.017z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://get.jenkins.io/art/", + "guidelines": "https://www.jenkins.io/press/", + "license": { + "type": "CC-BY-SA-3.0", + "url": "https://spdx.org/licenses/CC-BY-SA-3.0" + } + } + }, + { + "id": "jira", + "title": "Jira", + "category": "collaboration", + "aliases": [], + "domains": [ + "atlassian.com" + ], + "viewBox": 24, + "hex": "0052CC", + "path": "M11.571 11.513H0a5.218 5.218 0 0 0 5.232 5.215h2.13v2.057A5.215 5.215 0 0 0 12.575 24V12.518a1.005 1.005 0 0 0-1.005-1.005zm5.723-5.756H5.736a5.215 5.215 0 0 0 5.215 5.214h2.129v2.058a5.218 5.218 0 0 0 5.215 5.214V6.758a1.001 1.001 0 0 0-1.001-1.001zM23.013 0H11.455a5.215 5.215 0 0 0 5.215 5.215h2.129v2.057A5.215 5.215 0 0 0 24 12.483V1.005A1.001 1.001 0 0 0 23.013 0Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://atlassian.design/resources/logo-library", + "guidelines": "https://atlassian.design/foundations/logos/" + } + }, + { + "id": "kubernetes", + "title": "Kubernetes", + "category": "engineering", + "aliases": [ + "k8s" + ], + "domains": [ + "kubernetes.io" + ], + "viewBox": 24, + "hex": "326CE5", + "path": "M10.204 14.35l.007.01-.999 2.413a5.171 5.171 0 0 1-2.075-2.597l2.578-.437.004.005a.44.44 0 0 1 .484.606zm-.833-2.129a.44.44 0 0 0 .173-.756l.002-.011L7.585 9.7a5.143 5.143 0 0 0-.73 3.255l2.514-.725.002-.009zm1.145-1.98a.44.44 0 0 0 .699-.337l.01-.005.15-2.62a5.144 5.144 0 0 0-3.01 1.442l2.147 1.523.004-.002zm.76 2.75l.723.349.722-.347.18-.78-.5-.623h-.804l-.5.623.179.779zm1.5-3.095a.44.44 0 0 0 .7.336l.008.003 2.134-1.513a5.188 5.188 0 0 0-2.992-1.442l.148 2.615.002.001zm10.876 5.97l-5.773 7.181a1.6 1.6 0 0 1-1.248.594l-9.261.003a1.6 1.6 0 0 1-1.247-.596l-5.776-7.18a1.583 1.583 0 0 1-.307-1.34L2.1 5.573c.108-.47.425-.864.863-1.073L11.305.513a1.606 1.606 0 0 1 1.385 0l8.345 3.985c.438.209.755.604.863 1.073l2.062 8.955c.108.47-.005.963-.308 1.34zm-3.289-2.057c-.042-.01-.103-.026-.145-.034-.174-.033-.315-.025-.479-.038-.35-.037-.638-.067-.895-.148-.105-.04-.18-.165-.216-.216l-.201-.059a6.45 6.45 0 0 0-.105-2.332 6.465 6.465 0 0 0-.936-2.163c.052-.047.15-.133.177-.159.008-.09.001-.183.094-.282.197-.185.444-.338.743-.522.142-.084.273-.137.415-.242.032-.024.076-.062.11-.089.24-.191.295-.52.123-.736-.172-.216-.506-.236-.745-.045-.034.027-.08.062-.111.088-.134.116-.217.23-.33.35-.246.25-.45.458-.673.609-.097.056-.239.037-.303.033l-.19.135a6.545 6.545 0 0 0-4.146-2.003l-.012-.223c-.065-.062-.143-.115-.163-.25-.022-.268.015-.557.057-.905.023-.163.061-.298.068-.475.001-.04-.001-.099-.001-.142 0-.306-.224-.555-.5-.555-.275 0-.499.249-.499.555l.001.014c0 .041-.002.092 0 .128.006.177.044.312.067.475.042.348.078.637.056.906a.545.545 0 0 1-.162.258l-.012.211a6.424 6.424 0 0 0-4.166 2.003 8.373 8.373 0 0 1-.18-.128c-.09.012-.18.04-.297-.029-.223-.15-.427-.358-.673-.608-.113-.12-.195-.234-.329-.349-.03-.026-.077-.062-.111-.088a.594.594 0 0 0-.348-.132.481.481 0 0 0-.398.176c-.172.216-.117.546.123.737l.007.005.104.083c.142.105.272.159.414.242.299.185.546.338.743.522.076.082.09.226.1.288l.16.143a6.462 6.462 0 0 0-1.02 4.506l-.208.06c-.055.072-.133.184-.215.217-.257.081-.546.11-.895.147-.164.014-.305.006-.48.039-.037.007-.09.02-.133.03l-.004.002-.007.002c-.295.071-.484.342-.423.608.061.267.349.429.645.365l.007-.001.01-.003.129-.029c.17-.046.294-.113.448-.172.33-.118.604-.217.87-.256.112-.009.23.069.288.101l.217-.037a6.5 6.5 0 0 0 2.88 3.596l-.09.218c.033.084.069.199.044.282-.097.252-.263.517-.452.813-.091.136-.185.242-.268.399-.02.037-.045.095-.064.134-.128.275-.034.591.213.71.248.12.556-.007.69-.282v-.002c.02-.039.046-.09.062-.127.07-.162.094-.301.144-.458.132-.332.205-.68.387-.897.05-.06.13-.082.215-.105l.113-.205a6.453 6.453 0 0 0 4.609.012l.106.192c.086.028.18.042.256.155.136.232.229.507.342.84.05.156.074.295.145.457.016.037.043.09.062.129.133.276.442.402.69.282.247-.118.341-.435.213-.71-.02-.039-.045-.096-.065-.134-.083-.156-.177-.261-.268-.398-.19-.296-.346-.541-.443-.793-.04-.13.007-.21.038-.294-.018-.022-.059-.144-.083-.202a6.499 6.499 0 0 0 2.88-3.622c.064.01.176.03.213.038.075-.05.144-.114.28-.104.266.039.54.138.87.256.154.06.277.128.448.173.036.01.088.019.13.028l.009.003.007.001c.297.064.584-.098.645-.365.06-.266-.128-.537-.423-.608zM16.4 9.701l-1.95 1.746v.005a.44.44 0 0 0 .173.757l.003.01 2.526.728a5.199 5.199 0 0 0-.108-1.674A5.208 5.208 0 0 0 16.4 9.7zm-4.013 5.325a.437.437 0 0 0-.404-.232.44.44 0 0 0-.372.233h-.002l-1.268 2.292a5.164 5.164 0 0 0 3.326.003l-1.27-2.296h-.01zm1.888-1.293a.44.44 0 0 0-.27.036.44.44 0 0 0-.214.572l-.003.004 1.01 2.438a5.15 5.15 0 0 0 2.081-2.615l-2.6-.44-.004.005z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/kubernetes/kubernetes/tree/cac53883f4714452f3084a22e4be20d042a9df33/logo" + } + }, + { + "id": "linear", + "title": "Linear", + "category": "collaboration", + "aliases": [], + "domains": [ + "linear.app" + ], + "viewBox": 24, + "hex": "5E6AD2", + "path": "M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://linear.app" + } + }, + { + "id": "mariadb", + "title": "MariaDB", + "category": "data", + "aliases": [ + "maria-db" + ], + "domains": [ + "mariadb.org" + ], + "viewBox": 24, + "hex": "003545", + "path": "M23.157 4.412c-.676.284-.79.31-1.673.372-.65.045-.757.057-1.212.209-.75.246-1.395.75-2.02 1.59-.296.398-1.249 1.913-1.249 1.988 0 .057-.65.998-.915 1.32-.574.713-1.08 1.079-2.14 1.59-.77.36-1.224.524-4.102 1.477-1.073.353-2.133.738-2.367.864-.852.449-1.515 1.036-2.203 1.938-1.003 1.32-.972 1.313-3.042.947a12.264 12.264 0 00-.675-.063c-.644-.05-1.023.044-1.332.334L0 17.193l.177.088c.094.05.353.234.561.398.215.17.461.347.55.391.088.044.17.088.183.101.012.013-.089.17-.228.353-.435.581-.593.871-.574 1.048.019.164.032.17.43.17.517-.006.826-.056 1.261-.208.65-.233 2.058-.94 2.784-1.4.776-.5 1.717-.998 1.956-1.042.082-.02.354-.07.594-.114.58-.107 1.464-.095 2.587.05.108.013.373.045.6.064.227.025.43.057.454.076.026.012.474.037.998.056.934.026 1.104.007 1.3-.189.126-.133.385-.631.498-.985.209-.643.417-.921.366-.492-.113.966-.322 1.692-.713 2.411-.259.499-.663 1.092-.934 1.395-.322.347-.315.36.088.315.619-.063 1.471-.397 2.096-.82.827-.562 1.647-1.691 2.19-3.03.107-.27.22-.22.183.083-.013.094-.038.315-.057.498l-.031.328.353-.202c.833-.48 1.414-1.262 2.127-2.884.227-.518.877-2.922 1.073-3.976a9.64 9.64 0 01.271-1.042c.127-.429.196-.555.48-.858.183-.19.625-.555.978-.808.72-.505.953-.75 1.187-1.205.208-.417.284-1.13.132-1.357-.132-.202-.284-.196-.763.006Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://mariadb.com/about-us/logos/", + "guidelines": "https://mariadb.com/about-us/logos/" + } + }, + { + "id": "meta", + "title": "Meta", + "category": "ai", + "aliases": [ + "llama" + ], + "domains": [ + "meta.com" + ], + "viewBox": 24, + "hex": "0467DF", + "path": "M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.meta.com", + "guidelines": "https://www.facebook.com/brand/resources/meta/company-brand" + } + }, + { + "id": "miro", + "title": "Miro", + "category": "collaboration", + "aliases": [], + "domains": [ + "miro.com" + ], + "viewBox": 24, + "hex": "050038", + "path": "M17.392 0H13.9L17 4.808 10.444 0H6.949l3.102 6.3L3.494 0H0l3.05 8.131L0 24h3.494L10.05 6.985 6.949 24h3.494L17 5.494 13.899 24h3.493L24 3.672 17.392 0z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://miro.com" + } + }, + { + "id": "mistral-ai", + "title": "Mistral AI", + "category": "ai", + "aliases": [ + "mistral" + ], + "domains": [ + "mistral.ai" + ], + "viewBox": 24, + "hex": "FA520F", + "path": "M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://chat.mistral.ai" + } + }, + { + "id": "mongodb", + "title": "MongoDB", + "category": "data", + "aliases": [ + "mongo" + ], + "domains": [ + "mongodb.com" + ], + "viewBox": 24, + "hex": "47A248", + "path": "M17.193 9.555c-1.264-5.58-4.252-7.414-4.573-8.115-.28-.394-.53-.954-.735-1.44-.036.495-.055.685-.523 1.184-.723.566-4.438 3.682-4.74 10.02-.282 5.912 4.27 9.435 4.888 9.884l.07.05A73.49 73.49 0 0111.91 24h.481c.114-1.032.284-2.056.51-3.07.417-.296.604-.463.85-.693a11.342 11.342 0 003.639-8.464c.01-.814-.103-1.662-.197-2.218zm-5.336 8.195s0-8.291.275-8.29c.213 0 .49 10.695.49 10.695-.381-.045-.765-1.76-.765-2.405z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.mongodb.com/pressroom" + } + }, + { + "id": "mysql", + "title": "MySQL", + "category": "data", + "aliases": [], + "domains": [ + "mysql.com" + ], + "viewBox": 24, + "hex": "4479A1", + "path": "M16.405 5.501c-.115 0-.193.014-.274.033v.013h.014c.054.104.146.18.214.273.054.107.1.214.154.32l.014-.015c.094-.066.14-.172.14-.333-.04-.047-.046-.094-.08-.14-.04-.067-.126-.1-.18-.153zM5.77 18.695h-.927a50.854 50.854 0 00-.27-4.41h-.008l-1.41 4.41H2.45l-1.4-4.41h-.01a72.892 72.892 0 00-.195 4.41H0c.055-1.966.192-3.81.41-5.53h1.15l1.335 4.064h.008l1.347-4.064h1.095c.242 2.015.384 3.86.428 5.53zm4.017-4.08c-.378 2.045-.876 3.533-1.492 4.46-.482.716-1.01 1.073-1.583 1.073-.153 0-.34-.046-.566-.138v-.494c.11.017.24.026.386.026.268 0 .483-.075.647-.222.197-.18.295-.382.295-.605 0-.155-.077-.47-.23-.944L6.23 14.615h.91l.727 2.36c.164.536.233.91.205 1.123.4-1.064.678-2.227.835-3.483zm12.325 4.08h-2.63v-5.53h.885v4.85h1.745zm-3.32.135l-1.016-.5c.09-.076.177-.158.255-.25.433-.506.648-1.258.648-2.253 0-1.83-.718-2.746-2.155-2.746-.704 0-1.254.232-1.65.697-.43.508-.646 1.256-.646 2.245 0 .972.19 1.686.574 2.14.35.41.877.615 1.583.615.264 0 .506-.033.725-.098l1.325.772.36-.622zM15.5 17.588c-.225-.36-.337-.94-.337-1.736 0-1.393.424-2.09 1.27-2.09.443 0 .77.167.977.5.224.362.336.936.336 1.723 0 1.404-.424 2.108-1.27 2.108-.445 0-.77-.167-.978-.5zm-1.658-.425c0 .47-.172.856-.516 1.156-.344.3-.803.45-1.384.45-.543 0-1.064-.172-1.573-.515l.237-.476c.438.22.833.328 1.19.328.332 0 .593-.073.783-.22a.754.754 0 00.3-.615c0-.33-.23-.61-.648-.845-.388-.213-1.163-.657-1.163-.657-.422-.307-.632-.636-.632-1.177 0-.45.157-.81.47-1.085.315-.278.72-.415 1.22-.415.512 0 .98.136 1.4.41l-.213.476a2.726 2.726 0 00-1.064-.23c-.283 0-.502.068-.654.206a.685.685 0 00-.248.524c0 .328.234.61.666.85.393.215 1.187.67 1.187.67.433.305.648.63.648 1.168zm9.382-5.852c-.535-.014-.95.04-1.297.188-.1.04-.26.04-.274.167.055.053.063.14.11.214.08.134.218.313.346.407.14.11.28.216.427.31.26.16.555.255.81.416.145.094.293.213.44.313.073.05.12.14.214.172v-.02c-.046-.06-.06-.147-.105-.214-.067-.067-.134-.127-.2-.193a3.223 3.223 0 00-.695-.675c-.214-.146-.682-.35-.77-.595l-.013-.014c.146-.013.32-.066.46-.106.227-.06.435-.047.67-.106.106-.027.213-.06.32-.094v-.06c-.12-.12-.21-.283-.334-.395a8.867 8.867 0 00-1.104-.823c-.21-.134-.476-.22-.697-.334-.08-.04-.214-.06-.26-.127-.12-.146-.19-.34-.275-.514a17.69 17.69 0 01-.547-1.163c-.12-.262-.193-.523-.34-.763-.69-1.137-1.437-1.826-2.586-2.5-.247-.14-.543-.2-.856-.274-.167-.008-.334-.02-.5-.027-.11-.047-.216-.174-.31-.235-.38-.24-1.364-.76-1.644-.072-.18.434.267.862.422 1.082.115.153.26.328.34.5.047.116.06.235.107.356.106.294.207.622.347.897.073.14.153.287.247.413.054.073.146.107.167.227-.094.136-.1.334-.154.5-.24.757-.146 1.693.194 2.25.107.166.362.534.703.393.3-.12.234-.5.32-.835.02-.08.007-.133.048-.187v.015c.094.188.188.367.274.555.206.328.566.668.867.895.16.12.287.328.487.402v-.02h-.015c-.043-.058-.1-.086-.154-.133a3.445 3.445 0 01-.35-.4 8.76 8.76 0 01-.747-1.218c-.11-.21-.202-.436-.29-.643-.04-.08-.04-.2-.107-.24-.1.146-.247.273-.32.453-.127.288-.14.642-.188 1.01-.027.007-.014 0-.027.014-.214-.052-.287-.274-.367-.46-.2-.475-.233-1.238-.06-1.785.047-.14.247-.582.167-.716-.042-.127-.174-.2-.247-.303a2.478 2.478 0 01-.24-.427c-.16-.374-.24-.788-.414-1.162-.08-.173-.22-.354-.334-.513-.127-.18-.267-.307-.368-.52-.033-.073-.08-.194-.027-.274.014-.054.042-.075.094-.09.088-.072.335.022.422.062.247.1.455.194.662.334.094.066.195.193.315.226h.14c.214.047.455.014.655.073.355.114.675.28.962.46a5.953 5.953 0 012.085 2.286c.08.154.115.295.188.455.14.33.313.663.455.982.14.315.275.636.476.897.1.14.502.213.682.286.133.06.34.115.46.188.23.14.454.3.67.454.11.076.443.243.463.378z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.mysql.com/about/legal/logos.html", + "guidelines": "https://www.mysql.com/about/legal/logos.html" + } + }, + { + "id": "neon", + "title": "Neon", + "category": "cloud", + "aliases": [], + "domains": [ + "neon.tech" + ], + "viewBox": 24, + "hex": "34D59A", + "path": "M24 0V24l-9.365-8.045V24H0V0ZM2.942 21.087h8.751V9.563l9.365 8.204V2.919L2.942 2.914Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://neon.com/brand", + "guidelines": "https://neon.com/brand" + } + }, + { + "id": "netlify", + "title": "Netlify", + "category": "cloud", + "aliases": [], + "domains": [ + "netlify.com" + ], + "viewBox": 24, + "hex": "00C7B7", + "path": "M6.49 19.04h-.23L5.13 17.9v-.23l1.73-1.71h1.2l.15.15v1.2L6.5 19.04ZM5.13 6.31V6.1l1.13-1.13h.23L8.2 6.68v1.2l-.15.15h-1.2L5.13 6.31Zm9.96 9.09h-1.65l-.14-.13v-3.83c0-.68-.27-1.2-1.1-1.23-.42 0-.9 0-1.43.02l-.07.08v4.96l-.14.14H8.9l-.13-.14V8.73l.13-.14h3.7a2.6 2.6 0 0 1 2.61 2.6v4.08l-.13.14Zm-8.37-2.44H.14L0 12.82v-1.64l.14-.14h6.58l.14.14v1.64l-.14.14Zm17.14 0h-6.58l-.14-.14v-1.64l.14-.14h6.58l.14.14v1.64l-.14.14ZM11.05 6.55V1.64l.14-.14h1.65l.14.14v4.9l-.14.14h-1.65l-.14-.13Zm0 15.81v-4.9l.14-.14h1.65l.14.13v4.91l-.14.14h-1.65l-.14-.14Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.netlify.com/press/", + "guidelines": "https://www.netlify.com/press/" + } + }, + { + "id": "next-js", + "title": "Next.js", + "category": "framework", + "aliases": [ + "nextjs", + "next.js" + ], + "domains": [ + "nextjs.org" + ], + "viewBox": 24, + "hex": "000000", + "path": "M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://vercel.com/design/brands#next-js", + "guidelines": "https://vercel.com/design/brands#next-js" + } + }, + { + "id": "node-js", + "title": "Node.js", + "category": "framework", + "aliases": [ + "node", + "nodejs" + ], + "domains": [ + "nodejs.org" + ], + "viewBox": 24, + "hex": "5FA04E", + "path": "M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://nodejs.org/en/about/branding", + "guidelines": "https://nodejs.org/en/about/branding" + } + }, + { + "id": "notion", + "title": "Notion", + "category": "collaboration", + "aliases": [], + "domains": [ + "notion.so" + ], + "viewBox": 24, + "hex": "000000", + "path": "M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.notion.so" + } + }, + { + "id": "ollama", + "title": "Ollama", + "category": "ai", + "aliases": [], + "domains": [ + "ollama.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "M16.361 10.26a.894.894 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.752.752 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.06 1.06 0 0 0-.466 0zm-9.203.005c-.305.096-.533.32-.65.639a1.187 1.187 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.894.894 0 0 0-.565-.472 1.02 1.02 0 0 0-.474.007Zm4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.394.394 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.396.396 0 0 0-.193.032zm.39-2.195c-.373.036-.475.05-.654.086-.291.06-.68.195-.951.328-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a6.873 6.873 0 0 0-.855-.031zm.645.937a3.283 3.283 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3.115 3.115 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042-.57-.107-1.022-.334-1.35-.68-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002zm-5.503-11a1.653 1.653 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208a8.12 8.12 0 0 1-.187.152 5.324 5.324 0 0 0-.949 1.02 5.49 5.49 0 0 0-.94 2.339 6.625 6.625 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.405 7.405 0 0 0-.548 1.873c-.062.417-.071.552-.071.991 0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.915.915 0 0 0-.194-.25 1.74 1.74 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.787.787 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.136 3.136 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04.319 0 .398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2.096 2.096 0 0 1-.224.258.916.916 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a.845.845 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516.029-.217.029-1.016 0-1.258-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141.008-.005.057-.074.108-.152.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6.083 6.083 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.625 6.625 0 0 0-.024-1.358 5.512 5.512 0 0 0-.939-2.339 5.325 5.325 0 0 0-.95-1.02 8.097 8.097 0 0 1-.186-.152.692.692 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a6.805 6.805 0 0 0-.097.726c0 .036-.007.066-.015.066a.96.96 0 0 1-.149-.078A4.857 4.857 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a.958.958 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a6.71 6.71 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2.096 2.096 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.438 8.438 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013zm9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.693 3.693 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/ollama/ollama/issues/2152#issuecomment-1905286922" + } + }, + { + "id": "openai", + "title": "OpenAI", + "category": "ai", + "aliases": [ + "chatgpt", + "gpt", + "codex" + ], + "domains": [ + "openai.com", + "chatgpt.com" + ], + "viewBox": 20, + "hex": "000000", + "path": "M11.248 18.25q-.825 0-1.568-.314a4.3 4.3 0 0 1-1.32-.874 4 4 0 0 1-1.304.214 4 4 0 0 1-2.046-.544 4.27 4.27 0 0 1-1.518-1.485 4 4 0 0 1-.56-2.095q0-.48.131-1.04A4.4 4.4 0 0 1 2.04 10.71a4.07 4.07 0 0 1 .017-3.4 4.2 4.2 0 0 1 1.056-1.418 3.8 3.8 0 0 1 1.6-.842 3.9 3.9 0 0 1 .76-1.683q.593-.759 1.451-1.188a4.04 4.04 0 0 1 1.832-.429q.825 0 1.567.313.742.314 1.32.875a4 4 0 0 1 1.304-.215q1.106 0 2.046.545a4.14 4.14 0 0 1 1.501 1.485q.578.941.578 2.095 0 .48-.132 1.04.66.61 1.023 1.419.363.792.363 1.666 0 .892-.38 1.717a4.3 4.3 0 0 1-1.072 1.435 3.8 3.8 0 0 1-1.584.825 3.8 3.8 0 0 1-.775 1.683 4.06 4.06 0 0 1-1.436 1.188 4.04 4.04 0 0 1-1.832.429m-4.076-2.062q.825 0 1.435-.347l3.103-1.782a.36.36 0 0 0 .164-.313v-1.42L7.881 14.62a.67.67 0 0 1-.726 0l-3.118-1.798a.5.5 0 0 1-.017.115v.198q0 .841.396 1.551.413.693 1.139 1.089a3.2 3.2 0 0 0 1.617.412m.165-2.69a.4.4 0 0 0 .181.05q.083 0 .165-.05l1.238-.71-3.977-2.31a.7.7 0 0 1-.363-.643v-3.58q-.825.362-1.32 1.122a2.9 2.9 0 0 0-.495 1.65q0 .809.413 1.55.412.743 1.072 1.123zm3.91 3.663q.875 0 1.585-.396a2.96 2.96 0 0 0 1.534-2.64v-3.564a.32.32 0 0 0-.165-.297l-1.254-.726v4.604a.7.7 0 0 1-.363.643l-3.119 1.799a3 3 0 0 0 1.783.577m.627-6.039V8.878L10.01 7.822 8.129 8.878v2.244l1.881 1.056zM7.057 5.859a.7.7 0 0 1 .363-.644l3.119-1.798a3 3 0 0 0-1.782-.578q-.874 0-1.584.396A2.96 2.96 0 0 0 6.05 4.324a3.07 3.07 0 0 0-.396 1.551v3.547q0 .199.165.314l1.237.726zm8.383 7.887q.825-.364 1.303-1.123.495-.758.495-1.65a3.15 3.15 0 0 0-.412-1.55q-.413-.743-1.073-1.123l-3.086-1.782q-.099-.065-.181-.049a.3.3 0 0 0-.165.05l-1.238.692 3.993 2.327a.6.6 0 0 1 .264.264.64.64 0 0 1 .1.363zm-3.317-8.382a.63.63 0 0 1 .726 0l3.135 1.831v-.297q0-.792-.396-1.501a2.86 2.86 0 0 0-1.105-1.155q-.71-.43-1.65-.43-.825 0-1.436.347L8.294 5.941a.36.36 0 0 0-.165.314v1.418z", + "provenance": { + "provider": "Official brand asset", + "source": "https://openai.com/brand/", + "guidelines": "https://openai.com/brand/" + } + }, + { + "id": "openrouter", + "title": "OpenRouter", + "category": "ai", + "aliases": [ + "open-router" + ], + "domains": [ + "openrouter.ai" + ], + "viewBox": 24, + "hex": "94A3B8", + "path": "M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://openrouter.ai" + } + }, + { + "id": "opensearch", + "title": "OpenSearch", + "category": "data", + "aliases": [ + "open-search" + ], + "domains": [ + "opensearch.org" + ], + "viewBox": 24, + "hex": "005EB8", + "path": "M23.1515 8.8125a.8484.8484 0 0 0-.8484.8485c0 6.982-5.6601 12.6421-12.6421 12.6421a.8485.8485 0 0 0 0 1.6969C17.5802 24 24 17.5802 24 9.661a.8485.8485 0 0 0-.8485-.8485Zm-5.121 5.4375c.816-1.3311 1.6051-3.1058 1.4498-5.5905-.3216-5.1468-4.9832-9.0512-9.3851-8.6281C8.372.1971 6.6025 1.6017 6.7598 4.1177c.0683 1.0934.6034 1.7386 1.473 2.2348.8279.4722 1.8914.7713 3.097 1.1104 1.4563.4096 3.1455.8697 4.4438 1.8265 1.5561 1.1467 2.6198 2.4759 2.2569 4.9606Zm-16.561-9C.6535 6.581-.1355 8.3558.0197 10.8405c.3216 5.1468 4.9832 9.0512 9.385 8.6281 1.7233-.1657 3.4927-1.5703 3.3355-4.0863-.0683-1.0934-.6034-1.7386-1.4731-2.2348-.8278-.4722-1.8913-.7713-3.0969-1.1104-1.4563-.4096-3.1455-.8697-4.4438-1.8265-1.5561-1.1467-2.6198-2.476-2.257-4.9606Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://opensearch.org/trademark-brand-policy.html", + "guidelines": "https://opensearch.org/trademark-brand-policy.html" + } + }, + { + "id": "pagerduty", + "title": "PagerDuty", + "category": "engineering", + "aliases": [ + "pager-duty" + ], + "domains": [ + "pagerduty.com" + ], + "viewBox": 24, + "hex": "06AC38", + "path": "M16.965 1.18C15.085.164 13.769 0 10.683 0H3.73v14.55h6.926c2.743 0 4.8-.164 6.61-1.37 1.975-1.303 3.004-3.484 3.004-6.007 0-2.716-1.262-4.896-3.305-5.994zm-5.5 10.326h-4.21V3.113l3.977-.027c3.62-.028 5.43 1.234 5.43 4.128 0 3.113-2.248 4.292-5.197 4.292zM3.73 17.61h3.525V24H3.73Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.pagerduty.com/brand/", + "guidelines": "https://www.pagerduty.com/brand/" + } + }, + { + "id": "paypal", + "title": "PayPal", + "category": "business", + "aliases": [], + "domains": [ + "paypal.com" + ], + "viewBox": 24, + "hex": "002991", + "path": "M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.paypal.com/us", + "guidelines": "https://newsroom.paypal-corp.com/media-resources" + } + }, + { + "id": "perplexity", + "title": "Perplexity", + "category": "ai", + "aliases": [], + "domains": [ + "perplexity.ai" + ], + "viewBox": 24, + "hex": "1FB8CD", + "path": "M22.3977 7.0896h-2.3106V.0676l-7.5094 6.3542V.1577h-1.1554v6.1966L4.4904 0v7.0896H1.6023v10.3976h2.8882V24l6.932-6.3591v6.2005h1.1554v-6.0469l6.9318 6.1807v-6.4879h2.8882V7.0896zm-3.4657-4.531v4.531h-5.355l5.355-4.531zm-13.2862.0676 4.8691 4.4634H5.6458V2.6262zM2.7576 16.332V8.245h7.8476l-6.1149 6.1147v1.9723H2.7576zm2.8882 5.0404v-3.8852h.0001v-2.6488l5.7763-5.7764v7.0111l-5.7764 5.2993zm12.7086.0248-5.7766-5.1509V9.0618l5.7766 5.7766v6.5588zm2.8882-5.0652h-1.733v-1.9723L13.3948 8.245h7.8478v8.087z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.perplexity.ai" + } + }, + { + "id": "pinterest", + "title": "Pinterest", + "category": "channel", + "aliases": [], + "domains": [ + "pinterest.com" + ], + "viewBox": 24, + "hex": "BD081C", + "path": "M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://business.pinterest.com/en/brand-guidelines", + "guidelines": "https://business.pinterest.com/en/brand-guidelines" + } + }, + { + "id": "planetscale", + "title": "PlanetScale", + "category": "data", + "aliases": [ + "planet-scale" + ], + "domains": [ + "planetscale.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a11.981 11.981 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.962 11.962 0 0 1 0 12Zm12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://planetscale.com" + } + }, + { + "id": "postgresql", + "title": "PostgreSQL", + "category": "data", + "aliases": [ + "postgres" + ], + "domains": [ + "postgresql.org" + ], + "viewBox": 24, + "hex": "4169E1", + "path": "M23.5594 14.7228a.5269.5269 0 0 0-.0563-.1191c-.139-.2632-.4768-.3418-1.0074-.2321-1.6533.3411-2.2935.1312-2.5256-.0191 1.342-2.0482 2.445-4.522 3.0411-6.8297.2714-1.0507.7982-3.5237.1222-4.7316a1.5641 1.5641 0 0 0-.1509-.235C21.6931.9086 19.8007.0248 17.5099.0005c-1.4947-.0158-2.7705.3461-3.1161.4794a9.449 9.449 0 0 0-.5159-.0816 8.044 8.044 0 0 0-1.3114-.1278c-1.1822-.0184-2.2038.2642-3.0498.8406-.8573-.3211-4.7888-1.645-7.2219.0788C.9359 2.1526.3086 3.8733.4302 6.3043c.0409.818.5069 3.334 1.2423 5.7436.4598 1.5065.9387 2.7019 1.4334 3.582.553.9942 1.1259 1.5933 1.7143 1.7895.4474.1491 1.1327.1441 1.8581-.7279.8012-.9635 1.5903-1.8258 1.9446-2.2069.4351.2355.9064.3625 1.39.3772a.0569.0569 0 0 0 .0004.0041 11.0312 11.0312 0 0 0-.2472.3054c-.3389.4302-.4094.5197-1.5002.7443-.3102.064-1.1344.2339-1.1464.8115-.0025.1224.0329.2309.0919.3268.2269.4231.9216.6097 1.015.6331 1.3345.3335 2.5044.092 3.3714-.6787-.017 2.231.0775 4.4174.3454 5.0874.2212.5529.7618 1.9045 2.4692 1.9043.2505 0 .5263-.0291.8296-.0941 1.7819-.3821 2.5557-1.1696 2.855-2.9059.1503-.8707.4016-2.8753.5388-4.1012.0169-.0703.0357-.1207.057-.1362.0007-.0005.0697-.0471.4272.0307a.3673.3673 0 0 0 .0443.0068l.2539.0223.0149.001c.8468.0384 1.9114-.1426 2.5312-.4308.6438-.2988 1.8057-1.0323 1.5951-1.6698zM2.371 11.8765c-.7435-2.4358-1.1779-4.8851-1.2123-5.5719-.1086-2.1714.4171-3.6829 1.5623-4.4927 1.8367-1.2986 4.8398-.5408 6.108-.13-.0032.0032-.0066.0061-.0098.0094-2.0238 2.044-1.9758 5.536-1.9708 5.7495-.0002.0823.0066.1989.0162.3593.0348.5873.0996 1.6804-.0735 2.9184-.1609 1.1504.1937 2.2764.9728 3.0892.0806.0841.1648.1631.2518.2374-.3468.3714-1.1004 1.1926-1.9025 2.1576-.5677.6825-.9597.5517-1.0886.5087-.3919-.1307-.813-.5871-1.2381-1.3223-.4796-.839-.9635-2.0317-1.4155-3.5126zm6.0072 5.0871c-.1711-.0428-.3271-.1132-.4322-.1772.0889-.0394.2374-.0902.4833-.1409 1.2833-.2641 1.4815-.4506 1.9143-1.0002.0992-.126.2116-.2687.3673-.4426a.3549.3549 0 0 0 .0737-.1298c.1708-.1513.2724-.1099.4369-.0417.156.0646.3078.26.3695.4752.0291.1016.0619.2945-.0452.4444-.9043 1.2658-2.2216 1.2494-3.1676 1.0128zm2.094-3.988-.0525.141c-.133.3566-.2567.6881-.3334 1.003-.6674-.0021-1.3168-.2872-1.8105-.8024-.6279-.6551-.9131-1.5664-.7825-2.5004.1828-1.3079.1153-2.4468.079-3.0586-.005-.0857-.0095-.1607-.0122-.2199.2957-.2621 1.6659-.9962 2.6429-.7724.4459.1022.7176.4057.8305.928.5846 2.7038.0774 3.8307-.3302 4.7363-.084.1866-.1633.3629-.2311.5454zm7.3637 4.5725c-.0169.1768-.0358.376-.0618.5959l-.146.4383a.3547.3547 0 0 0-.0182.1077c-.0059.4747-.054.6489-.115.8693-.0634.2292-.1353.4891-.1794 1.0575-.11 1.4143-.8782 2.2267-2.4172 2.5565-1.5155.3251-1.7843-.4968-2.0212-1.2217a6.5824 6.5824 0 0 0-.0769-.2266c-.2154-.5858-.1911-1.4119-.1574-2.5551.0165-.5612-.0249-1.9013-.3302-2.6462.0044-.2932.0106-.5909.019-.8918a.3529.3529 0 0 0-.0153-.1126 1.4927 1.4927 0 0 0-.0439-.208c-.1226-.4283-.4213-.7866-.7797-.9351-.1424-.059-.4038-.1672-.7178-.0869.067-.276.1831-.5875.309-.9249l.0529-.142c.0595-.16.134-.3257.213-.5012.4265-.9476 1.0106-2.2453.3766-5.1772-.2374-1.0981-1.0304-1.6343-2.2324-1.5098-.7207.0746-1.3799.3654-1.7088.5321a5.6716 5.6716 0 0 0-.1958.1041c.0918-1.1064.4386-3.1741 1.7357-4.4823a4.0306 4.0306 0 0 1 .3033-.276.3532.3532 0 0 0 .1447-.0644c.7524-.5706 1.6945-.8506 2.802-.8325.4091.0067.8017.0339 1.1742.081 1.939.3544 3.2439 1.4468 4.0359 2.3827.8143.9623 1.2552 1.9315 1.4312 2.4543-1.3232-.1346-2.2234.1268-2.6797.779-.9926 1.4189.543 4.1729 1.2811 5.4964.1353.2426.2522.4522.2889.5413.2403.5825.5515.9713.7787 1.2552.0696.087.1372.1714.1885.245-.4008.1155-1.1208.3825-1.0552 1.717-.0123.1563-.0423.4469-.0834.8148-.0461.2077-.0702.4603-.0994.7662zm.8905-1.6211c-.0405-.8316.2691-.9185.5967-1.0105a2.8566 2.8566 0 0 0 .135-.0406 1.202 1.202 0 0 0 .1342.103c.5703.3765 1.5823.4213 3.0068.1344-.2016.1769-.5189.3994-.9533.6011-.4098.1903-1.0957.333-1.7473.3636-.7197.0336-1.0859-.0807-1.1721-.151zm.5695-9.2712c-.0059.3508-.0542.6692-.1054 1.0017-.055.3576-.112.7274-.1264 1.1762-.0142.4368.0404.8909.0932 1.3301.1066.887.216 1.8003-.2075 2.7014a3.5272 3.5272 0 0 1-.1876-.3856c-.0527-.1276-.1669-.3326-.3251-.6162-.6156-1.1041-2.0574-3.6896-1.3193-4.7446.3795-.5427 1.3408-.5661 2.1781-.463zm.2284 7.0137a12.3762 12.3762 0 0 0-.0853-.1074l-.0355-.0444c.7262-1.1995.5842-2.3862.4578-3.4385-.0519-.4318-.1009-.8396-.0885-1.2226.0129-.4061.0666-.7543.1185-1.0911.0639-.415.1288-.8443.1109-1.3505.0134-.0531.0188-.1158.0118-.1902-.0457-.4855-.5999-1.938-1.7294-3.253-.6076-.7073-1.4896-1.4972-2.6889-2.0395.5251-.1066 1.2328-.2035 2.0244-.1859 2.0515.0456 3.6746.8135 4.8242 2.2824a.908.908 0 0 1 .0667.1002c.7231 1.3556-.2762 6.2751-2.9867 10.5405zm-8.8166-6.1162c-.025.1794-.3089.4225-.6211.4225a.5821.5821 0 0 1-.0809-.0056c-.1873-.026-.3765-.144-.5059-.3156-.0458-.0605-.1203-.178-.1055-.2844.0055-.0401.0261-.0985.0925-.1488.1182-.0894.3518-.1226.6096-.0867.3163.0441.6426.1938.6113.4186zm7.9305-.4114c.0111.0792-.049.201-.1531.3102-.0683.0717-.212.1961-.4079.2232a.5456.5456 0 0 1-.075.0052c-.2935 0-.5414-.2344-.5607-.3717-.024-.1765.2641-.3106.5611-.352.297-.0414.6111.0088.6356.1851z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://wiki.postgresql.org/wiki/Logo", + "guidelines": "https://www.postgresql.org/about/policies/trademarks/" + } + }, + { + "id": "prisma", + "title": "Prisma", + "category": "data", + "aliases": [], + "domains": [ + "prisma.io" + ], + "viewBox": 24, + "hex": "2D3748", + "path": "M21.8068 18.2848L13.5528.7565c-.207-.4382-.639-.7273-1.1286-.7541-.5023-.0293-.9523.213-1.2062.6253L2.266 15.1271c-.2773.4518-.2718 1.0091.0158 1.4555l4.3759 6.7786c.2608.4046.7127.6388 1.1823.6388.1332 0 .267-.0188.3987-.0577l12.7019-3.7568c.3891-.1151.7072-.3904.8737-.7553s.1633-.7828-.0075-1.1454zm-1.8481.7519L9.1814 22.2242c-.3292.0975-.6448-.1873-.5756-.5194l3.8501-18.4386c.072-.3448.5486-.3996.699-.0803l7.1288 15.138c.1344.2856-.019.6224-.325.7128z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/prisma/presskit/tree/4bcb64181f266723439d955d60afa1c55fefa715" + } + }, + { + "id": "prometheus", + "title": "Prometheus", + "category": "engineering", + "aliases": [], + "domains": [ + "prometheus.io" + ], + "viewBox": 24, + "hex": "E6522C", + "path": "M12 0C5.373 0 0 5.372 0 12c0 6.627 5.373 12 12 12s12-5.373 12-12c0-6.628-5.373-12-12-12zm0 22.46c-1.885 0-3.414-1.26-3.414-2.814h6.828c0 1.553-1.528 2.813-3.414 2.813zm5.64-3.745H6.36v-2.046h11.28v2.046zm-.04-3.098H6.391c-.037-.043-.075-.086-.111-.13-1.155-1.401-1.427-2.133-1.69-2.879-.005-.025 1.4.287 2.395.511 0 0 .513.119 1.262.255-.72-.843-1.147-1.915-1.147-3.01 0-2.406 1.845-4.508 1.18-6.207.648.053 1.34 1.367 1.387 3.422.689-.951.977-2.69.977-3.755 0-1.103.727-2.385 1.454-2.429-.648 1.069.168 1.984.894 4.256.272.854.237 2.29.447 3.201.07-1.892.395-4.652 1.595-5.605-.529 1.2.079 2.702.494 3.424.671 1.164 1.078 2.047 1.078 3.716a4.642 4.642 0 01-1.11 2.996c.792-.149 1.34-.283 1.34-.283l2.573-.502s-.374 1.538-1.81 3.019z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://prometheus.io" + } + }, + { + "id": "pulumi", + "title": "Pulumi", + "category": "engineering", + "aliases": [], + "domains": [ + "pulumi.com" + ], + "viewBox": 24, + "hex": "8A3391", + "path": "M11.997 0C10.226 0 8.79.83 8.79 1.856c0 1.025 1.436 1.856 3.207 1.856 1.772 0 3.208-.831 3.208-1.856C15.205.83 13.77 0 11.997 0zM5.95 3.488c-1.772 0-3.208.83-3.208 1.856C2.742 6.369 4.178 7.2 5.95 7.2c1.771 0 3.207-.831 3.207-1.856 0-1.025-1.436-1.856-3.207-1.856zm12.103 0c-1.772 0-3.208.83-3.208 1.856 0 1.025 1.436 1.856 3.208 1.856 1.771 0 3.207-.831 3.207-1.856 0-1.025-1.436-1.856-3.207-1.856zm-6.056 3.495c-1.771 0-3.207.831-3.207 1.856 0 1.025 1.436 1.856 3.207 1.856 1.772 0 3.208-.83 3.208-1.856 0-1.025-1.436-1.856-3.208-1.856zm-10.127.67a1.157 1.157 0 0 0-.55.151c-.888.513-.89 2.172-.004 3.706.886 1.534 2.324 2.362 3.211 1.85.888-.513.89-2.171.003-3.706-.72-1.246-1.803-2.027-2.66-2zm20.257.004c-.857-.026-1.941.754-2.661 2-.886 1.535-.884 3.194.003 3.707.888.512 2.325-.316 3.211-1.85.886-1.534.885-3.193-.003-3.706a1.157 1.157 0 0 0-.55-.15zm-6.048 3.492c-.857-.026-1.94.754-2.66 2-.886 1.535-.885 3.194.003 3.706.887.513 2.325-.316 3.21-1.85.887-1.534.885-3.193-.003-3.706a1.157 1.157 0 0 0-.55-.15zm-8.16.001a1.157 1.157 0 0 0-.55.151c-.888.513-.89 2.172-.004 3.706.886 1.535 2.324 2.363 3.211 1.85.888-.512.89-2.171.003-3.705-.72-1.247-1.803-2.028-2.66-2.002zm-6.047 3.494a1.157 1.157 0 0 0-.55.151c-.888.513-.89 2.172-.004 3.706.886 1.534 2.324 2.362 3.212 1.85.887-.513.888-2.172.003-3.706-.72-1.246-1.804-2.027-2.661-2.001zm20.258.002c-.857-.026-1.941.755-2.66 2.001-.887 1.535-.885 3.193.003 3.706.887.512 2.325-.316 3.21-1.85.886-1.534.885-3.193-.003-3.706a1.157 1.157 0 0 0-.55-.15zm-6.047 3.492c-.858-.026-1.942.754-2.661 2-.886 1.535-.885 3.194.003 3.706.888.513 2.325-.315 3.21-1.85.887-1.533.885-3.193-.002-3.705a1.157 1.157 0 0 0-.55-.151zm-8.163.003a1.157 1.157 0 0 0-.55.151c-.887.513-.889 2.172-.003 3.706.886 1.534 2.323 2.363 3.211 1.85.888-.512.89-2.171.004-3.706-.72-1.246-1.804-2.027-2.662-2z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.pulumi.com", + "guidelines": "https://www.pulumi.com/brand/" + } + }, + { + "id": "python", + "title": "Python", + "category": "language", + "aliases": [], + "domains": [ + "python.org" + ], + "viewBox": 24, + "hex": "3776AB", + "path": "M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.python.org/community/logos/", + "guidelines": "https://www.python.org/community/logos/" + } + }, + { + "id": "pytorch", + "title": "PyTorch", + "category": "framework", + "aliases": [], + "domains": [ + "pytorch.org" + ], + "viewBox": 24, + "hex": "EE4C2C", + "path": "M12.005 0L4.952 7.053a9.865 9.865 0 000 14.022 9.866 9.866 0 0014.022 0c3.984-3.9 3.986-10.205.085-14.023l-1.744 1.743c2.904 2.905 2.904 7.634 0 10.538s-7.634 2.904-10.538 0-2.904-7.634 0-10.538l4.647-4.646.582-.665zm3.568 3.899a1.327 1.327 0 00-1.327 1.327 1.327 1.327 0 001.327 1.328A1.327 1.327 0 0016.9 5.226 1.327 1.327 0 0015.573 3.9z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/pytorch/pytorch.github.io/blob/8f083bd12192ca12d5e1c1f3d236f4831d823d8f/assets/images/logo.svg", + "guidelines": "https://github.com/pytorch/pytorch.github.io/blob/381117ec296f002b2de475402ef29cca6c55e209/assets/brand-guidelines/PyTorch-Brand-Guidelines.pdf" + } + }, + { + "id": "qwen", + "title": "QWen", + "category": "ai", + "aliases": [], + "domains": [ + "qwen.ai" + ], + "viewBox": 24, + "hex": "6950EF", + "path": "M23.919 14.545 20.817 9.17l1.47-2.544a.56.56 0 0 0 0-.566l-1.633-2.83a.57.57 0 0 0-.49-.283h-6.207L12.487.402a.57.57 0 0 0-.49-.284H8.732a.56.56 0 0 0-.49.284L5.139 5.775h-2.94a.56.56 0 0 0-.49.284L.077 8.887a.56.56 0 0 0 0 .567L3.18 14.83l-1.47 2.545a.56.56 0 0 0 0 .566l1.634 2.83a.57.57 0 0 0 .49.283h6.205l1.47 2.545a.57.57 0 0 0 .49.284h3.266a.57.57 0 0 0 .49-.284l3.104-5.375h2.94a.57.57 0 0 0 .49-.283l1.634-2.828a.55.55 0 0 0-.004-.568M8.733.686l1.634 2.828-1.634 2.828H21.8L20.164 9.17H7.425L5.63 6.06Zm1.306 19.801-6.205-.002 1.634-2.83h3.265L2.201 6.344h3.267q3.182 5.517 6.367 11.032zm10.124-5.66L18.53 12l-6.532 11.315-1.634-2.83c2.129-3.673 4.25-7.351 6.373-11.028h3.592l3.102 5.374z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://qwen.ai" + } + }, + { + "id": "rabbitmq", + "title": "RabbitMQ", + "category": "data", + "aliases": [ + "rabbit-mq" + ], + "domains": [ + "rabbitmq.com" + ], + "viewBox": 24, + "hex": "FF6600", + "path": "M23.035 9.601h-7.677a.956.956 0 01-.962-.962V.962a.956.956 0 00-.962-.956H10.56a.956.956 0 00-.962.956V8.64a.956.956 0 01-.962.962H5.762a.956.956 0 01-.961-.962V.962A.956.956 0 003.839 0H.959a.956.956 0 00-.956.962v22.076A.956.956 0 00.965 24h22.07a.956.956 0 00.962-.962V10.58a.956.956 0 00-.962-.98zm-3.86 8.152a1.437 1.437 0 01-1.437 1.443h-1.924a1.437 1.437 0 01-1.436-1.443v-1.917a1.437 1.437 0 011.436-1.443h1.924a1.437 1.437 0 011.437 1.443z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.rabbitmq.com", + "guidelines": "https://www.rabbitmq.com/trademark-guidelines.html" + } + }, + { + "id": "railway", + "title": "Railway", + "category": "cloud", + "aliases": [], + "domains": [ + "railway.com", + "railway.app" + ], + "viewBox": 24, + "hex": "0B0D0E", + "path": "M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://railway.app" + } + }, + { + "id": "react", + "title": "React", + "category": "framework", + "aliases": [ + "reactjs" + ], + "domains": [ + "react.dev" + ], + "viewBox": 24, + "hex": "61DAFB", + "path": "M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/facebook/create-react-app/blob/282c03f9525fdf8061ffa1ec50dce89296d916bd/test/fixtures/relative-paths/src/logo.svg" + } + }, + { + "id": "reddit", + "title": "Reddit", + "category": "channel", + "aliases": [], + "domains": [ + "reddit.com" + ], + "viewBox": 24, + "hex": "FF4500", + "path": "M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.redditinc.com/brand", + "guidelines": "https://www.redditinc.com/brand" + } + }, + { + "id": "redis", + "title": "Redis", + "category": "data", + "aliases": [], + "domains": [ + "redis.io" + ], + "viewBox": 24, + "hex": "FF4438", + "path": "M22.71 13.145c-1.66 2.092-3.452 4.483-7.038 4.483-3.203 0-4.397-2.825-4.48-5.12.701 1.484 2.073 2.685 4.214 2.63 4.117-.133 6.94-3.852 6.94-7.239 0-4.05-3.022-6.972-8.268-6.972-3.752 0-8.4 1.428-11.455 3.685C2.59 6.937 3.885 9.958 4.35 9.626c2.648-1.904 4.748-3.13 6.784-3.744C8.12 9.244.886 17.05 0 18.425c.1 1.261 1.66 4.648 2.424 4.648.232 0 .431-.133.664-.365a100.49 100.49 0 0 0 5.54-6.765c.222 3.104 1.748 6.898 6.014 6.898 3.819 0 7.604-2.756 9.33-8.965.2-.764-.73-1.361-1.261-.73zm-4.349-5.013c0 1.959-1.926 2.922-3.685 2.922-.941 0-1.664-.247-2.235-.568 1.051-1.592 2.092-3.225 3.21-4.973 1.972.334 2.71 1.43 2.71 2.619z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://redis.io/brand-guidelines", + "guidelines": "https://redis.io/brand-guidelines" + } + }, + { + "id": "render", + "title": "Render", + "category": "cloud", + "aliases": [], + "domains": [ + "render.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "M18.263.007c-3.121-.147-5.744 2.109-6.192 5.082-.018.138-.045.272-.067.405-.696 3.703-3.936 6.507-7.827 6.507-1.388 0-2.691-.356-3.825-.979a.2024.2024 0 0 0-.302.178V24H12v-8.999c0-1.656 1.338-3 2.987-3h2.988c3.382 0 6.103-2.817 5.97-6.244-.12-3.084-2.61-5.603-5.682-5.75", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://render.com" + } + }, + { + "id": "replicate", + "title": "Replicate", + "category": "ai", + "aliases": [], + "domains": [ + "replicate.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "M24 10.262v2.712h-9.518V24h-3.034V10.262zm0-5.131v2.717H8.755V24H5.722V5.131zM24 0v2.717H3.034V24H0V0z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://replicate.com" + } + }, + { + "id": "rust", + "title": "Rust", + "category": "language", + "aliases": [], + "domains": [ + "rust-lang.org" + ], + "viewBox": 24, + "hex": "000000", + "path": "M23.8346 11.7033l-1.0073-.6236a13.7268 13.7268 0 00-.0283-.2936l.8656-.8069a.3483.3483 0 00-.1154-.578l-1.1066-.414a8.4958 8.4958 0 00-.087-.2856l.6904-.9587a.3462.3462 0 00-.2257-.5446l-1.1663-.1894a9.3574 9.3574 0 00-.1407-.2622l.49-1.0761a.3437.3437 0 00-.0274-.3361.3486.3486 0 00-.3006-.154l-1.1845.0416a6.7444 6.7444 0 00-.1873-.2268l.2723-1.153a.3472.3472 0 00-.417-.4172l-1.1532.2724a14.0183 14.0183 0 00-.2278-.1873l.0415-1.1845a.3442.3442 0 00-.49-.328l-1.076.491c-.0872-.0476-.1742-.0952-.2623-.1407l-.1903-1.1673A.3483.3483 0 0016.256.955l-.9597.6905a8.4867 8.4867 0 00-.2855-.086l-.414-1.1066a.3483.3483 0 00-.5781-.1154l-.8069.8666a9.2936 9.2936 0 00-.2936-.0284L12.2946.1683a.3462.3462 0 00-.5892 0l-.6236 1.0073a13.7383 13.7383 0 00-.2936.0284L9.9803.3374a.3462.3462 0 00-.578.1154l-.4141 1.1065c-.0962.0274-.1903.0567-.2855.086L7.744.955a.3483.3483 0 00-.5447.2258L7.009 2.348a9.3574 9.3574 0 00-.2622.1407l-1.0762-.491a.3462.3462 0 00-.49.328l.0416 1.1845a7.9826 7.9826 0 00-.2278.1873L3.8413 3.425a.3472.3472 0 00-.4171.4171l.2713 1.1531c-.0628.075-.1255.1509-.1863.2268l-1.1845-.0415a.3462.3462 0 00-.328.49l.491 1.0761a9.167 9.167 0 00-.1407.2622l-1.1662.1894a.3483.3483 0 00-.2258.5446l.6904.9587a13.303 13.303 0 00-.087.2855l-1.1065.414a.3483.3483 0 00-.1155.5781l.8656.807a9.2936 9.2936 0 00-.0283.2935l-1.0073.6236a.3442.3442 0 000 .5892l1.0073.6236c.008.0982.0182.1964.0283.2936l-.8656.8079a.3462.3462 0 00.1155.578l1.1065.4141c.0273.0962.0567.1914.087.2855l-.6904.9587a.3452.3452 0 00.2268.5447l1.1662.1893c.0456.088.0922.1751.1408.2622l-.491 1.0762a.3462.3462 0 00.328.49l1.1834-.0415c.0618.0769.1235.1528.1873.2277l-.2713 1.1541a.3462.3462 0 00.4171.4161l1.153-.2713c.075.0638.151.1255.2279.1863l-.0415 1.1845a.3442.3442 0 00.49.327l1.0761-.49c.087.0486.1741.0951.2622.1407l.1903 1.1662a.3483.3483 0 00.5447.2268l.9587-.6904a9.299 9.299 0 00.2855.087l.414 1.1066a.3452.3452 0 00.5781.1154l.8079-.8656c.0972.0111.1954.0203.2936.0294l.6236 1.0073a.3472.3472 0 00.5892 0l.6236-1.0073c.0982-.0091.1964-.0183.2936-.0294l.8069.8656a.3483.3483 0 00.578-.1154l.4141-1.1066a8.4626 8.4626 0 00.2855-.087l.9587.6904a.3452.3452 0 00.5447-.2268l.1903-1.1662c.088-.0456.1751-.0931.2622-.1407l1.0762.49a.3472.3472 0 00.49-.327l-.0415-1.1845a6.7267 6.7267 0 00.2267-.1863l1.1531.2713a.3472.3472 0 00.4171-.416l-.2713-1.1542c.0628-.0749.1255-.1508.1863-.2278l1.1845.0415a.3442.3442 0 00.328-.49l-.49-1.076c.0475-.0872.0951-.1742.1407-.2623l1.1662-.1893a.3483.3483 0 00.2258-.5447l-.6904-.9587.087-.2855 1.1066-.414a.3462.3462 0 00.1154-.5781l-.8656-.8079c.0101-.0972.0202-.1954.0283-.2936l1.0073-.6236a.3442.3442 0 000-.5892zm-6.7413 8.3551a.7138.7138 0 01.2986-1.396.714.714 0 11-.2997 1.396zm-.3422-2.3142a.649.649 0 00-.7715.5l-.3573 1.6685c-1.1035.501-2.3285.7795-3.6193.7795a8.7368 8.7368 0 01-3.6951-.814l-.3574-1.6684a.648.648 0 00-.7714-.499l-1.473.3158a8.7216 8.7216 0 01-.7613-.898h7.1676c.081 0 .1356-.0141.1356-.088v-2.536c0-.074-.0536-.0881-.1356-.0881h-2.0966v-1.6077h2.2677c.2065 0 1.1065.0587 1.394 1.2088.0901.3533.2875 1.5044.4232 1.8729.1346.413.6833 1.2381 1.2685 1.2381h3.5716a.7492.7492 0 00.1296-.0131 8.7874 8.7874 0 01-.8119.9526zM6.8369 20.024a.714.714 0 11-.2997-1.396.714.714 0 01.2997 1.396zM4.1177 8.9972a.7137.7137 0 11-1.304.5791.7137.7137 0 011.304-.579zm-.8352 1.9813l1.5347-.6824a.65.65 0 00.33-.8585l-.3158-.7147h1.2432v5.6025H3.5669a8.7753 8.7753 0 01-.2834-3.348zm6.7343-.5437V8.7836h2.9601c.153 0 1.0792.1772 1.0792.8697 0 .575-.7107.7815-1.2948.7815zm10.7574 1.4862c0 .2187-.008.4363-.0243.651h-.9c-.09 0-.1265.0586-.1265.1477v.413c0 .973-.5487 1.1846-1.0296 1.2382-.4576.0517-.9648-.1913-1.0275-.4717-.2704-1.5186-.7198-1.8436-1.4305-2.4034.8817-.5599 1.799-1.386 1.799-2.4915 0-1.1936-.819-1.9458-1.3769-2.3153-.7825-.5163-1.6491-.6195-1.883-.6195H5.4682a8.7651 8.7651 0 014.907-2.7699l1.0974 1.151a.648.648 0 00.9182.0213l1.227-1.1743a8.7753 8.7753 0 016.0044 4.2762l-.8403 1.8982a.652.652 0 00.33.8585l1.6178.7188c.0283.2875.0425.577.0425.8717zm-9.3006-9.5993a.7128.7128 0 11.984 1.0316.7137.7137 0 01-.984-1.0316zm8.3389 6.71a.7107.7107 0 01.9395-.3625.7137.7137 0 11-.9405.3635z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.rust-lang.org", + "guidelines": "https://www.rust-lang.org/policies/media-guide", + "license": { + "type": "CC-BY-SA-4.0", + "url": "https://spdx.org/licenses/CC-BY-SA-4.0" + } + } + }, + { + "id": "sentry", + "title": "Sentry", + "category": "engineering", + "aliases": [], + "domains": [ + "sentry.io" + ], + "viewBox": 24, + "hex": "362D59", + "path": "M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://sentry.io/branding/" + } + }, + { + "id": "shopify", + "title": "Shopify", + "category": "business", + "aliases": [], + "domains": [ + "shopify.com" + ], + "viewBox": 24, + "hex": "7AB55C", + "path": "M15.337 23.979l7.216-1.561s-2.604-17.613-2.625-17.73c-.018-.116-.114-.192-.211-.192s-1.929-.136-1.929-.136-1.275-1.274-1.439-1.411c-.045-.037-.075-.057-.121-.074l-.914 21.104h.023zM11.71 11.305s-.81-.424-1.774-.424c-1.447 0-1.504.906-1.504 1.141 0 1.232 3.24 1.715 3.24 4.629 0 2.295-1.44 3.76-3.406 3.76-2.354 0-3.54-1.465-3.54-1.465l.646-2.086s1.245 1.066 2.28 1.066c.675 0 .975-.545.975-.932 0-1.619-2.654-1.694-2.654-4.359-.034-2.237 1.571-4.416 4.827-4.416 1.257 0 1.875.361 1.875.361l-.945 2.715-.02.01zM11.17.83c.136 0 .271.038.405.135-.984.465-2.064 1.639-2.508 3.992-.656.213-1.293.405-1.889.578C7.697 3.75 8.951.84 11.17.84V.83zm1.235 2.949v.135c-.754.232-1.583.484-2.394.736.466-1.777 1.333-2.645 2.085-2.971.193.501.309 1.176.309 2.1zm.539-2.234c.694.074 1.141.867 1.429 1.755-.349.114-.735.231-1.158.366v-.252c0-.752-.096-1.371-.271-1.871v.002zm2.992 1.289c-.02 0-.06.021-.078.021s-.289.075-.714.21c-.423-1.233-1.176-2.37-2.508-2.37h-.115C12.135.209 11.669 0 11.265 0 8.159 0 6.675 3.877 6.21 5.846c-1.194.365-2.063.636-2.16.674-.675.213-.694.232-.772.87-.075.462-1.83 14.063-1.83 14.063L15.009 24l.927-21.166z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.shopify.com/brand-assets", + "guidelines": "https://www.shopify.com/brand-assets" + } + }, + { + "id": "snowflake", + "title": "Snowflake", + "category": "data", + "aliases": [], + "domains": [ + "snowflake.com" + ], + "viewBox": 24, + "hex": "29B5E8", + "path": "M24 3.459c0 .646-.418 1.18-1.141 1.18-.723 0-1.142-.534-1.142-1.18 0-.647.419-1.18 1.142-1.18.723 0 1.141.533 1.141 1.18zm-.228 0c0-.533-.38-.951-.913-.951s-.913.38-.913.95c0 .533.38.952.913.952.57 0 .913-.419.913-.951zm-1.37-.533h.495c.266 0 .456.152.456.38 0 .153-.076.229-.19.305l.19.266v.038h-.266l-.19-.266h-.229v.266h-.266zm.495.228h-.229v.267h.229c.114 0 .152-.038.152-.114.038-.077-.038-.153-.152-.153zM7.602 12.4c.038-.151.076-.304.076-.456 0-.114-.038-.228-.038-.342-.114-.343-.304-.647-.646-.838l-4.87-2.777c-.685-.38-1.56-.152-1.94.533-.381.685-.153 1.56.532 1.94l2.701 1.56-2.701 1.56c-.685.38-.913 1.256-.533 1.94.38.685 1.256.914 1.94.533l4.832-2.777c.343-.267.571-.533.647-.876zm1.332 2.626c-.266-.038-.57.038-.837.19l-4.832 2.777c-.685.38-.913 1.256-.532 1.94.38.686 1.255.914 1.94.533l2.701-1.56v3.12c0 .8.647 1.408 1.446 1.408.799 0 1.407-.647 1.407-1.408v-5.592c0-.761-.57-1.37-1.293-1.408zm4.946-6.088c.266.038.57-.038.837-.19l4.832-2.777c.685-.38.913-1.256.532-1.94-.38-.686-1.255-.914-1.94-.533l-2.701 1.56V1.975c0-.799-.647-1.408-1.446-1.408-.799 0-1.446.609-1.446 1.408V7.53c0 .76.609 1.37 1.332 1.407zM3.265 5.97l4.832 2.777c.266.152.533.19.837.19.723-.038 1.331-.684 1.331-1.407V1.975c0-.799-.646-1.408-1.407-1.408-.799 0-1.446.647-1.446 1.408v3.12l-2.701-1.56c-.685-.38-1.56-.152-1.94.533-.419.646-.19 1.521.494 1.902zm9.093 6.011a.412.412 0 00-.114-.266l-.57-.571a.346.346 0 00-.267-.114.412.412 0 00-.266.114l-.571.57a.411.411 0 00-.114.267c0 .076.038.19.114.267l.57.57a.345.345 0 00.267.114c.076 0 .19-.038.266-.114l.571-.57a.412.412 0 00.114-.267zm1.598.533L11.94 14.53c-.039.038-.153.114-.229.114h-.608a.411.411 0 01-.267-.114L8.82 12.514a.408.408 0 01-.076-.229v-.608c0-.076.038-.19.114-.267l2.016-2.016a.41.41 0 01.267-.114h.608a.41.41 0 01.267.114l2.016 2.016a.347.347 0 01.114.267v.608c-.076.077-.114.19-.19.229zm5.593 5.44l-4.832-2.777c-.266-.152-.57-.19-.837-.152-.723.038-1.332.684-1.332 1.408v5.554c0 .8.647 1.408 1.408 1.408.799 0 1.446-.647 1.446-1.408v-3.12l2.7 1.56c.686.38 1.561.152 1.941-.533.419-.646.19-1.521-.494-1.94zm2.549-7.533l-2.701 1.56 2.7 1.56c.686.38.914 1.256.533 1.94-.38.685-1.255.913-1.94.533l-4.832-2.778a1.644 1.644 0 01-.647-.798c-.037-.153-.076-.305-.076-.457 0-.114.039-.228.039-.342.114-.343.342-.647.646-.837l4.832-2.778c.685-.38 1.56-.152 1.94.533.457.609.19 1.484-.494 1.864", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.snowflake.com/brand-guidelines/", + "guidelines": "https://www.snowflake.com/brand-guidelines/" + } + }, + { + "id": "spring", + "title": "Spring", + "category": "framework", + "aliases": [ + "spring-boot" + ], + "domains": [ + "spring.io" + ], + "viewBox": 24, + "hex": "6DB33F", + "path": "M21.8537 1.4158a10.4504 10.4504 0 0 1-1.284 2.2471A11.9666 11.9666 0 1 0 3.8518 20.7757l.4445.3951a11.9543 11.9543 0 0 0 19.6316-8.2971c.3457-3.0126-.568-6.8649-2.0743-11.458zM5.5805 20.8745a1.0174 1.0174 0 1 1-.1482-1.4323 1.0396 1.0396 0 0 1 .1482 1.4323zm16.1991-3.5806c-2.9385 3.9263-9.2601 2.5928-13.2852 2.7904 0 0-.7161.0494-1.4323.1481 0 0 .2717-.1234.6174-.2469 2.8398-.9877 4.1732-1.1853 5.9018-2.0743 3.2349-1.6545 6.4698-5.2844 7.1118-9.0379-1.2347 3.6053-4.9881 6.7167-8.3959 7.9761-2.3459.8643-6.5685 1.7039-6.5685 1.7039l-.1729-.0988c-2.8645-1.4076-2.9632-7.6304 2.2718-9.6306 2.2966-.889 4.4696-.395 6.9637-.9877 2.6422-.6174 5.7043-2.5929 6.939-5.1857 1.3828 4.1732 3.062 10.643.0493 14.6434z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://spring.io/trademarks" + } + }, + { + "id": "sqlite", + "title": "SQLite", + "category": "data", + "aliases": [], + "domains": [ + "sqlite.org" + ], + "viewBox": 24, + "hex": "003B57", + "path": "M21.678.521c-1.032-.92-2.28-.55-3.513.544a8.71 8.71 0 0 0-.547.535c-2.109 2.237-4.066 6.38-4.674 9.544.237.48.422 1.093.544 1.561a13.044 13.044 0 0 1 .164.703s-.019-.071-.096-.296l-.05-.146a1.689 1.689 0 0 0-.033-.08c-.138-.32-.518-.995-.686-1.289-.143.423-.27.818-.376 1.176.484.884.778 2.4.778 2.4s-.025-.099-.147-.442c-.107-.303-.644-1.244-.772-1.464-.217.804-.304 1.346-.226 1.478.152.256.296.698.422 1.186.286 1.1.485 2.44.485 2.44l.017.224a22.41 22.41 0 0 0 .056 2.748c.095 1.146.273 2.13.5 2.657l.155-.084c-.334-1.038-.47-2.399-.41-3.967.09-2.398.642-5.29 1.661-8.304 1.723-4.55 4.113-8.201 6.3-9.945-1.993 1.8-4.692 7.63-5.5 9.788-.904 2.416-1.545 4.684-1.931 6.857.666-2.037 2.821-2.912 2.821-2.912s1.057-1.304 2.292-3.166c-.74.169-1.955.458-2.362.629-.6.251-.762.337-.762.337s1.945-1.184 3.613-1.72C21.695 7.9 24.195 2.767 21.678.521m-18.573.543A1.842 1.842 0 0 0 1.27 2.9v16.608a1.84 1.84 0 0 0 1.835 1.834h9.418a22.953 22.953 0 0 1-.052-2.707c-.006-.062-.011-.141-.016-.2a27.01 27.01 0 0 0-.473-2.378c-.121-.47-.275-.898-.369-1.057-.116-.197-.098-.31-.097-.432 0-.12.015-.245.037-.386a9.98 9.98 0 0 1 .234-1.045l.217-.028c-.017-.035-.014-.065-.031-.097l-.041-.381a32.8 32.8 0 0 1 .382-1.194l.2-.019c-.008-.016-.01-.038-.018-.053l-.043-.316c.63-3.28 2.587-7.443 4.8-9.791.066-.069.133-.128.198-.194Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/sqlite/sqlite/blob/43e862723ec680542ca6f608f9963c0993dd7324/art/sqlite370.eps" + } + }, + { + "id": "stripe", + "title": "Stripe", + "category": "business", + "aliases": [], + "domains": [ + "stripe.com" + ], + "viewBox": 24, + "hex": "635BFF", + "path": "M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://stripe.com/newsroom/information" + } + }, + { + "id": "supabase", + "title": "Supabase", + "category": "cloud", + "aliases": [], + "domains": [ + "supabase.com" + ], + "viewBox": 24, + "hex": "3FCF8E", + "path": "M11.9 1.036c-.015-.986-1.26-1.41-1.874-.637L.764 12.05C-.33 13.427.65 15.455 2.409 15.455h9.579l.113 7.51c.014.985 1.259 1.408 1.873.636l9.262-11.653c1.093-1.375.113-3.403-1.645-3.403h-9.642z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/supabase/supabase/blob/4031a7549f5d46da7bc79c01d56be4177dc7c114/packages/common/assets/images/supabase-logo-wordmark--light.svg" + } + }, + { + "id": "svelte", + "title": "Svelte", + "category": "framework", + "aliases": [], + "domains": [ + "svelte.dev" + ], + "viewBox": 24, + "hex": "FF3E00", + "path": "M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/sveltejs/branding/blob/c4dfca6743572087a6aef0e109ffe3d95596e86a/svelte-logo.svg" + } + }, + { + "id": "telegram", + "title": "Telegram", + "category": "channel", + "aliases": [], + "domains": [ + "telegram.org", + "t.me" + ], + "viewBox": 24, + "hex": "26A5E4", + "path": "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://telegram.org/tour/screenshots" + } + }, + { + "id": "tensorflow", + "title": "TensorFlow", + "category": "framework", + "aliases": [], + "domains": [ + "tensorflow.org" + ], + "viewBox": 24, + "hex": "FF6F00", + "path": "M1.292 5.856L11.54 0v24l-4.095-2.378V7.603l-6.168 3.564.015-5.31zm21.43 5.311l-.014-5.31L12.46 0v24l4.095-2.378V14.87l3.092 1.788-.018-4.618-3.074-1.756V7.603l6.168 3.564z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.tensorflow.org" + } + }, + { + "id": "terraform", + "title": "Terraform", + "category": "engineering", + "aliases": [], + "domains": [ + "terraform.io" + ], + "viewBox": 24, + "hex": "844FBA", + "path": "M1.44 0v7.575l6.561 3.79V3.787zm21.12 4.227l-6.561 3.791v7.574l6.56-3.787zM8.72 4.23v7.575l6.561 3.787V8.018zm0 8.405v7.575L15.28 24v-7.578z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.hashicorp.com/brand", + "guidelines": "https://www.hashicorp.com/brand" + } + }, + { + "id": "tiktok", + "title": "TikTok", + "category": "channel", + "aliases": [ + "douyin", + "抖音" + ], + "domains": [ + "tiktok.com", + "douyin.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://tiktok.com" + } + }, + { + "id": "trello", + "title": "Trello", + "category": "collaboration", + "aliases": [], + "domains": [ + "trello.com" + ], + "viewBox": 24, + "hex": "0052CC", + "path": "M21.147 0H2.853A2.86 2.86 0 000 2.853v18.294A2.86 2.86 0 002.853 24h18.294A2.86 2.86 0 0024 21.147V2.853A2.86 2.86 0 0021.147 0zM10.34 17.287a.953.953 0 01-.953.953h-4a.954.954 0 01-.954-.953V5.38a.953.953 0 01.954-.953h4a.954.954 0 01.953.953zm9.233-5.467a.944.944 0 01-.953.947h-4a.947.947 0 01-.953-.947V5.38a.953.953 0 01.953-.953h4a.954.954 0 01.953.953z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://atlassian.design/resources/logo-library", + "guidelines": "https://atlassian.design/foundations/logos" + } + }, + { + "id": "typescript", + "title": "TypeScript", + "category": "language", + "aliases": [ + "ts" + ], + "domains": [ + "typescriptlang.org" + ], + "viewBox": 24, + "hex": "3178C6", + "path": "M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.typescriptlang.org/branding", + "guidelines": "https://www.typescriptlang.org/branding" + } + }, + { + "id": "vercel", + "title": "Vercel", + "category": "cloud", + "aliases": [], + "domains": [ + "vercel.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "m12 1.608 12 20.784H0Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://vercel.com/geist/brands", + "guidelines": "https://vercel.com/geist/brands" + } + }, + { + "id": "vue", + "title": "Vue.js", + "category": "framework", + "aliases": [ + "vuejs", + "vue.js" + ], + "domains": [ + "vuejs.org" + ], + "viewBox": 24, + "hex": "4FC08D", + "path": "M24,1.61H14.06L12,5.16,9.94,1.61H0L12,22.39ZM12,14.08,5.16,2.23H9.59L12,6.41l2.41-4.18h4.43Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://github.com/vuejs/art/blob/a1c78b74569b70a25300925b4eacfefcc143b8f6/logo.svg", + "guidelines": "https://github.com/vuejs/art/blob/a1c78b74569b70a25300925b4eacfefcc143b8f6/README.md", + "license": { + "type": "CC-BY-NC-SA-4.0", + "url": "https://spdx.org/licenses/CC-BY-NC-SA-4.0" + } + } + }, + { + "id": "wechat", + "title": "WeChat", + "category": "channel", + "aliases": [ + "weixin", + "微信" + ], + "domains": [ + "weixin.qq.com" + ], + "viewBox": 24, + "hex": "07C160", + "path": "M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://wechat.design/tool/brand", + "guidelines": "https://wechat.design/brand/main-brand" + } + }, + { + "id": "whatsapp", + "title": "WhatsApp", + "category": "channel", + "aliases": [], + "domains": [ + "whatsapp.com" + ], + "viewBox": 24, + "hex": "25D366", + "path": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://about.meta.com/brand/resources/whatsapp/whatsapp-brand", + "guidelines": "https://about.meta.com/brand/resources/whatsapp/whatsapp-brand" + } + }, + { + "id": "woocommerce", + "title": "WooCommerce", + "category": "business", + "aliases": [ + "woo-commerce" + ], + "domains": [ + "woocommerce.com" + ], + "viewBox": 24, + "hex": "96588A", + "path": "M.754 9.58a.754.754 0 00-.754.758v2.525c0 .42.339.758.758.758h3.135l1.431.799-.326-.799h2.373a.757.757 0 00.758-.758v-2.525a.757.757 0 00-.758-.758H.754zm2.709.445h.03c.065.001.124.023.179.067a.26.26 0 01.103.19.29.29 0 01-.033.16c-.13.239-.236.64-.322 1.199-.083.541-.114.965-.094 1.267a.392.392 0 01-.039.219.213.213 0 01-.176.12c-.086.006-.177-.034-.263-.124-.31-.316-.555-.788-.735-1.416-.216.425-.375.744-.478.957-.196.376-.363.568-.502.578-.09.007-.166-.069-.233-.228-.17-.436-.352-1.277-.548-2.524a.297.297 0 01.054-.222c.047-.064.116-.095.21-.102.169-.013.265.065.288.238.103.695.217 1.284.336 1.766l.727-1.387c.066-.126.15-.192.25-.199.146-.01.237.083.273.28.083.441.188.817.315 1.136.086-.844.233-1.453.44-1.828a.255.255 0 01.218-.147zm1.293.36c.056 0 .116.006.18.02.232.05.411.177.53.386.107.18.161.395.161.654 0 .343-.087.654-.26.94-.2.332-.459.5-.781.5a.88.88 0 01-.18-.022.763.763 0 01-.531-.384 1.287 1.287 0 01-.158-.659c0-.342.085-.655.258-.937.202-.333.462-.498.78-.498zm2.084 0c.056 0 .116.006.18.02.236.05.411.177.53.386.107.18.16.395.16.654 0 .343-.086.654-.259.94-.2.332-.459.5-.781.5a.88.88 0 01-.18-.022.763.763 0 01-.531-.384 1.287 1.287 0 01-.16-.659c0-.342.087-.655.26-.937.202-.333.462-.498.78-.498zm4.437.047c-.305 0-.546.102-.718.304-.173.203-.256.49-.256.856 0 .395.086.697.256.906.17.21.418.316.744.316.315 0 .559-.107.728-.316.17-.21.256-.504.256-.883s-.087-.673-.26-.879c-.176-.202-.424-.304-.75-.304zm-1.466.002a1.13 1.13 0 00-.84.326c-.223.22-.332.499-.332.838 0 .362.108.658.328.88.22.223.505.336.861.336.103 0 .22-.016.346-.052v-.54c-.117.034-.216.051-.303.051a.545.545 0 01-.422-.177c-.106-.12-.16-.278-.16-.48 0-.19.053-.348.156-.468a.498.498 0 01.397-.181c.103 0 .212.015.332.049v-.537a1.394 1.394 0 00-.363-.045zm12.414 0a1.135 1.135 0 00-.84.326c-.223.22-.332.499-.332.838 0 .362.108.658.328.88.22.223.506.336.861.336.103 0 .22-.016.346-.052v-.54c-.116.034-.216.051-.303.051a.545.545 0 01-.422-.177c-.106-.12-.16-.278-.16-.48 0-.19.053-.348.156-.468a.498.498 0 01.397-.181c.103 0 .212.015.332.049v-.537a1.394 1.394 0 00-.363-.045zm-9.598.06l-.29 2.264h.579l.156-1.559.395 1.559h.412l.379-1.555.164 1.555h.603l-.304-2.264h-.791l-.12.508c-.03.13-.06.264-.087.4l-.067.352a29.97 29.97 0 00-.258-1.26h-.771zm2.768 0l-.29 2.264h.579l.156-1.559.396 1.559h.412l.375-1.555.165 1.555h.603l-.305-2.264h-.789l-.119.508c-.03.13-.06.264-.086.4l-.066.352c-.063-.352-.15-.771-.26-1.26h-.771zm3.988 0v2.264h.611v-1.031h.012l.494 1.03h.645l-.489-1.019a.61.61 0 00.37-.552.598.598 0 00-.25-.506c-.167-.123-.394-.186-.68-.186h-.713zm3.377 0v2.264H24v-.483h-.63v-.414h.54v-.468h-.54v-.416h.626v-.483H22.76zm-4.793.004v2.264h1.24v-.483h-.627v-.416h.541v-.468h-.54v-.415h.622v-.482h-1.236zm2.025.432c.146.003.25.025.313.072.063.046.091.12.091.227 0 .156-.135.236-.404.24v-.54zm-15.22.011c-.104 0-.205.069-.301.211a1.078 1.078 0 00-.2.639c0 .096.02.2.06.303.049.13.117.198.196.215.083.016.173-.02.27-.106.123-.11.205-.273.252-.492.016-.077.023-.16.023-.246 0-.097-.02-.2-.06-.303-.05-.13-.116-.198-.196-.215a.246.246 0 00-.045-.006zm2.083 0c-.103 0-.204.069-.3.211a1.078 1.078 0 00-.2.639c0 .096.02.2.06.303.049.13.117.198.196.215.083.016.173-.02.27-.106.123-.11.205-.273.252-.492.013-.077.023-.16.023-.246 0-.097-.02-.2-.06-.303-.05-.13-.116-.198-.196-.215a.246.246 0 00-.045-.006zm4.428.006c.233 0 .354.218.354.66-.004.273-.038.46-.098.553a.293.293 0 01-.262.139.266.266 0 01-.242-.139c-.056-.093-.084-.28-.084-.562 0-.436.11-.65.332-.65Z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://woocommerce.com/style-guide", + "guidelines": "https://woocommerce.com/trademark-guidelines" + } + }, + { + "id": "wordpress", + "title": "WordPress", + "category": "business", + "aliases": [], + "domains": [ + "wordpress.org", + "wordpress.com" + ], + "viewBox": 24, + "hex": "21759B", + "path": "M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://wordpress.org/about/logos", + "guidelines": "https://wordpressfoundation.org/trademark-policy" + } + }, + { + "id": "x", + "title": "X", + "category": "channel", + "aliases": [ + "twitter" + ], + "domains": [ + "x.com", + "twitter.com" + ], + "viewBox": 24, + "hex": "000000", + "path": "M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://x.com", + "guidelines": "https://about.x.com/en/who-we-are/brand-toolkit" + } + }, + { + "id": "youtube", + "title": "YouTube", + "category": "channel", + "aliases": [], + "domains": [ + "youtube.com", + "youtu.be" + ], + "viewBox": 24, + "hex": "FF0000", + "path": "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors", + "guidelines": "https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors" + } + }, + { + "id": "zendesk", + "title": "Zendesk", + "category": "business", + "aliases": [], + "domains": [ + "zendesk.com" + ], + "viewBox": 24, + "hex": "03363D", + "path": "M12.914 2.904V16.29L24 2.905H12.914zM0 2.906C0 5.966 2.483 8.45 5.543 8.45s5.542-2.484 5.543-5.544H0zm11.086 4.807L0 21.096h11.086V7.713zm7.37 7.84c-3.063 0-5.542 2.48-5.542 5.543H24c0-3.06-2.48-5.543-5.543-5.543z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://brandland.zendesk.com", + "guidelines": "https://brandland.zendesk.com" + } + }, + { + "id": "zoom", + "title": "Zoom", + "category": "collaboration", + "aliases": [], + "domains": [ + "zoom.us" + ], + "viewBox": 24, + "hex": "0B5CFF", + "path": "M5.033 14.649H.743a.74.74 0 0 1-.686-.458.74.74 0 0 1 .16-.808L3.19 10.41H1.06A1.06 1.06 0 0 1 0 9.35h3.957c.301 0 .57.18.686.458a.74.74 0 0 1-.161.808L1.51 13.59h2.464c.585 0 1.06.475 1.06 1.06zM24 11.338c0-1.14-.927-2.066-2.066-2.066-.61 0-1.158.265-1.537.686a2.061 2.061 0 0 0-1.536-.686c-1.14 0-2.066.926-2.066 2.066v3.311a1.06 1.06 0 0 0 1.06-1.06v-2.251a1.004 1.004 0 0 1 2.013 0v2.251c0 .586.474 1.06 1.06 1.06v-3.311a1.004 1.004 0 0 1 2.012 0v2.251c0 .586.475 1.06 1.06 1.06zM16.265 12a2.728 2.728 0 1 1-5.457 0 2.728 2.728 0 0 1 5.457 0zm-1.06 0a1.669 1.669 0 1 0-3.338 0 1.669 1.669 0 0 0 3.338 0zm-4.82 0a2.728 2.728 0 1 1-5.458 0 2.728 2.728 0 0 1 5.457 0zm-1.06 0a1.669 1.669 0 1 0-3.338 0 1.669 1.669 0 0 0 3.338 0z", + "provenance": { + "provider": "Simple Icons", + "providerVersion": "16.28.0", + "source": "https://brand.zoom.us/media-library/", + "guidelines": "https://brand.zoom.us/usage-legal/" + } + } +]); diff --git a/vendor/archify/renderers/shared/generated-validators.mjs b/vendor/archify/renderers/shared/generated-validators.mjs new file mode 100644 index 0000000..bd7b678 --- /dev/null +++ b/vendor/archify/renderers/shared/generated-validators.mjs @@ -0,0 +1,134 @@ +// Generated by scripts/generate-validators.mjs. Do not edit by hand. +import { validatePortablePath } from './portable-path.mjs'; +"use strict";const workflowSchema = validate20;const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/tt-a1i/archify/schemas/workflow.schema.json","title":"Archify Workflow Diagram","type":"object","additionalProperties":false,"required":["schema_version","diagram_type","meta","lanes","nodes","edges"],"properties":{"schema_version":{"enum":[1,2]},"diagram_type":{"const":"workflow"},"meta":{"type":"object","additionalProperties":false,"required":["title","output"],"properties":{"title":{"type":"string","minLength":1},"locale":{"$ref":"common.schema.json#/$defs/locale"},"translations":{"$ref":"common.schema.json#/$defs/translations"},"subtitle":{"type":"string"},"output":{"$ref":"common.schema.json#/$defs/portableOutputPath"},"animation":{"enum":["trace","none"]},"visual_preset":{"enum":["classic","signal-flow","blueprint","editorial"]},"quality_profile":{"enum":["standard","showcase"]},"repository":{"$ref":"common.schema.json#/$defs/repository"},"views":{"$ref":"common.schema.json#/$defs/guidedViews"},"legend":{"type":"object","additionalProperties":false,"properties":{"mode":{"$ref":"common.schema.json#/$defs/legendMode"},"entries":{"type":"object","additionalProperties":false,"properties":{"frontend":{"$ref":"common.schema.json#/$defs/legendEntry"},"backend":{"$ref":"common.schema.json#/$defs/legendEntry"},"database":{"$ref":"common.schema.json#/$defs/legendEntry"},"cloud":{"$ref":"common.schema.json#/$defs/legendEntry"},"security":{"$ref":"common.schema.json#/$defs/legendEntry"},"messagebus":{"$ref":"common.schema.json#/$defs/legendEntry"},"external":{"$ref":"common.schema.json#/$defs/legendEntry"}}}}},"viewBox":{"type":"array","prefixItems":[{"type":"number","minimum":700},{"type":"number","minimum":240}],"items":false,"minItems":2,"maxItems":2}}},"lanes":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false,"required":["id","label"],"properties":{"id":{"$ref":"common.schema.json#/$defs/id"},"label":{"type":"string","minLength":1},"variant":{"enum":["normal","exception"]}}}},"phases":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","label","fromCol","toCol"],"properties":{"id":{"$ref":"common.schema.json#/$defs/id"},"label":{"type":"string","minLength":1},"fromCol":{"type":"integer","minimum":0,"maximum":5},"toCol":{"type":"integer","minimum":0,"maximum":5},"variant":{"enum":["default","emphasis","security","dashed"]}}}},"groups":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","label","lane","fromCol","toCol"],"properties":{"id":{"$ref":"common.schema.json#/$defs/id"},"label":{"type":"string","minLength":1},"lane":{"$ref":"common.schema.json#/$defs/id"},"fromCol":{"type":"integer","minimum":0,"maximum":5},"toCol":{"type":"integer","minimum":0,"maximum":5},"variant":{"enum":["default","emphasis","security","dashed"]}}}},"mainPath":{"type":"array","minItems":2,"items":{"$ref":"common.schema.json#/$defs/id"}},"semanticChecks":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"allowedRoots":{"type":"array","items":{"$ref":"common.schema.json#/$defs/id"}},"allowedTerminals":{"type":"array","items":{"$ref":"common.schema.json#/$defs/id"}},"requiredEdges":{"type":"array","items":{"$ref":"#/$defs/semanticRelation"}},"requiredPaths":{"type":"array","items":{"$ref":"#/$defs/semanticRelation"}}}},"nodes":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false,"required":["id","lane","col","type","label"],"properties":{"id":{"$ref":"common.schema.json#/$defs/id"},"lane":{"$ref":"common.schema.json#/$defs/id"},"col":{"type":"integer","minimum":0,"maximum":5},"type":{"$ref":"common.schema.json#/$defs/componentType"},"label":{"type":"string","minLength":1},"sublabel":{"type":"string"},"tag":{"type":"string"},"icon":{"$ref":"common.schema.json#/$defs/nodeIcon"},"brand":{"$ref":"common.schema.json#/$defs/brandMark"},"sources":{"$ref":"common.schema.json#/$defs/sourceReferences"},"width":{"type":"number","minimum":32},"height":{"type":"number","minimum":32},"yOffset":{"type":"number"}}}},"edges":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["from","to"],"properties":{"id":{"$ref":"common.schema.json#/$defs/id"},"from":{"$ref":"common.schema.json#/$defs/id"},"to":{"$ref":"common.schema.json#/$defs/id"},"label":{"type":"string"},"variant":{"$ref":"common.schema.json#/$defs/variant"},"role":{"enum":["main","branch","async","return","error"]},"fromSide":{"$ref":"#/$defs/side"},"toSide":{"$ref":"#/$defs/side"},"route":{"enum":["auto","straight","drop","outside-right","return-left","bottom-channel","up-channel"]},"via":{"type":"array","items":{"$ref":"common.schema.json#/$defs/point"}},"labelAt":{"$ref":"common.schema.json#/$defs/point"},"labelDx":{"type":"number"},"labelDy":{"type":"number"},"labelSegment":{"type":"integer","minimum":0},"channelX":{"type":"number"},"channelY":{"type":"number"},"bias":{"type":"number","minimum":0,"maximum":1},"width":{"type":"number","minimum":0.5}}}},"cards":{"$ref":"common.schema.json#/$defs/cards"}},"$defs":{"semanticRelation":{"type":"object","additionalProperties":false,"required":["from","to"],"properties":{"from":{"$ref":"common.schema.json#/$defs/id"},"to":{"$ref":"common.schema.json#/$defs/id"}}},"side":{"enum":["left","right","top","bottom"]}}};const schema33 = {"type":"string","pattern":"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$","maxLength":35};const schema34 = {"type":"object","propertyNames":{"pattern":"^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"},"additionalProperties":{"type":"string","minLength":1,"maxLength":400},"maxProperties":600};const schema35 = {"description":"A portable POSIX-relative HTML output path. It must be safe to interpret identically on every supported host filesystem.","type":"string","minLength":1,"pattern":"^[^/]+(?:/[^/]+)*$","allOf":[{"pattern":"(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"},{"not":{"pattern":"[\\\\<>:\"|?*\\u0000-\\u001F\\u007F-\\u009F]"}},{"not":{"pattern":"[\\uD800-\\uDFFF]"}},{"not":{"pattern":"(^|/)[.]{1,2}(/|$)"}},{"not":{"pattern":"[. ](/|$)"}},{"not":{"pattern":"(^|/)[^/]{256}"}},{"not":{"pattern":"(^|/)(?:[Cc][Oo][Nn]|[Pp][Rr][Nn]|[Aa][Uu][Xx]|[Nn][Uu][Ll]|[Cc][Oo][Nn][Ii][Nn][$]|[Cc][Oo][Nn][Oo][Uu][Tt][$]|[Cc][Oo][Mm][1-9¹²³]|[Ll][Pp][Tt][1-9¹²³])(?:[.]|/|$)"}},{"not":{"pattern":"(^|/)[^/]*~[1-9][0-9]*(?:[.]|/|$)"}}]};const schema36 = {"type":"object","additionalProperties":false,"required":["url","revision"],"properties":{"url":{"type":"string","minLength":1},"provider":{"enum":["github","gitee"]},"link_mode":{"enum":["web","local-only"]},"revision":{"type":"string","pattern":"^[a-fA-F0-9]{40}$"}}};const schema40 = {"enum":["auto","all","hidden"]};const schema41 = {"type":"object","additionalProperties":false,"minProperties":1,"properties":{"label":{"type":"string","minLength":1,"maxLength":80},"visible":{"type":"boolean"}}};const schema38 = {"type":"string","pattern":"^[a-zA-Z][a-zA-Z0-9_-]*$"};const schema60 = {"enum":["frontend","backend","database","cloud","security","messagebus","external"]};const schema61 = {"description":"Decorative corner icon. Omit the icon field for the type default; none hides it. Does not change type, color, or legend grouping.","enum":["calendar","clock","person","briefcase","flag","moon","frontend","backend","database","cloud","security","messagebus","external","start","active","waiting","success","failure","neutral","none"]};const schema62 = {"oneOf":[{"type":"string","minLength":1,"maxLength":2048,"anyOf":[{"maxLength":80,"pattern":"^[^\\r\\n]+$"},{"pattern":"^https?://"}]},{"type":"object","additionalProperties":false,"required":["url","sha256"],"properties":{"url":{"type":"string","minLength":8,"maxLength":2048,"pattern":"^https?://"},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"}}}]};const schema63 = {"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","additionalProperties":false,"required":["path"],"properties":{"path":{"type":"string","minLength":1,"maxLength":240},"line":{"type":"integer","minimum":1},"end_line":{"type":"integer","minimum":1},"label":{"type":"string","minLength":1,"maxLength":48}}}};const schema67 = {"enum":["default","emphasis","security","dashed"]};const schema68 = {"enum":["left","right","top","bottom"]};const schema70 = {"type":"array","prefixItems":[{"type":"number"},{"type":"number"}],"items":false,"minItems":2,"maxItems":2};const schema72 = {"type":"array","items":{"type":"object","additionalProperties":false,"required":["dot","title","items"],"properties":{"dot":{"enum":["cyan","emerald","violet","amber","rose","orange","slate"]},"title":{"type":"string","minLength":1},"items":{"type":"array","items":{"type":"string"}}}}};const func1 = Object.prototype.hasOwnProperty;const func3 = function ucs2length(str) { + const len = str.length; + let length = 0; + let pos = 0; + while (pos < len) { + length += 1; + const value = str.charCodeAt(pos++); + if (value >= 0xd800 && value <= 0xdbff && pos < len + && (str.charCodeAt(pos) & 0xfc00) === 0xdc00) pos += 1; + } + return length; +};const pattern4 = new RegExp("^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$", "u");const pattern5 = new RegExp("^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$", "u");const pattern6 = new RegExp("(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$", "u");const pattern7 = new RegExp("[\\\\<>:\"|?*\\u0000-\\u001F\\u007F-\\u009F]", "u");const pattern8 = new RegExp("[\\uD800-\\uDFFF]", "u");const pattern9 = new RegExp("(^|/)[.]{1,2}(/|$)", "u");const pattern10 = new RegExp("[. ](/|$)", "u");const pattern11 = new RegExp("(^|/)[^/]{256}", "u");const pattern12 = new RegExp("(^|/)(?:[Cc][Oo][Nn]|[Pp][Rr][Nn]|[Aa][Uu][Xx]|[Nn][Uu][Ll]|[Cc][Oo][Nn][Ii][Nn][$]|[Cc][Oo][Nn][Oo][Uu][Tt][$]|[Cc][Oo][Mm][1-9¹²³]|[Ll][Pp][Tt][1-9¹²³])(?:[.]|/|$)", "u");const pattern13 = new RegExp("(^|/)[^/]*~[1-9][0-9]*(?:[.]|/|$)", "u");const pattern14 = new RegExp("^[^/]+(?:/[^/]+)*$", "u");const pattern15 = new RegExp("^[a-fA-F0-9]{40}$", "u");const pattern16 = new RegExp("^[a-zA-Z][a-zA-Z0-9_-]*$", "u");const pattern29 = new RegExp("^[^\\r\\n]+$", "u");const pattern30 = new RegExp("^https?://", "u");const pattern32 = new RegExp("^[a-f0-9]{64}$", "u");const schema37 = {"type":"array","maxItems":5,"items":{"type":"object","additionalProperties":false,"required":["id","label","focus"],"properties":{"id":{"$ref":"#/$defs/id"},"label":{"type":"string","minLength":1,"maxLength":48},"focus":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/id"}},"note":{"type":"string","maxLength":140}}}};function validate22(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate22.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(Array.isArray(data)){if(data.length > 5){const err0 = {instancePath,schemaPath:"#/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}const len0 = data.length;for(let i0=0; i0 48){const err7 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}if(func3(data2) < 1){const err8 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}else {const err9 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}if(data0.focus !== undefined){let data3 = data0.focus;if(Array.isArray(data3)){if(data3.length < 1){const err10 = {instancePath:instancePath+"/" + i0+"/focus",schemaPath:"#/items/properties/focus/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}const len1 = data3.length;for(let i1=0; i1 140){const err14 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/maxLength",keyword:"maxLength",params:{limit: 140},message:"must NOT have more than 140 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}else {const err16 = {instancePath:instancePath+"/" + i0,schemaPath:"#/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}validate22.errors = vErrors;return errors === 0;}validate22.evaluated = {"items":true,"dynamicProps":false,"dynamicItems":false};const schema55 = {"type":"object","additionalProperties":false,"required":["from","to"],"properties":{"from":{"$ref":"common.schema.json#/$defs/id"},"to":{"$ref":"common.schema.json#/$defs/id"}}};function validate24(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){let vErrors = null;let errors = 0;const evaluated0 = validate24.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.from === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "from"},message:"must have required property '"+"from"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.to === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "to"},message:"must have required property '"+"to"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}for(const key0 in data){if(!((key0 === "from") || (key0 === "to"))){const err2 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}if(data.from !== undefined){let data0 = data.from;if(typeof data0 === "string"){if(!pattern16.test(data0)){const err3 = {instancePath:instancePath+"/from",schemaPath:"common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$"},message:"must match pattern \""+"^[a-zA-Z][a-zA-Z0-9_-]*$"+"\""};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}else {const err4 = {instancePath:instancePath+"/from",schemaPath:"common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}if(data.to !== undefined){let data1 = data.to;if(typeof data1 === "string"){if(!pattern16.test(data1)){const err5 = {instancePath:instancePath+"/to",schemaPath:"common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$"},message:"must match pattern \""+"^[a-zA-Z][a-zA-Z0-9_-]*$"+"\""};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}else {const err6 = {instancePath:instancePath+"/to",schemaPath:"common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}else {const err7 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}validate24.errors = vErrors;return errors === 0;}validate24.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false};function validate20(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://github.com/tt-a1i/archify/schemas/workflow.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate20.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.schema_version === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "schema_version"},message:"must have required property '"+"schema_version"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.diagram_type === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "diagram_type"},message:"must have required property '"+"diagram_type"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}if(data.meta === undefined){const err2 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "meta"},message:"must have required property '"+"meta"+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}if(data.lanes === undefined){const err3 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "lanes"},message:"must have required property '"+"lanes"+"'"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}if(data.nodes === undefined){const err4 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "nodes"},message:"must have required property '"+"nodes"+"'"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}if(data.edges === undefined){const err5 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "edges"},message:"must have required property '"+"edges"+"'"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}for(const key0 in data){if(!(func1.call(schema31.properties, key0))){const err6 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.schema_version !== undefined){let data0 = data.schema_version;if(!((data0 === 1) || (data0 === 2))){const err7 = {instancePath:instancePath+"/schema_version",schemaPath:"#/properties/schema_version/enum",keyword:"enum",params:{allowedValues: schema31.properties.schema_version.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}if(data.diagram_type !== undefined){if("workflow" !== data.diagram_type){const err8 = {instancePath:instancePath+"/diagram_type",schemaPath:"#/properties/diagram_type/const",keyword:"const",params:{allowedValue: "workflow"},message:"must be equal to constant"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}if(data.meta !== undefined){let data2 = data.meta;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.title === undefined){const err9 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "title"},message:"must have required property '"+"title"+"'"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}if(data2.output === undefined){const err10 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "output"},message:"must have required property '"+"output"+"'"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}for(const key1 in data2){if(!(func1.call(schema31.properties.meta.properties, key1))){const err11 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}if(data2.title !== undefined){let data3 = data2.title;if(typeof data3 === "string"){if(func3(data3) < 1){const err12 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}else {const err13 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}if(data2.locale !== undefined){let data4 = data2.locale;if(typeof data4 === "string"){if(func3(data4) > 35){const err14 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/maxLength",keyword:"maxLength",params:{limit: 35},message:"must NOT have more than 35 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}if(!pattern4.test(data4)){const err15 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"+"\""};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}else {const err16 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}if(data2.translations !== undefined){let data5 = data2.translations;if(data5 && typeof data5 == "object" && !Array.isArray(data5)){if(Object.keys(data5).length > 600){const err17 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/maxProperties",keyword:"maxProperties",params:{limit: 600},message:"must NOT have more than 600 properties"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}for(const key2 in data5){const _errs15 = errors;if(typeof key2 === "string"){if(!pattern5.test(key2)){const err18 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"},message:"must match pattern \""+"^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"+"\"",propertyName:key2};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var valid4 = _errs15 === errors;if(!valid4){const err19 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}for(const key3 in data5){let data6 = data5[key3];if(typeof data6 === "string"){if(func3(data6) > 400){const err20 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}if(func3(data6) < 1){const err21 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}else {const err22 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}}else {const err23 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}if(data2.subtitle !== undefined){if(typeof data2.subtitle !== "string"){const err24 = {instancePath:instancePath+"/meta/subtitle",schemaPath:"#/properties/meta/properties/subtitle/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}if(data2.output !== undefined){let data8 = data2.output;if(typeof data8 === "string"){if(!pattern6.test(data8)){const err25 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/0/pattern",keyword:"pattern",params:{pattern: "(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"},message:"must match pattern \""+"(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"+"\""};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}const _errs26 = errors;const _errs27 = errors;if(typeof data8 === "string"){if(!pattern7.test(data8)){const err26 = {};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}var valid8 = _errs27 === errors;if(valid8){const err27 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/1/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}else {errors = _errs26;if(vErrors !== null){if(_errs26){vErrors.length = _errs26;}else {vErrors = null;}}}const _errs29 = errors;const _errs30 = errors;if(typeof data8 === "string"){if(!pattern8.test(data8)){const err28 = {};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}var valid9 = _errs30 === errors;if(valid9){const err29 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/2/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}const _errs32 = errors;const _errs33 = errors;if(typeof data8 === "string"){if(!pattern9.test(data8)){const err30 = {};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}var valid10 = _errs33 === errors;if(valid10){const err31 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/3/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {errors = _errs32;if(vErrors !== null){if(_errs32){vErrors.length = _errs32;}else {vErrors = null;}}}const _errs35 = errors;const _errs36 = errors;if(typeof data8 === "string"){if(!pattern10.test(data8)){const err32 = {};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}var valid11 = _errs36 === errors;if(valid11){const err33 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/4/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}else {errors = _errs35;if(vErrors !== null){if(_errs35){vErrors.length = _errs35;}else {vErrors = null;}}}const _errs38 = errors;const _errs39 = errors;if(typeof data8 === "string"){if(!pattern11.test(data8)){const err34 = {};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}}var valid12 = _errs39 === errors;if(valid12){const err35 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/5/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}else {errors = _errs38;if(vErrors !== null){if(_errs38){vErrors.length = _errs38;}else {vErrors = null;}}}const _errs41 = errors;const _errs42 = errors;if(typeof data8 === "string"){if(!pattern12.test(data8)){const err36 = {};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}}var valid13 = _errs42 === errors;if(valid13){const err37 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/6/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {errors = _errs41;if(vErrors !== null){if(_errs41){vErrors.length = _errs41;}else {vErrors = null;}}}const _errs44 = errors;const _errs45 = errors;if(typeof data8 === "string"){if(!pattern13.test(data8)){const err38 = {};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}var valid14 = _errs45 === errors;if(valid14){const err39 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/7/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}else {errors = _errs44;if(vErrors !== null){if(_errs44){vErrors.length = _errs44;}else {vErrors = null;}}}if(typeof data8 === "string"){if(func3(data8) < 1){const err40 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}if(!pattern14.test(data8)){const err41 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/pattern",keyword:"pattern",params:{pattern: "^[^/]+(?:/[^/]+)*$"},message:"must match pattern \""+"^[^/]+(?:/[^/]+)*$"+"\""};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}else {const err42 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;}}if(data2.animation !== undefined){let data9 = data2.animation;if(!((data9 === "trace") || (data9 === "none"))){const err43 = {instancePath:instancePath+"/meta/animation",schemaPath:"#/properties/meta/properties/animation/enum",keyword:"enum",params:{allowedValues: schema31.properties.meta.properties.animation.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}}if(data2.visual_preset !== undefined){let data10 = data2.visual_preset;if(!((((data10 === "classic") || (data10 === "signal-flow")) || (data10 === "blueprint")) || (data10 === "editorial"))){const err44 = {instancePath:instancePath+"/meta/visual_preset",schemaPath:"#/properties/meta/properties/visual_preset/enum",keyword:"enum",params:{allowedValues: schema31.properties.meta.properties.visual_preset.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}}if(data2.quality_profile !== undefined){let data11 = data2.quality_profile;if(!((data11 === "standard") || (data11 === "showcase"))){const err45 = {instancePath:instancePath+"/meta/quality_profile",schemaPath:"#/properties/meta/properties/quality_profile/enum",keyword:"enum",params:{allowedValues: schema31.properties.meta.properties.quality_profile.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}}if(data2.repository !== undefined){let data12 = data2.repository;if(data12 && typeof data12 == "object" && !Array.isArray(data12)){if(data12.url === undefined){const err46 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}if(data12.revision === undefined){const err47 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "revision"},message:"must have required property '"+"revision"+"'"};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}for(const key4 in data12){if(!((((key4 === "url") || (key4 === "provider")) || (key4 === "link_mode")) || (key4 === "revision"))){const err48 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err48];}else {vErrors.push(err48);}errors++;}}if(data12.url !== undefined){let data13 = data12.url;if(typeof data13 === "string"){if(func3(data13) < 1){const err49 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err49];}else {vErrors.push(err49);}errors++;}}else {const err50 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err50];}else {vErrors.push(err50);}errors++;}}if(data12.provider !== undefined){let data14 = data12.provider;if(!((data14 === "github") || (data14 === "gitee"))){const err51 = {instancePath:instancePath+"/meta/repository/provider",schemaPath:"common.schema.json#/$defs/repository/properties/provider/enum",keyword:"enum",params:{allowedValues: schema36.properties.provider.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err51];}else {vErrors.push(err51);}errors++;}}if(data12.link_mode !== undefined){let data15 = data12.link_mode;if(!((data15 === "web") || (data15 === "local-only"))){const err52 = {instancePath:instancePath+"/meta/repository/link_mode",schemaPath:"common.schema.json#/$defs/repository/properties/link_mode/enum",keyword:"enum",params:{allowedValues: schema36.properties.link_mode.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err52];}else {vErrors.push(err52);}errors++;}}if(data12.revision !== undefined){let data16 = data12.revision;if(typeof data16 === "string"){if(!pattern15.test(data16)){const err53 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/pattern",keyword:"pattern",params:{pattern: "^[a-fA-F0-9]{40}$"},message:"must match pattern \""+"^[a-fA-F0-9]{40}$"+"\""};if(vErrors === null){vErrors = [err53];}else {vErrors.push(err53);}errors++;}}else {const err54 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err54];}else {vErrors.push(err54);}errors++;}}}else {const err55 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err55];}else {vErrors.push(err55);}errors++;}}if(data2.views !== undefined){if(!(validate22(data2.views, {instancePath:instancePath+"/meta/views",parentData:data2,parentDataProperty:"views",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);errors = vErrors.length;}}if(data2.legend !== undefined){let data18 = data2.legend;if(data18 && typeof data18 == "object" && !Array.isArray(data18)){for(const key5 in data18){if(!((key5 === "mode") || (key5 === "entries"))){const err56 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err56];}else {vErrors.push(err56);}errors++;}}if(data18.mode !== undefined){let data19 = data18.mode;if(!(((data19 === "auto") || (data19 === "all")) || (data19 === "hidden"))){const err57 = {instancePath:instancePath+"/meta/legend/mode",schemaPath:"common.schema.json#/$defs/legendMode/enum",keyword:"enum",params:{allowedValues: schema40.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err57];}else {vErrors.push(err57);}errors++;}}if(data18.entries !== undefined){let data20 = data18.entries;if(data20 && typeof data20 == "object" && !Array.isArray(data20)){for(const key6 in data20){if(!(((((((key6 === "frontend") || (key6 === "backend")) || (key6 === "database")) || (key6 === "cloud")) || (key6 === "security")) || (key6 === "messagebus")) || (key6 === "external"))){const err58 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err58];}else {vErrors.push(err58);}errors++;}}if(data20.frontend !== undefined){let data21 = data20.frontend;if(data21 && typeof data21 == "object" && !Array.isArray(data21)){if(Object.keys(data21).length < 1){const err59 = {instancePath:instancePath+"/meta/legend/entries/frontend",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err59];}else {vErrors.push(err59);}errors++;}for(const key7 in data21){if(!((key7 === "label") || (key7 === "visible"))){const err60 = {instancePath:instancePath+"/meta/legend/entries/frontend",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err60];}else {vErrors.push(err60);}errors++;}}if(data21.label !== undefined){let data22 = data21.label;if(typeof data22 === "string"){if(func3(data22) > 80){const err61 = {instancePath:instancePath+"/meta/legend/entries/frontend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err61];}else {vErrors.push(err61);}errors++;}if(func3(data22) < 1){const err62 = {instancePath:instancePath+"/meta/legend/entries/frontend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err62];}else {vErrors.push(err62);}errors++;}}else {const err63 = {instancePath:instancePath+"/meta/legend/entries/frontend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err63];}else {vErrors.push(err63);}errors++;}}if(data21.visible !== undefined){if(typeof data21.visible !== "boolean"){const err64 = {instancePath:instancePath+"/meta/legend/entries/frontend/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err64];}else {vErrors.push(err64);}errors++;}}}else {const err65 = {instancePath:instancePath+"/meta/legend/entries/frontend",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err65];}else {vErrors.push(err65);}errors++;}}if(data20.backend !== undefined){let data24 = data20.backend;if(data24 && typeof data24 == "object" && !Array.isArray(data24)){if(Object.keys(data24).length < 1){const err66 = {instancePath:instancePath+"/meta/legend/entries/backend",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err66];}else {vErrors.push(err66);}errors++;}for(const key8 in data24){if(!((key8 === "label") || (key8 === "visible"))){const err67 = {instancePath:instancePath+"/meta/legend/entries/backend",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err67];}else {vErrors.push(err67);}errors++;}}if(data24.label !== undefined){let data25 = data24.label;if(typeof data25 === "string"){if(func3(data25) > 80){const err68 = {instancePath:instancePath+"/meta/legend/entries/backend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err68];}else {vErrors.push(err68);}errors++;}if(func3(data25) < 1){const err69 = {instancePath:instancePath+"/meta/legend/entries/backend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err69];}else {vErrors.push(err69);}errors++;}}else {const err70 = {instancePath:instancePath+"/meta/legend/entries/backend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err70];}else {vErrors.push(err70);}errors++;}}if(data24.visible !== undefined){if(typeof data24.visible !== "boolean"){const err71 = {instancePath:instancePath+"/meta/legend/entries/backend/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err71];}else {vErrors.push(err71);}errors++;}}}else {const err72 = {instancePath:instancePath+"/meta/legend/entries/backend",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err72];}else {vErrors.push(err72);}errors++;}}if(data20.database !== undefined){let data27 = data20.database;if(data27 && typeof data27 == "object" && !Array.isArray(data27)){if(Object.keys(data27).length < 1){const err73 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err73];}else {vErrors.push(err73);}errors++;}for(const key9 in data27){if(!((key9 === "label") || (key9 === "visible"))){const err74 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key9},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err74];}else {vErrors.push(err74);}errors++;}}if(data27.label !== undefined){let data28 = data27.label;if(typeof data28 === "string"){if(func3(data28) > 80){const err75 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err75];}else {vErrors.push(err75);}errors++;}if(func3(data28) < 1){const err76 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err76];}else {vErrors.push(err76);}errors++;}}else {const err77 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err77];}else {vErrors.push(err77);}errors++;}}if(data27.visible !== undefined){if(typeof data27.visible !== "boolean"){const err78 = {instancePath:instancePath+"/meta/legend/entries/database/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err78];}else {vErrors.push(err78);}errors++;}}}else {const err79 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err79];}else {vErrors.push(err79);}errors++;}}if(data20.cloud !== undefined){let data30 = data20.cloud;if(data30 && typeof data30 == "object" && !Array.isArray(data30)){if(Object.keys(data30).length < 1){const err80 = {instancePath:instancePath+"/meta/legend/entries/cloud",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err80];}else {vErrors.push(err80);}errors++;}for(const key10 in data30){if(!((key10 === "label") || (key10 === "visible"))){const err81 = {instancePath:instancePath+"/meta/legend/entries/cloud",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key10},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err81];}else {vErrors.push(err81);}errors++;}}if(data30.label !== undefined){let data31 = data30.label;if(typeof data31 === "string"){if(func3(data31) > 80){const err82 = {instancePath:instancePath+"/meta/legend/entries/cloud/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err82];}else {vErrors.push(err82);}errors++;}if(func3(data31) < 1){const err83 = {instancePath:instancePath+"/meta/legend/entries/cloud/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err83];}else {vErrors.push(err83);}errors++;}}else {const err84 = {instancePath:instancePath+"/meta/legend/entries/cloud/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err84];}else {vErrors.push(err84);}errors++;}}if(data30.visible !== undefined){if(typeof data30.visible !== "boolean"){const err85 = {instancePath:instancePath+"/meta/legend/entries/cloud/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err85];}else {vErrors.push(err85);}errors++;}}}else {const err86 = {instancePath:instancePath+"/meta/legend/entries/cloud",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err86];}else {vErrors.push(err86);}errors++;}}if(data20.security !== undefined){let data33 = data20.security;if(data33 && typeof data33 == "object" && !Array.isArray(data33)){if(Object.keys(data33).length < 1){const err87 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err87];}else {vErrors.push(err87);}errors++;}for(const key11 in data33){if(!((key11 === "label") || (key11 === "visible"))){const err88 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key11},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err88];}else {vErrors.push(err88);}errors++;}}if(data33.label !== undefined){let data34 = data33.label;if(typeof data34 === "string"){if(func3(data34) > 80){const err89 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err89];}else {vErrors.push(err89);}errors++;}if(func3(data34) < 1){const err90 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err90];}else {vErrors.push(err90);}errors++;}}else {const err91 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err91];}else {vErrors.push(err91);}errors++;}}if(data33.visible !== undefined){if(typeof data33.visible !== "boolean"){const err92 = {instancePath:instancePath+"/meta/legend/entries/security/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err92];}else {vErrors.push(err92);}errors++;}}}else {const err93 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err93];}else {vErrors.push(err93);}errors++;}}if(data20.messagebus !== undefined){let data36 = data20.messagebus;if(data36 && typeof data36 == "object" && !Array.isArray(data36)){if(Object.keys(data36).length < 1){const err94 = {instancePath:instancePath+"/meta/legend/entries/messagebus",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err94];}else {vErrors.push(err94);}errors++;}for(const key12 in data36){if(!((key12 === "label") || (key12 === "visible"))){const err95 = {instancePath:instancePath+"/meta/legend/entries/messagebus",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key12},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err95];}else {vErrors.push(err95);}errors++;}}if(data36.label !== undefined){let data37 = data36.label;if(typeof data37 === "string"){if(func3(data37) > 80){const err96 = {instancePath:instancePath+"/meta/legend/entries/messagebus/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err96];}else {vErrors.push(err96);}errors++;}if(func3(data37) < 1){const err97 = {instancePath:instancePath+"/meta/legend/entries/messagebus/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err97];}else {vErrors.push(err97);}errors++;}}else {const err98 = {instancePath:instancePath+"/meta/legend/entries/messagebus/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err98];}else {vErrors.push(err98);}errors++;}}if(data36.visible !== undefined){if(typeof data36.visible !== "boolean"){const err99 = {instancePath:instancePath+"/meta/legend/entries/messagebus/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err99];}else {vErrors.push(err99);}errors++;}}}else {const err100 = {instancePath:instancePath+"/meta/legend/entries/messagebus",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err100];}else {vErrors.push(err100);}errors++;}}if(data20.external !== undefined){let data39 = data20.external;if(data39 && typeof data39 == "object" && !Array.isArray(data39)){if(Object.keys(data39).length < 1){const err101 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err101];}else {vErrors.push(err101);}errors++;}for(const key13 in data39){if(!((key13 === "label") || (key13 === "visible"))){const err102 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key13},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err102];}else {vErrors.push(err102);}errors++;}}if(data39.label !== undefined){let data40 = data39.label;if(typeof data40 === "string"){if(func3(data40) > 80){const err103 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err103];}else {vErrors.push(err103);}errors++;}if(func3(data40) < 1){const err104 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err104];}else {vErrors.push(err104);}errors++;}}else {const err105 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err105];}else {vErrors.push(err105);}errors++;}}if(data39.visible !== undefined){if(typeof data39.visible !== "boolean"){const err106 = {instancePath:instancePath+"/meta/legend/entries/external/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err106];}else {vErrors.push(err106);}errors++;}}}else {const err107 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err107];}else {vErrors.push(err107);}errors++;}}}else {const err108 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err108];}else {vErrors.push(err108);}errors++;}}}else {const err109 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err109];}else {vErrors.push(err109);}errors++;}}if(data2.viewBox !== undefined){let data42 = data2.viewBox;if(Array.isArray(data42)){if(data42.length > 2){const err110 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err110];}else {vErrors.push(err110);}errors++;}if(data42.length < 2){const err111 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err111];}else {vErrors.push(err111);}errors++;}const len0 = data42.length;if(len0 > 0){let data43 = data42[0];if((typeof data43 == "number") && (isFinite(data43))){if(data43 < 700 || isNaN(data43)){const err112 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 700},message:"must be >= 700"};if(vErrors === null){vErrors = [err112];}else {vErrors.push(err112);}errors++;}}else {const err113 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err113];}else {vErrors.push(err113);}errors++;}}if(len0 > 1){let data44 = data42[1];if((typeof data44 == "number") && (isFinite(data44))){if(data44 < 240 || isNaN(data44)){const err114 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/minimum",keyword:"minimum",params:{comparison: ">=", limit: 240},message:"must be >= 240"};if(vErrors === null){vErrors = [err114];}else {vErrors.push(err114);}errors++;}}else {const err115 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err115];}else {vErrors.push(err115);}errors++;}}const len1 = data42.length;if(!(len1 <= 2)){const err116 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err116];}else {vErrors.push(err116);}errors++;}}else {const err117 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err117];}else {vErrors.push(err117);}errors++;}}}else {const err118 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err118];}else {vErrors.push(err118);}errors++;}}if(data.lanes !== undefined){let data45 = data.lanes;if(Array.isArray(data45)){if(data45.length < 1){const err119 = {instancePath:instancePath+"/lanes",schemaPath:"#/properties/lanes/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err119];}else {vErrors.push(err119);}errors++;}const len2 = data45.length;for(let i0=0; i0 5 || isNaN(data54)){const err140 = {instancePath:instancePath+"/phases/" + i1+"/fromCol",schemaPath:"#/properties/phases/items/properties/fromCol/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5},message:"must be <= 5"};if(vErrors === null){vErrors = [err140];}else {vErrors.push(err140);}errors++;}if(data54 < 0 || isNaN(data54)){const err141 = {instancePath:instancePath+"/phases/" + i1+"/fromCol",schemaPath:"#/properties/phases/items/properties/fromCol/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err141];}else {vErrors.push(err141);}errors++;}}}if(data51.toCol !== undefined){let data55 = data51.toCol;if(!(((typeof data55 == "number") && (!(data55 % 1) && !isNaN(data55))) && (isFinite(data55)))){const err142 = {instancePath:instancePath+"/phases/" + i1+"/toCol",schemaPath:"#/properties/phases/items/properties/toCol/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err142];}else {vErrors.push(err142);}errors++;}if((typeof data55 == "number") && (isFinite(data55))){if(data55 > 5 || isNaN(data55)){const err143 = {instancePath:instancePath+"/phases/" + i1+"/toCol",schemaPath:"#/properties/phases/items/properties/toCol/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5},message:"must be <= 5"};if(vErrors === null){vErrors = [err143];}else {vErrors.push(err143);}errors++;}if(data55 < 0 || isNaN(data55)){const err144 = {instancePath:instancePath+"/phases/" + i1+"/toCol",schemaPath:"#/properties/phases/items/properties/toCol/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err144];}else {vErrors.push(err144);}errors++;}}}if(data51.variant !== undefined){let data56 = data51.variant;if(!((((data56 === "default") || (data56 === "emphasis")) || (data56 === "security")) || (data56 === "dashed"))){const err145 = {instancePath:instancePath+"/phases/" + i1+"/variant",schemaPath:"#/properties/phases/items/properties/variant/enum",keyword:"enum",params:{allowedValues: schema31.properties.phases.items.properties.variant.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err145];}else {vErrors.push(err145);}errors++;}}}else {const err146 = {instancePath:instancePath+"/phases/" + i1,schemaPath:"#/properties/phases/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err146];}else {vErrors.push(err146);}errors++;}}}else {const err147 = {instancePath:instancePath+"/phases",schemaPath:"#/properties/phases/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err147];}else {vErrors.push(err147);}errors++;}}if(data.groups !== undefined){let data57 = data.groups;if(Array.isArray(data57)){const len4 = data57.length;for(let i2=0; i2 5 || isNaN(data62)){const err161 = {instancePath:instancePath+"/groups/" + i2+"/fromCol",schemaPath:"#/properties/groups/items/properties/fromCol/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5},message:"must be <= 5"};if(vErrors === null){vErrors = [err161];}else {vErrors.push(err161);}errors++;}if(data62 < 0 || isNaN(data62)){const err162 = {instancePath:instancePath+"/groups/" + i2+"/fromCol",schemaPath:"#/properties/groups/items/properties/fromCol/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err162];}else {vErrors.push(err162);}errors++;}}}if(data58.toCol !== undefined){let data63 = data58.toCol;if(!(((typeof data63 == "number") && (!(data63 % 1) && !isNaN(data63))) && (isFinite(data63)))){const err163 = {instancePath:instancePath+"/groups/" + i2+"/toCol",schemaPath:"#/properties/groups/items/properties/toCol/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err163];}else {vErrors.push(err163);}errors++;}if((typeof data63 == "number") && (isFinite(data63))){if(data63 > 5 || isNaN(data63)){const err164 = {instancePath:instancePath+"/groups/" + i2+"/toCol",schemaPath:"#/properties/groups/items/properties/toCol/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5},message:"must be <= 5"};if(vErrors === null){vErrors = [err164];}else {vErrors.push(err164);}errors++;}if(data63 < 0 || isNaN(data63)){const err165 = {instancePath:instancePath+"/groups/" + i2+"/toCol",schemaPath:"#/properties/groups/items/properties/toCol/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err165];}else {vErrors.push(err165);}errors++;}}}if(data58.variant !== undefined){let data64 = data58.variant;if(!((((data64 === "default") || (data64 === "emphasis")) || (data64 === "security")) || (data64 === "dashed"))){const err166 = {instancePath:instancePath+"/groups/" + i2+"/variant",schemaPath:"#/properties/groups/items/properties/variant/enum",keyword:"enum",params:{allowedValues: schema31.properties.groups.items.properties.variant.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err166];}else {vErrors.push(err166);}errors++;}}}else {const err167 = {instancePath:instancePath+"/groups/" + i2,schemaPath:"#/properties/groups/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err167];}else {vErrors.push(err167);}errors++;}}}else {const err168 = {instancePath:instancePath+"/groups",schemaPath:"#/properties/groups/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err168];}else {vErrors.push(err168);}errors++;}}if(data.mainPath !== undefined){let data65 = data.mainPath;if(Array.isArray(data65)){if(data65.length < 2){const err169 = {instancePath:instancePath+"/mainPath",schemaPath:"#/properties/mainPath/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err169];}else {vErrors.push(err169);}errors++;}const len5 = data65.length;for(let i3=0; i3 5 || isNaN(data80)){const err196 = {instancePath:instancePath+"/nodes/" + i8+"/col",schemaPath:"#/properties/nodes/items/properties/col/maximum",keyword:"maximum",params:{comparison: "<=", limit: 5},message:"must be <= 5"};if(vErrors === null){vErrors = [err196];}else {vErrors.push(err196);}errors++;}if(data80 < 0 || isNaN(data80)){const err197 = {instancePath:instancePath+"/nodes/" + i8+"/col",schemaPath:"#/properties/nodes/items/properties/col/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err197];}else {vErrors.push(err197);}errors++;}}}if(data77.type !== undefined){let data81 = data77.type;if(!(((((((data81 === "frontend") || (data81 === "backend")) || (data81 === "database")) || (data81 === "cloud")) || (data81 === "security")) || (data81 === "messagebus")) || (data81 === "external"))){const err198 = {instancePath:instancePath+"/nodes/" + i8+"/type",schemaPath:"common.schema.json#/$defs/componentType/enum",keyword:"enum",params:{allowedValues: schema60.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err198];}else {vErrors.push(err198);}errors++;}}if(data77.label !== undefined){let data82 = data77.label;if(typeof data82 === "string"){if(func3(data82) < 1){const err199 = {instancePath:instancePath+"/nodes/" + i8+"/label",schemaPath:"#/properties/nodes/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err199];}else {vErrors.push(err199);}errors++;}}else {const err200 = {instancePath:instancePath+"/nodes/" + i8+"/label",schemaPath:"#/properties/nodes/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err200];}else {vErrors.push(err200);}errors++;}}if(data77.sublabel !== undefined){if(typeof data77.sublabel !== "string"){const err201 = {instancePath:instancePath+"/nodes/" + i8+"/sublabel",schemaPath:"#/properties/nodes/items/properties/sublabel/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err201];}else {vErrors.push(err201);}errors++;}}if(data77.tag !== undefined){if(typeof data77.tag !== "string"){const err202 = {instancePath:instancePath+"/nodes/" + i8+"/tag",schemaPath:"#/properties/nodes/items/properties/tag/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err202];}else {vErrors.push(err202);}errors++;}}if(data77.icon !== undefined){let data85 = data77.icon;if(!((((((((((((((((((((data85 === "calendar") || (data85 === "clock")) || (data85 === "person")) || (data85 === "briefcase")) || (data85 === "flag")) || (data85 === "moon")) || (data85 === "frontend")) || (data85 === "backend")) || (data85 === "database")) || (data85 === "cloud")) || (data85 === "security")) || (data85 === "messagebus")) || (data85 === "external")) || (data85 === "start")) || (data85 === "active")) || (data85 === "waiting")) || (data85 === "success")) || (data85 === "failure")) || (data85 === "neutral")) || (data85 === "none"))){const err203 = {instancePath:instancePath+"/nodes/" + i8+"/icon",schemaPath:"common.schema.json#/$defs/nodeIcon/enum",keyword:"enum",params:{allowedValues: schema61.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err203];}else {vErrors.push(err203);}errors++;}}if(data77.brand !== undefined){let data86 = data77.brand;const _errs223 = errors;let valid70 = false;let passing0 = null;const _errs224 = errors;const _errs226 = errors;let valid71 = false;const _errs227 = errors;if(typeof data86 === "string"){if(func3(data86) > 80){const err204 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err204];}else {vErrors.push(err204);}errors++;}if(!pattern29.test(data86)){const err205 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/pattern",keyword:"pattern",params:{pattern: "^[^\\r\\n]+$"},message:"must match pattern \""+"^[^\\r\\n]+$"+"\""};if(vErrors === null){vErrors = [err205];}else {vErrors.push(err205);}errors++;}}var _valid1 = _errs227 === errors;valid71 = valid71 || _valid1;const _errs228 = errors;if(typeof data86 === "string"){if(!pattern30.test(data86)){const err206 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/1/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err206];}else {vErrors.push(err206);}errors++;}}var _valid1 = _errs228 === errors;valid71 = valid71 || _valid1;if(!valid71){const err207 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err207];}else {vErrors.push(err207);}errors++;}else {errors = _errs226;if(vErrors !== null){if(_errs226){vErrors.length = _errs226;}else {vErrors = null;}}}if(typeof data86 === "string"){if(func3(data86) > 2048){const err208 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err208];}else {vErrors.push(err208);}errors++;}if(func3(data86) < 1){const err209 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err209];}else {vErrors.push(err209);}errors++;}}else {const err210 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err210];}else {vErrors.push(err210);}errors++;}var _valid0 = _errs224 === errors;if(_valid0){valid70 = true;passing0 = 0;}const _errs229 = errors;if(data86 && typeof data86 == "object" && !Array.isArray(data86)){if(data86.url === undefined){const err211 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err211];}else {vErrors.push(err211);}errors++;}if(data86.sha256 === undefined){const err212 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "sha256"},message:"must have required property '"+"sha256"+"'"};if(vErrors === null){vErrors = [err212];}else {vErrors.push(err212);}errors++;}for(const key19 in data86){if(!((key19 === "url") || (key19 === "sha256"))){const err213 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key19},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err213];}else {vErrors.push(err213);}errors++;}}if(data86.url !== undefined){let data87 = data86.url;if(typeof data87 === "string"){if(func3(data87) > 2048){const err214 = {instancePath:instancePath+"/nodes/" + i8+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err214];}else {vErrors.push(err214);}errors++;}if(func3(data87) < 8){const err215 = {instancePath:instancePath+"/nodes/" + i8+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"};if(vErrors === null){vErrors = [err215];}else {vErrors.push(err215);}errors++;}if(!pattern30.test(data87)){const err216 = {instancePath:instancePath+"/nodes/" + i8+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err216];}else {vErrors.push(err216);}errors++;}}else {const err217 = {instancePath:instancePath+"/nodes/" + i8+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err217];}else {vErrors.push(err217);}errors++;}}if(data86.sha256 !== undefined){let data88 = data86.sha256;if(typeof data88 === "string"){if(!pattern32.test(data88)){const err218 = {instancePath:instancePath+"/nodes/" + i8+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/pattern",keyword:"pattern",params:{pattern: "^[a-f0-9]{64}$"},message:"must match pattern \""+"^[a-f0-9]{64}$"+"\""};if(vErrors === null){vErrors = [err218];}else {vErrors.push(err218);}errors++;}}else {const err219 = {instancePath:instancePath+"/nodes/" + i8+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err219];}else {vErrors.push(err219);}errors++;}}}else {const err220 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err220];}else {vErrors.push(err220);}errors++;}var _valid0 = _errs229 === errors;if(_valid0 && valid70){valid70 = false;passing0 = [passing0, 1];}else {if(_valid0){valid70 = true;passing0 = 1;}}if(!valid70){const err221 = {instancePath:instancePath+"/nodes/" + i8+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err221];}else {vErrors.push(err221);}errors++;}else {errors = _errs223;if(vErrors !== null){if(_errs223){vErrors.length = _errs223;}else {vErrors = null;}}}}if(data77.sources !== undefined){let data89 = data77.sources;if(Array.isArray(data89)){if(data89.length > 3){const err222 = {instancePath:instancePath+"/nodes/" + i8+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err222];}else {vErrors.push(err222);}errors++;}if(data89.length < 1){const err223 = {instancePath:instancePath+"/nodes/" + i8+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err223];}else {vErrors.push(err223);}errors++;}const len11 = data89.length;for(let i9=0; i9 240){const err226 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"};if(vErrors === null){vErrors = [err226];}else {vErrors.push(err226);}errors++;}if(func3(data91) < 1){const err227 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err227];}else {vErrors.push(err227);}errors++;}}else {const err228 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err228];}else {vErrors.push(err228);}errors++;}}if(data90.line !== undefined){let data92 = data90.line;if(!(((typeof data92 == "number") && (!(data92 % 1) && !isNaN(data92))) && (isFinite(data92)))){const err229 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err229];}else {vErrors.push(err229);}errors++;}if((typeof data92 == "number") && (isFinite(data92))){if(data92 < 1 || isNaN(data92)){const err230 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err230];}else {vErrors.push(err230);}errors++;}}}if(data90.end_line !== undefined){let data93 = data90.end_line;if(!(((typeof data93 == "number") && (!(data93 % 1) && !isNaN(data93))) && (isFinite(data93)))){const err231 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err231];}else {vErrors.push(err231);}errors++;}if((typeof data93 == "number") && (isFinite(data93))){if(data93 < 1 || isNaN(data93)){const err232 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err232];}else {vErrors.push(err232);}errors++;}}}if(data90.label !== undefined){let data94 = data90.label;if(typeof data94 === "string"){if(func3(data94) > 48){const err233 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err233];}else {vErrors.push(err233);}errors++;}if(func3(data94) < 1){const err234 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err234];}else {vErrors.push(err234);}errors++;}}else {const err235 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err235];}else {vErrors.push(err235);}errors++;}}}else {const err236 = {instancePath:instancePath+"/nodes/" + i8+"/sources/" + i9,schemaPath:"common.schema.json#/$defs/sourceReferences/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err236];}else {vErrors.push(err236);}errors++;}}}else {const err237 = {instancePath:instancePath+"/nodes/" + i8+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err237];}else {vErrors.push(err237);}errors++;}}if(data77.width !== undefined){let data95 = data77.width;if((typeof data95 == "number") && (isFinite(data95))){if(data95 < 32 || isNaN(data95)){const err238 = {instancePath:instancePath+"/nodes/" + i8+"/width",schemaPath:"#/properties/nodes/items/properties/width/minimum",keyword:"minimum",params:{comparison: ">=", limit: 32},message:"must be >= 32"};if(vErrors === null){vErrors = [err238];}else {vErrors.push(err238);}errors++;}}else {const err239 = {instancePath:instancePath+"/nodes/" + i8+"/width",schemaPath:"#/properties/nodes/items/properties/width/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err239];}else {vErrors.push(err239);}errors++;}}if(data77.height !== undefined){let data96 = data77.height;if((typeof data96 == "number") && (isFinite(data96))){if(data96 < 32 || isNaN(data96)){const err240 = {instancePath:instancePath+"/nodes/" + i8+"/height",schemaPath:"#/properties/nodes/items/properties/height/minimum",keyword:"minimum",params:{comparison: ">=", limit: 32},message:"must be >= 32"};if(vErrors === null){vErrors = [err240];}else {vErrors.push(err240);}errors++;}}else {const err241 = {instancePath:instancePath+"/nodes/" + i8+"/height",schemaPath:"#/properties/nodes/items/properties/height/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err241];}else {vErrors.push(err241);}errors++;}}if(data77.yOffset !== undefined){let data97 = data77.yOffset;if(!((typeof data97 == "number") && (isFinite(data97)))){const err242 = {instancePath:instancePath+"/nodes/" + i8+"/yOffset",schemaPath:"#/properties/nodes/items/properties/yOffset/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err242];}else {vErrors.push(err242);}errors++;}}}else {const err243 = {instancePath:instancePath+"/nodes/" + i8,schemaPath:"#/properties/nodes/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err243];}else {vErrors.push(err243);}errors++;}}}else {const err244 = {instancePath:instancePath+"/nodes",schemaPath:"#/properties/nodes/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err244];}else {vErrors.push(err244);}errors++;}}if(data.edges !== undefined){let data98 = data.edges;if(Array.isArray(data98)){const len12 = data98.length;for(let i10=0; i10 2){const err260 = {instancePath:instancePath+"/edges/" + i10+"/via/" + i11,schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err260];}else {vErrors.push(err260);}errors++;}if(data110.length < 2){const err261 = {instancePath:instancePath+"/edges/" + i10+"/via/" + i11,schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err261];}else {vErrors.push(err261);}errors++;}const len14 = data110.length;if(len14 > 0){let data111 = data110[0];if(!((typeof data111 == "number") && (isFinite(data111)))){const err262 = {instancePath:instancePath+"/edges/" + i10+"/via/" + i11+"/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err262];}else {vErrors.push(err262);}errors++;}}if(len14 > 1){let data112 = data110[1];if(!((typeof data112 == "number") && (isFinite(data112)))){const err263 = {instancePath:instancePath+"/edges/" + i10+"/via/" + i11+"/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err263];}else {vErrors.push(err263);}errors++;}}const len15 = data110.length;if(!(len15 <= 2)){const err264 = {instancePath:instancePath+"/edges/" + i10+"/via/" + i11,schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err264];}else {vErrors.push(err264);}errors++;}}else {const err265 = {instancePath:instancePath+"/edges/" + i10+"/via/" + i11,schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err265];}else {vErrors.push(err265);}errors++;}}}else {const err266 = {instancePath:instancePath+"/edges/" + i10+"/via",schemaPath:"#/properties/edges/items/properties/via/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err266];}else {vErrors.push(err266);}errors++;}}if(data99.labelAt !== undefined){let data113 = data99.labelAt;if(Array.isArray(data113)){if(data113.length > 2){const err267 = {instancePath:instancePath+"/edges/" + i10+"/labelAt",schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err267];}else {vErrors.push(err267);}errors++;}if(data113.length < 2){const err268 = {instancePath:instancePath+"/edges/" + i10+"/labelAt",schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err268];}else {vErrors.push(err268);}errors++;}const len16 = data113.length;if(len16 > 0){let data114 = data113[0];if(!((typeof data114 == "number") && (isFinite(data114)))){const err269 = {instancePath:instancePath+"/edges/" + i10+"/labelAt/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err269];}else {vErrors.push(err269);}errors++;}}if(len16 > 1){let data115 = data113[1];if(!((typeof data115 == "number") && (isFinite(data115)))){const err270 = {instancePath:instancePath+"/edges/" + i10+"/labelAt/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err270];}else {vErrors.push(err270);}errors++;}}const len17 = data113.length;if(!(len17 <= 2)){const err271 = {instancePath:instancePath+"/edges/" + i10+"/labelAt",schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err271];}else {vErrors.push(err271);}errors++;}}else {const err272 = {instancePath:instancePath+"/edges/" + i10+"/labelAt",schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err272];}else {vErrors.push(err272);}errors++;}}if(data99.labelDx !== undefined){let data116 = data99.labelDx;if(!((typeof data116 == "number") && (isFinite(data116)))){const err273 = {instancePath:instancePath+"/edges/" + i10+"/labelDx",schemaPath:"#/properties/edges/items/properties/labelDx/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err273];}else {vErrors.push(err273);}errors++;}}if(data99.labelDy !== undefined){let data117 = data99.labelDy;if(!((typeof data117 == "number") && (isFinite(data117)))){const err274 = {instancePath:instancePath+"/edges/" + i10+"/labelDy",schemaPath:"#/properties/edges/items/properties/labelDy/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err274];}else {vErrors.push(err274);}errors++;}}if(data99.labelSegment !== undefined){let data118 = data99.labelSegment;if(!(((typeof data118 == "number") && (!(data118 % 1) && !isNaN(data118))) && (isFinite(data118)))){const err275 = {instancePath:instancePath+"/edges/" + i10+"/labelSegment",schemaPath:"#/properties/edges/items/properties/labelSegment/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err275];}else {vErrors.push(err275);}errors++;}if((typeof data118 == "number") && (isFinite(data118))){if(data118 < 0 || isNaN(data118)){const err276 = {instancePath:instancePath+"/edges/" + i10+"/labelSegment",schemaPath:"#/properties/edges/items/properties/labelSegment/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err276];}else {vErrors.push(err276);}errors++;}}}if(data99.channelX !== undefined){let data119 = data99.channelX;if(!((typeof data119 == "number") && (isFinite(data119)))){const err277 = {instancePath:instancePath+"/edges/" + i10+"/channelX",schemaPath:"#/properties/edges/items/properties/channelX/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err277];}else {vErrors.push(err277);}errors++;}}if(data99.channelY !== undefined){let data120 = data99.channelY;if(!((typeof data120 == "number") && (isFinite(data120)))){const err278 = {instancePath:instancePath+"/edges/" + i10+"/channelY",schemaPath:"#/properties/edges/items/properties/channelY/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err278];}else {vErrors.push(err278);}errors++;}}if(data99.bias !== undefined){let data121 = data99.bias;if((typeof data121 == "number") && (isFinite(data121))){if(data121 > 1 || isNaN(data121)){const err279 = {instancePath:instancePath+"/edges/" + i10+"/bias",schemaPath:"#/properties/edges/items/properties/bias/maximum",keyword:"maximum",params:{comparison: "<=", limit: 1},message:"must be <= 1"};if(vErrors === null){vErrors = [err279];}else {vErrors.push(err279);}errors++;}if(data121 < 0 || isNaN(data121)){const err280 = {instancePath:instancePath+"/edges/" + i10+"/bias",schemaPath:"#/properties/edges/items/properties/bias/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err280];}else {vErrors.push(err280);}errors++;}}else {const err281 = {instancePath:instancePath+"/edges/" + i10+"/bias",schemaPath:"#/properties/edges/items/properties/bias/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err281];}else {vErrors.push(err281);}errors++;}}if(data99.width !== undefined){let data122 = data99.width;if((typeof data122 == "number") && (isFinite(data122))){if(data122 < 0.5 || isNaN(data122)){const err282 = {instancePath:instancePath+"/edges/" + i10+"/width",schemaPath:"#/properties/edges/items/properties/width/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0.5},message:"must be >= 0.5"};if(vErrors === null){vErrors = [err282];}else {vErrors.push(err282);}errors++;}}else {const err283 = {instancePath:instancePath+"/edges/" + i10+"/width",schemaPath:"#/properties/edges/items/properties/width/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err283];}else {vErrors.push(err283);}errors++;}}}else {const err284 = {instancePath:instancePath+"/edges/" + i10,schemaPath:"#/properties/edges/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err284];}else {vErrors.push(err284);}errors++;}}}else {const err285 = {instancePath:instancePath+"/edges",schemaPath:"#/properties/edges/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err285];}else {vErrors.push(err285);}errors++;}}if(data.cards !== undefined){let data123 = data.cards;if(Array.isArray(data123)){const len18 = data123.length;for(let i12=0; i12 5){const err0 = {instancePath,schemaPath:"#/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}const len0 = data.length;for(let i0=0; i0 48){const err7 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}if(func3(data2) < 1){const err8 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}else {const err9 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}if(data0.focus !== undefined){let data3 = data0.focus;if(Array.isArray(data3)){if(data3.length < 1){const err10 = {instancePath:instancePath+"/" + i0+"/focus",schemaPath:"#/items/properties/focus/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}const len1 = data3.length;for(let i1=0; i1 140){const err14 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/maxLength",keyword:"maxLength",params:{limit: 140},message:"must NOT have more than 140 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}else {const err16 = {instancePath:instancePath+"/" + i0,schemaPath:"#/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}validate28.errors = vErrors;return errors === 0;}validate28.evaluated = {"items":true,"dynamicProps":false,"dynamicItems":false};function validate27(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://github.com/tt-a1i/archify/schemas/sequence.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate27.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.schema_version === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "schema_version"},message:"must have required property '"+"schema_version"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.diagram_type === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "diagram_type"},message:"must have required property '"+"diagram_type"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}if(data.meta === undefined){const err2 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "meta"},message:"must have required property '"+"meta"+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}if(data.participants === undefined){const err3 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "participants"},message:"must have required property '"+"participants"+"'"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}if(data.messages === undefined){const err4 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "messages"},message:"must have required property '"+"messages"+"'"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}for(const key0 in data){if(!((((((((key0 === "schema_version") || (key0 === "diagram_type")) || (key0 === "meta")) || (key0 === "participants")) || (key0 === "segments")) || (key0 === "messages")) || (key0 === "activations")) || (key0 === "cards"))){const err5 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}if(data.schema_version !== undefined){if(1 !== data.schema_version){const err6 = {instancePath:instancePath+"/schema_version",schemaPath:"#/properties/schema_version/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.diagram_type !== undefined){if("sequence" !== data.diagram_type){const err7 = {instancePath:instancePath+"/diagram_type",schemaPath:"#/properties/diagram_type/const",keyword:"const",params:{allowedValue: "sequence"},message:"must be equal to constant"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}if(data.meta !== undefined){let data2 = data.meta;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.title === undefined){const err8 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "title"},message:"must have required property '"+"title"+"'"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}if(data2.output === undefined){const err9 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "output"},message:"must have required property '"+"output"+"'"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}for(const key1 in data2){if(!(func1.call(schema73.properties.meta.properties, key1))){const err10 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}}if(data2.title !== undefined){let data3 = data2.title;if(typeof data3 === "string"){if(func3(data3) < 1){const err11 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}else {const err12 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}if(data2.locale !== undefined){let data4 = data2.locale;if(typeof data4 === "string"){if(func3(data4) > 35){const err13 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/maxLength",keyword:"maxLength",params:{limit: 35},message:"must NOT have more than 35 characters"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}if(!pattern4.test(data4)){const err14 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"+"\""};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}if(data2.translations !== undefined){let data5 = data2.translations;if(data5 && typeof data5 == "object" && !Array.isArray(data5)){if(Object.keys(data5).length > 600){const err16 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/maxProperties",keyword:"maxProperties",params:{limit: 600},message:"must NOT have more than 600 properties"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}for(const key2 in data5){const _errs15 = errors;if(typeof key2 === "string"){if(!pattern5.test(key2)){const err17 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"},message:"must match pattern \""+"^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"+"\"",propertyName:key2};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}var valid4 = _errs15 === errors;if(!valid4){const err18 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}for(const key3 in data5){let data6 = data5[key3];if(typeof data6 === "string"){if(func3(data6) > 400){const err19 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}if(func3(data6) < 1){const err20 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}else {const err21 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}}else {const err22 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}if(data2.subtitle !== undefined){if(typeof data2.subtitle !== "string"){const err23 = {instancePath:instancePath+"/meta/subtitle",schemaPath:"#/properties/meta/properties/subtitle/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}if(data2.output !== undefined){let data8 = data2.output;if(typeof data8 === "string"){if(!pattern6.test(data8)){const err24 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/0/pattern",keyword:"pattern",params:{pattern: "(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"},message:"must match pattern \""+"(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"+"\""};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}const _errs26 = errors;const _errs27 = errors;if(typeof data8 === "string"){if(!pattern7.test(data8)){const err25 = {};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}var valid8 = _errs27 === errors;if(valid8){const err26 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/1/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}else {errors = _errs26;if(vErrors !== null){if(_errs26){vErrors.length = _errs26;}else {vErrors = null;}}}const _errs29 = errors;const _errs30 = errors;if(typeof data8 === "string"){if(!pattern8.test(data8)){const err27 = {};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var valid9 = _errs30 === errors;if(valid9){const err28 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/2/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}const _errs32 = errors;const _errs33 = errors;if(typeof data8 === "string"){if(!pattern9.test(data8)){const err29 = {};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}}var valid10 = _errs33 === errors;if(valid10){const err30 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/3/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}else {errors = _errs32;if(vErrors !== null){if(_errs32){vErrors.length = _errs32;}else {vErrors = null;}}}const _errs35 = errors;const _errs36 = errors;if(typeof data8 === "string"){if(!pattern10.test(data8)){const err31 = {};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}}var valid11 = _errs36 === errors;if(valid11){const err32 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/4/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}else {errors = _errs35;if(vErrors !== null){if(_errs35){vErrors.length = _errs35;}else {vErrors = null;}}}const _errs38 = errors;const _errs39 = errors;if(typeof data8 === "string"){if(!pattern11.test(data8)){const err33 = {};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}}var valid12 = _errs39 === errors;if(valid12){const err34 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/5/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}else {errors = _errs38;if(vErrors !== null){if(_errs38){vErrors.length = _errs38;}else {vErrors = null;}}}const _errs41 = errors;const _errs42 = errors;if(typeof data8 === "string"){if(!pattern12.test(data8)){const err35 = {};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}}var valid13 = _errs42 === errors;if(valid13){const err36 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/6/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}else {errors = _errs41;if(vErrors !== null){if(_errs41){vErrors.length = _errs41;}else {vErrors = null;}}}const _errs44 = errors;const _errs45 = errors;if(typeof data8 === "string"){if(!pattern13.test(data8)){const err37 = {};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}}var valid14 = _errs45 === errors;if(valid14){const err38 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/7/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}else {errors = _errs44;if(vErrors !== null){if(_errs44){vErrors.length = _errs44;}else {vErrors = null;}}}if(typeof data8 === "string"){if(func3(data8) < 1){const err39 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}if(!pattern14.test(data8)){const err40 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/pattern",keyword:"pattern",params:{pattern: "^[^/]+(?:/[^/]+)*$"},message:"must match pattern \""+"^[^/]+(?:/[^/]+)*$"+"\""};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}}else {const err41 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}if(data2.animation !== undefined){let data9 = data2.animation;if(!((data9 === "trace") || (data9 === "none"))){const err42 = {instancePath:instancePath+"/meta/animation",schemaPath:"common.schema.json#/$defs/animation/enum",keyword:"enum",params:{allowedValues: schema77.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;}}if(data2.visual_preset !== undefined){let data10 = data2.visual_preset;if(!((((data10 === "classic") || (data10 === "signal-flow")) || (data10 === "blueprint")) || (data10 === "editorial"))){const err43 = {instancePath:instancePath+"/meta/visual_preset",schemaPath:"common.schema.json#/$defs/visualPreset/enum",keyword:"enum",params:{allowedValues: schema78.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}}if(data2.quality_profile !== undefined){let data11 = data2.quality_profile;if(!((data11 === "standard") || (data11 === "showcase"))){const err44 = {instancePath:instancePath+"/meta/quality_profile",schemaPath:"common.schema.json#/$defs/qualityProfile/enum",keyword:"enum",params:{allowedValues: schema79.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}}if(data2.column_fit !== undefined){let data12 = data2.column_fit;if(!((data12 === "fixed") || (data12 === "spread"))){const err45 = {instancePath:instancePath+"/meta/column_fit",schemaPath:"#/properties/meta/properties/column_fit/enum",keyword:"enum",params:{allowedValues: schema73.properties.meta.properties.column_fit.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}}if(data2.repository !== undefined){let data13 = data2.repository;if(data13 && typeof data13 == "object" && !Array.isArray(data13)){if(data13.url === undefined){const err46 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}if(data13.revision === undefined){const err47 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "revision"},message:"must have required property '"+"revision"+"'"};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}for(const key4 in data13){if(!((((key4 === "url") || (key4 === "provider")) || (key4 === "link_mode")) || (key4 === "revision"))){const err48 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err48];}else {vErrors.push(err48);}errors++;}}if(data13.url !== undefined){let data14 = data13.url;if(typeof data14 === "string"){if(func3(data14) < 1){const err49 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err49];}else {vErrors.push(err49);}errors++;}}else {const err50 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err50];}else {vErrors.push(err50);}errors++;}}if(data13.provider !== undefined){let data15 = data13.provider;if(!((data15 === "github") || (data15 === "gitee"))){const err51 = {instancePath:instancePath+"/meta/repository/provider",schemaPath:"common.schema.json#/$defs/repository/properties/provider/enum",keyword:"enum",params:{allowedValues: schema36.properties.provider.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err51];}else {vErrors.push(err51);}errors++;}}if(data13.link_mode !== undefined){let data16 = data13.link_mode;if(!((data16 === "web") || (data16 === "local-only"))){const err52 = {instancePath:instancePath+"/meta/repository/link_mode",schemaPath:"common.schema.json#/$defs/repository/properties/link_mode/enum",keyword:"enum",params:{allowedValues: schema36.properties.link_mode.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err52];}else {vErrors.push(err52);}errors++;}}if(data13.revision !== undefined){let data17 = data13.revision;if(typeof data17 === "string"){if(!pattern15.test(data17)){const err53 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/pattern",keyword:"pattern",params:{pattern: "^[a-fA-F0-9]{40}$"},message:"must match pattern \""+"^[a-fA-F0-9]{40}$"+"\""};if(vErrors === null){vErrors = [err53];}else {vErrors.push(err53);}errors++;}}else {const err54 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err54];}else {vErrors.push(err54);}errors++;}}}else {const err55 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err55];}else {vErrors.push(err55);}errors++;}}if(data2.views !== undefined){if(!(validate28(data2.views, {instancePath:instancePath+"/meta/views",parentData:data2,parentDataProperty:"views",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate28.errors : vErrors.concat(validate28.errors);errors = vErrors.length;}}if(data2.legend !== undefined){let data19 = data2.legend;if(data19 && typeof data19 == "object" && !Array.isArray(data19)){for(const key5 in data19){if(!((key5 === "mode") || (key5 === "entries"))){const err56 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err56];}else {vErrors.push(err56);}errors++;}}if(data19.mode !== undefined){let data20 = data19.mode;if(!(((data20 === "auto") || (data20 === "all")) || (data20 === "hidden"))){const err57 = {instancePath:instancePath+"/meta/legend/mode",schemaPath:"common.schema.json#/$defs/legendMode/enum",keyword:"enum",params:{allowedValues: schema40.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err57];}else {vErrors.push(err57);}errors++;}}if(data19.entries !== undefined){let data21 = data19.entries;if(data21 && typeof data21 == "object" && !Array.isArray(data21)){for(const key6 in data21){if(!(((((key6 === "default") || (key6 === "emphasis")) || (key6 === "security")) || (key6 === "dashed")) || (key6 === "return"))){const err58 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err58];}else {vErrors.push(err58);}errors++;}}if(data21.default !== undefined){let data22 = data21.default;if(data22 && typeof data22 == "object" && !Array.isArray(data22)){if(Object.keys(data22).length < 1){const err59 = {instancePath:instancePath+"/meta/legend/entries/default",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err59];}else {vErrors.push(err59);}errors++;}for(const key7 in data22){if(!((key7 === "label") || (key7 === "visible"))){const err60 = {instancePath:instancePath+"/meta/legend/entries/default",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err60];}else {vErrors.push(err60);}errors++;}}if(data22.label !== undefined){let data23 = data22.label;if(typeof data23 === "string"){if(func3(data23) > 80){const err61 = {instancePath:instancePath+"/meta/legend/entries/default/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err61];}else {vErrors.push(err61);}errors++;}if(func3(data23) < 1){const err62 = {instancePath:instancePath+"/meta/legend/entries/default/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err62];}else {vErrors.push(err62);}errors++;}}else {const err63 = {instancePath:instancePath+"/meta/legend/entries/default/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err63];}else {vErrors.push(err63);}errors++;}}if(data22.visible !== undefined){if(typeof data22.visible !== "boolean"){const err64 = {instancePath:instancePath+"/meta/legend/entries/default/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err64];}else {vErrors.push(err64);}errors++;}}}else {const err65 = {instancePath:instancePath+"/meta/legend/entries/default",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err65];}else {vErrors.push(err65);}errors++;}}if(data21.emphasis !== undefined){let data25 = data21.emphasis;if(data25 && typeof data25 == "object" && !Array.isArray(data25)){if(Object.keys(data25).length < 1){const err66 = {instancePath:instancePath+"/meta/legend/entries/emphasis",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err66];}else {vErrors.push(err66);}errors++;}for(const key8 in data25){if(!((key8 === "label") || (key8 === "visible"))){const err67 = {instancePath:instancePath+"/meta/legend/entries/emphasis",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err67];}else {vErrors.push(err67);}errors++;}}if(data25.label !== undefined){let data26 = data25.label;if(typeof data26 === "string"){if(func3(data26) > 80){const err68 = {instancePath:instancePath+"/meta/legend/entries/emphasis/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err68];}else {vErrors.push(err68);}errors++;}if(func3(data26) < 1){const err69 = {instancePath:instancePath+"/meta/legend/entries/emphasis/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err69];}else {vErrors.push(err69);}errors++;}}else {const err70 = {instancePath:instancePath+"/meta/legend/entries/emphasis/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err70];}else {vErrors.push(err70);}errors++;}}if(data25.visible !== undefined){if(typeof data25.visible !== "boolean"){const err71 = {instancePath:instancePath+"/meta/legend/entries/emphasis/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err71];}else {vErrors.push(err71);}errors++;}}}else {const err72 = {instancePath:instancePath+"/meta/legend/entries/emphasis",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err72];}else {vErrors.push(err72);}errors++;}}if(data21.security !== undefined){let data28 = data21.security;if(data28 && typeof data28 == "object" && !Array.isArray(data28)){if(Object.keys(data28).length < 1){const err73 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err73];}else {vErrors.push(err73);}errors++;}for(const key9 in data28){if(!((key9 === "label") || (key9 === "visible"))){const err74 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key9},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err74];}else {vErrors.push(err74);}errors++;}}if(data28.label !== undefined){let data29 = data28.label;if(typeof data29 === "string"){if(func3(data29) > 80){const err75 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err75];}else {vErrors.push(err75);}errors++;}if(func3(data29) < 1){const err76 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err76];}else {vErrors.push(err76);}errors++;}}else {const err77 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err77];}else {vErrors.push(err77);}errors++;}}if(data28.visible !== undefined){if(typeof data28.visible !== "boolean"){const err78 = {instancePath:instancePath+"/meta/legend/entries/security/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err78];}else {vErrors.push(err78);}errors++;}}}else {const err79 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err79];}else {vErrors.push(err79);}errors++;}}if(data21.dashed !== undefined){let data31 = data21.dashed;if(data31 && typeof data31 == "object" && !Array.isArray(data31)){if(Object.keys(data31).length < 1){const err80 = {instancePath:instancePath+"/meta/legend/entries/dashed",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err80];}else {vErrors.push(err80);}errors++;}for(const key10 in data31){if(!((key10 === "label") || (key10 === "visible"))){const err81 = {instancePath:instancePath+"/meta/legend/entries/dashed",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key10},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err81];}else {vErrors.push(err81);}errors++;}}if(data31.label !== undefined){let data32 = data31.label;if(typeof data32 === "string"){if(func3(data32) > 80){const err82 = {instancePath:instancePath+"/meta/legend/entries/dashed/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err82];}else {vErrors.push(err82);}errors++;}if(func3(data32) < 1){const err83 = {instancePath:instancePath+"/meta/legend/entries/dashed/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err83];}else {vErrors.push(err83);}errors++;}}else {const err84 = {instancePath:instancePath+"/meta/legend/entries/dashed/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err84];}else {vErrors.push(err84);}errors++;}}if(data31.visible !== undefined){if(typeof data31.visible !== "boolean"){const err85 = {instancePath:instancePath+"/meta/legend/entries/dashed/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err85];}else {vErrors.push(err85);}errors++;}}}else {const err86 = {instancePath:instancePath+"/meta/legend/entries/dashed",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err86];}else {vErrors.push(err86);}errors++;}}if(data21.return !== undefined){let data34 = data21.return;if(data34 && typeof data34 == "object" && !Array.isArray(data34)){if(Object.keys(data34).length < 1){const err87 = {instancePath:instancePath+"/meta/legend/entries/return",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err87];}else {vErrors.push(err87);}errors++;}for(const key11 in data34){if(!((key11 === "label") || (key11 === "visible"))){const err88 = {instancePath:instancePath+"/meta/legend/entries/return",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key11},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err88];}else {vErrors.push(err88);}errors++;}}if(data34.label !== undefined){let data35 = data34.label;if(typeof data35 === "string"){if(func3(data35) > 80){const err89 = {instancePath:instancePath+"/meta/legend/entries/return/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err89];}else {vErrors.push(err89);}errors++;}if(func3(data35) < 1){const err90 = {instancePath:instancePath+"/meta/legend/entries/return/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err90];}else {vErrors.push(err90);}errors++;}}else {const err91 = {instancePath:instancePath+"/meta/legend/entries/return/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err91];}else {vErrors.push(err91);}errors++;}}if(data34.visible !== undefined){if(typeof data34.visible !== "boolean"){const err92 = {instancePath:instancePath+"/meta/legend/entries/return/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err92];}else {vErrors.push(err92);}errors++;}}}else {const err93 = {instancePath:instancePath+"/meta/legend/entries/return",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err93];}else {vErrors.push(err93);}errors++;}}}else {const err94 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err94];}else {vErrors.push(err94);}errors++;}}}else {const err95 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err95];}else {vErrors.push(err95);}errors++;}}if(data2.viewBox !== undefined){let data37 = data2.viewBox;if(Array.isArray(data37)){if(data37.length > 2){const err96 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err96];}else {vErrors.push(err96);}errors++;}if(data37.length < 2){const err97 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err97];}else {vErrors.push(err97);}errors++;}const len0 = data37.length;if(len0 > 0){let data38 = data37[0];if((typeof data38 == "number") && (isFinite(data38))){if(data38 < 480 || isNaN(data38)){const err98 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 480},message:"must be >= 480"};if(vErrors === null){vErrors = [err98];}else {vErrors.push(err98);}errors++;}}else {const err99 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err99];}else {vErrors.push(err99);}errors++;}}if(len0 > 1){let data39 = data37[1];if((typeof data39 == "number") && (isFinite(data39))){if(data39 < 480 || isNaN(data39)){const err100 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/minimum",keyword:"minimum",params:{comparison: ">=", limit: 480},message:"must be >= 480"};if(vErrors === null){vErrors = [err100];}else {vErrors.push(err100);}errors++;}}else {const err101 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err101];}else {vErrors.push(err101);}errors++;}}const len1 = data37.length;if(!(len1 <= 2)){const err102 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err102];}else {vErrors.push(err102);}errors++;}}else {const err103 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err103];}else {vErrors.push(err103);}errors++;}}}else {const err104 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err104];}else {vErrors.push(err104);}errors++;}}if(data.participants !== undefined){let data40 = data.participants;if(Array.isArray(data40)){if(data40.length < 2){const err105 = {instancePath:instancePath+"/participants",schemaPath:"#/properties/participants/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err105];}else {vErrors.push(err105);}errors++;}const len2 = data40.length;for(let i0=0; i0 80){const err117 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err117];}else {vErrors.push(err117);}errors++;}if(!pattern29.test(data47)){const err118 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/pattern",keyword:"pattern",params:{pattern: "^[^\\r\\n]+$"},message:"must match pattern \""+"^[^\\r\\n]+$"+"\""};if(vErrors === null){vErrors = [err118];}else {vErrors.push(err118);}errors++;}}var _valid1 = _errs140 === errors;valid42 = valid42 || _valid1;const _errs141 = errors;if(typeof data47 === "string"){if(!pattern30.test(data47)){const err119 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/1/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err119];}else {vErrors.push(err119);}errors++;}}var _valid1 = _errs141 === errors;valid42 = valid42 || _valid1;if(!valid42){const err120 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err120];}else {vErrors.push(err120);}errors++;}else {errors = _errs139;if(vErrors !== null){if(_errs139){vErrors.length = _errs139;}else {vErrors = null;}}}if(typeof data47 === "string"){if(func3(data47) > 2048){const err121 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err121];}else {vErrors.push(err121);}errors++;}if(func3(data47) < 1){const err122 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err122];}else {vErrors.push(err122);}errors++;}}else {const err123 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err123];}else {vErrors.push(err123);}errors++;}var _valid0 = _errs137 === errors;if(_valid0){valid41 = true;passing0 = 0;}const _errs142 = errors;if(data47 && typeof data47 == "object" && !Array.isArray(data47)){if(data47.url === undefined){const err124 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err124];}else {vErrors.push(err124);}errors++;}if(data47.sha256 === undefined){const err125 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "sha256"},message:"must have required property '"+"sha256"+"'"};if(vErrors === null){vErrors = [err125];}else {vErrors.push(err125);}errors++;}for(const key13 in data47){if(!((key13 === "url") || (key13 === "sha256"))){const err126 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key13},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err126];}else {vErrors.push(err126);}errors++;}}if(data47.url !== undefined){let data48 = data47.url;if(typeof data48 === "string"){if(func3(data48) > 2048){const err127 = {instancePath:instancePath+"/participants/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err127];}else {vErrors.push(err127);}errors++;}if(func3(data48) < 8){const err128 = {instancePath:instancePath+"/participants/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"};if(vErrors === null){vErrors = [err128];}else {vErrors.push(err128);}errors++;}if(!pattern30.test(data48)){const err129 = {instancePath:instancePath+"/participants/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err129];}else {vErrors.push(err129);}errors++;}}else {const err130 = {instancePath:instancePath+"/participants/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err130];}else {vErrors.push(err130);}errors++;}}if(data47.sha256 !== undefined){let data49 = data47.sha256;if(typeof data49 === "string"){if(!pattern32.test(data49)){const err131 = {instancePath:instancePath+"/participants/" + i0+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/pattern",keyword:"pattern",params:{pattern: "^[a-f0-9]{64}$"},message:"must match pattern \""+"^[a-f0-9]{64}$"+"\""};if(vErrors === null){vErrors = [err131];}else {vErrors.push(err131);}errors++;}}else {const err132 = {instancePath:instancePath+"/participants/" + i0+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err132];}else {vErrors.push(err132);}errors++;}}}else {const err133 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err133];}else {vErrors.push(err133);}errors++;}var _valid0 = _errs142 === errors;if(_valid0 && valid41){valid41 = false;passing0 = [passing0, 1];}else {if(_valid0){valid41 = true;passing0 = 1;}}if(!valid41){const err134 = {instancePath:instancePath+"/participants/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err134];}else {vErrors.push(err134);}errors++;}else {errors = _errs136;if(vErrors !== null){if(_errs136){vErrors.length = _errs136;}else {vErrors = null;}}}}if(data41.sources !== undefined){let data50 = data41.sources;if(Array.isArray(data50)){if(data50.length > 3){const err135 = {instancePath:instancePath+"/participants/" + i0+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err135];}else {vErrors.push(err135);}errors++;}if(data50.length < 1){const err136 = {instancePath:instancePath+"/participants/" + i0+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err136];}else {vErrors.push(err136);}errors++;}const len3 = data50.length;for(let i1=0; i1 240){const err139 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"};if(vErrors === null){vErrors = [err139];}else {vErrors.push(err139);}errors++;}if(func3(data52) < 1){const err140 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err140];}else {vErrors.push(err140);}errors++;}}else {const err141 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err141];}else {vErrors.push(err141);}errors++;}}if(data51.line !== undefined){let data53 = data51.line;if(!(((typeof data53 == "number") && (!(data53 % 1) && !isNaN(data53))) && (isFinite(data53)))){const err142 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err142];}else {vErrors.push(err142);}errors++;}if((typeof data53 == "number") && (isFinite(data53))){if(data53 < 1 || isNaN(data53)){const err143 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err143];}else {vErrors.push(err143);}errors++;}}}if(data51.end_line !== undefined){let data54 = data51.end_line;if(!(((typeof data54 == "number") && (!(data54 % 1) && !isNaN(data54))) && (isFinite(data54)))){const err144 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err144];}else {vErrors.push(err144);}errors++;}if((typeof data54 == "number") && (isFinite(data54))){if(data54 < 1 || isNaN(data54)){const err145 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err145];}else {vErrors.push(err145);}errors++;}}}if(data51.label !== undefined){let data55 = data51.label;if(typeof data55 === "string"){if(func3(data55) > 48){const err146 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err146];}else {vErrors.push(err146);}errors++;}if(func3(data55) < 1){const err147 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err147];}else {vErrors.push(err147);}errors++;}}else {const err148 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err148];}else {vErrors.push(err148);}errors++;}}}else {const err149 = {instancePath:instancePath+"/participants/" + i0+"/sources/" + i1,schemaPath:"common.schema.json#/$defs/sourceReferences/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err149];}else {vErrors.push(err149);}errors++;}}}else {const err150 = {instancePath:instancePath+"/participants/" + i0+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err150];}else {vErrors.push(err150);}errors++;}}}else {const err151 = {instancePath:instancePath+"/participants/" + i0,schemaPath:"#/properties/participants/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err151];}else {vErrors.push(err151);}errors++;}}}else {const err152 = {instancePath:instancePath+"/participants",schemaPath:"#/properties/participants/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err152];}else {vErrors.push(err152);}errors++;}}if(data.segments !== undefined){let data56 = data.segments;if(Array.isArray(data56)){const len4 = data56.length;for(let i2=0; i2=", limit: 160},message:"must be >= 160"};if(vErrors === null){vErrors = [err175];}else {vErrors.push(err175);}errors++;}}else {const err176 = {instancePath:instancePath+"/messages/" + i3+"/y",schemaPath:"#/properties/messages/items/properties/y/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err176];}else {vErrors.push(err176);}errors++;}}if(data62.label !== undefined){let data67 = data62.label;if(typeof data67 === "string"){if(func3(data67) < 1){const err177 = {instancePath:instancePath+"/messages/" + i3+"/label",schemaPath:"#/properties/messages/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err177];}else {vErrors.push(err177);}errors++;}}else {const err178 = {instancePath:instancePath+"/messages/" + i3+"/label",schemaPath:"#/properties/messages/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err178];}else {vErrors.push(err178);}errors++;}}if(data62.variant !== undefined){let data68 = data62.variant;if(!(((((data68 === "default") || (data68 === "emphasis")) || (data68 === "security")) || (data68 === "dashed")) || (data68 === "return"))){const err179 = {instancePath:instancePath+"/messages/" + i3+"/variant",schemaPath:"#/properties/messages/items/properties/variant/enum",keyword:"enum",params:{allowedValues: schema73.properties.messages.items.properties.variant.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err179];}else {vErrors.push(err179);}errors++;}}if(data62.note !== undefined){if(typeof data62.note !== "string"){const err180 = {instancePath:instancePath+"/messages/" + i3+"/note",schemaPath:"#/properties/messages/items/properties/note/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err180];}else {vErrors.push(err180);}errors++;}}}else {const err181 = {instancePath:instancePath+"/messages/" + i3,schemaPath:"#/properties/messages/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err181];}else {vErrors.push(err181);}errors++;}}}else {const err182 = {instancePath:instancePath+"/messages",schemaPath:"#/properties/messages/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err182];}else {vErrors.push(err182);}errors++;}}if(data.activations !== undefined){let data70 = data.activations;if(Array.isArray(data70)){const len6 = data70.length;for(let i4=0; i4 5){const err0 = {instancePath,schemaPath:"#/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}const len0 = data.length;for(let i0=0; i0 48){const err7 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}if(func3(data2) < 1){const err8 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}else {const err9 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}if(data0.focus !== undefined){let data3 = data0.focus;if(Array.isArray(data3)){if(data3.length < 1){const err10 = {instancePath:instancePath+"/" + i0+"/focus",schemaPath:"#/items/properties/focus/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}const len1 = data3.length;for(let i1=0; i1 140){const err14 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/maxLength",keyword:"maxLength",params:{limit: 140},message:"must NOT have more than 140 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}else {const err16 = {instancePath:instancePath+"/" + i0,schemaPath:"#/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}validate31.errors = vErrors;return errors === 0;}validate31.evaluated = {"items":true,"dynamicProps":false,"dynamicItems":false};function validate30(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://github.com/tt-a1i/archify/schemas/dataflow.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate30.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.schema_version === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "schema_version"},message:"must have required property '"+"schema_version"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.diagram_type === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "diagram_type"},message:"must have required property '"+"diagram_type"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}if(data.meta === undefined){const err2 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "meta"},message:"must have required property '"+"meta"+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}if(data.stages === undefined){const err3 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "stages"},message:"must have required property '"+"stages"+"'"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}if(data.nodes === undefined){const err4 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "nodes"},message:"must have required property '"+"nodes"+"'"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}if(data.flows === undefined){const err5 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "flows"},message:"must have required property '"+"flows"+"'"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}for(const key0 in data){if(!(((((((key0 === "schema_version") || (key0 === "diagram_type")) || (key0 === "meta")) || (key0 === "stages")) || (key0 === "nodes")) || (key0 === "flows")) || (key0 === "cards"))){const err6 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.schema_version !== undefined){if(1 !== data.schema_version){const err7 = {instancePath:instancePath+"/schema_version",schemaPath:"#/properties/schema_version/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}if(data.diagram_type !== undefined){if("dataflow" !== data.diagram_type){const err8 = {instancePath:instancePath+"/diagram_type",schemaPath:"#/properties/diagram_type/const",keyword:"const",params:{allowedValue: "dataflow"},message:"must be equal to constant"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}if(data.meta !== undefined){let data2 = data.meta;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.title === undefined){const err9 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "title"},message:"must have required property '"+"title"+"'"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}if(data2.output === undefined){const err10 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "output"},message:"must have required property '"+"output"+"'"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}for(const key1 in data2){if(!(func1.call(schema101.properties.meta.properties, key1))){const err11 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}if(data2.title !== undefined){let data3 = data2.title;if(typeof data3 === "string"){if(func3(data3) < 1){const err12 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}else {const err13 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}if(data2.locale !== undefined){let data4 = data2.locale;if(typeof data4 === "string"){if(func3(data4) > 35){const err14 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/maxLength",keyword:"maxLength",params:{limit: 35},message:"must NOT have more than 35 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}if(!pattern4.test(data4)){const err15 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"+"\""};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}else {const err16 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}if(data2.translations !== undefined){let data5 = data2.translations;if(data5 && typeof data5 == "object" && !Array.isArray(data5)){if(Object.keys(data5).length > 600){const err17 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/maxProperties",keyword:"maxProperties",params:{limit: 600},message:"must NOT have more than 600 properties"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}for(const key2 in data5){const _errs15 = errors;if(typeof key2 === "string"){if(!pattern5.test(key2)){const err18 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"},message:"must match pattern \""+"^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"+"\"",propertyName:key2};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var valid4 = _errs15 === errors;if(!valid4){const err19 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}for(const key3 in data5){let data6 = data5[key3];if(typeof data6 === "string"){if(func3(data6) > 400){const err20 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}if(func3(data6) < 1){const err21 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}else {const err22 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}}else {const err23 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}if(data2.subtitle !== undefined){if(typeof data2.subtitle !== "string"){const err24 = {instancePath:instancePath+"/meta/subtitle",schemaPath:"#/properties/meta/properties/subtitle/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}if(data2.output !== undefined){let data8 = data2.output;if(typeof data8 === "string"){if(!pattern6.test(data8)){const err25 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/0/pattern",keyword:"pattern",params:{pattern: "(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"},message:"must match pattern \""+"(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"+"\""};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}const _errs26 = errors;const _errs27 = errors;if(typeof data8 === "string"){if(!pattern7.test(data8)){const err26 = {};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}var valid8 = _errs27 === errors;if(valid8){const err27 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/1/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}else {errors = _errs26;if(vErrors !== null){if(_errs26){vErrors.length = _errs26;}else {vErrors = null;}}}const _errs29 = errors;const _errs30 = errors;if(typeof data8 === "string"){if(!pattern8.test(data8)){const err28 = {};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}var valid9 = _errs30 === errors;if(valid9){const err29 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/2/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}const _errs32 = errors;const _errs33 = errors;if(typeof data8 === "string"){if(!pattern9.test(data8)){const err30 = {};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}var valid10 = _errs33 === errors;if(valid10){const err31 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/3/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {errors = _errs32;if(vErrors !== null){if(_errs32){vErrors.length = _errs32;}else {vErrors = null;}}}const _errs35 = errors;const _errs36 = errors;if(typeof data8 === "string"){if(!pattern10.test(data8)){const err32 = {};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}var valid11 = _errs36 === errors;if(valid11){const err33 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/4/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}else {errors = _errs35;if(vErrors !== null){if(_errs35){vErrors.length = _errs35;}else {vErrors = null;}}}const _errs38 = errors;const _errs39 = errors;if(typeof data8 === "string"){if(!pattern11.test(data8)){const err34 = {};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}}var valid12 = _errs39 === errors;if(valid12){const err35 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/5/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}else {errors = _errs38;if(vErrors !== null){if(_errs38){vErrors.length = _errs38;}else {vErrors = null;}}}const _errs41 = errors;const _errs42 = errors;if(typeof data8 === "string"){if(!pattern12.test(data8)){const err36 = {};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}}var valid13 = _errs42 === errors;if(valid13){const err37 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/6/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {errors = _errs41;if(vErrors !== null){if(_errs41){vErrors.length = _errs41;}else {vErrors = null;}}}const _errs44 = errors;const _errs45 = errors;if(typeof data8 === "string"){if(!pattern13.test(data8)){const err38 = {};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}var valid14 = _errs45 === errors;if(valid14){const err39 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/7/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}else {errors = _errs44;if(vErrors !== null){if(_errs44){vErrors.length = _errs44;}else {vErrors = null;}}}if(typeof data8 === "string"){if(func3(data8) < 1){const err40 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}if(!pattern14.test(data8)){const err41 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/pattern",keyword:"pattern",params:{pattern: "^[^/]+(?:/[^/]+)*$"},message:"must match pattern \""+"^[^/]+(?:/[^/]+)*$"+"\""};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}else {const err42 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;}}if(data2.animation !== undefined){let data9 = data2.animation;if(!((data9 === "trace") || (data9 === "none"))){const err43 = {instancePath:instancePath+"/meta/animation",schemaPath:"common.schema.json#/$defs/animation/enum",keyword:"enum",params:{allowedValues: schema77.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}}if(data2.visual_preset !== undefined){let data10 = data2.visual_preset;if(!((((data10 === "classic") || (data10 === "signal-flow")) || (data10 === "blueprint")) || (data10 === "editorial"))){const err44 = {instancePath:instancePath+"/meta/visual_preset",schemaPath:"common.schema.json#/$defs/visualPreset/enum",keyword:"enum",params:{allowedValues: schema78.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}}if(data2.quality_profile !== undefined){let data11 = data2.quality_profile;if(!((data11 === "standard") || (data11 === "showcase"))){const err45 = {instancePath:instancePath+"/meta/quality_profile",schemaPath:"common.schema.json#/$defs/qualityProfile/enum",keyword:"enum",params:{allowedValues: schema79.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}}if(data2.repository !== undefined){let data12 = data2.repository;if(data12 && typeof data12 == "object" && !Array.isArray(data12)){if(data12.url === undefined){const err46 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}if(data12.revision === undefined){const err47 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "revision"},message:"must have required property '"+"revision"+"'"};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}for(const key4 in data12){if(!((((key4 === "url") || (key4 === "provider")) || (key4 === "link_mode")) || (key4 === "revision"))){const err48 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err48];}else {vErrors.push(err48);}errors++;}}if(data12.url !== undefined){let data13 = data12.url;if(typeof data13 === "string"){if(func3(data13) < 1){const err49 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err49];}else {vErrors.push(err49);}errors++;}}else {const err50 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err50];}else {vErrors.push(err50);}errors++;}}if(data12.provider !== undefined){let data14 = data12.provider;if(!((data14 === "github") || (data14 === "gitee"))){const err51 = {instancePath:instancePath+"/meta/repository/provider",schemaPath:"common.schema.json#/$defs/repository/properties/provider/enum",keyword:"enum",params:{allowedValues: schema36.properties.provider.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err51];}else {vErrors.push(err51);}errors++;}}if(data12.link_mode !== undefined){let data15 = data12.link_mode;if(!((data15 === "web") || (data15 === "local-only"))){const err52 = {instancePath:instancePath+"/meta/repository/link_mode",schemaPath:"common.schema.json#/$defs/repository/properties/link_mode/enum",keyword:"enum",params:{allowedValues: schema36.properties.link_mode.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err52];}else {vErrors.push(err52);}errors++;}}if(data12.revision !== undefined){let data16 = data12.revision;if(typeof data16 === "string"){if(!pattern15.test(data16)){const err53 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/pattern",keyword:"pattern",params:{pattern: "^[a-fA-F0-9]{40}$"},message:"must match pattern \""+"^[a-fA-F0-9]{40}$"+"\""};if(vErrors === null){vErrors = [err53];}else {vErrors.push(err53);}errors++;}}else {const err54 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err54];}else {vErrors.push(err54);}errors++;}}}else {const err55 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err55];}else {vErrors.push(err55);}errors++;}}if(data2.views !== undefined){if(!(validate31(data2.views, {instancePath:instancePath+"/meta/views",parentData:data2,parentDataProperty:"views",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate31.errors : vErrors.concat(validate31.errors);errors = vErrors.length;}}if(data2.legend !== undefined){let data18 = data2.legend;if(data18 && typeof data18 == "object" && !Array.isArray(data18)){for(const key5 in data18){if(!((key5 === "mode") || (key5 === "entries"))){const err56 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err56];}else {vErrors.push(err56);}errors++;}}if(data18.mode !== undefined){let data19 = data18.mode;if(!(((data19 === "auto") || (data19 === "all")) || (data19 === "hidden"))){const err57 = {instancePath:instancePath+"/meta/legend/mode",schemaPath:"common.schema.json#/$defs/legendMode/enum",keyword:"enum",params:{allowedValues: schema40.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err57];}else {vErrors.push(err57);}errors++;}}if(data18.entries !== undefined){let data20 = data18.entries;if(data20 && typeof data20 == "object" && !Array.isArray(data20)){for(const key6 in data20){if(!(((((key6 === "default") || (key6 === "emphasis")) || (key6 === "security")) || (key6 === "dashed")) || (key6 === "database"))){const err58 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err58];}else {vErrors.push(err58);}errors++;}}if(data20.default !== undefined){let data21 = data20.default;if(data21 && typeof data21 == "object" && !Array.isArray(data21)){if(Object.keys(data21).length < 1){const err59 = {instancePath:instancePath+"/meta/legend/entries/default",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err59];}else {vErrors.push(err59);}errors++;}for(const key7 in data21){if(!((key7 === "label") || (key7 === "visible"))){const err60 = {instancePath:instancePath+"/meta/legend/entries/default",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err60];}else {vErrors.push(err60);}errors++;}}if(data21.label !== undefined){let data22 = data21.label;if(typeof data22 === "string"){if(func3(data22) > 80){const err61 = {instancePath:instancePath+"/meta/legend/entries/default/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err61];}else {vErrors.push(err61);}errors++;}if(func3(data22) < 1){const err62 = {instancePath:instancePath+"/meta/legend/entries/default/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err62];}else {vErrors.push(err62);}errors++;}}else {const err63 = {instancePath:instancePath+"/meta/legend/entries/default/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err63];}else {vErrors.push(err63);}errors++;}}if(data21.visible !== undefined){if(typeof data21.visible !== "boolean"){const err64 = {instancePath:instancePath+"/meta/legend/entries/default/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err64];}else {vErrors.push(err64);}errors++;}}}else {const err65 = {instancePath:instancePath+"/meta/legend/entries/default",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err65];}else {vErrors.push(err65);}errors++;}}if(data20.emphasis !== undefined){let data24 = data20.emphasis;if(data24 && typeof data24 == "object" && !Array.isArray(data24)){if(Object.keys(data24).length < 1){const err66 = {instancePath:instancePath+"/meta/legend/entries/emphasis",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err66];}else {vErrors.push(err66);}errors++;}for(const key8 in data24){if(!((key8 === "label") || (key8 === "visible"))){const err67 = {instancePath:instancePath+"/meta/legend/entries/emphasis",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err67];}else {vErrors.push(err67);}errors++;}}if(data24.label !== undefined){let data25 = data24.label;if(typeof data25 === "string"){if(func3(data25) > 80){const err68 = {instancePath:instancePath+"/meta/legend/entries/emphasis/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err68];}else {vErrors.push(err68);}errors++;}if(func3(data25) < 1){const err69 = {instancePath:instancePath+"/meta/legend/entries/emphasis/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err69];}else {vErrors.push(err69);}errors++;}}else {const err70 = {instancePath:instancePath+"/meta/legend/entries/emphasis/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err70];}else {vErrors.push(err70);}errors++;}}if(data24.visible !== undefined){if(typeof data24.visible !== "boolean"){const err71 = {instancePath:instancePath+"/meta/legend/entries/emphasis/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err71];}else {vErrors.push(err71);}errors++;}}}else {const err72 = {instancePath:instancePath+"/meta/legend/entries/emphasis",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err72];}else {vErrors.push(err72);}errors++;}}if(data20.security !== undefined){let data27 = data20.security;if(data27 && typeof data27 == "object" && !Array.isArray(data27)){if(Object.keys(data27).length < 1){const err73 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err73];}else {vErrors.push(err73);}errors++;}for(const key9 in data27){if(!((key9 === "label") || (key9 === "visible"))){const err74 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key9},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err74];}else {vErrors.push(err74);}errors++;}}if(data27.label !== undefined){let data28 = data27.label;if(typeof data28 === "string"){if(func3(data28) > 80){const err75 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err75];}else {vErrors.push(err75);}errors++;}if(func3(data28) < 1){const err76 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err76];}else {vErrors.push(err76);}errors++;}}else {const err77 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err77];}else {vErrors.push(err77);}errors++;}}if(data27.visible !== undefined){if(typeof data27.visible !== "boolean"){const err78 = {instancePath:instancePath+"/meta/legend/entries/security/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err78];}else {vErrors.push(err78);}errors++;}}}else {const err79 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err79];}else {vErrors.push(err79);}errors++;}}if(data20.dashed !== undefined){let data30 = data20.dashed;if(data30 && typeof data30 == "object" && !Array.isArray(data30)){if(Object.keys(data30).length < 1){const err80 = {instancePath:instancePath+"/meta/legend/entries/dashed",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err80];}else {vErrors.push(err80);}errors++;}for(const key10 in data30){if(!((key10 === "label") || (key10 === "visible"))){const err81 = {instancePath:instancePath+"/meta/legend/entries/dashed",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key10},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err81];}else {vErrors.push(err81);}errors++;}}if(data30.label !== undefined){let data31 = data30.label;if(typeof data31 === "string"){if(func3(data31) > 80){const err82 = {instancePath:instancePath+"/meta/legend/entries/dashed/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err82];}else {vErrors.push(err82);}errors++;}if(func3(data31) < 1){const err83 = {instancePath:instancePath+"/meta/legend/entries/dashed/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err83];}else {vErrors.push(err83);}errors++;}}else {const err84 = {instancePath:instancePath+"/meta/legend/entries/dashed/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err84];}else {vErrors.push(err84);}errors++;}}if(data30.visible !== undefined){if(typeof data30.visible !== "boolean"){const err85 = {instancePath:instancePath+"/meta/legend/entries/dashed/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err85];}else {vErrors.push(err85);}errors++;}}}else {const err86 = {instancePath:instancePath+"/meta/legend/entries/dashed",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err86];}else {vErrors.push(err86);}errors++;}}if(data20.database !== undefined){let data33 = data20.database;if(data33 && typeof data33 == "object" && !Array.isArray(data33)){if(Object.keys(data33).length < 1){const err87 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err87];}else {vErrors.push(err87);}errors++;}for(const key11 in data33){if(!((key11 === "label") || (key11 === "visible"))){const err88 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key11},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err88];}else {vErrors.push(err88);}errors++;}}if(data33.label !== undefined){let data34 = data33.label;if(typeof data34 === "string"){if(func3(data34) > 80){const err89 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err89];}else {vErrors.push(err89);}errors++;}if(func3(data34) < 1){const err90 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err90];}else {vErrors.push(err90);}errors++;}}else {const err91 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err91];}else {vErrors.push(err91);}errors++;}}if(data33.visible !== undefined){if(typeof data33.visible !== "boolean"){const err92 = {instancePath:instancePath+"/meta/legend/entries/database/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err92];}else {vErrors.push(err92);}errors++;}}}else {const err93 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err93];}else {vErrors.push(err93);}errors++;}}}else {const err94 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err94];}else {vErrors.push(err94);}errors++;}}}else {const err95 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err95];}else {vErrors.push(err95);}errors++;}}if(data2.viewBox !== undefined){let data36 = data2.viewBox;if(Array.isArray(data36)){if(data36.length > 2){const err96 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err96];}else {vErrors.push(err96);}errors++;}if(data36.length < 2){const err97 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err97];}else {vErrors.push(err97);}errors++;}const len0 = data36.length;if(len0 > 0){let data37 = data36[0];if((typeof data37 == "number") && (isFinite(data37))){if(data37 < 360 || isNaN(data37)){const err98 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 360},message:"must be >= 360"};if(vErrors === null){vErrors = [err98];}else {vErrors.push(err98);}errors++;}}else {const err99 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err99];}else {vErrors.push(err99);}errors++;}}if(len0 > 1){let data38 = data36[1];if((typeof data38 == "number") && (isFinite(data38))){if(data38 < 360 || isNaN(data38)){const err100 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/minimum",keyword:"minimum",params:{comparison: ">=", limit: 360},message:"must be >= 360"};if(vErrors === null){vErrors = [err100];}else {vErrors.push(err100);}errors++;}}else {const err101 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err101];}else {vErrors.push(err101);}errors++;}}const len1 = data36.length;if(!(len1 <= 2)){const err102 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err102];}else {vErrors.push(err102);}errors++;}}else {const err103 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err103];}else {vErrors.push(err103);}errors++;}}}else {const err104 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err104];}else {vErrors.push(err104);}errors++;}}if(data.stages !== undefined){let data39 = data.stages;if(Array.isArray(data39)){if(data39.length > 5){const err105 = {instancePath:instancePath+"/stages",schemaPath:"#/properties/stages/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"};if(vErrors === null){vErrors = [err105];}else {vErrors.push(err105);}errors++;}if(data39.length < 2){const err106 = {instancePath:instancePath+"/stages",schemaPath:"#/properties/stages/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err106];}else {vErrors.push(err106);}errors++;}const len2 = data39.length;for(let i0=0; i0 80){const err128 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err128];}else {vErrors.push(err128);}errors++;}if(!pattern29.test(data50)){const err129 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/pattern",keyword:"pattern",params:{pattern: "^[^\\r\\n]+$"},message:"must match pattern \""+"^[^\\r\\n]+$"+"\""};if(vErrors === null){vErrors = [err129];}else {vErrors.push(err129);}errors++;}}var _valid1 = _errs148 === errors;valid45 = valid45 || _valid1;const _errs149 = errors;if(typeof data50 === "string"){if(!pattern30.test(data50)){const err130 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/1/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err130];}else {vErrors.push(err130);}errors++;}}var _valid1 = _errs149 === errors;valid45 = valid45 || _valid1;if(!valid45){const err131 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err131];}else {vErrors.push(err131);}errors++;}else {errors = _errs147;if(vErrors !== null){if(_errs147){vErrors.length = _errs147;}else {vErrors = null;}}}if(typeof data50 === "string"){if(func3(data50) > 2048){const err132 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err132];}else {vErrors.push(err132);}errors++;}if(func3(data50) < 1){const err133 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err133];}else {vErrors.push(err133);}errors++;}}else {const err134 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err134];}else {vErrors.push(err134);}errors++;}var _valid0 = _errs145 === errors;if(_valid0){valid44 = true;passing0 = 0;}const _errs150 = errors;if(data50 && typeof data50 == "object" && !Array.isArray(data50)){if(data50.url === undefined){const err135 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err135];}else {vErrors.push(err135);}errors++;}if(data50.sha256 === undefined){const err136 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "sha256"},message:"must have required property '"+"sha256"+"'"};if(vErrors === null){vErrors = [err136];}else {vErrors.push(err136);}errors++;}for(const key14 in data50){if(!((key14 === "url") || (key14 === "sha256"))){const err137 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key14},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err137];}else {vErrors.push(err137);}errors++;}}if(data50.url !== undefined){let data51 = data50.url;if(typeof data51 === "string"){if(func3(data51) > 2048){const err138 = {instancePath:instancePath+"/nodes/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err138];}else {vErrors.push(err138);}errors++;}if(func3(data51) < 8){const err139 = {instancePath:instancePath+"/nodes/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"};if(vErrors === null){vErrors = [err139];}else {vErrors.push(err139);}errors++;}if(!pattern30.test(data51)){const err140 = {instancePath:instancePath+"/nodes/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err140];}else {vErrors.push(err140);}errors++;}}else {const err141 = {instancePath:instancePath+"/nodes/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err141];}else {vErrors.push(err141);}errors++;}}if(data50.sha256 !== undefined){let data52 = data50.sha256;if(typeof data52 === "string"){if(!pattern32.test(data52)){const err142 = {instancePath:instancePath+"/nodes/" + i1+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/pattern",keyword:"pattern",params:{pattern: "^[a-f0-9]{64}$"},message:"must match pattern \""+"^[a-f0-9]{64}$"+"\""};if(vErrors === null){vErrors = [err142];}else {vErrors.push(err142);}errors++;}}else {const err143 = {instancePath:instancePath+"/nodes/" + i1+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err143];}else {vErrors.push(err143);}errors++;}}}else {const err144 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err144];}else {vErrors.push(err144);}errors++;}var _valid0 = _errs150 === errors;if(_valid0 && valid44){valid44 = false;passing0 = [passing0, 1];}else {if(_valid0){valid44 = true;passing0 = 1;}}if(!valid44){const err145 = {instancePath:instancePath+"/nodes/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err145];}else {vErrors.push(err145);}errors++;}else {errors = _errs144;if(vErrors !== null){if(_errs144){vErrors.length = _errs144;}else {vErrors = null;}}}}if(data43.sources !== undefined){let data53 = data43.sources;if(Array.isArray(data53)){if(data53.length > 3){const err146 = {instancePath:instancePath+"/nodes/" + i1+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err146];}else {vErrors.push(err146);}errors++;}if(data53.length < 1){const err147 = {instancePath:instancePath+"/nodes/" + i1+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err147];}else {vErrors.push(err147);}errors++;}const len4 = data53.length;for(let i2=0; i2 240){const err150 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"};if(vErrors === null){vErrors = [err150];}else {vErrors.push(err150);}errors++;}if(func3(data55) < 1){const err151 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err151];}else {vErrors.push(err151);}errors++;}}else {const err152 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err152];}else {vErrors.push(err152);}errors++;}}if(data54.line !== undefined){let data56 = data54.line;if(!(((typeof data56 == "number") && (!(data56 % 1) && !isNaN(data56))) && (isFinite(data56)))){const err153 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err153];}else {vErrors.push(err153);}errors++;}if((typeof data56 == "number") && (isFinite(data56))){if(data56 < 1 || isNaN(data56)){const err154 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err154];}else {vErrors.push(err154);}errors++;}}}if(data54.end_line !== undefined){let data57 = data54.end_line;if(!(((typeof data57 == "number") && (!(data57 % 1) && !isNaN(data57))) && (isFinite(data57)))){const err155 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err155];}else {vErrors.push(err155);}errors++;}if((typeof data57 == "number") && (isFinite(data57))){if(data57 < 1 || isNaN(data57)){const err156 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err156];}else {vErrors.push(err156);}errors++;}}}if(data54.label !== undefined){let data58 = data54.label;if(typeof data58 === "string"){if(func3(data58) > 48){const err157 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err157];}else {vErrors.push(err157);}errors++;}if(func3(data58) < 1){const err158 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err158];}else {vErrors.push(err158);}errors++;}}else {const err159 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err159];}else {vErrors.push(err159);}errors++;}}}else {const err160 = {instancePath:instancePath+"/nodes/" + i1+"/sources/" + i2,schemaPath:"common.schema.json#/$defs/sourceReferences/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err160];}else {vErrors.push(err160);}errors++;}}}else {const err161 = {instancePath:instancePath+"/nodes/" + i1+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err161];}else {vErrors.push(err161);}errors++;}}if(data43.stage !== undefined){let data59 = data43.stage;if(!(((typeof data59 == "number") && (!(data59 % 1) && !isNaN(data59))) && (isFinite(data59)))){const err162 = {instancePath:instancePath+"/nodes/" + i1+"/stage",schemaPath:"#/properties/nodes/items/properties/stage/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err162];}else {vErrors.push(err162);}errors++;}if((typeof data59 == "number") && (isFinite(data59))){if(data59 < 0 || isNaN(data59)){const err163 = {instancePath:instancePath+"/nodes/" + i1+"/stage",schemaPath:"#/properties/nodes/items/properties/stage/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err163];}else {vErrors.push(err163);}errors++;}}}if(data43.row !== undefined){let data60 = data43.row;if(!(((typeof data60 == "number") && (!(data60 % 1) && !isNaN(data60))) && (isFinite(data60)))){const err164 = {instancePath:instancePath+"/nodes/" + i1+"/row",schemaPath:"#/properties/nodes/items/properties/row/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err164];}else {vErrors.push(err164);}errors++;}if((typeof data60 == "number") && (isFinite(data60))){if(data60 < 0 || isNaN(data60)){const err165 = {instancePath:instancePath+"/nodes/" + i1+"/row",schemaPath:"#/properties/nodes/items/properties/row/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err165];}else {vErrors.push(err165);}errors++;}}}if(data43.width !== undefined){let data61 = data43.width;if((typeof data61 == "number") && (isFinite(data61))){if(data61 < 48 || isNaN(data61)){const err166 = {instancePath:instancePath+"/nodes/" + i1+"/width",schemaPath:"#/properties/nodes/items/properties/width/minimum",keyword:"minimum",params:{comparison: ">=", limit: 48},message:"must be >= 48"};if(vErrors === null){vErrors = [err166];}else {vErrors.push(err166);}errors++;}}else {const err167 = {instancePath:instancePath+"/nodes/" + i1+"/width",schemaPath:"#/properties/nodes/items/properties/width/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err167];}else {vErrors.push(err167);}errors++;}}if(data43.height !== undefined){let data62 = data43.height;if((typeof data62 == "number") && (isFinite(data62))){if(data62 < 36 || isNaN(data62)){const err168 = {instancePath:instancePath+"/nodes/" + i1+"/height",schemaPath:"#/properties/nodes/items/properties/height/minimum",keyword:"minimum",params:{comparison: ">=", limit: 36},message:"must be >= 36"};if(vErrors === null){vErrors = [err168];}else {vErrors.push(err168);}errors++;}}else {const err169 = {instancePath:instancePath+"/nodes/" + i1+"/height",schemaPath:"#/properties/nodes/items/properties/height/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err169];}else {vErrors.push(err169);}errors++;}}if(data43.yOffset !== undefined){let data63 = data43.yOffset;if(!((typeof data63 == "number") && (isFinite(data63)))){const err170 = {instancePath:instancePath+"/nodes/" + i1+"/yOffset",schemaPath:"#/properties/nodes/items/properties/yOffset/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err170];}else {vErrors.push(err170);}errors++;}}}else {const err171 = {instancePath:instancePath+"/nodes/" + i1,schemaPath:"#/properties/nodes/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err171];}else {vErrors.push(err171);}errors++;}}}else {const err172 = {instancePath:instancePath+"/nodes",schemaPath:"#/properties/nodes/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err172];}else {vErrors.push(err172);}errors++;}}if(data.flows !== undefined){let data64 = data.flows;if(Array.isArray(data64)){const len5 = data64.length;for(let i3=0; i3 2){const err192 = {instancePath:instancePath+"/flows/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err192];}else {vErrors.push(err192);}errors++;}if(data77.length < 2){const err193 = {instancePath:instancePath+"/flows/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err193];}else {vErrors.push(err193);}errors++;}const len6 = data77.length;if(len6 > 0){let data78 = data77[0];if(!((typeof data78 == "number") && (isFinite(data78)))){const err194 = {instancePath:instancePath+"/flows/" + i3+"/labelAt/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err194];}else {vErrors.push(err194);}errors++;}}if(len6 > 1){let data79 = data77[1];if(!((typeof data79 == "number") && (isFinite(data79)))){const err195 = {instancePath:instancePath+"/flows/" + i3+"/labelAt/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err195];}else {vErrors.push(err195);}errors++;}}const len7 = data77.length;if(!(len7 <= 2)){const err196 = {instancePath:instancePath+"/flows/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err196];}else {vErrors.push(err196);}errors++;}}else {const err197 = {instancePath:instancePath+"/flows/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err197];}else {vErrors.push(err197);}errors++;}}if(data65.labelDx !== undefined){let data80 = data65.labelDx;if(!((typeof data80 == "number") && (isFinite(data80)))){const err198 = {instancePath:instancePath+"/flows/" + i3+"/labelDx",schemaPath:"#/properties/flows/items/properties/labelDx/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err198];}else {vErrors.push(err198);}errors++;}}if(data65.labelDy !== undefined){let data81 = data65.labelDy;if(!((typeof data81 == "number") && (isFinite(data81)))){const err199 = {instancePath:instancePath+"/flows/" + i3+"/labelDy",schemaPath:"#/properties/flows/items/properties/labelDy/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err199];}else {vErrors.push(err199);}errors++;}}if(data65.labelSegment !== undefined){let data82 = data65.labelSegment;if(!(((typeof data82 == "number") && (!(data82 % 1) && !isNaN(data82))) && (isFinite(data82)))){const err200 = {instancePath:instancePath+"/flows/" + i3+"/labelSegment",schemaPath:"#/properties/flows/items/properties/labelSegment/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err200];}else {vErrors.push(err200);}errors++;}if((typeof data82 == "number") && (isFinite(data82))){if(data82 < 0 || isNaN(data82)){const err201 = {instancePath:instancePath+"/flows/" + i3+"/labelSegment",schemaPath:"#/properties/flows/items/properties/labelSegment/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err201];}else {vErrors.push(err201);}errors++;}}}if(data65.via !== undefined){let data83 = data65.via;if(Array.isArray(data83)){const len8 = data83.length;for(let i4=0; i4 2){const err202 = {instancePath:instancePath+"/flows/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err202];}else {vErrors.push(err202);}errors++;}if(data84.length < 2){const err203 = {instancePath:instancePath+"/flows/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err203];}else {vErrors.push(err203);}errors++;}const len9 = data84.length;if(len9 > 0){let data85 = data84[0];if(!((typeof data85 == "number") && (isFinite(data85)))){const err204 = {instancePath:instancePath+"/flows/" + i3+"/via/" + i4+"/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err204];}else {vErrors.push(err204);}errors++;}}if(len9 > 1){let data86 = data84[1];if(!((typeof data86 == "number") && (isFinite(data86)))){const err205 = {instancePath:instancePath+"/flows/" + i3+"/via/" + i4+"/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err205];}else {vErrors.push(err205);}errors++;}}const len10 = data84.length;if(!(len10 <= 2)){const err206 = {instancePath:instancePath+"/flows/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err206];}else {vErrors.push(err206);}errors++;}}else {const err207 = {instancePath:instancePath+"/flows/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err207];}else {vErrors.push(err207);}errors++;}}}else {const err208 = {instancePath:instancePath+"/flows/" + i3+"/via",schemaPath:"#/properties/flows/items/properties/via/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err208];}else {vErrors.push(err208);}errors++;}}if(data65.width !== undefined){let data87 = data65.width;if((typeof data87 == "number") && (isFinite(data87))){if(data87 < 0.5 || isNaN(data87)){const err209 = {instancePath:instancePath+"/flows/" + i3+"/width",schemaPath:"common.schema.json#/$defs/relationshipWidth/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0.5},message:"must be >= 0.5"};if(vErrors === null){vErrors = [err209];}else {vErrors.push(err209);}errors++;}}else {const err210 = {instancePath:instancePath+"/flows/" + i3+"/width",schemaPath:"common.schema.json#/$defs/relationshipWidth/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err210];}else {vErrors.push(err210);}errors++;}}}else {const err211 = {instancePath:instancePath+"/flows/" + i3,schemaPath:"#/properties/flows/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err211];}else {vErrors.push(err211);}errors++;}}}else {const err212 = {instancePath:instancePath+"/flows",schemaPath:"#/properties/flows/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err212];}else {vErrors.push(err212);}errors++;}}if(data.cards !== undefined){let data88 = data.cards;if(Array.isArray(data88)){const len11 = data88.length;for(let i5=0; i5 5){const err0 = {instancePath,schemaPath:"#/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}const len0 = data.length;for(let i0=0; i0 48){const err7 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}if(func3(data2) < 1){const err8 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}else {const err9 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}if(data0.focus !== undefined){let data3 = data0.focus;if(Array.isArray(data3)){if(data3.length < 1){const err10 = {instancePath:instancePath+"/" + i0+"/focus",schemaPath:"#/items/properties/focus/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}const len1 = data3.length;for(let i1=0; i1 140){const err14 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/maxLength",keyword:"maxLength",params:{limit: 140},message:"must NOT have more than 140 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}else {const err16 = {instancePath:instancePath+"/" + i0,schemaPath:"#/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}validate34.errors = vErrors;return errors === 0;}validate34.evaluated = {"items":true,"dynamicProps":false,"dynamicItems":false};function validate33(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://github.com/tt-a1i/archify/schemas/lifecycle.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate33.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.schema_version === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "schema_version"},message:"must have required property '"+"schema_version"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.diagram_type === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "diagram_type"},message:"must have required property '"+"diagram_type"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}if(data.meta === undefined){const err2 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "meta"},message:"must have required property '"+"meta"+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}if(data.lanes === undefined){const err3 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "lanes"},message:"must have required property '"+"lanes"+"'"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}if(data.states === undefined){const err4 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "states"},message:"must have required property '"+"states"+"'"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}if(data.transitions === undefined){const err5 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "transitions"},message:"must have required property '"+"transitions"+"'"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}for(const key0 in data){if(!(((((((key0 === "schema_version") || (key0 === "diagram_type")) || (key0 === "meta")) || (key0 === "lanes")) || (key0 === "states")) || (key0 === "transitions")) || (key0 === "cards"))){const err6 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.schema_version !== undefined){let data0 = data.schema_version;if(!((data0 === 1) || (data0 === 2))){const err7 = {instancePath:instancePath+"/schema_version",schemaPath:"#/properties/schema_version/enum",keyword:"enum",params:{allowedValues: schema133.properties.schema_version.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}if(data.diagram_type !== undefined){if("lifecycle" !== data.diagram_type){const err8 = {instancePath:instancePath+"/diagram_type",schemaPath:"#/properties/diagram_type/const",keyword:"const",params:{allowedValue: "lifecycle"},message:"must be equal to constant"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}if(data.meta !== undefined){let data2 = data.meta;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.title === undefined){const err9 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "title"},message:"must have required property '"+"title"+"'"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}if(data2.output === undefined){const err10 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "output"},message:"must have required property '"+"output"+"'"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}for(const key1 in data2){if(!(func1.call(schema133.properties.meta.properties, key1))){const err11 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}if(data2.title !== undefined){let data3 = data2.title;if(typeof data3 === "string"){if(func3(data3) < 1){const err12 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}else {const err13 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}if(data2.locale !== undefined){let data4 = data2.locale;if(typeof data4 === "string"){if(func3(data4) > 35){const err14 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/maxLength",keyword:"maxLength",params:{limit: 35},message:"must NOT have more than 35 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}if(!pattern4.test(data4)){const err15 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"+"\""};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}else {const err16 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}if(data2.translations !== undefined){let data5 = data2.translations;if(data5 && typeof data5 == "object" && !Array.isArray(data5)){if(Object.keys(data5).length > 600){const err17 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/maxProperties",keyword:"maxProperties",params:{limit: 600},message:"must NOT have more than 600 properties"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}for(const key2 in data5){const _errs15 = errors;if(typeof key2 === "string"){if(!pattern5.test(key2)){const err18 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"},message:"must match pattern \""+"^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"+"\"",propertyName:key2};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}var valid4 = _errs15 === errors;if(!valid4){const err19 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}for(const key3 in data5){let data6 = data5[key3];if(typeof data6 === "string"){if(func3(data6) > 400){const err20 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}if(func3(data6) < 1){const err21 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}else {const err22 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}}else {const err23 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}if(data2.subtitle !== undefined){if(typeof data2.subtitle !== "string"){const err24 = {instancePath:instancePath+"/meta/subtitle",schemaPath:"#/properties/meta/properties/subtitle/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}if(data2.output !== undefined){let data8 = data2.output;if(typeof data8 === "string"){if(!pattern6.test(data8)){const err25 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/0/pattern",keyword:"pattern",params:{pattern: "(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"},message:"must match pattern \""+"(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"+"\""};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}const _errs26 = errors;const _errs27 = errors;if(typeof data8 === "string"){if(!pattern7.test(data8)){const err26 = {};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}var valid8 = _errs27 === errors;if(valid8){const err27 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/1/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}else {errors = _errs26;if(vErrors !== null){if(_errs26){vErrors.length = _errs26;}else {vErrors = null;}}}const _errs29 = errors;const _errs30 = errors;if(typeof data8 === "string"){if(!pattern8.test(data8)){const err28 = {};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}var valid9 = _errs30 === errors;if(valid9){const err29 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/2/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}const _errs32 = errors;const _errs33 = errors;if(typeof data8 === "string"){if(!pattern9.test(data8)){const err30 = {};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}var valid10 = _errs33 === errors;if(valid10){const err31 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/3/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {errors = _errs32;if(vErrors !== null){if(_errs32){vErrors.length = _errs32;}else {vErrors = null;}}}const _errs35 = errors;const _errs36 = errors;if(typeof data8 === "string"){if(!pattern10.test(data8)){const err32 = {};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}var valid11 = _errs36 === errors;if(valid11){const err33 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/4/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}else {errors = _errs35;if(vErrors !== null){if(_errs35){vErrors.length = _errs35;}else {vErrors = null;}}}const _errs38 = errors;const _errs39 = errors;if(typeof data8 === "string"){if(!pattern11.test(data8)){const err34 = {};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}}var valid12 = _errs39 === errors;if(valid12){const err35 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/5/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}else {errors = _errs38;if(vErrors !== null){if(_errs38){vErrors.length = _errs38;}else {vErrors = null;}}}const _errs41 = errors;const _errs42 = errors;if(typeof data8 === "string"){if(!pattern12.test(data8)){const err36 = {};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}}var valid13 = _errs42 === errors;if(valid13){const err37 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/6/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {errors = _errs41;if(vErrors !== null){if(_errs41){vErrors.length = _errs41;}else {vErrors = null;}}}const _errs44 = errors;const _errs45 = errors;if(typeof data8 === "string"){if(!pattern13.test(data8)){const err38 = {};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}var valid14 = _errs45 === errors;if(valid14){const err39 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/7/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}else {errors = _errs44;if(vErrors !== null){if(_errs44){vErrors.length = _errs44;}else {vErrors = null;}}}if(typeof data8 === "string"){if(func3(data8) < 1){const err40 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}if(!pattern14.test(data8)){const err41 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/pattern",keyword:"pattern",params:{pattern: "^[^/]+(?:/[^/]+)*$"},message:"must match pattern \""+"^[^/]+(?:/[^/]+)*$"+"\""};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}else {const err42 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;}}if(data2.animation !== undefined){let data9 = data2.animation;if(!((data9 === "trace") || (data9 === "none"))){const err43 = {instancePath:instancePath+"/meta/animation",schemaPath:"common.schema.json#/$defs/animation/enum",keyword:"enum",params:{allowedValues: schema77.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}}if(data2.visual_preset !== undefined){let data10 = data2.visual_preset;if(!((((data10 === "classic") || (data10 === "signal-flow")) || (data10 === "blueprint")) || (data10 === "editorial"))){const err44 = {instancePath:instancePath+"/meta/visual_preset",schemaPath:"common.schema.json#/$defs/visualPreset/enum",keyword:"enum",params:{allowedValues: schema78.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}}if(data2.quality_profile !== undefined){let data11 = data2.quality_profile;if(!((data11 === "standard") || (data11 === "showcase"))){const err45 = {instancePath:instancePath+"/meta/quality_profile",schemaPath:"common.schema.json#/$defs/qualityProfile/enum",keyword:"enum",params:{allowedValues: schema79.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}}if(data2.repository !== undefined){let data12 = data2.repository;if(data12 && typeof data12 == "object" && !Array.isArray(data12)){if(data12.url === undefined){const err46 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}if(data12.revision === undefined){const err47 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "revision"},message:"must have required property '"+"revision"+"'"};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}for(const key4 in data12){if(!((((key4 === "url") || (key4 === "provider")) || (key4 === "link_mode")) || (key4 === "revision"))){const err48 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err48];}else {vErrors.push(err48);}errors++;}}if(data12.url !== undefined){let data13 = data12.url;if(typeof data13 === "string"){if(func3(data13) < 1){const err49 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err49];}else {vErrors.push(err49);}errors++;}}else {const err50 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err50];}else {vErrors.push(err50);}errors++;}}if(data12.provider !== undefined){let data14 = data12.provider;if(!((data14 === "github") || (data14 === "gitee"))){const err51 = {instancePath:instancePath+"/meta/repository/provider",schemaPath:"common.schema.json#/$defs/repository/properties/provider/enum",keyword:"enum",params:{allowedValues: schema36.properties.provider.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err51];}else {vErrors.push(err51);}errors++;}}if(data12.link_mode !== undefined){let data15 = data12.link_mode;if(!((data15 === "web") || (data15 === "local-only"))){const err52 = {instancePath:instancePath+"/meta/repository/link_mode",schemaPath:"common.schema.json#/$defs/repository/properties/link_mode/enum",keyword:"enum",params:{allowedValues: schema36.properties.link_mode.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err52];}else {vErrors.push(err52);}errors++;}}if(data12.revision !== undefined){let data16 = data12.revision;if(typeof data16 === "string"){if(!pattern15.test(data16)){const err53 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/pattern",keyword:"pattern",params:{pattern: "^[a-fA-F0-9]{40}$"},message:"must match pattern \""+"^[a-fA-F0-9]{40}$"+"\""};if(vErrors === null){vErrors = [err53];}else {vErrors.push(err53);}errors++;}}else {const err54 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err54];}else {vErrors.push(err54);}errors++;}}}else {const err55 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err55];}else {vErrors.push(err55);}errors++;}}if(data2.views !== undefined){if(!(validate34(data2.views, {instancePath:instancePath+"/meta/views",parentData:data2,parentDataProperty:"views",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate34.errors : vErrors.concat(validate34.errors);errors = vErrors.length;}}if(data2.legend !== undefined){let data18 = data2.legend;if(data18 && typeof data18 == "object" && !Array.isArray(data18)){for(const key5 in data18){if(!((key5 === "mode") || (key5 === "entries"))){const err56 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err56];}else {vErrors.push(err56);}errors++;}}if(data18.mode !== undefined){let data19 = data18.mode;if(!(((data19 === "auto") || (data19 === "all")) || (data19 === "hidden"))){const err57 = {instancePath:instancePath+"/meta/legend/mode",schemaPath:"common.schema.json#/$defs/legendMode/enum",keyword:"enum",params:{allowedValues: schema40.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err57];}else {vErrors.push(err57);}errors++;}}if(data18.entries !== undefined){let data20 = data18.entries;if(data20 && typeof data20 == "object" && !Array.isArray(data20)){for(const key6 in data20){if(!((((((((key6 === "start") || (key6 === "active")) || (key6 === "waiting")) || (key6 === "decision")) || (key6 === "success")) || (key6 === "failure")) || (key6 === "neutral")) || (key6 === "external"))){const err58 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err58];}else {vErrors.push(err58);}errors++;}}if(data20.start !== undefined){let data21 = data20.start;if(data21 && typeof data21 == "object" && !Array.isArray(data21)){if(Object.keys(data21).length < 1){const err59 = {instancePath:instancePath+"/meta/legend/entries/start",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err59];}else {vErrors.push(err59);}errors++;}for(const key7 in data21){if(!((key7 === "label") || (key7 === "visible"))){const err60 = {instancePath:instancePath+"/meta/legend/entries/start",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err60];}else {vErrors.push(err60);}errors++;}}if(data21.label !== undefined){let data22 = data21.label;if(typeof data22 === "string"){if(func3(data22) > 80){const err61 = {instancePath:instancePath+"/meta/legend/entries/start/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err61];}else {vErrors.push(err61);}errors++;}if(func3(data22) < 1){const err62 = {instancePath:instancePath+"/meta/legend/entries/start/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err62];}else {vErrors.push(err62);}errors++;}}else {const err63 = {instancePath:instancePath+"/meta/legend/entries/start/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err63];}else {vErrors.push(err63);}errors++;}}if(data21.visible !== undefined){if(typeof data21.visible !== "boolean"){const err64 = {instancePath:instancePath+"/meta/legend/entries/start/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err64];}else {vErrors.push(err64);}errors++;}}}else {const err65 = {instancePath:instancePath+"/meta/legend/entries/start",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err65];}else {vErrors.push(err65);}errors++;}}if(data20.active !== undefined){let data24 = data20.active;if(data24 && typeof data24 == "object" && !Array.isArray(data24)){if(Object.keys(data24).length < 1){const err66 = {instancePath:instancePath+"/meta/legend/entries/active",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err66];}else {vErrors.push(err66);}errors++;}for(const key8 in data24){if(!((key8 === "label") || (key8 === "visible"))){const err67 = {instancePath:instancePath+"/meta/legend/entries/active",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err67];}else {vErrors.push(err67);}errors++;}}if(data24.label !== undefined){let data25 = data24.label;if(typeof data25 === "string"){if(func3(data25) > 80){const err68 = {instancePath:instancePath+"/meta/legend/entries/active/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err68];}else {vErrors.push(err68);}errors++;}if(func3(data25) < 1){const err69 = {instancePath:instancePath+"/meta/legend/entries/active/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err69];}else {vErrors.push(err69);}errors++;}}else {const err70 = {instancePath:instancePath+"/meta/legend/entries/active/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err70];}else {vErrors.push(err70);}errors++;}}if(data24.visible !== undefined){if(typeof data24.visible !== "boolean"){const err71 = {instancePath:instancePath+"/meta/legend/entries/active/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err71];}else {vErrors.push(err71);}errors++;}}}else {const err72 = {instancePath:instancePath+"/meta/legend/entries/active",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err72];}else {vErrors.push(err72);}errors++;}}if(data20.waiting !== undefined){let data27 = data20.waiting;if(data27 && typeof data27 == "object" && !Array.isArray(data27)){if(Object.keys(data27).length < 1){const err73 = {instancePath:instancePath+"/meta/legend/entries/waiting",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err73];}else {vErrors.push(err73);}errors++;}for(const key9 in data27){if(!((key9 === "label") || (key9 === "visible"))){const err74 = {instancePath:instancePath+"/meta/legend/entries/waiting",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key9},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err74];}else {vErrors.push(err74);}errors++;}}if(data27.label !== undefined){let data28 = data27.label;if(typeof data28 === "string"){if(func3(data28) > 80){const err75 = {instancePath:instancePath+"/meta/legend/entries/waiting/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err75];}else {vErrors.push(err75);}errors++;}if(func3(data28) < 1){const err76 = {instancePath:instancePath+"/meta/legend/entries/waiting/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err76];}else {vErrors.push(err76);}errors++;}}else {const err77 = {instancePath:instancePath+"/meta/legend/entries/waiting/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err77];}else {vErrors.push(err77);}errors++;}}if(data27.visible !== undefined){if(typeof data27.visible !== "boolean"){const err78 = {instancePath:instancePath+"/meta/legend/entries/waiting/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err78];}else {vErrors.push(err78);}errors++;}}}else {const err79 = {instancePath:instancePath+"/meta/legend/entries/waiting",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err79];}else {vErrors.push(err79);}errors++;}}if(data20.decision !== undefined){let data30 = data20.decision;if(data30 && typeof data30 == "object" && !Array.isArray(data30)){if(Object.keys(data30).length < 1){const err80 = {instancePath:instancePath+"/meta/legend/entries/decision",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err80];}else {vErrors.push(err80);}errors++;}for(const key10 in data30){if(!((key10 === "label") || (key10 === "visible"))){const err81 = {instancePath:instancePath+"/meta/legend/entries/decision",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key10},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err81];}else {vErrors.push(err81);}errors++;}}if(data30.label !== undefined){let data31 = data30.label;if(typeof data31 === "string"){if(func3(data31) > 80){const err82 = {instancePath:instancePath+"/meta/legend/entries/decision/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err82];}else {vErrors.push(err82);}errors++;}if(func3(data31) < 1){const err83 = {instancePath:instancePath+"/meta/legend/entries/decision/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err83];}else {vErrors.push(err83);}errors++;}}else {const err84 = {instancePath:instancePath+"/meta/legend/entries/decision/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err84];}else {vErrors.push(err84);}errors++;}}if(data30.visible !== undefined){if(typeof data30.visible !== "boolean"){const err85 = {instancePath:instancePath+"/meta/legend/entries/decision/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err85];}else {vErrors.push(err85);}errors++;}}}else {const err86 = {instancePath:instancePath+"/meta/legend/entries/decision",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err86];}else {vErrors.push(err86);}errors++;}}if(data20.success !== undefined){let data33 = data20.success;if(data33 && typeof data33 == "object" && !Array.isArray(data33)){if(Object.keys(data33).length < 1){const err87 = {instancePath:instancePath+"/meta/legend/entries/success",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err87];}else {vErrors.push(err87);}errors++;}for(const key11 in data33){if(!((key11 === "label") || (key11 === "visible"))){const err88 = {instancePath:instancePath+"/meta/legend/entries/success",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key11},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err88];}else {vErrors.push(err88);}errors++;}}if(data33.label !== undefined){let data34 = data33.label;if(typeof data34 === "string"){if(func3(data34) > 80){const err89 = {instancePath:instancePath+"/meta/legend/entries/success/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err89];}else {vErrors.push(err89);}errors++;}if(func3(data34) < 1){const err90 = {instancePath:instancePath+"/meta/legend/entries/success/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err90];}else {vErrors.push(err90);}errors++;}}else {const err91 = {instancePath:instancePath+"/meta/legend/entries/success/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err91];}else {vErrors.push(err91);}errors++;}}if(data33.visible !== undefined){if(typeof data33.visible !== "boolean"){const err92 = {instancePath:instancePath+"/meta/legend/entries/success/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err92];}else {vErrors.push(err92);}errors++;}}}else {const err93 = {instancePath:instancePath+"/meta/legend/entries/success",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err93];}else {vErrors.push(err93);}errors++;}}if(data20.failure !== undefined){let data36 = data20.failure;if(data36 && typeof data36 == "object" && !Array.isArray(data36)){if(Object.keys(data36).length < 1){const err94 = {instancePath:instancePath+"/meta/legend/entries/failure",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err94];}else {vErrors.push(err94);}errors++;}for(const key12 in data36){if(!((key12 === "label") || (key12 === "visible"))){const err95 = {instancePath:instancePath+"/meta/legend/entries/failure",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key12},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err95];}else {vErrors.push(err95);}errors++;}}if(data36.label !== undefined){let data37 = data36.label;if(typeof data37 === "string"){if(func3(data37) > 80){const err96 = {instancePath:instancePath+"/meta/legend/entries/failure/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err96];}else {vErrors.push(err96);}errors++;}if(func3(data37) < 1){const err97 = {instancePath:instancePath+"/meta/legend/entries/failure/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err97];}else {vErrors.push(err97);}errors++;}}else {const err98 = {instancePath:instancePath+"/meta/legend/entries/failure/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err98];}else {vErrors.push(err98);}errors++;}}if(data36.visible !== undefined){if(typeof data36.visible !== "boolean"){const err99 = {instancePath:instancePath+"/meta/legend/entries/failure/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err99];}else {vErrors.push(err99);}errors++;}}}else {const err100 = {instancePath:instancePath+"/meta/legend/entries/failure",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err100];}else {vErrors.push(err100);}errors++;}}if(data20.neutral !== undefined){let data39 = data20.neutral;if(data39 && typeof data39 == "object" && !Array.isArray(data39)){if(Object.keys(data39).length < 1){const err101 = {instancePath:instancePath+"/meta/legend/entries/neutral",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err101];}else {vErrors.push(err101);}errors++;}for(const key13 in data39){if(!((key13 === "label") || (key13 === "visible"))){const err102 = {instancePath:instancePath+"/meta/legend/entries/neutral",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key13},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err102];}else {vErrors.push(err102);}errors++;}}if(data39.label !== undefined){let data40 = data39.label;if(typeof data40 === "string"){if(func3(data40) > 80){const err103 = {instancePath:instancePath+"/meta/legend/entries/neutral/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err103];}else {vErrors.push(err103);}errors++;}if(func3(data40) < 1){const err104 = {instancePath:instancePath+"/meta/legend/entries/neutral/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err104];}else {vErrors.push(err104);}errors++;}}else {const err105 = {instancePath:instancePath+"/meta/legend/entries/neutral/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err105];}else {vErrors.push(err105);}errors++;}}if(data39.visible !== undefined){if(typeof data39.visible !== "boolean"){const err106 = {instancePath:instancePath+"/meta/legend/entries/neutral/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err106];}else {vErrors.push(err106);}errors++;}}}else {const err107 = {instancePath:instancePath+"/meta/legend/entries/neutral",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err107];}else {vErrors.push(err107);}errors++;}}if(data20.external !== undefined){let data42 = data20.external;if(data42 && typeof data42 == "object" && !Array.isArray(data42)){if(Object.keys(data42).length < 1){const err108 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err108];}else {vErrors.push(err108);}errors++;}for(const key14 in data42){if(!((key14 === "label") || (key14 === "visible"))){const err109 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key14},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err109];}else {vErrors.push(err109);}errors++;}}if(data42.label !== undefined){let data43 = data42.label;if(typeof data43 === "string"){if(func3(data43) > 80){const err110 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err110];}else {vErrors.push(err110);}errors++;}if(func3(data43) < 1){const err111 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err111];}else {vErrors.push(err111);}errors++;}}else {const err112 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err112];}else {vErrors.push(err112);}errors++;}}if(data42.visible !== undefined){if(typeof data42.visible !== "boolean"){const err113 = {instancePath:instancePath+"/meta/legend/entries/external/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err113];}else {vErrors.push(err113);}errors++;}}}else {const err114 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err114];}else {vErrors.push(err114);}errors++;}}}else {const err115 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err115];}else {vErrors.push(err115);}errors++;}}}else {const err116 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err116];}else {vErrors.push(err116);}errors++;}}if(data2.viewBox !== undefined){let data45 = data2.viewBox;if(Array.isArray(data45)){if(data45.length > 2){const err117 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err117];}else {vErrors.push(err117);}errors++;}if(data45.length < 2){const err118 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err118];}else {vErrors.push(err118);}errors++;}const len0 = data45.length;if(len0 > 0){let data46 = data45[0];if((typeof data46 == "number") && (isFinite(data46))){if(data46 < 420 || isNaN(data46)){const err119 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 420},message:"must be >= 420"};if(vErrors === null){vErrors = [err119];}else {vErrors.push(err119);}errors++;}}else {const err120 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err120];}else {vErrors.push(err120);}errors++;}}if(len0 > 1){let data47 = data45[1];if((typeof data47 == "number") && (isFinite(data47))){if(data47 < 300 || isNaN(data47)){const err121 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/minimum",keyword:"minimum",params:{comparison: ">=", limit: 300},message:"must be >= 300"};if(vErrors === null){vErrors = [err121];}else {vErrors.push(err121);}errors++;}}else {const err122 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err122];}else {vErrors.push(err122);}errors++;}}const len1 = data45.length;if(!(len1 <= 2)){const err123 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err123];}else {vErrors.push(err123);}errors++;}}else {const err124 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err124];}else {vErrors.push(err124);}errors++;}}}else {const err125 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err125];}else {vErrors.push(err125);}errors++;}}if(data.lanes !== undefined){let data48 = data.lanes;if(Array.isArray(data48)){if(data48.length > 4){const err126 = {instancePath:instancePath+"/lanes",schemaPath:"#/properties/lanes/maxItems",keyword:"maxItems",params:{limit: 4},message:"must NOT have more than 4 items"};if(vErrors === null){vErrors = [err126];}else {vErrors.push(err126);}errors++;}if(data48.length < 1){const err127 = {instancePath:instancePath+"/lanes",schemaPath:"#/properties/lanes/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err127];}else {vErrors.push(err127);}errors++;}const len2 = data48.length;for(let i0=0; i0 80){const err152 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err152];}else {vErrors.push(err152);}errors++;}if(!pattern29.test(data60)){const err153 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/pattern",keyword:"pattern",params:{pattern: "^[^\\r\\n]+$"},message:"must match pattern \""+"^[^\\r\\n]+$"+"\""};if(vErrors === null){vErrors = [err153];}else {vErrors.push(err153);}errors++;}}var _valid1 = _errs174 === errors;valid51 = valid51 || _valid1;const _errs175 = errors;if(typeof data60 === "string"){if(!pattern30.test(data60)){const err154 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/1/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err154];}else {vErrors.push(err154);}errors++;}}var _valid1 = _errs175 === errors;valid51 = valid51 || _valid1;if(!valid51){const err155 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err155];}else {vErrors.push(err155);}errors++;}else {errors = _errs173;if(vErrors !== null){if(_errs173){vErrors.length = _errs173;}else {vErrors = null;}}}if(typeof data60 === "string"){if(func3(data60) > 2048){const err156 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err156];}else {vErrors.push(err156);}errors++;}if(func3(data60) < 1){const err157 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err157];}else {vErrors.push(err157);}errors++;}}else {const err158 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err158];}else {vErrors.push(err158);}errors++;}var _valid0 = _errs171 === errors;if(_valid0){valid50 = true;passing0 = 0;}const _errs176 = errors;if(data60 && typeof data60 == "object" && !Array.isArray(data60)){if(data60.url === undefined){const err159 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err159];}else {vErrors.push(err159);}errors++;}if(data60.sha256 === undefined){const err160 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "sha256"},message:"must have required property '"+"sha256"+"'"};if(vErrors === null){vErrors = [err160];}else {vErrors.push(err160);}errors++;}for(const key17 in data60){if(!((key17 === "url") || (key17 === "sha256"))){const err161 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key17},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err161];}else {vErrors.push(err161);}errors++;}}if(data60.url !== undefined){let data61 = data60.url;if(typeof data61 === "string"){if(func3(data61) > 2048){const err162 = {instancePath:instancePath+"/states/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err162];}else {vErrors.push(err162);}errors++;}if(func3(data61) < 8){const err163 = {instancePath:instancePath+"/states/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"};if(vErrors === null){vErrors = [err163];}else {vErrors.push(err163);}errors++;}if(!pattern30.test(data61)){const err164 = {instancePath:instancePath+"/states/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err164];}else {vErrors.push(err164);}errors++;}}else {const err165 = {instancePath:instancePath+"/states/" + i1+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err165];}else {vErrors.push(err165);}errors++;}}if(data60.sha256 !== undefined){let data62 = data60.sha256;if(typeof data62 === "string"){if(!pattern32.test(data62)){const err166 = {instancePath:instancePath+"/states/" + i1+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/pattern",keyword:"pattern",params:{pattern: "^[a-f0-9]{64}$"},message:"must match pattern \""+"^[a-f0-9]{64}$"+"\""};if(vErrors === null){vErrors = [err166];}else {vErrors.push(err166);}errors++;}}else {const err167 = {instancePath:instancePath+"/states/" + i1+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err167];}else {vErrors.push(err167);}errors++;}}}else {const err168 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err168];}else {vErrors.push(err168);}errors++;}var _valid0 = _errs176 === errors;if(_valid0 && valid50){valid50 = false;passing0 = [passing0, 1];}else {if(_valid0){valid50 = true;passing0 = 1;}}if(!valid50){const err169 = {instancePath:instancePath+"/states/" + i1+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err169];}else {vErrors.push(err169);}errors++;}else {errors = _errs170;if(vErrors !== null){if(_errs170){vErrors.length = _errs170;}else {vErrors = null;}}}}if(data53.sources !== undefined){let data63 = data53.sources;if(Array.isArray(data63)){if(data63.length > 3){const err170 = {instancePath:instancePath+"/states/" + i1+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err170];}else {vErrors.push(err170);}errors++;}if(data63.length < 1){const err171 = {instancePath:instancePath+"/states/" + i1+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err171];}else {vErrors.push(err171);}errors++;}const len4 = data63.length;for(let i2=0; i2 240){const err174 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"};if(vErrors === null){vErrors = [err174];}else {vErrors.push(err174);}errors++;}if(func3(data65) < 1){const err175 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err175];}else {vErrors.push(err175);}errors++;}}else {const err176 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err176];}else {vErrors.push(err176);}errors++;}}if(data64.line !== undefined){let data66 = data64.line;if(!(((typeof data66 == "number") && (!(data66 % 1) && !isNaN(data66))) && (isFinite(data66)))){const err177 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err177];}else {vErrors.push(err177);}errors++;}if((typeof data66 == "number") && (isFinite(data66))){if(data66 < 1 || isNaN(data66)){const err178 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err178];}else {vErrors.push(err178);}errors++;}}}if(data64.end_line !== undefined){let data67 = data64.end_line;if(!(((typeof data67 == "number") && (!(data67 % 1) && !isNaN(data67))) && (isFinite(data67)))){const err179 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err179];}else {vErrors.push(err179);}errors++;}if((typeof data67 == "number") && (isFinite(data67))){if(data67 < 1 || isNaN(data67)){const err180 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err180];}else {vErrors.push(err180);}errors++;}}}if(data64.label !== undefined){let data68 = data64.label;if(typeof data68 === "string"){if(func3(data68) > 48){const err181 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err181];}else {vErrors.push(err181);}errors++;}if(func3(data68) < 1){const err182 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err182];}else {vErrors.push(err182);}errors++;}}else {const err183 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err183];}else {vErrors.push(err183);}errors++;}}}else {const err184 = {instancePath:instancePath+"/states/" + i1+"/sources/" + i2,schemaPath:"common.schema.json#/$defs/sourceReferences/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err184];}else {vErrors.push(err184);}errors++;}}}else {const err185 = {instancePath:instancePath+"/states/" + i1+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err185];}else {vErrors.push(err185);}errors++;}}if(data53.step !== undefined){if(typeof data53.step !== "string"){const err186 = {instancePath:instancePath+"/states/" + i1+"/step",schemaPath:"#/properties/states/items/properties/step/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err186];}else {vErrors.push(err186);}errors++;}}if(data53.lane !== undefined){let data70 = data53.lane;if(typeof data70 === "string"){if(!pattern16.test(data70)){const err187 = {instancePath:instancePath+"/states/" + i1+"/lane",schemaPath:"common.schema.json#/$defs/id/pattern",keyword:"pattern",params:{pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$"},message:"must match pattern \""+"^[a-zA-Z][a-zA-Z0-9_-]*$"+"\""};if(vErrors === null){vErrors = [err187];}else {vErrors.push(err187);}errors++;}}else {const err188 = {instancePath:instancePath+"/states/" + i1+"/lane",schemaPath:"common.schema.json#/$defs/id/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err188];}else {vErrors.push(err188);}errors++;}}if(data53.col !== undefined){let data71 = data53.col;if(!(((typeof data71 == "number") && (!(data71 % 1) && !isNaN(data71))) && (isFinite(data71)))){const err189 = {instancePath:instancePath+"/states/" + i1+"/col",schemaPath:"#/properties/states/items/properties/col/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err189];}else {vErrors.push(err189);}errors++;}if((typeof data71 == "number") && (isFinite(data71))){if(data71 > 4 || isNaN(data71)){const err190 = {instancePath:instancePath+"/states/" + i1+"/col",schemaPath:"#/properties/states/items/properties/col/maximum",keyword:"maximum",params:{comparison: "<=", limit: 4},message:"must be <= 4"};if(vErrors === null){vErrors = [err190];}else {vErrors.push(err190);}errors++;}if(data71 < 0 || isNaN(data71)){const err191 = {instancePath:instancePath+"/states/" + i1+"/col",schemaPath:"#/properties/states/items/properties/col/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err191];}else {vErrors.push(err191);}errors++;}}}if(data53.width !== undefined){let data72 = data53.width;if((typeof data72 == "number") && (isFinite(data72))){if(data72 < 48 || isNaN(data72)){const err192 = {instancePath:instancePath+"/states/" + i1+"/width",schemaPath:"#/properties/states/items/properties/width/minimum",keyword:"minimum",params:{comparison: ">=", limit: 48},message:"must be >= 48"};if(vErrors === null){vErrors = [err192];}else {vErrors.push(err192);}errors++;}}else {const err193 = {instancePath:instancePath+"/states/" + i1+"/width",schemaPath:"#/properties/states/items/properties/width/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err193];}else {vErrors.push(err193);}errors++;}}if(data53.height !== undefined){let data73 = data53.height;if((typeof data73 == "number") && (isFinite(data73))){if(data73 < 36 || isNaN(data73)){const err194 = {instancePath:instancePath+"/states/" + i1+"/height",schemaPath:"#/properties/states/items/properties/height/minimum",keyword:"minimum",params:{comparison: ">=", limit: 36},message:"must be >= 36"};if(vErrors === null){vErrors = [err194];}else {vErrors.push(err194);}errors++;}}else {const err195 = {instancePath:instancePath+"/states/" + i1+"/height",schemaPath:"#/properties/states/items/properties/height/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err195];}else {vErrors.push(err195);}errors++;}}if(data53.yOffset !== undefined){let data74 = data53.yOffset;if(!((typeof data74 == "number") && (isFinite(data74)))){const err196 = {instancePath:instancePath+"/states/" + i1+"/yOffset",schemaPath:"#/properties/states/items/properties/yOffset/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err196];}else {vErrors.push(err196);}errors++;}}}else {const err197 = {instancePath:instancePath+"/states/" + i1,schemaPath:"#/properties/states/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err197];}else {vErrors.push(err197);}errors++;}}}else {const err198 = {instancePath:instancePath+"/states",schemaPath:"#/properties/states/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err198];}else {vErrors.push(err198);}errors++;}}if(data.transitions !== undefined){let data75 = data.transitions;if(Array.isArray(data75)){const len5 = data75.length;for(let i3=0; i3=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err216];}else {vErrors.push(err216);}errors++;}}else {const err217 = {instancePath:instancePath+"/transitions/" + i3+"/cornerRadius",schemaPath:"#/properties/transitions/items/properties/cornerRadius/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err217];}else {vErrors.push(err217);}errors++;}}if(data76.labelAt !== undefined){let data89 = data76.labelAt;if(Array.isArray(data89)){if(data89.length > 2){const err218 = {instancePath:instancePath+"/transitions/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err218];}else {vErrors.push(err218);}errors++;}if(data89.length < 2){const err219 = {instancePath:instancePath+"/transitions/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err219];}else {vErrors.push(err219);}errors++;}const len6 = data89.length;if(len6 > 0){let data90 = data89[0];if(!((typeof data90 == "number") && (isFinite(data90)))){const err220 = {instancePath:instancePath+"/transitions/" + i3+"/labelAt/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err220];}else {vErrors.push(err220);}errors++;}}if(len6 > 1){let data91 = data89[1];if(!((typeof data91 == "number") && (isFinite(data91)))){const err221 = {instancePath:instancePath+"/transitions/" + i3+"/labelAt/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err221];}else {vErrors.push(err221);}errors++;}}const len7 = data89.length;if(!(len7 <= 2)){const err222 = {instancePath:instancePath+"/transitions/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err222];}else {vErrors.push(err222);}errors++;}}else {const err223 = {instancePath:instancePath+"/transitions/" + i3+"/labelAt",schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err223];}else {vErrors.push(err223);}errors++;}}if(data76.labelDx !== undefined){let data92 = data76.labelDx;if(!((typeof data92 == "number") && (isFinite(data92)))){const err224 = {instancePath:instancePath+"/transitions/" + i3+"/labelDx",schemaPath:"#/properties/transitions/items/properties/labelDx/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err224];}else {vErrors.push(err224);}errors++;}}if(data76.labelDy !== undefined){let data93 = data76.labelDy;if(!((typeof data93 == "number") && (isFinite(data93)))){const err225 = {instancePath:instancePath+"/transitions/" + i3+"/labelDy",schemaPath:"#/properties/transitions/items/properties/labelDy/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err225];}else {vErrors.push(err225);}errors++;}}if(data76.labelSegment !== undefined){let data94 = data76.labelSegment;if(!(((typeof data94 == "number") && (!(data94 % 1) && !isNaN(data94))) && (isFinite(data94)))){const err226 = {instancePath:instancePath+"/transitions/" + i3+"/labelSegment",schemaPath:"#/properties/transitions/items/properties/labelSegment/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err226];}else {vErrors.push(err226);}errors++;}if((typeof data94 == "number") && (isFinite(data94))){if(data94 < 0 || isNaN(data94)){const err227 = {instancePath:instancePath+"/transitions/" + i3+"/labelSegment",schemaPath:"#/properties/transitions/items/properties/labelSegment/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err227];}else {vErrors.push(err227);}errors++;}}}if(data76.via !== undefined){let data95 = data76.via;if(Array.isArray(data95)){const len8 = data95.length;for(let i4=0; i4 2){const err228 = {instancePath:instancePath+"/transitions/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err228];}else {vErrors.push(err228);}errors++;}if(data96.length < 2){const err229 = {instancePath:instancePath+"/transitions/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err229];}else {vErrors.push(err229);}errors++;}const len9 = data96.length;if(len9 > 0){let data97 = data96[0];if(!((typeof data97 == "number") && (isFinite(data97)))){const err230 = {instancePath:instancePath+"/transitions/" + i3+"/via/" + i4+"/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err230];}else {vErrors.push(err230);}errors++;}}if(len9 > 1){let data98 = data96[1];if(!((typeof data98 == "number") && (isFinite(data98)))){const err231 = {instancePath:instancePath+"/transitions/" + i3+"/via/" + i4+"/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err231];}else {vErrors.push(err231);}errors++;}}const len10 = data96.length;if(!(len10 <= 2)){const err232 = {instancePath:instancePath+"/transitions/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err232];}else {vErrors.push(err232);}errors++;}}else {const err233 = {instancePath:instancePath+"/transitions/" + i3+"/via/" + i4,schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err233];}else {vErrors.push(err233);}errors++;}}}else {const err234 = {instancePath:instancePath+"/transitions/" + i3+"/via",schemaPath:"#/properties/transitions/items/properties/via/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err234];}else {vErrors.push(err234);}errors++;}}if(data76.width !== undefined){let data99 = data76.width;if((typeof data99 == "number") && (isFinite(data99))){if(data99 < 0.5 || isNaN(data99)){const err235 = {instancePath:instancePath+"/transitions/" + i3+"/width",schemaPath:"common.schema.json#/$defs/relationshipWidth/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0.5},message:"must be >= 0.5"};if(vErrors === null){vErrors = [err235];}else {vErrors.push(err235);}errors++;}}else {const err236 = {instancePath:instancePath+"/transitions/" + i3+"/width",schemaPath:"common.schema.json#/$defs/relationshipWidth/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err236];}else {vErrors.push(err236);}errors++;}}}else {const err237 = {instancePath:instancePath+"/transitions/" + i3,schemaPath:"#/properties/transitions/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err237];}else {vErrors.push(err237);}errors++;}}}else {const err238 = {instancePath:instancePath+"/transitions",schemaPath:"#/properties/transitions/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err238];}else {vErrors.push(err238);}errors++;}}if(data.cards !== undefined){let data100 = data.cards;if(Array.isArray(data100)){const len11 = data100.length;for(let i5=0; i5 5){const err0 = {instancePath,schemaPath:"#/maxItems",keyword:"maxItems",params:{limit: 5},message:"must NOT have more than 5 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}const len0 = data.length;for(let i0=0; i0 48){const err7 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}if(func3(data2) < 1){const err8 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}else {const err9 = {instancePath:instancePath+"/" + i0+"/label",schemaPath:"#/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}if(data0.focus !== undefined){let data3 = data0.focus;if(Array.isArray(data3)){if(data3.length < 1){const err10 = {instancePath:instancePath+"/" + i0+"/focus",schemaPath:"#/items/properties/focus/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}const len1 = data3.length;for(let i1=0; i1 140){const err14 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/maxLength",keyword:"maxLength",params:{limit: 140},message:"must NOT have more than 140 characters"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/" + i0+"/note",schemaPath:"#/items/properties/note/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}}else {const err16 = {instancePath:instancePath+"/" + i0,schemaPath:"#/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}else {const err17 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}validate37.errors = vErrors;return errors === 0;}validate37.evaluated = {"items":true,"dynamicProps":false,"dynamicItems":false};function validate36(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="https://github.com/tt-a1i/archify/schemas/architecture.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate36.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.schema_version === undefined){const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "schema_version"},message:"must have required property '"+"schema_version"+"'"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data.diagram_type === undefined){const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "diagram_type"},message:"must have required property '"+"diagram_type"+"'"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}if(data.meta === undefined){const err2 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "meta"},message:"must have required property '"+"meta"+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}if(data.components === undefined){const err3 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}for(const key0 in data){if(!((((((((key0 === "schema_version") || (key0 === "diagram_type")) || (key0 === "meta")) || (key0 === "layout")) || (key0 === "components")) || (key0 === "boundaries")) || (key0 === "connections")) || (key0 === "cards"))){const err4 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}if(data.schema_version !== undefined){if(1 !== data.schema_version){const err5 = {instancePath:instancePath+"/schema_version",schemaPath:"#/properties/schema_version/const",keyword:"const",params:{allowedValue: 1},message:"must be equal to constant"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}if(data.diagram_type !== undefined){if("architecture" !== data.diagram_type){const err6 = {instancePath:instancePath+"/diagram_type",schemaPath:"#/properties/diagram_type/const",keyword:"const",params:{allowedValue: "architecture"},message:"must be equal to constant"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.meta !== undefined){let data2 = data.meta;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.title === undefined){const err7 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "title"},message:"must have required property '"+"title"+"'"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}if(data2.output === undefined){const err8 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/required",keyword:"required",params:{missingProperty: "output"},message:"must have required property '"+"output"+"'"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}for(const key1 in data2){if(!(func1.call(schema169.properties.meta.properties, key1))){const err9 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}if(data2.title !== undefined){let data3 = data2.title;if(typeof data3 === "string"){if(func3(data3) < 1){const err10 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}}else {const err11 = {instancePath:instancePath+"/meta/title",schemaPath:"#/properties/meta/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}if(data2.locale !== undefined){let data4 = data2.locale;if(typeof data4 === "string"){if(func3(data4) > 35){const err12 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/maxLength",keyword:"maxLength",params:{limit: 35},message:"must NOT have more than 35 characters"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}if(!pattern4.test(data4)){const err13 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/pattern",keyword:"pattern",params:{pattern: "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"},message:"must match pattern \""+"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$"+"\""};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}else {const err14 = {instancePath:instancePath+"/meta/locale",schemaPath:"common.schema.json#/$defs/locale/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}if(data2.translations !== undefined){let data5 = data2.translations;if(data5 && typeof data5 == "object" && !Array.isArray(data5)){if(Object.keys(data5).length > 600){const err15 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/maxProperties",keyword:"maxProperties",params:{limit: 600},message:"must NOT have more than 600 properties"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}for(const key2 in data5){const _errs15 = errors;if(typeof key2 === "string"){if(!pattern5.test(key2)){const err16 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"},message:"must match pattern \""+"^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$"+"\"",propertyName:key2};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}var valid4 = _errs15 === errors;if(!valid4){const err17 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}for(const key3 in data5){let data6 = data5[key3];if(typeof data6 === "string"){if(func3(data6) > 400){const err18 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/maxLength",keyword:"maxLength",params:{limit: 400},message:"must NOT have more than 400 characters"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}if(func3(data6) < 1){const err19 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}else {const err20 = {instancePath:instancePath+"/meta/translations/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"common.schema.json#/$defs/translations/additionalProperties/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}}else {const err21 = {instancePath:instancePath+"/meta/translations",schemaPath:"common.schema.json#/$defs/translations/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}if(data2.subtitle !== undefined){if(typeof data2.subtitle !== "string"){const err22 = {instancePath:instancePath+"/meta/subtitle",schemaPath:"#/properties/meta/properties/subtitle/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}if(data2.output !== undefined){let data8 = data2.output;if(typeof data8 === "string"){if(!pattern6.test(data8)){const err23 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/0/pattern",keyword:"pattern",params:{pattern: "(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"},message:"must match pattern \""+"(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$"+"\""};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}const _errs26 = errors;const _errs27 = errors;if(typeof data8 === "string"){if(!pattern7.test(data8)){const err24 = {};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}var valid8 = _errs27 === errors;if(valid8){const err25 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/1/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}else {errors = _errs26;if(vErrors !== null){if(_errs26){vErrors.length = _errs26;}else {vErrors = null;}}}const _errs29 = errors;const _errs30 = errors;if(typeof data8 === "string"){if(!pattern8.test(data8)){const err26 = {};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}var valid9 = _errs30 === errors;if(valid9){const err27 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/2/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}else {errors = _errs29;if(vErrors !== null){if(_errs29){vErrors.length = _errs29;}else {vErrors = null;}}}const _errs32 = errors;const _errs33 = errors;if(typeof data8 === "string"){if(!pattern9.test(data8)){const err28 = {};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}var valid10 = _errs33 === errors;if(valid10){const err29 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/3/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}else {errors = _errs32;if(vErrors !== null){if(_errs32){vErrors.length = _errs32;}else {vErrors = null;}}}const _errs35 = errors;const _errs36 = errors;if(typeof data8 === "string"){if(!pattern10.test(data8)){const err30 = {};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}var valid11 = _errs36 === errors;if(valid11){const err31 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/4/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}else {errors = _errs35;if(vErrors !== null){if(_errs35){vErrors.length = _errs35;}else {vErrors = null;}}}const _errs38 = errors;const _errs39 = errors;if(typeof data8 === "string"){if(!pattern11.test(data8)){const err32 = {};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}var valid12 = _errs39 === errors;if(valid12){const err33 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/5/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}else {errors = _errs38;if(vErrors !== null){if(_errs38){vErrors.length = _errs38;}else {vErrors = null;}}}const _errs41 = errors;const _errs42 = errors;if(typeof data8 === "string"){if(!pattern12.test(data8)){const err34 = {};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}}var valid13 = _errs42 === errors;if(valid13){const err35 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/6/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}else {errors = _errs41;if(vErrors !== null){if(_errs41){vErrors.length = _errs41;}else {vErrors = null;}}}const _errs44 = errors;const _errs45 = errors;if(typeof data8 === "string"){if(!pattern13.test(data8)){const err36 = {};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}}var valid14 = _errs45 === errors;if(valid14){const err37 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/allOf/7/not",keyword:"not",params:{},message:"must NOT be valid"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {errors = _errs44;if(vErrors !== null){if(_errs44){vErrors.length = _errs44;}else {vErrors = null;}}}if(typeof data8 === "string"){if(func3(data8) < 1){const err38 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}if(!pattern14.test(data8)){const err39 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/pattern",keyword:"pattern",params:{pattern: "^[^/]+(?:/[^/]+)*$"},message:"must match pattern \""+"^[^/]+(?:/[^/]+)*$"+"\""};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}}else {const err40 = {instancePath:instancePath+"/meta/output",schemaPath:"common.schema.json#/$defs/portableOutputPath/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}}if(data2.animation !== undefined){let data9 = data2.animation;if(!((data9 === "trace") || (data9 === "none"))){const err41 = {instancePath:instancePath+"/meta/animation",schemaPath:"common.schema.json#/$defs/animation/enum",keyword:"enum",params:{allowedValues: schema77.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}if(data2.visual_preset !== undefined){let data10 = data2.visual_preset;if(!((((data10 === "classic") || (data10 === "signal-flow")) || (data10 === "blueprint")) || (data10 === "editorial"))){const err42 = {instancePath:instancePath+"/meta/visual_preset",schemaPath:"common.schema.json#/$defs/visualPreset/enum",keyword:"enum",params:{allowedValues: schema78.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;}}if(data2.quality_profile !== undefined){let data11 = data2.quality_profile;if(!((data11 === "standard") || (data11 === "showcase"))){const err43 = {instancePath:instancePath+"/meta/quality_profile",schemaPath:"common.schema.json#/$defs/qualityProfile/enum",keyword:"enum",params:{allowedValues: schema79.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}}if(data2.engineering_profile !== undefined){if(!(data2.engineering_profile === "deployment-ownership")){const err44 = {instancePath:instancePath+"/meta/engineering_profile",schemaPath:"#/properties/meta/properties/engineering_profile/enum",keyword:"enum",params:{allowedValues: schema169.properties.meta.properties.engineering_profile.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}}if(data2.repository !== undefined){let data13 = data2.repository;if(data13 && typeof data13 == "object" && !Array.isArray(data13)){if(data13.url === undefined){const err45 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}if(data13.revision === undefined){const err46 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/required",keyword:"required",params:{missingProperty: "revision"},message:"must have required property '"+"revision"+"'"};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}for(const key4 in data13){if(!((((key4 === "url") || (key4 === "provider")) || (key4 === "link_mode")) || (key4 === "revision"))){const err47 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key4},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}}if(data13.url !== undefined){let data14 = data13.url;if(typeof data14 === "string"){if(func3(data14) < 1){const err48 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err48];}else {vErrors.push(err48);}errors++;}}else {const err49 = {instancePath:instancePath+"/meta/repository/url",schemaPath:"common.schema.json#/$defs/repository/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err49];}else {vErrors.push(err49);}errors++;}}if(data13.provider !== undefined){let data15 = data13.provider;if(!((data15 === "github") || (data15 === "gitee"))){const err50 = {instancePath:instancePath+"/meta/repository/provider",schemaPath:"common.schema.json#/$defs/repository/properties/provider/enum",keyword:"enum",params:{allowedValues: schema36.properties.provider.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err50];}else {vErrors.push(err50);}errors++;}}if(data13.link_mode !== undefined){let data16 = data13.link_mode;if(!((data16 === "web") || (data16 === "local-only"))){const err51 = {instancePath:instancePath+"/meta/repository/link_mode",schemaPath:"common.schema.json#/$defs/repository/properties/link_mode/enum",keyword:"enum",params:{allowedValues: schema36.properties.link_mode.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err51];}else {vErrors.push(err51);}errors++;}}if(data13.revision !== undefined){let data17 = data13.revision;if(typeof data17 === "string"){if(!pattern15.test(data17)){const err52 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/pattern",keyword:"pattern",params:{pattern: "^[a-fA-F0-9]{40}$"},message:"must match pattern \""+"^[a-fA-F0-9]{40}$"+"\""};if(vErrors === null){vErrors = [err52];}else {vErrors.push(err52);}errors++;}}else {const err53 = {instancePath:instancePath+"/meta/repository/revision",schemaPath:"common.schema.json#/$defs/repository/properties/revision/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err53];}else {vErrors.push(err53);}errors++;}}}else {const err54 = {instancePath:instancePath+"/meta/repository",schemaPath:"common.schema.json#/$defs/repository/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err54];}else {vErrors.push(err54);}errors++;}}if(data2.views !== undefined){if(!(validate37(data2.views, {instancePath:instancePath+"/meta/views",parentData:data2,parentDataProperty:"views",rootData,dynamicAnchors}))){vErrors = vErrors === null ? validate37.errors : vErrors.concat(validate37.errors);errors = vErrors.length;}}if(data2.legend !== undefined){let data19 = data2.legend;if(data19 && typeof data19 == "object" && !Array.isArray(data19)){for(const key5 in data19){if(!((key5 === "mode") || (key5 === "entries"))){const err55 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err55];}else {vErrors.push(err55);}errors++;}}if(data19.mode !== undefined){let data20 = data19.mode;if(!(((data20 === "auto") || (data20 === "all")) || (data20 === "hidden"))){const err56 = {instancePath:instancePath+"/meta/legend/mode",schemaPath:"common.schema.json#/$defs/legendMode/enum",keyword:"enum",params:{allowedValues: schema40.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err56];}else {vErrors.push(err56);}errors++;}}if(data19.entries !== undefined){let data21 = data19.entries;if(data21 && typeof data21 == "object" && !Array.isArray(data21)){for(const key6 in data21){if(!(((((((key6 === "frontend") || (key6 === "backend")) || (key6 === "database")) || (key6 === "cloud")) || (key6 === "security")) || (key6 === "messagebus")) || (key6 === "external"))){const err57 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err57];}else {vErrors.push(err57);}errors++;}}if(data21.frontend !== undefined){let data22 = data21.frontend;if(data22 && typeof data22 == "object" && !Array.isArray(data22)){if(Object.keys(data22).length < 1){const err58 = {instancePath:instancePath+"/meta/legend/entries/frontend",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err58];}else {vErrors.push(err58);}errors++;}for(const key7 in data22){if(!((key7 === "label") || (key7 === "visible"))){const err59 = {instancePath:instancePath+"/meta/legend/entries/frontend",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err59];}else {vErrors.push(err59);}errors++;}}if(data22.label !== undefined){let data23 = data22.label;if(typeof data23 === "string"){if(func3(data23) > 80){const err60 = {instancePath:instancePath+"/meta/legend/entries/frontend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err60];}else {vErrors.push(err60);}errors++;}if(func3(data23) < 1){const err61 = {instancePath:instancePath+"/meta/legend/entries/frontend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err61];}else {vErrors.push(err61);}errors++;}}else {const err62 = {instancePath:instancePath+"/meta/legend/entries/frontend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err62];}else {vErrors.push(err62);}errors++;}}if(data22.visible !== undefined){if(typeof data22.visible !== "boolean"){const err63 = {instancePath:instancePath+"/meta/legend/entries/frontend/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err63];}else {vErrors.push(err63);}errors++;}}}else {const err64 = {instancePath:instancePath+"/meta/legend/entries/frontend",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err64];}else {vErrors.push(err64);}errors++;}}if(data21.backend !== undefined){let data25 = data21.backend;if(data25 && typeof data25 == "object" && !Array.isArray(data25)){if(Object.keys(data25).length < 1){const err65 = {instancePath:instancePath+"/meta/legend/entries/backend",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err65];}else {vErrors.push(err65);}errors++;}for(const key8 in data25){if(!((key8 === "label") || (key8 === "visible"))){const err66 = {instancePath:instancePath+"/meta/legend/entries/backend",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err66];}else {vErrors.push(err66);}errors++;}}if(data25.label !== undefined){let data26 = data25.label;if(typeof data26 === "string"){if(func3(data26) > 80){const err67 = {instancePath:instancePath+"/meta/legend/entries/backend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err67];}else {vErrors.push(err67);}errors++;}if(func3(data26) < 1){const err68 = {instancePath:instancePath+"/meta/legend/entries/backend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err68];}else {vErrors.push(err68);}errors++;}}else {const err69 = {instancePath:instancePath+"/meta/legend/entries/backend/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err69];}else {vErrors.push(err69);}errors++;}}if(data25.visible !== undefined){if(typeof data25.visible !== "boolean"){const err70 = {instancePath:instancePath+"/meta/legend/entries/backend/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err70];}else {vErrors.push(err70);}errors++;}}}else {const err71 = {instancePath:instancePath+"/meta/legend/entries/backend",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err71];}else {vErrors.push(err71);}errors++;}}if(data21.database !== undefined){let data28 = data21.database;if(data28 && typeof data28 == "object" && !Array.isArray(data28)){if(Object.keys(data28).length < 1){const err72 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err72];}else {vErrors.push(err72);}errors++;}for(const key9 in data28){if(!((key9 === "label") || (key9 === "visible"))){const err73 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key9},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err73];}else {vErrors.push(err73);}errors++;}}if(data28.label !== undefined){let data29 = data28.label;if(typeof data29 === "string"){if(func3(data29) > 80){const err74 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err74];}else {vErrors.push(err74);}errors++;}if(func3(data29) < 1){const err75 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err75];}else {vErrors.push(err75);}errors++;}}else {const err76 = {instancePath:instancePath+"/meta/legend/entries/database/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err76];}else {vErrors.push(err76);}errors++;}}if(data28.visible !== undefined){if(typeof data28.visible !== "boolean"){const err77 = {instancePath:instancePath+"/meta/legend/entries/database/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err77];}else {vErrors.push(err77);}errors++;}}}else {const err78 = {instancePath:instancePath+"/meta/legend/entries/database",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err78];}else {vErrors.push(err78);}errors++;}}if(data21.cloud !== undefined){let data31 = data21.cloud;if(data31 && typeof data31 == "object" && !Array.isArray(data31)){if(Object.keys(data31).length < 1){const err79 = {instancePath:instancePath+"/meta/legend/entries/cloud",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err79];}else {vErrors.push(err79);}errors++;}for(const key10 in data31){if(!((key10 === "label") || (key10 === "visible"))){const err80 = {instancePath:instancePath+"/meta/legend/entries/cloud",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key10},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err80];}else {vErrors.push(err80);}errors++;}}if(data31.label !== undefined){let data32 = data31.label;if(typeof data32 === "string"){if(func3(data32) > 80){const err81 = {instancePath:instancePath+"/meta/legend/entries/cloud/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err81];}else {vErrors.push(err81);}errors++;}if(func3(data32) < 1){const err82 = {instancePath:instancePath+"/meta/legend/entries/cloud/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err82];}else {vErrors.push(err82);}errors++;}}else {const err83 = {instancePath:instancePath+"/meta/legend/entries/cloud/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err83];}else {vErrors.push(err83);}errors++;}}if(data31.visible !== undefined){if(typeof data31.visible !== "boolean"){const err84 = {instancePath:instancePath+"/meta/legend/entries/cloud/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err84];}else {vErrors.push(err84);}errors++;}}}else {const err85 = {instancePath:instancePath+"/meta/legend/entries/cloud",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err85];}else {vErrors.push(err85);}errors++;}}if(data21.security !== undefined){let data34 = data21.security;if(data34 && typeof data34 == "object" && !Array.isArray(data34)){if(Object.keys(data34).length < 1){const err86 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err86];}else {vErrors.push(err86);}errors++;}for(const key11 in data34){if(!((key11 === "label") || (key11 === "visible"))){const err87 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key11},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err87];}else {vErrors.push(err87);}errors++;}}if(data34.label !== undefined){let data35 = data34.label;if(typeof data35 === "string"){if(func3(data35) > 80){const err88 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err88];}else {vErrors.push(err88);}errors++;}if(func3(data35) < 1){const err89 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err89];}else {vErrors.push(err89);}errors++;}}else {const err90 = {instancePath:instancePath+"/meta/legend/entries/security/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err90];}else {vErrors.push(err90);}errors++;}}if(data34.visible !== undefined){if(typeof data34.visible !== "boolean"){const err91 = {instancePath:instancePath+"/meta/legend/entries/security/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err91];}else {vErrors.push(err91);}errors++;}}}else {const err92 = {instancePath:instancePath+"/meta/legend/entries/security",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err92];}else {vErrors.push(err92);}errors++;}}if(data21.messagebus !== undefined){let data37 = data21.messagebus;if(data37 && typeof data37 == "object" && !Array.isArray(data37)){if(Object.keys(data37).length < 1){const err93 = {instancePath:instancePath+"/meta/legend/entries/messagebus",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err93];}else {vErrors.push(err93);}errors++;}for(const key12 in data37){if(!((key12 === "label") || (key12 === "visible"))){const err94 = {instancePath:instancePath+"/meta/legend/entries/messagebus",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key12},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err94];}else {vErrors.push(err94);}errors++;}}if(data37.label !== undefined){let data38 = data37.label;if(typeof data38 === "string"){if(func3(data38) > 80){const err95 = {instancePath:instancePath+"/meta/legend/entries/messagebus/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err95];}else {vErrors.push(err95);}errors++;}if(func3(data38) < 1){const err96 = {instancePath:instancePath+"/meta/legend/entries/messagebus/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err96];}else {vErrors.push(err96);}errors++;}}else {const err97 = {instancePath:instancePath+"/meta/legend/entries/messagebus/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err97];}else {vErrors.push(err97);}errors++;}}if(data37.visible !== undefined){if(typeof data37.visible !== "boolean"){const err98 = {instancePath:instancePath+"/meta/legend/entries/messagebus/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err98];}else {vErrors.push(err98);}errors++;}}}else {const err99 = {instancePath:instancePath+"/meta/legend/entries/messagebus",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err99];}else {vErrors.push(err99);}errors++;}}if(data21.external !== undefined){let data40 = data21.external;if(data40 && typeof data40 == "object" && !Array.isArray(data40)){if(Object.keys(data40).length < 1){const err100 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"};if(vErrors === null){vErrors = [err100];}else {vErrors.push(err100);}errors++;}for(const key13 in data40){if(!((key13 === "label") || (key13 === "visible"))){const err101 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key13},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err101];}else {vErrors.push(err101);}errors++;}}if(data40.label !== undefined){let data41 = data40.label;if(typeof data41 === "string"){if(func3(data41) > 80){const err102 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err102];}else {vErrors.push(err102);}errors++;}if(func3(data41) < 1){const err103 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err103];}else {vErrors.push(err103);}errors++;}}else {const err104 = {instancePath:instancePath+"/meta/legend/entries/external/label",schemaPath:"common.schema.json#/$defs/legendEntry/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err104];}else {vErrors.push(err104);}errors++;}}if(data40.visible !== undefined){if(typeof data40.visible !== "boolean"){const err105 = {instancePath:instancePath+"/meta/legend/entries/external/visible",schemaPath:"common.schema.json#/$defs/legendEntry/properties/visible/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"};if(vErrors === null){vErrors = [err105];}else {vErrors.push(err105);}errors++;}}}else {const err106 = {instancePath:instancePath+"/meta/legend/entries/external",schemaPath:"common.schema.json#/$defs/legendEntry/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err106];}else {vErrors.push(err106);}errors++;}}}else {const err107 = {instancePath:instancePath+"/meta/legend/entries",schemaPath:"#/properties/meta/properties/legend/properties/entries/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err107];}else {vErrors.push(err107);}errors++;}}}else {const err108 = {instancePath:instancePath+"/meta/legend",schemaPath:"#/properties/meta/properties/legend/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err108];}else {vErrors.push(err108);}errors++;}}if(data2.viewBox !== undefined){let data43 = data2.viewBox;if(Array.isArray(data43)){if(data43.length > 2){const err109 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err109];}else {vErrors.push(err109);}errors++;}if(data43.length < 2){const err110 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err110];}else {vErrors.push(err110);}errors++;}const len0 = data43.length;if(len0 > 0){let data44 = data43[0];if((typeof data44 == "number") && (isFinite(data44))){if(data44 < 320 || isNaN(data44)){const err111 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 320},message:"must be >= 320"};if(vErrors === null){vErrors = [err111];}else {vErrors.push(err111);}errors++;}}else {const err112 = {instancePath:instancePath+"/meta/viewBox/0",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err112];}else {vErrors.push(err112);}errors++;}}if(len0 > 1){let data45 = data43[1];if((typeof data45 == "number") && (isFinite(data45))){if(data45 < 240 || isNaN(data45)){const err113 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/minimum",keyword:"minimum",params:{comparison: ">=", limit: 240},message:"must be >= 240"};if(vErrors === null){vErrors = [err113];}else {vErrors.push(err113);}errors++;}}else {const err114 = {instancePath:instancePath+"/meta/viewBox/1",schemaPath:"#/properties/meta/properties/viewBox/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err114];}else {vErrors.push(err114);}errors++;}}const len1 = data43.length;if(!(len1 <= 2)){const err115 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err115];}else {vErrors.push(err115);}errors++;}}else {const err116 = {instancePath:instancePath+"/meta/viewBox",schemaPath:"#/properties/meta/properties/viewBox/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err116];}else {vErrors.push(err116);}errors++;}}}else {const err117 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err117];}else {vErrors.push(err117);}errors++;}}if(data.layout !== undefined){let data46 = data.layout;if(data46 && typeof data46 == "object" && !Array.isArray(data46)){if(data46.mode === undefined){const err118 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/required",keyword:"required",params:{missingProperty: "mode"},message:"must have required property '"+"mode"+"'"};if(vErrors === null){vErrors = [err118];}else {vErrors.push(err118);}errors++;}for(const key14 in data46){if(!(((((((key14 === "mode") || (key14 === "origin")) || (key14 === "cols")) || (key14 === "gapX")) || (key14 === "gapY")) || (key14 === "cellW")) || (key14 === "cellH"))){const err119 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key14},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err119];}else {vErrors.push(err119);}errors++;}}if(data46.mode !== undefined){if(!(data46.mode === "grid")){const err120 = {instancePath:instancePath+"/layout/mode",schemaPath:"#/properties/layout/properties/mode/enum",keyword:"enum",params:{allowedValues: schema169.properties.layout.properties.mode.enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err120];}else {vErrors.push(err120);}errors++;}}if(data46.origin !== undefined){let data48 = data46.origin;if(Array.isArray(data48)){if(data48.length > 2){const err121 = {instancePath:instancePath+"/layout/origin",schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err121];}else {vErrors.push(err121);}errors++;}if(data48.length < 2){const err122 = {instancePath:instancePath+"/layout/origin",schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err122];}else {vErrors.push(err122);}errors++;}const len2 = data48.length;if(len2 > 0){let data49 = data48[0];if(!((typeof data49 == "number") && (isFinite(data49)))){const err123 = {instancePath:instancePath+"/layout/origin/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err123];}else {vErrors.push(err123);}errors++;}}if(len2 > 1){let data50 = data48[1];if(!((typeof data50 == "number") && (isFinite(data50)))){const err124 = {instancePath:instancePath+"/layout/origin/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err124];}else {vErrors.push(err124);}errors++;}}const len3 = data48.length;if(!(len3 <= 2)){const err125 = {instancePath:instancePath+"/layout/origin",schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err125];}else {vErrors.push(err125);}errors++;}}else {const err126 = {instancePath:instancePath+"/layout/origin",schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err126];}else {vErrors.push(err126);}errors++;}}if(data46.cols !== undefined){let data51 = data46.cols;if(!(((typeof data51 == "number") && (!(data51 % 1) && !isNaN(data51))) && (isFinite(data51)))){const err127 = {instancePath:instancePath+"/layout/cols",schemaPath:"#/properties/layout/properties/cols/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err127];}else {vErrors.push(err127);}errors++;}if((typeof data51 == "number") && (isFinite(data51))){if(data51 > 12 || isNaN(data51)){const err128 = {instancePath:instancePath+"/layout/cols",schemaPath:"#/properties/layout/properties/cols/maximum",keyword:"maximum",params:{comparison: "<=", limit: 12},message:"must be <= 12"};if(vErrors === null){vErrors = [err128];}else {vErrors.push(err128);}errors++;}if(data51 < 1 || isNaN(data51)){const err129 = {instancePath:instancePath+"/layout/cols",schemaPath:"#/properties/layout/properties/cols/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err129];}else {vErrors.push(err129);}errors++;}}}if(data46.gapX !== undefined){let data52 = data46.gapX;if((typeof data52 == "number") && (isFinite(data52))){if(data52 < 0 || isNaN(data52)){const err130 = {instancePath:instancePath+"/layout/gapX",schemaPath:"#/properties/layout/properties/gapX/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err130];}else {vErrors.push(err130);}errors++;}}else {const err131 = {instancePath:instancePath+"/layout/gapX",schemaPath:"#/properties/layout/properties/gapX/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err131];}else {vErrors.push(err131);}errors++;}}if(data46.gapY !== undefined){let data53 = data46.gapY;if((typeof data53 == "number") && (isFinite(data53))){if(data53 < 0 || isNaN(data53)){const err132 = {instancePath:instancePath+"/layout/gapY",schemaPath:"#/properties/layout/properties/gapY/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err132];}else {vErrors.push(err132);}errors++;}}else {const err133 = {instancePath:instancePath+"/layout/gapY",schemaPath:"#/properties/layout/properties/gapY/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err133];}else {vErrors.push(err133);}errors++;}}if(data46.cellW !== undefined){let data54 = data46.cellW;if((typeof data54 == "number") && (isFinite(data54))){if(data54 < 40 || isNaN(data54)){const err134 = {instancePath:instancePath+"/layout/cellW",schemaPath:"#/properties/layout/properties/cellW/minimum",keyword:"minimum",params:{comparison: ">=", limit: 40},message:"must be >= 40"};if(vErrors === null){vErrors = [err134];}else {vErrors.push(err134);}errors++;}}else {const err135 = {instancePath:instancePath+"/layout/cellW",schemaPath:"#/properties/layout/properties/cellW/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err135];}else {vErrors.push(err135);}errors++;}}if(data46.cellH !== undefined){let data55 = data46.cellH;if((typeof data55 == "number") && (isFinite(data55))){if(data55 < 24 || isNaN(data55)){const err136 = {instancePath:instancePath+"/layout/cellH",schemaPath:"#/properties/layout/properties/cellH/minimum",keyword:"minimum",params:{comparison: ">=", limit: 24},message:"must be >= 24"};if(vErrors === null){vErrors = [err136];}else {vErrors.push(err136);}errors++;}}else {const err137 = {instancePath:instancePath+"/layout/cellH",schemaPath:"#/properties/layout/properties/cellH/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err137];}else {vErrors.push(err137);}errors++;}}}else {const err138 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err138];}else {vErrors.push(err138);}errors++;}}if(data.components !== undefined){let data56 = data.components;if(Array.isArray(data56)){if(data56.length < 1){const err139 = {instancePath:instancePath+"/components",schemaPath:"#/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err139];}else {vErrors.push(err139);}errors++;}const len4 = data56.length;for(let i0=0; i0 80){const err152 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/maxLength",keyword:"maxLength",params:{limit: 80},message:"must NOT have more than 80 characters"};if(vErrors === null){vErrors = [err152];}else {vErrors.push(err152);}errors++;}if(!pattern29.test(data64)){const err153 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/0/pattern",keyword:"pattern",params:{pattern: "^[^\\r\\n]+$"},message:"must match pattern \""+"^[^\\r\\n]+$"+"\""};if(vErrors === null){vErrors = [err153];}else {vErrors.push(err153);}errors++;}}var _valid1 = _errs179 === errors;valid49 = valid49 || _valid1;const _errs180 = errors;if(typeof data64 === "string"){if(!pattern30.test(data64)){const err154 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf/1/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err154];}else {vErrors.push(err154);}errors++;}}var _valid1 = _errs180 === errors;valid49 = valid49 || _valid1;if(!valid49){const err155 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err155];}else {vErrors.push(err155);}errors++;}else {errors = _errs178;if(vErrors !== null){if(_errs178){vErrors.length = _errs178;}else {vErrors = null;}}}if(typeof data64 === "string"){if(func3(data64) > 2048){const err156 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err156];}else {vErrors.push(err156);}errors++;}if(func3(data64) < 1){const err157 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err157];}else {vErrors.push(err157);}errors++;}}else {const err158 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err158];}else {vErrors.push(err158);}errors++;}var _valid0 = _errs176 === errors;if(_valid0){valid48 = true;passing0 = 0;}const _errs181 = errors;if(data64 && typeof data64 == "object" && !Array.isArray(data64)){if(data64.url === undefined){const err159 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "url"},message:"must have required property '"+"url"+"'"};if(vErrors === null){vErrors = [err159];}else {vErrors.push(err159);}errors++;}if(data64.sha256 === undefined){const err160 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/required",keyword:"required",params:{missingProperty: "sha256"},message:"must have required property '"+"sha256"+"'"};if(vErrors === null){vErrors = [err160];}else {vErrors.push(err160);}errors++;}for(const key16 in data64){if(!((key16 === "url") || (key16 === "sha256"))){const err161 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key16},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err161];}else {vErrors.push(err161);}errors++;}}if(data64.url !== undefined){let data65 = data64.url;if(typeof data65 === "string"){if(func3(data65) > 2048){const err162 = {instancePath:instancePath+"/components/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/maxLength",keyword:"maxLength",params:{limit: 2048},message:"must NOT have more than 2048 characters"};if(vErrors === null){vErrors = [err162];}else {vErrors.push(err162);}errors++;}if(func3(data65) < 8){const err163 = {instancePath:instancePath+"/components/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"};if(vErrors === null){vErrors = [err163];}else {vErrors.push(err163);}errors++;}if(!pattern30.test(data65)){const err164 = {instancePath:instancePath+"/components/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/pattern",keyword:"pattern",params:{pattern: "^https?://"},message:"must match pattern \""+"^https?://"+"\""};if(vErrors === null){vErrors = [err164];}else {vErrors.push(err164);}errors++;}}else {const err165 = {instancePath:instancePath+"/components/" + i0+"/brand/url",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/url/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err165];}else {vErrors.push(err165);}errors++;}}if(data64.sha256 !== undefined){let data66 = data64.sha256;if(typeof data66 === "string"){if(!pattern32.test(data66)){const err166 = {instancePath:instancePath+"/components/" + i0+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/pattern",keyword:"pattern",params:{pattern: "^[a-f0-9]{64}$"},message:"must match pattern \""+"^[a-f0-9]{64}$"+"\""};if(vErrors === null){vErrors = [err166];}else {vErrors.push(err166);}errors++;}}else {const err167 = {instancePath:instancePath+"/components/" + i0+"/brand/sha256",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/properties/sha256/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err167];}else {vErrors.push(err167);}errors++;}}}else {const err168 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err168];}else {vErrors.push(err168);}errors++;}var _valid0 = _errs181 === errors;if(_valid0 && valid48){valid48 = false;passing0 = [passing0, 1];}else {if(_valid0){valid48 = true;passing0 = 1;}}if(!valid48){const err169 = {instancePath:instancePath+"/components/" + i0+"/brand",schemaPath:"common.schema.json#/$defs/brandMark/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err169];}else {vErrors.push(err169);}errors++;}else {errors = _errs175;if(vErrors !== null){if(_errs175){vErrors.length = _errs175;}else {vErrors = null;}}}}if(data57.sources !== undefined){let data67 = data57.sources;if(Array.isArray(data67)){if(data67.length > 3){const err170 = {instancePath:instancePath+"/components/" + i0+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/maxItems",keyword:"maxItems",params:{limit: 3},message:"must NOT have more than 3 items"};if(vErrors === null){vErrors = [err170];}else {vErrors.push(err170);}errors++;}if(data67.length < 1){const err171 = {instancePath:instancePath+"/components/" + i0+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err171];}else {vErrors.push(err171);}errors++;}const len5 = data67.length;for(let i1=0; i1 240){const err174 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/maxLength",keyword:"maxLength",params:{limit: 240},message:"must NOT have more than 240 characters"};if(vErrors === null){vErrors = [err174];}else {vErrors.push(err174);}errors++;}if(func3(data69) < 1){const err175 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err175];}else {vErrors.push(err175);}errors++;}}else {const err176 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/path",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/path/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err176];}else {vErrors.push(err176);}errors++;}}if(data68.line !== undefined){let data70 = data68.line;if(!(((typeof data70 == "number") && (!(data70 % 1) && !isNaN(data70))) && (isFinite(data70)))){const err177 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err177];}else {vErrors.push(err177);}errors++;}if((typeof data70 == "number") && (isFinite(data70))){if(data70 < 1 || isNaN(data70)){const err178 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err178];}else {vErrors.push(err178);}errors++;}}}if(data68.end_line !== undefined){let data71 = data68.end_line;if(!(((typeof data71 == "number") && (!(data71 % 1) && !isNaN(data71))) && (isFinite(data71)))){const err179 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err179];}else {vErrors.push(err179);}errors++;}if((typeof data71 == "number") && (isFinite(data71))){if(data71 < 1 || isNaN(data71)){const err180 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/end_line",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/end_line/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"};if(vErrors === null){vErrors = [err180];}else {vErrors.push(err180);}errors++;}}}if(data68.label !== undefined){let data72 = data68.label;if(typeof data72 === "string"){if(func3(data72) > 48){const err181 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"};if(vErrors === null){vErrors = [err181];}else {vErrors.push(err181);}errors++;}if(func3(data72) < 1){const err182 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err182];}else {vErrors.push(err182);}errors++;}}else {const err183 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1+"/label",schemaPath:"common.schema.json#/$defs/sourceReferences/items/properties/label/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err183];}else {vErrors.push(err183);}errors++;}}}else {const err184 = {instancePath:instancePath+"/components/" + i0+"/sources/" + i1,schemaPath:"common.schema.json#/$defs/sourceReferences/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err184];}else {vErrors.push(err184);}errors++;}}}else {const err185 = {instancePath:instancePath+"/components/" + i0+"/sources",schemaPath:"common.schema.json#/$defs/sourceReferences/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err185];}else {vErrors.push(err185);}errors++;}}if(data57.row !== undefined){let data73 = data57.row;if(!(((typeof data73 == "number") && (!(data73 % 1) && !isNaN(data73))) && (isFinite(data73)))){const err186 = {instancePath:instancePath+"/components/" + i0+"/row",schemaPath:"#/properties/components/items/properties/row/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err186];}else {vErrors.push(err186);}errors++;}if((typeof data73 == "number") && (isFinite(data73))){if(data73 < 0 || isNaN(data73)){const err187 = {instancePath:instancePath+"/components/" + i0+"/row",schemaPath:"#/properties/components/items/properties/row/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err187];}else {vErrors.push(err187);}errors++;}}}if(data57.col !== undefined){let data74 = data57.col;if(!(((typeof data74 == "number") && (!(data74 % 1) && !isNaN(data74))) && (isFinite(data74)))){const err188 = {instancePath:instancePath+"/components/" + i0+"/col",schemaPath:"#/properties/components/items/properties/col/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err188];}else {vErrors.push(err188);}errors++;}if((typeof data74 == "number") && (isFinite(data74))){if(data74 < 0 || isNaN(data74)){const err189 = {instancePath:instancePath+"/components/" + i0+"/col",schemaPath:"#/properties/components/items/properties/col/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err189];}else {vErrors.push(err189);}errors++;}}}if(data57.pos !== undefined){let data75 = data57.pos;if(Array.isArray(data75)){if(data75.length > 2){const err190 = {instancePath:instancePath+"/components/" + i0+"/pos",schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err190];}else {vErrors.push(err190);}errors++;}if(data75.length < 2){const err191 = {instancePath:instancePath+"/components/" + i0+"/pos",schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err191];}else {vErrors.push(err191);}errors++;}const len6 = data75.length;if(len6 > 0){let data76 = data75[0];if(!((typeof data76 == "number") && (isFinite(data76)))){const err192 = {instancePath:instancePath+"/components/" + i0+"/pos/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err192];}else {vErrors.push(err192);}errors++;}}if(len6 > 1){let data77 = data75[1];if(!((typeof data77 == "number") && (isFinite(data77)))){const err193 = {instancePath:instancePath+"/components/" + i0+"/pos/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err193];}else {vErrors.push(err193);}errors++;}}const len7 = data75.length;if(!(len7 <= 2)){const err194 = {instancePath:instancePath+"/components/" + i0+"/pos",schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err194];}else {vErrors.push(err194);}errors++;}}else {const err195 = {instancePath:instancePath+"/components/" + i0+"/pos",schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err195];}else {vErrors.push(err195);}errors++;}}if(data57.size !== undefined){let data78 = data57.size;if(Array.isArray(data78)){if(data78.length > 2){const err196 = {instancePath:instancePath+"/components/" + i0+"/size",schemaPath:"#/properties/components/items/properties/size/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err196];}else {vErrors.push(err196);}errors++;}if(data78.length < 2){const err197 = {instancePath:instancePath+"/components/" + i0+"/size",schemaPath:"#/properties/components/items/properties/size/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err197];}else {vErrors.push(err197);}errors++;}const len8 = data78.length;if(len8 > 0){let data79 = data78[0];if((typeof data79 == "number") && (isFinite(data79))){if(data79 <= 0 || isNaN(data79)){const err198 = {instancePath:instancePath+"/components/" + i0+"/size/0",schemaPath:"#/properties/components/items/properties/size/prefixItems/0/exclusiveMinimum",keyword:"exclusiveMinimum",params:{comparison: ">", limit: 0},message:"must be > 0"};if(vErrors === null){vErrors = [err198];}else {vErrors.push(err198);}errors++;}}else {const err199 = {instancePath:instancePath+"/components/" + i0+"/size/0",schemaPath:"#/properties/components/items/properties/size/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err199];}else {vErrors.push(err199);}errors++;}}if(len8 > 1){let data80 = data78[1];if((typeof data80 == "number") && (isFinite(data80))){if(data80 <= 0 || isNaN(data80)){const err200 = {instancePath:instancePath+"/components/" + i0+"/size/1",schemaPath:"#/properties/components/items/properties/size/prefixItems/1/exclusiveMinimum",keyword:"exclusiveMinimum",params:{comparison: ">", limit: 0},message:"must be > 0"};if(vErrors === null){vErrors = [err200];}else {vErrors.push(err200);}errors++;}}else {const err201 = {instancePath:instancePath+"/components/" + i0+"/size/1",schemaPath:"#/properties/components/items/properties/size/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err201];}else {vErrors.push(err201);}errors++;}}const len9 = data78.length;if(!(len9 <= 2)){const err202 = {instancePath:instancePath+"/components/" + i0+"/size",schemaPath:"#/properties/components/items/properties/size/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err202];}else {vErrors.push(err202);}errors++;}}else {const err203 = {instancePath:instancePath+"/components/" + i0+"/size",schemaPath:"#/properties/components/items/properties/size/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err203];}else {vErrors.push(err203);}errors++;}}}else {const err204 = {instancePath:instancePath+"/components/" + i0,schemaPath:"#/properties/components/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err204];}else {vErrors.push(err204);}errors++;}}}else {const err205 = {instancePath:instancePath+"/components",schemaPath:"#/properties/components/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err205];}else {vErrors.push(err205);}errors++;}}if(data.boundaries !== undefined){let data81 = data.boundaries;if(Array.isArray(data81)){const len10 = data81.length;for(let i2=0; i2=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err217];}else {vErrors.push(err217);}errors++;}}else {const err218 = {instancePath:instancePath+"/boundaries/" + i2+"/pad",schemaPath:"#/properties/boundaries/items/properties/pad/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err218];}else {vErrors.push(err218);}errors++;}}}else {const err219 = {instancePath:instancePath+"/boundaries/" + i2,schemaPath:"#/properties/boundaries/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err219];}else {vErrors.push(err219);}errors++;}}}else {const err220 = {instancePath:instancePath+"/boundaries",schemaPath:"#/properties/boundaries/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err220];}else {vErrors.push(err220);}errors++;}}if(data.connections !== undefined){let data88 = data.connections;if(Array.isArray(data88)){const len12 = data88.length;for(let i4=0; i4 2){const err235 = {instancePath:instancePath+"/connections/" + i4+"/via/" + i5,schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err235];}else {vErrors.push(err235);}errors++;}if(data99.length < 2){const err236 = {instancePath:instancePath+"/connections/" + i4+"/via/" + i5,schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err236];}else {vErrors.push(err236);}errors++;}const len14 = data99.length;if(len14 > 0){let data100 = data99[0];if(!((typeof data100 == "number") && (isFinite(data100)))){const err237 = {instancePath:instancePath+"/connections/" + i4+"/via/" + i5+"/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err237];}else {vErrors.push(err237);}errors++;}}if(len14 > 1){let data101 = data99[1];if(!((typeof data101 == "number") && (isFinite(data101)))){const err238 = {instancePath:instancePath+"/connections/" + i4+"/via/" + i5+"/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err238];}else {vErrors.push(err238);}errors++;}}const len15 = data99.length;if(!(len15 <= 2)){const err239 = {instancePath:instancePath+"/connections/" + i4+"/via/" + i5,schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err239];}else {vErrors.push(err239);}errors++;}}else {const err240 = {instancePath:instancePath+"/connections/" + i4+"/via/" + i5,schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err240];}else {vErrors.push(err240);}errors++;}}}else {const err241 = {instancePath:instancePath+"/connections/" + i4+"/via",schemaPath:"#/properties/connections/items/properties/via/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err241];}else {vErrors.push(err241);}errors++;}}if(data89.labelAt !== undefined){let data102 = data89.labelAt;if(Array.isArray(data102)){if(data102.length > 2){const err242 = {instancePath:instancePath+"/connections/" + i4+"/labelAt",schemaPath:"common.schema.json#/$defs/point/maxItems",keyword:"maxItems",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err242];}else {vErrors.push(err242);}errors++;}if(data102.length < 2){const err243 = {instancePath:instancePath+"/connections/" + i4+"/labelAt",schemaPath:"common.schema.json#/$defs/point/minItems",keyword:"minItems",params:{limit: 2},message:"must NOT have fewer than 2 items"};if(vErrors === null){vErrors = [err243];}else {vErrors.push(err243);}errors++;}const len16 = data102.length;if(len16 > 0){let data103 = data102[0];if(!((typeof data103 == "number") && (isFinite(data103)))){const err244 = {instancePath:instancePath+"/connections/" + i4+"/labelAt/0",schemaPath:"common.schema.json#/$defs/point/prefixItems/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err244];}else {vErrors.push(err244);}errors++;}}if(len16 > 1){let data104 = data102[1];if(!((typeof data104 == "number") && (isFinite(data104)))){const err245 = {instancePath:instancePath+"/connections/" + i4+"/labelAt/1",schemaPath:"common.schema.json#/$defs/point/prefixItems/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err245];}else {vErrors.push(err245);}errors++;}}const len17 = data102.length;if(!(len17 <= 2)){const err246 = {instancePath:instancePath+"/connections/" + i4+"/labelAt",schemaPath:"common.schema.json#/$defs/point/items",keyword:"items",params:{limit: 2},message:"must NOT have more than 2 items"};if(vErrors === null){vErrors = [err246];}else {vErrors.push(err246);}errors++;}}else {const err247 = {instancePath:instancePath+"/connections/" + i4+"/labelAt",schemaPath:"common.schema.json#/$defs/point/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err247];}else {vErrors.push(err247);}errors++;}}if(data89.labelDx !== undefined){let data105 = data89.labelDx;if(!((typeof data105 == "number") && (isFinite(data105)))){const err248 = {instancePath:instancePath+"/connections/" + i4+"/labelDx",schemaPath:"#/properties/connections/items/properties/labelDx/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err248];}else {vErrors.push(err248);}errors++;}}if(data89.labelDy !== undefined){let data106 = data89.labelDy;if(!((typeof data106 == "number") && (isFinite(data106)))){const err249 = {instancePath:instancePath+"/connections/" + i4+"/labelDy",schemaPath:"#/properties/connections/items/properties/labelDy/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err249];}else {vErrors.push(err249);}errors++;}}if(data89.labelSegment !== undefined){let data107 = data89.labelSegment;if(!(((typeof data107 == "number") && (!(data107 % 1) && !isNaN(data107))) && (isFinite(data107)))){const err250 = {instancePath:instancePath+"/connections/" + i4+"/labelSegment",schemaPath:"#/properties/connections/items/properties/labelSegment/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err250];}else {vErrors.push(err250);}errors++;}if((typeof data107 == "number") && (isFinite(data107))){if(data107 < 0 || isNaN(data107)){const err251 = {instancePath:instancePath+"/connections/" + i4+"/labelSegment",schemaPath:"#/properties/connections/items/properties/labelSegment/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err251];}else {vErrors.push(err251);}errors++;}}}if(data89.width !== undefined){let data108 = data89.width;if((typeof data108 == "number") && (isFinite(data108))){if(data108 < 0.5 || isNaN(data108)){const err252 = {instancePath:instancePath+"/connections/" + i4+"/width",schemaPath:"common.schema.json#/$defs/relationshipWidth/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0.5},message:"must be >= 0.5"};if(vErrors === null){vErrors = [err252];}else {vErrors.push(err252);}errors++;}}else {const err253 = {instancePath:instancePath+"/connections/" + i4+"/width",schemaPath:"common.schema.json#/$defs/relationshipWidth/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err253];}else {vErrors.push(err253);}errors++;}}}else {const err254 = {instancePath:instancePath+"/connections/" + i4,schemaPath:"#/properties/connections/items/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err254];}else {vErrors.push(err254);}errors++;}}}else {const err255 = {instancePath:instancePath+"/connections",schemaPath:"#/properties/connections/type",keyword:"type",params:{type: "array"},message:"must be array"};if(vErrors === null){vErrors = [err255];}else {vErrors.push(err255);}errors++;}}if(data.cards !== undefined){let data109 = data.cards;if(Array.isArray(data109)){const len18 = data109.length;for(let i6=0; i6` into the output. Used by the validators as a backstop. +export function isFinitePoint(...coords) { + return coords.every((c) => Number.isFinite(c)); +} + +// Separation the layout solver guarantees and separation this check re-derives +// travel through different arithmetic, so a pair the solver placed at exactly +// its minimum can land a few ulps short of this comparison. In #583 the CLI's +// own receipt has the pair `793.6 + 160 + 8` read as 2**-43 past a5's left edge +// at 961.5999999999999 — one ulp at that magnitude. 0.0001 is the numeric +// tolerance the route and layout geometry in this file already compares with. +const NUMERIC_CLEARANCE_PX = 0.0001; + +export function rectsOverlap(a, b, gap = 0) { + // Non-finite geometry means "unknown", not "overlapping". Every comparison + // below is false for NaN, so without this guard the negation reports a + // collision for every pair. Callers surface non-finite pos/size through their + // own diagnostic; reporting it again as an overlap buries that message under + // one bogus separation hint per pair. + if (!isFinitePoint(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)) { + return false; + } + return !( + a.x + a.width + gap <= b.x + NUMERIC_CLEARANCE_PX || + b.x + b.width + gap <= a.x + NUMERIC_CLEARANCE_PX || + a.y + a.height + gap <= b.y + NUMERIC_CLEARANCE_PX || + b.y + b.height + gap <= a.y + NUMERIC_CLEARANCE_PX + ); +} + +export function segmentIntersectsRect(segment, rect, gap = 0) { + const box = { + x1: rect.x - gap, + y1: rect.y - gap, + x2: rect.x + rect.width + gap, + y2: rect.y + rect.height + gap + }; + const [a, b] = [segment.start, segment.end]; + if (pointInBox(a, box) || pointInBox(b, box)) return true; + return ( + segmentsIntersect(a, b, [box.x1, box.y1], [box.x2, box.y1]) || + segmentsIntersect(a, b, [box.x2, box.y1], [box.x2, box.y2]) || + segmentsIntersect(a, b, [box.x2, box.y2], [box.x1, box.y2]) || + segmentsIntersect(a, b, [box.x1, box.y2], [box.x1, box.y1]) + ); +} + +export function segmentRectClearance(segment, rect) { + if (!segment || !rect) return null; + const { start, end } = segment; + if (!Array.isArray(start) || !Array.isArray(end) || start.length !== 2 || end.length !== 2) return null; + if (!isFinitePoint(start[0], start[1], end[0], end[1], rect.x, rect.y, rect.width, rect.height)) return null; + if (rect.width < 0 || rect.height < 0) return null; + if (segmentIntersectsRect(segment, rect)) return 0; + + const right = rect.x + rect.width; + const bottom = rect.y + rect.height; + let clearance = Math.min(pointRectDistance(start, rect), pointRectDistance(end, rect)); + const corners = [rect.x, rect.y, right, rect.y, right, bottom, rect.x, bottom]; + for (let index = 0; index < 8; index += 2) { + const distance = pointSegmentDistanceXY(corners[index], corners[index + 1], start, end); + if (distance < clearance) clearance = distance; + } + return clearance; +} + +// The same clearance, but the caller already knows the threshold it compares +// against: when an axis gap alone proves the distance cannot be smaller, the +// exact distance is never computed and Infinity is returned. +export function segmentRectClearanceWithin(segment, rect, limit) { + if (!segment || !rect) return segmentRectClearance(segment, rect); + const { start, end } = segment; + if (!Array.isArray(start) || !Array.isArray(end) || start.length !== 2 || end.length !== 2) return null; + if (!isFinitePoint(start[0], start[1], end[0], end[1], rect.x, rect.y, rect.width, rect.height)) return null; + if (rect.width < 0 || rect.height < 0) return null; + if (segmentIntersectsRect(segment, rect)) return 0; + const minX = Math.min(start[0], end[0]); + const maxX = Math.max(start[0], end[0]); + const minY = Math.min(start[1], end[1]); + const maxY = Math.max(start[1], end[1]); + const gapX = Math.max(rect.x - maxX, minX - (rect.x + rect.width)); + const gapY = Math.max(rect.y - maxY, minY - (rect.y + rect.height)); + if (Math.max(gapX, gapY) > limit) return Number.POSITIVE_INFINITY; + return segmentRectClearance(segment, rect); +} + +export function segmentRectIntersectionLength(segment, rect) { + if (!segment || !rect) return null; + const { start, end } = segment; + if (!Array.isArray(start) || !Array.isArray(end) || start.length !== 2 || end.length !== 2) return null; + if (!isFinitePoint(...start, ...end, rect.x, rect.y, rect.width, rect.height)) return null; + if (rect.width < 0 || rect.height < 0) return null; + + const dx = end[0] - start[0]; + const dy = end[1] - start[1]; + const length = Math.hypot(dx, dy); + if (length <= 0.0000001) return 0; + const bounds = [ + [-dx, start[0] - rect.x], + [dx, rect.x + rect.width - start[0]], + [-dy, start[1] - rect.y], + [dy, rect.y + rect.height - start[1]], + ]; + let enter = 0; + let leave = 1; + for (const [direction, distance] of bounds) { + if (Math.abs(direction) <= 0.0000001) { + if (distance < -0.0000001) return 0; + continue; + } + const ratio = distance / direction; + if (direction < 0) enter = Math.max(enter, ratio); + else leave = Math.min(leave, ratio); + if (enter > leave + 0.0000001) return 0; + } + return length * Math.max(0, leave - enter); +} + +const ROUTE_INDEX_CELL_SIZE = 128; +const ROUTE_INDEX_MAX_CELLS_PER_ROUTE = 256; + +function routeBounds(route) { + let x1 = Infinity; + let y1 = Infinity; + let x2 = -Infinity; + let y2 = -Infinity; + for (const [x, y] of route.points) { + x1 = Math.min(x1, x); + y1 = Math.min(y1, y); + x2 = Math.max(x2, x); + y2 = Math.max(y2, y); + } + return { x1, y1, x2, y2 }; +} + +function createRouteCandidateIndex(routes) { + const cells = new Map(); + const globalRouteIndexes = []; + + routes.forEach((route, routeIndex) => { + const bounds = routeBounds(route); + const x1 = Math.floor(bounds.x1 / ROUTE_INDEX_CELL_SIZE); + const y1 = Math.floor(bounds.y1 / ROUTE_INDEX_CELL_SIZE); + const x2 = Math.floor(bounds.x2 / ROUTE_INDEX_CELL_SIZE); + const y2 = Math.floor(bounds.y2 / ROUTE_INDEX_CELL_SIZE); + if (![x1, y1, x2, y2].every(Number.isSafeInteger)) { + globalRouteIndexes.push(routeIndex); + return; + } + const cellCount = (x2 - x1 + 1) * (y2 - y1 + 1); + + // Very long routes are cheaper to test once per label than to duplicate + // across a large part of the index. + if (!Number.isSafeInteger(cellCount) || cellCount > ROUTE_INDEX_MAX_CELLS_PER_ROUTE) { + globalRouteIndexes.push(routeIndex); + return; + } + for (let y = y1; y <= y2; y += 1) { + for (let x = x1; x <= x2; x += 1) { + const key = `${x}:${y}`; + const bucket = cells.get(key); + if (bucket) bucket.push(routeIndex); + else cells.set(key, [routeIndex]); + } + } + }); + + return function candidates(rect, threshold) { + const x1 = Math.floor((rect.x - threshold) / ROUTE_INDEX_CELL_SIZE); + const y1 = Math.floor((rect.y - threshold) / ROUTE_INDEX_CELL_SIZE); + const x2 = Math.floor((rect.x + rect.width + threshold) / ROUTE_INDEX_CELL_SIZE); + const y2 = Math.floor((rect.y + rect.height + threshold) / ROUTE_INDEX_CELL_SIZE); + const cellCount = (x2 - x1 + 1) * (y2 - y1 + 1); + + // A huge threshold offers no selectivity and can overflow cell iteration. + if (![x1, y1, x2, y2].every(Number.isSafeInteger) + || !Number.isSafeInteger(cellCount) + || cellCount > Math.max(64, cells.size * 4)) return routes; + + const routeIndexes = new Set(globalRouteIndexes); + for (let y = y1; y <= y2; y += 1) { + for (let x = x1; x <= x2; x += 1) { + for (const routeIndex of cells.get(`${x}:${y}`) || []) routeIndexes.add(routeIndex); + } + } + return [...routeIndexes] + .sort((left, right) => left - right) + .map((routeIndex) => routes[routeIndex]); + }; +} + +export function collectLabelRouteClearance({ labels, routedRelations, threshold }) { + if (!Number.isFinite(threshold) || threshold < 0) return []; + if (threshold === 0) return []; + const routeCandidates = asArray(routedRelations).map((entry, fallbackIndex) => { + const relation = entry?.relation || entry; + const points = normalizeRoutePoints(entry?.points || relation?.routePoints); + if (!relation || points.length < 2) return null; + return { + relation, + relationIndex: Number.isInteger(entry?.relationIndex) ? entry.relationIndex : fallbackIndex, + points, + }; + }).filter(Boolean); + const seenRoutes = new Set(); + const routes = routeCandidates.filter((route) => { + const identity = relationshipIdentity(route.relation, route.relationIndex); + if (seenRoutes.has(identity)) return false; + seenRoutes.add(identity); + return true; + }); + const hits = []; + const seenLabels = new Set(); + const candidateRoutes = createRouteCandidateIndex(routes); + + for (const [fallbackIndex, label] of asArray(labels).entries()) { + const rect = label?.rect || label; + if (!rect || !isFinitePoint(rect.x, rect.y, rect.width, rect.height) || rect.width < 0 || rect.height < 0) continue; + const relationIndex = Number.isInteger(label?.relationIndex) ? label.relationIndex : fallbackIndex; + const labelIdentity = relationshipIdentity(label?.relation, relationIndex); + if (seenLabels.has(labelIdentity)) continue; + seenLabels.add(labelIdentity); + for (const route of candidateRoutes(rect, threshold)) { + if (relationIndex === route.relationIndex || sameRelationship(label?.relation, route.relation)) continue; + let nearest = null; + for (let segmentIndex = 0; segmentIndex < route.points.length - 1; segmentIndex += 1) { + const start = route.points[segmentIndex]; + const end = route.points[segmentIndex + 1]; + const clearance = segmentRectClearance({ start, end }, rect); + if (clearance == null) continue; + if (!nearest || clearance < nearest.clearance) { + nearest = { + clearance, + intersectionLength: segmentRectIntersectionLength({ start, end }, rect), + segmentIndex, + start, + end, + }; + } + } + if (!nearest || nearest.clearance + 0.0001 >= threshold) continue; + hits.push({ + label, + labelRelation: label?.relation, + labelRelationIndex: relationIndex, + otherRelation: route.relation, + otherRelationIndex: route.relationIndex, + rect, + ...nearest, + threshold, + }); + } + } + return hits; +} + +export function minimumLabelRouteClearance(measurements) { + if (!asArray(measurements).length) return null; + const minimum = measurements.reduce( + (value, hit) => Math.min(value, hit.clearance), + Infinity, + ); + return Math.round(minimum * 10) / 10; +} + +function relationshipIdentity(relation, relationIndex) { + if (relation?.key !== undefined) return `key:${relation.key}`; + if (relation?.id) return `id:${relation.from || ''}\u0000${relation.to || ''}\u0000${relation.id}`; + return `index:${relationIndex}`; +} + +function sameRelationship(left, right) { + if (!left || !right) return false; + if (left === right) return true; + if (left.key !== undefined && right.key !== undefined) return left.key === right.key; + return Boolean(left.id && right.id && left.id === right.id && left.from === right.from && left.to === right.to); +} + +function relationshipSubject(diagramType, relationCollection, relationIndex, relation) { + return { + diagramType, + collection: relationCollection, + index: relationIndex, + ...(relation?.id ? { id: relation.id } : {}), + ...(relation?.from ? { from: relation.from } : {}), + ...(relation?.to ? { to: relation.to } : {}), + }; +} + +const AUTHORED_PLAN_KEYS = ['route', 'via', 'channelX', 'channelY', 'fromSide', 'toSide', 'bias', 'labelAt', 'labelDx', 'labelDy', 'labelSegment']; + +// Re-planning advice only applies when the failing relation actually carries +// authored controls; telling an author to remove controls that do not exist +// just sends them hunting for a field that was never written. +function rePlanHint(relations, fallback) { + const controls = new Set(); + for (const relation of asArray(relations)) { + for (const key of AUTHORED_PLAN_KEYS) if (relation?.[key] !== undefined) controls.add(key); + } + if (!controls.size) return fallback; + return `if the authored ${[...controls].join('/')} are not required by the user, remove them so the renderer can re-plan; otherwise preserve that intent and ${fallback}`; +} + +const ENDPOINT_SIDE_RULES = { + left: { + axis: 'horizontal', + sourceSign: -1, + targetSign: 1, + sourceDirection: 'leftward', + targetDirection: 'rightward from the left', + }, + right: { + axis: 'horizontal', + sourceSign: 1, + targetSign: -1, + sourceDirection: 'rightward', + targetDirection: 'leftward from the right', + }, + top: { + axis: 'vertical', + sourceSign: -1, + targetSign: 1, + sourceDirection: 'upward', + targetDirection: 'downward from above', + }, + bottom: { + axis: 'vertical', + sourceSign: 1, + targetSign: -1, + sourceDirection: 'downward', + targetDirection: 'upward from below', + }, +}; + +function endpointSideIssue(points, endpoint, side) { + const rule = ENDPOINT_SIDE_RULES[side]; + if (!rule) return null; + const normalized = normalizeRoutePoints(points); + if (normalized.length < 2) return null; + const segmentIndex = endpoint === 'source' ? 0 : normalized.length - 2; + const start = normalized[segmentIndex]; + const end = normalized[segmentIndex + 1]; + const dx = end[0] - start[0]; + const dy = end[1] - start[1]; + const along = rule.axis === 'horizontal' ? dx : dy; + const across = rule.axis === 'horizontal' ? dy : dx; + const expectedSign = endpoint === 'source' ? rule.sourceSign : rule.targetSign; + if (Math.abs(across) <= 0.0001 && along * expectedSign > 0.0001) return null; + return { + endpoint, + side, + segmentIndex, + start, + end, + expectedAxis: rule.axis, + expectedDirection: endpoint === 'source' ? rule.sourceDirection : rule.targetDirection, + }; +} + +// A side is a direction contract, not just a point on a box border. This pure +// predicate lets automatic routers prefer a dogleg whose first and final +// segments leave/enter the chosen sides perpendicularly. +export function routeHonorsEndpointSides(points, fromSide, toSide) { + return !endpointSideIssue(points, 'source', fromSide) + && !endpointSideIssue(points, 'target', toSide); +} +// Explicit fromSide/toSide are authored geometry, so a tangent or backwards +// endpoint segment changes their meaning. Fail this universally instead of +// leaving a malformed arrow for visual review to discover. Named routes and +// authored via points already carry their own geometry semantics: when they +// omit endpoint sides, do not invent a relative-position side and then reject +// the route for disagreeing with that invention. Pure automatic routes may +// still be checked against renderer-inferred sides. +export function cleanEndpointSideProblems({ + relations, + endpointIds, + pathFor, + diagramType, + relationCollection, + fromSideFor, + toSideFor, + shouldCheckRelation = () => true, + routeHint = 'align the first/final via segment with fromSide/toSide, change the side, or remove explicit routing so auto can choose a perpendicular approach', +}) { + const problems = []; + for (const [relationIndex, relation] of asArray(relations).entries()) { + if (!relation || !endpointIds?.has(relation.from) || !endpointIds?.has(relation.to)) continue; + if (!shouldCheckRelation(relation, relationIndex)) continue; + const points = pathFor(relation)?.points; + if (!Array.isArray(points) || points.length < 2) continue; + const authoredFromSide = relation.fromSide && relation.fromSide !== 'auto' ? relation.fromSide : null; + const authoredToSide = relation.toSide && relation.toSide !== 'auto' ? relation.toSide : null; + const hasAuthoredRouteGeometry = Boolean( + (relation.route && relation.route !== 'auto') || Array.isArray(relation.via), + ); + const inferredFromSide = !hasAuthoredRouteGeometry && typeof fromSideFor === 'function' + ? fromSideFor(relation) + : null; + const inferredToSide = !hasAuthoredRouteGeometry && typeof toSideFor === 'function' + ? toSideFor(relation) + : null; + const fromSide = authoredFromSide ?? inferredFromSide; + const toSide = authoredToSide ?? inferredToSide; + const checks = [ + fromSide + ? { ...endpointSideIssue(points, 'source', fromSide), sideOrigin: authoredFromSide ? 'authored' : 'inferred' } + : null, + toSide + ? { ...endpointSideIssue(points, 'target', toSide), sideOrigin: authoredToSide ? 'authored' : 'inferred' } + : null, + ].filter((issue) => issue?.endpoint); + for (const issue of checks) { + const relationId = relation.id ? ` id "${relation.id}"` : ''; + const authoredField = issue.endpoint === 'source' ? 'fromSide' : 'toSide'; + const sideField = issue.sideOrigin === 'inferred' ? `inferred ${authoredField}` : authoredField; + const segmentRole = issue.endpoint === 'source' ? 'first' : 'final'; + const from = issue.start.map((value) => Math.round(value * 10) / 10).join(', '); + const to = issue.end.map((value) => Math.round(value * 10) / 10).join(', '); + const message = `[clean-flow/endpoint-side-direction] ${diagramType} ${relationCollection}[${relationIndex}]${relationId} "${relation.from}" -> "${relation.to}" ${segmentRole} segment ${issue.segmentIndex} [${from}] -> [${to}] does not honor ${sideField} "${issue.side}" — it must run ${issue.expectedAxis} ${issue.expectedDirection}; ${routeHint}.`; + recordDiagnostic({ + code: 'clean-flow/endpoint-side-direction', + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, relationIndex, relation), + evidence: { + endpoint: issue.endpoint, + authoredField, + sideOrigin: issue.sideOrigin, + side: issue.side, + segmentIndex: issue.segmentIndex, + from: issue.start, + to: issue.end, + expectedAxis: issue.expectedAxis, + expectedDirection: issue.expectedDirection, + }, + supportedFixes: [routeHint], + }); + problems.push(message); + } + } + return problems; +} + +// One mechanical quality gate for every renderer-owned relationship path. +// A renderer supplies its semantic obstacle set; source/target boxes are +// always exempt because paths are expected to terminate on their boundaries. +// Containers, lifelines, and other intentionally pass-through geometry should +// simply not be supplied as obstacles. +export function cleanFlowProblems({ + relations, + obstacles, + pathFor, + diagramType, + relationCollection, + obstacleKind, + clearance = 2, + routeHint = 'adjust fromSide/toSide, set route/via or channel coordinates, or move the obstacle' +}) { + // A relationship hidden behind an unrelated opaque node changes the + // diagram's meaning, so this is a correctness invariant rather than an + // opt-in composition preference. Keep it active even when the author omits + // quality_profile; standard/showcase still control stricter visual budgets. + const problems = []; + const obstacleList = [...obstacles]; + const obstacleIds = new Set(obstacleList.map((obstacle) => obstacle?.id)); + for (const [relationIndex, relation] of asArray(relations).entries()) { + if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') continue; + if (!obstacleIds.has(relation.from) || !obstacleIds.has(relation.to)) continue; + const points = pathFor(relation)?.points; + if (!Array.isArray(points) || points.length < 2) continue; + if (!points.every((point) => Array.isArray(point) && point.length === 2 && isFinitePoint(...point))) continue; + + const endpointIds = new Set([relation.from, relation.to]); + for (const obstacle of obstacleList) { + if (!obstacle || endpointIds.has(obstacle.id)) continue; + if (!isFinitePoint(obstacle.x, obstacle.y, obstacle.width, obstacle.height)) continue; + let hitSegment = -1; + for (let segmentIndex = 0; segmentIndex < points.length - 1; segmentIndex += 1) { + if (segmentIntersectsRect({ start: points[segmentIndex], end: points[segmentIndex + 1] }, obstacle, clearance)) { + hitSegment = segmentIndex; + break; + } + } + if (hitSegment === -1) continue; + const from = points[hitSegment].map(Math.round).join(', '); + const to = points[hitSegment + 1].map(Math.round).join(', '); + const relationId = relation.id ? ` id "${relation.id}"` : ''; + const hint = rePlanHint([relation], routeHint); + const message = `[clean-flow/edge-through-node] ${diagramType} ${relationCollection}[${relationIndex}]${relationId} "${relation.from}" -> "${relation.to}" crosses ${obstacleKind} "${obstacle.id}" (unrelated to this relationship) on segment ${hitSegment} [${from}] -> [${to}] (${clearance}px clearance) — ${hint}.`; + recordDiagnostic({ + code: 'clean-flow/edge-through-node', + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, relationIndex, relation), + evidence: { + obstacleKind, + obstacleId: obstacle.id, + segmentIndex: hitSegment, + from: points[hitSegment], + to: points[hitSegment + 1], + clearancePx: clearance, + }, + supportedFixes: [hint], + }); + problems.push(message); + } + } + return problems; +} + +// Build a read-only analysis copy of a polyline with straight-through +// waypoints removed. A waypoint on a forward-collinear run is not a visual +// endpoint, so treating it as one would hide a proper X that lands exactly on +// that waypoint. Reversals and real bends stay split: their shared point can +// still be an authored touch rather than a crossing. The source points are +// retained on every merged segment so diagnostics can name the authored +// segment that contains a hit without changing rendered/receipt geometry. +export function forwardCollinearAnalysisSegments(points) { + const segments = []; + for (let segmentIndex = 0; segmentIndex < asArray(points).length - 1; segmentIndex += 1) { + const authoredStart = points[segmentIndex]; + const authoredEnd = points[segmentIndex + 1]; + const start = Array.isArray(authoredStart) ? [...authoredStart] : authoredStart; + const end = Array.isArray(authoredEnd) ? [...authoredEnd] : authoredEnd; + const sourceSegment = { start, end, segmentIndex }; + const previous = segments.at(-1); + if (previous && segmentsContinueForward(previous.start, previous.end, start, end)) { + previous.end = end; + previous.sourceSegments.push(sourceSegment); + continue; + } + segments.push({ + start, + end, + segmentIndex, + sourceSegments: [sourceSegment], + }); + } + return segments; +} + +export function sourceSegmentIndexAtPoint(segment, point) { + const source = asArray(segment?.sourceSegments).find(({ start, end }) => ( + pointLiesOnSegment(point, start, end) + )); + return source?.segmentIndex ?? segment?.segmentIndex ?? 0; +} + +function authoredAnalysisSegments(points) { + return asArray(points).slice(0, -1).map((start, segmentIndex) => ({ + start, + end: points[segmentIndex + 1], + segmentIndex, + sourceSegments: [{ start, end: points[segmentIndex + 1], segmentIndex }], + })); +} + +function segmentsContinueForward(firstStart, firstEnd, secondStart, secondEnd) { + if (![firstStart, firstEnd, secondStart, secondEnd].every((point) => ( + Array.isArray(point) && point.length === 2 && isFinitePoint(...point) + ))) return false; + const epsilon = 0.0001; + if (Math.abs(firstEnd[0] - secondStart[0]) > epsilon + || Math.abs(firstEnd[1] - secondStart[1]) > epsilon) return false; + const firstVector = [firstEnd[0] - firstStart[0], firstEnd[1] - firstStart[1]]; + const secondVector = [secondEnd[0] - secondStart[0], secondEnd[1] - secondStart[1]]; + const firstLength = Math.hypot(...firstVector); + const secondLength = Math.hypot(...secondVector); + if (firstLength <= epsilon || secondLength <= epsilon) return false; + const cross = firstVector[0] * secondVector[1] - firstVector[1] * secondVector[0]; + if (Math.abs(cross) > epsilon) return false; + const dot = firstVector[0] * secondVector[0] + firstVector[1] * secondVector[1]; + return dot > epsilon; +} + +function pointLiesOnSegment(point, start, end) { + if (![point, start, end].every((candidate) => ( + Array.isArray(candidate) && candidate.length === 2 && isFinitePoint(...candidate) + ))) return false; + const epsilon = 0.0001; + const length = Math.hypot(end[0] - start[0], end[1] - start[1]); + if (length <= epsilon) return Math.hypot(point[0] - start[0], point[1] - start[1]) <= epsilon; + if (Math.abs(crossProduct(start, end, point)) > epsilon * length) return false; + return point[0] >= Math.min(start[0], end[0]) - epsilon + && point[0] <= Math.max(start[0], end[0]) + epsilon + && point[1] >= Math.min(start[1], end[1]) - epsilon + && point[1] <= Math.max(start[1], end[1]) + epsilon; +} + +// Authored shared endpoints retain their junction interpretation by default. +// Renderers can opt their automatic routes into proper interior X checks; +// endpoint touches and collinear trunks still are not proper crossings. +export function cleanCrossingProblems({ + relations, + endpointIds, + pathFor, + diagramType, + relationCollection, + profile = 'standard', + profileIsAuthoritative = false, + mergeForwardCollinearWaypoints = false, + includeSharedEndpoints = () => false, + crossingResolved = () => false, + warnInStandard = false, + onDiagnostic = recordDiagnostic, + routeHint = 'adjust route/via or channel coordinates so the relationships use separate corridors' +}) { + const severity = qualityProfileForGate(profile, profileIsAuthoritative) === 'showcase' ? 'error' : 'warning'; + if (severity === 'warning' && !warnInStandard) return []; + const routed = asArray(relations).map((relation, index) => { + if (!relation || !endpointIds.has(relation.from) || !endpointIds.has(relation.to)) return null; + const points = pathFor(relation)?.points; + if (!Array.isArray(points) || points.length < 2) return null; + if (!points.every((point) => Array.isArray(point) && point.length === 2 && isFinitePoint(...point))) return null; + return { + relation, + index, + points, + analysisSegments: mergeForwardCollinearWaypoints + ? forwardCollinearAnalysisSegments(points) + : authoredAnalysisSegments(points), + }; + }).filter(Boolean); + const problems = []; + + for (let leftIndex = 0; leftIndex < routed.length; leftIndex += 1) { + const left = routed[leftIndex]; + for (let rightIndex = leftIndex + 1; rightIndex < routed.length; rightIndex += 1) { + const right = routed[rightIndex]; + if ([left.relation.from, left.relation.to].some((id) => id === right.relation.from || id === right.relation.to) + && !includeSharedEndpoints(left.relation, right.relation)) continue; + + let hit = null; + for (const leftSegment of left.analysisSegments) { + if (hit) break; + for (const rightSegment of right.analysisSegments) { + const point = properSegmentIntersection( + leftSegment.start, + leftSegment.end, + rightSegment.start, + rightSegment.end + ); + if (point) { + hit = { + point, + leftSegment: sourceSegmentIndexAtPoint(leftSegment, point), + rightSegment: sourceSegmentIndexAtPoint(rightSegment, point), + }; + break; + } + } + } + if (!hit) continue; + if (crossingResolved(left.relation, right.relation, hit)) continue; + + const describe = ({ relation, index }) => { + const id = relation.id ? ` id "${relation.id}"` : ''; + return `${relationCollection}[${index}]${id} "${relation.from}" -> "${relation.to}"`; + }; + const point = hit.point.map((value) => Math.round(value * 10) / 10).join(', '); + const hint = rePlanHint([left.relation, right.relation], routeHint); + const message = `[composition/proper-crossing] ${severity === 'error' ? 'showcase' : 'standard'} ${diagramType} ${describe(left)} crosses ${describe(right)} at [${point}] (segments ${hit.leftSegment} and ${hit.rightSegment}) — ${hint}.`; + onDiagnostic({ + code: 'composition/proper-crossing', + severity, + message, + subject: relationshipSubject(diagramType, relationCollection, left.index, left.relation), + evidence: { + otherRelationship: relationshipSubject(diagramType, relationCollection, right.index, right.relation), + point: hit.point, + segmentIndex: hit.leftSegment, + otherSegmentIndex: hit.rightSegment, + }, + supportedFixes: [hint], + }); + if (severity === 'error') problems.push(message); + } + } + return problems; +} + +// Two unrelated relationships that occupy the same visible corridor can read +// as one authored branch or merge even when neither relationship crosses a +// node or forms a proper X. Keep authored shared endpoints exempt by default; +// automatic architecture routes opt in because they promise separate ports. +// Workflow v2 opts into shared-endpoint checks with a bounded terminal-trunk +// exception. Other callers keep their existing authored-junction contract. +// The counterflow-only opt-in serves older workflow exports without a root +// readable-v2 contract; full shared-endpoint checking takes precedence. +// Tiny overlaps below the route rhythm +// floor are ignored to avoid turning sub-pixel rounding into a quality debt. +export function collectAmbiguousCorridors({ + routedRelations, + minOverlapPx = 8, + includeSharedEndpoints = () => false, + includeSharedEndpointCounterflow = () => false, + allowShortWorkflowTrunks = false, +}) { + const routed = asArray(routedRelations).map((entry, fallbackIndex) => { + const relation = entry?.relation; + if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null; + const points = normalizeRoutePoints(entry?.points); + if (points.length < 2) return null; + return { + relation, + relationIndex: Number.isInteger(entry.relationIndex) ? entry.relationIndex : fallbackIndex, + points, + }; + }).filter(Boolean); + const hits = []; + + for (let leftIndex = 0; leftIndex < routed.length; leftIndex += 1) { + const left = routed[leftIndex]; + for (let rightIndex = leftIndex + 1; rightIndex < routed.length; rightIndex += 1) { + const right = routed[rightIndex]; + const sharedEndpoint = [left.relation.from, left.relation.to].some((id) => id === right.relation.from || id === right.relation.to); + const counterflowOnly = sharedEndpoint && !includeSharedEndpoints(left.relation, right.relation); + if (counterflowOnly && !includeSharedEndpointCounterflow(left.relation, right.relation)) continue; + + let longest = null; + for (let leftSegment = 0; leftSegment < left.points.length - 1; leftSegment += 1) { + for (let rightSegment = 0; rightSegment < right.points.length - 1; rightSegment += 1) { + const overlap = collinearAxisOverlap( + left.points[leftSegment], + left.points[leftSegment + 1], + right.points[rightSegment], + right.points[rightSegment + 1], + ); + if (!overlap || overlap.length + 0.0001 < minOverlapPx) continue; + if (allowShortWorkflowTrunks && shortWorkflowTrunk(left, right, leftSegment, rightSegment, overlap.length)) continue; + if (counterflowOnly) { + const leftDelta = left.points[leftSegment + 1].map((value, axis) => value - left.points[leftSegment][axis]); + const rightDelta = right.points[rightSegment + 1].map((value, axis) => value - right.points[rightSegment][axis]); + if (leftDelta[0] * rightDelta[0] + leftDelta[1] * rightDelta[1] >= 0) continue; + } + if (!longest || overlap.length > longest.overlapLength + 0.0001) { + longest = { + left, + right, + leftSegment, + rightSegment, + overlapLength: overlap.length, + overlapStart: overlap.start, + overlapEnd: overlap.end, + }; + } + } + } + if (longest) hits.push(longest); + } + } + return hits; +} + +function shortWorkflowTrunk(left, right, leftSegment, rightSegment, length) { + if (length > 24 + 0.0001) return false; + const a = left.relation, b = right.relation; + const variant = (edge) => edge.variant || 'default'; + const width = (edge) => edge.width || (variant(edge) === 'emphasis' ? 1.8 : 1.4); + if (variant(a) !== variant(b) || width(a) !== width(b) || (a.role || '') !== (b.role || '')) return false; + const same = (p, q) => Math.abs(p[0] - q[0]) < 0.0001 && Math.abs(p[1] - q[1]) < 0.0001; + const source = a.from === b.from && leftSegment === 0 && rightSegment === 0 + && same(left.points[0], right.points[0]); + const target = a.to === b.to && leftSegment === left.points.length - 2 && rightSegment === right.points.length - 2 + && same(left.points.at(-1), right.points.at(-1)); + if (!source && !target) return false; + const p = left.points[leftSegment], q = left.points[leftSegment + 1]; + const r = right.points[rightSegment], s = right.points[rightSegment + 1]; + return (q[0] - p[0]) * (s[0] - r[0]) + (q[1] - p[1]) * (s[1] - r[1]) > 0; +} + +// Bundled arrow markers are 7 stroke-widths across the direction of travel. +// Callers select the automatic routes they own; explicit junctions are preserved. +export function collectArrowheadCollisions({ routedRelations, allowShortWorkflowTrunks = false }) { + const incoming = new Map(); + const hits = []; + for (const entry of asArray(routedRelations)) { + const points = normalizeRoutePoints(entry.points); + if (points.length < 2 || !entry.relation?.to) continue; + const tip = points.at(-1); + const previous = points.at(-2); + const vertical = Math.abs(tip[0] - previous[0]) < 0.0001; + const axis = vertical ? 0 : 1; + const direction = Math.sign(tip[1 - axis] - previous[1 - axis]); + const halfWidth = 3.5 * (entry.relation.width || (entry.relation.variant === 'emphasis' ? 1.8 : 1.5)); + const key = `${entry.relation.to}\u0000${axis}\u0000${direction}`; + const siblings = incoming.get(key) || []; + const current = { ...entry, tip, halfWidth }; + for (const sibling of siblings) { + if (Math.abs(tip[1 - axis] - sibling.tip[1 - axis]) > 0.0001) continue; + const distance = Math.abs(tip[axis] - sibling.tip[axis]); + const minimum = halfWidth + sibling.halfWidth; + if (allowShortWorkflowTrunks) { + const left = { ...sibling, points: normalizeRoutePoints(sibling.points) }; + const right = { ...entry, points }; + const overlap = collinearAxisOverlap(left.points.at(-2), left.points.at(-1), points.at(-2), tip); + if (overlap && shortWorkflowTrunk(left, right, left.points.length - 2, points.length - 2, overlap.length)) continue; + } + if (distance < minimum - 0.0001) hits.push({ left: sibling, right: current, distance, minimum }); + } + siblings.push(current); + incoming.set(key, siblings); + } + return hits; +} + +export function cleanAmbiguousCorridorProblems({ + relations, + endpointIds, + pathFor, + diagramType, + relationCollection, + profile = 'standard', + profileIsAuthoritative = false, + routeHint = 'adjust route/via or channel coordinates so the relationships use separate corridors', + minOverlapPx = 8, + includeSharedEndpoints = () => false, + includeSharedEndpointCounterflow = () => false, + allowShortWorkflowTrunks = false, + onDiagnostic = recordDiagnostic, +}) { + const severity = qualityProfileForGate(profile, profileIsAuthoritative) === 'showcase' ? 'error' : 'warning'; + if (severity === 'warning' && !allowShortWorkflowTrunks) return []; + const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor }); + + return collectAmbiguousCorridors({ routedRelations, minOverlapPx, includeSharedEndpoints, includeSharedEndpointCounterflow, allowShortWorkflowTrunks }).map((hit) => { + const describe = ({ relation, relationIndex }) => { + const id = relation.id ? ` id "${relation.id}"` : ''; + return `${relationCollection}[${relationIndex}]${id} "${relation.from}" -> "${relation.to}"`; + }; + const length = Math.round(hit.overlapLength * 10) / 10; + const from = hit.overlapStart.map((value) => Math.round(value * 10) / 10).join(', '); + const to = hit.overlapEnd.map((value) => Math.round(value * 10) / 10).join(', '); + const hint = rePlanHint([hit.left.relation, hit.right.relation], routeHint); + const message = `[composition/ambiguous-corridor] ${severity === 'error' ? 'showcase' : 'standard'} ${diagramType} ${describe(hit.left)} shares a ${length}px corridor with ${describe(hit.right)} at [${from}] -> [${to}] (segments ${hit.leftSegment} and ${hit.rightSegment}; minimum ${minOverlapPx}px) — ${hint}.`; + onDiagnostic({ + code: 'composition/ambiguous-corridor', + severity, + message, + subject: relationshipSubject(diagramType, relationCollection, hit.left.relationIndex, hit.left.relation), + evidence: { + otherRelationship: relationshipSubject(diagramType, relationCollection, hit.right.relationIndex, hit.right.relation), + overlapLengthPx: length, + minimumPx: minOverlapPx, + from: hit.overlapStart, + to: hit.overlapEnd, + segmentIndex: hit.leftSegment, + otherSegmentIndex: hit.rightSegment, + }, + supportedFixes: [hint], + }); + return severity === 'error' ? message : null; + }).filter(Boolean); +} + +// Relationship paths may cross a structural frame, but they must not borrow a +// frame side as a routing corridor. Rounded rectangle corners are trimmed from +// the modeled straight sides so a short corner touch is not mistaken for a +// border run. Any positive straight overlap beyond the numeric epsilon is a +// hard failure in every quality profile; 16px belongs only to the separate, +// neutral short-segment metric and is not a corridor exemption. +export function collectBorderRuns({ routedRelations, frames }) { + const hits = []; + for (const routed of asArray(routedRelations)) { + const routeSegments = Array.isArray(routed?.segments) + ? routed.segments + : asArray(routed?.points).slice(0, -1).map((start, index) => ({ start, end: routed.points[index + 1] })); + if (!routeSegments.length) continue; + if (!routeSegments.every((segment) => ( + Array.isArray(segment?.start) && segment.start.length === 2 && isFinitePoint(...segment.start) + && Array.isArray(segment?.end) && segment.end.length === 2 && isFinitePoint(...segment.end) + ))) continue; + for (const [frameIndex, frame] of asArray(frames).entries()) { + for (const border of frameBorderSegments(frame)) { + const overlaps = []; + for (let segmentIndex = 0; segmentIndex < routeSegments.length; segmentIndex += 1) { + const segment = routeSegments[segmentIndex]; + const overlap = collinearAxisOverlap( + segment.start, + segment.end, + border.start, + border.end, + ); + if (!overlap || overlap.length <= 0.0001) continue; + overlaps.push({ ...overlap, segmentIndex }); + } + if (!overlaps.length) continue; + const merged = mergeBorderOverlaps(overlaps, border); + const longest = [...merged].sort((left, right) => right.length - left.length || left.low - right.low)[0]; + hits.push({ + ...routed, + frame, + frameIndex, + side: border.side, + segmentIndex: overlaps.reduce((min, overlap) => Math.min(min, overlap.segmentIndex), Infinity), + overlapLength: merged.reduce((total, overlap) => total + overlap.length, 0), + overlapStart: longest.start, + overlapEnd: longest.end, + }); + } + } + } + return hits; +} + +export function cleanBorderRunProblems({ + relations, + endpointIds, + frames, + pathFor, + diagramType, + relationCollection, + profile, + profileIsAuthoritative = false, + routeHint = 'adjust route/via or channel coordinates so the relationship crosses the frame perpendicularly through a clear opening' +}) { + if (!qualityProfileForGate(profile, profileIsAuthoritative)) return []; + const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor }); + return collectBorderRuns({ routedRelations, frames }).map((hit) => { + const relation = hit.relation || {}; + const relationId = relation.id ? ` id "${relation.id}"` : ''; + const frameKind = hit.frame?.kind || hit.frame?.shape || 'frame'; + const frameIdentity = hit.frame?.label || hit.frame?.id || hit.frameIndex; + const length = Math.round(hit.overlapLength * 10) / 10; + const from = hit.overlapStart.map((value) => Math.round(value * 10) / 10).join(', '); + const to = hit.overlapEnd.map((value) => Math.round(value * 10) / 10).join(', '); + const hint = rePlanHint([relation], routeHint); + const message = `[composition/container-border-run] ${diagramType} ${relationCollection}[${hit.relationIndex}]${relationId} "${relation.from}" -> "${relation.to}" follows ${frameKind} "${frameIdentity}" ${hit.side} border for ${length}px on segment ${hit.segmentIndex} [${from}] -> [${to}] — ${hint}.`; + recordDiagnostic({ + code: 'composition/container-border-run', + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, hit.relationIndex, relation), + evidence: { + frameKind, + frameId: hit.frame?.id, + frameLabel: hit.frame?.label, + side: hit.side, + segmentIndex: hit.segmentIndex, + overlapLengthPx: length, + from: hit.overlapStart, + to: hit.overlapEnd, + }, + supportedFixes: [hint], + }); + return message; + }); +} + +export function routeBudgetMetrics({ + routedRelations, + bendsPerRelationship = 2, + stretch = 1.35, + segmentPx = 16, + microSegmentPx = 8, +}) { + let maxBends = 0; + let routesOverSuggestedBends = 0; + let maxStretch = null; + let routesOverSuggestedStretch = 0; + let minSegmentPx = null; + let minInteriorSegmentPx = null; + let shortSegmentCount = 0; + let shortEndpointSegmentCount = 0; + let shortInteriorSegmentCount = 0; + let microSegmentCount = 0; + + for (const routed of asArray(routedRelations)) { + const points = normalizeRoutePoints(routed?.points); + if (points.length < 2) continue; + const bends = Math.max(0, points.length - 2); + maxBends = Math.max(maxBends, bends); + if (bends > bendsPerRelationship) routesOverSuggestedBends += 1; + + let routeLength = 0; + for (let index = 0; index < points.length - 1; index += 1) { + const length = Math.abs(points[index + 1][0] - points[index][0]) + Math.abs(points[index + 1][1] - points[index][1]); + if (length <= 0.0001) continue; + const position = segmentPosition(index, points.length - 1); + routeLength += length; + minSegmentPx = minSegmentPx == null ? length : Math.min(minSegmentPx, length); + if (position === 'interior') { + minInteriorSegmentPx = minInteriorSegmentPx == null ? length : Math.min(minInteriorSegmentPx, length); + } + if (length < segmentPx) { + shortSegmentCount += 1; + if (position === 'interior') shortInteriorSegmentCount += 1; + else shortEndpointSegmentCount += 1; + } + if (length < microSegmentPx) microSegmentCount += 1; + } + const direct = Math.abs(points.at(-1)[0] - points[0][0]) + Math.abs(points.at(-1)[1] - points[0][1]); + if (direct > 0.0001) { + const routeStretch = routeLength / direct; + maxStretch = maxStretch == null ? routeStretch : Math.max(maxStretch, routeStretch); + if (routeStretch > stretch + 0.0001) routesOverSuggestedStretch += 1; + } + } + + return { + maxBends, + routesOverSuggestedBends, + maxStretch, + routesOverSuggestedStretch, + minSegmentPx, + minInteriorSegmentPx, + shortSegmentCount, + shortEndpointSegmentCount, + shortInteriorSegmentCount, + microSegmentCount, + }; +} + +export function collectRouteRhythmIssues({ + routedRelations, + interiorSegmentPx = 16, + microSegmentPx = 8, +}) { + const issues = []; + for (const [fallbackIndex, routed] of asArray(routedRelations).entries()) { + const points = normalizeRoutePoints(routed?.points); + if (points.length < 2) continue; + for (let segmentIndex = 0; segmentIndex < points.length - 1; segmentIndex += 1) { + const start = points[segmentIndex]; + const end = points[segmentIndex + 1]; + const length = Math.abs(end[0] - start[0]) + Math.abs(end[1] - start[1]); + if (length <= 0.0001) continue; + const position = segmentPosition(segmentIndex, points.length - 1); + const code = length < microSegmentPx - 0.0001 + ? 'composition/micro-segment' + : position === 'interior' && length < interiorSegmentPx - 0.0001 + ? 'composition/short-interior-segment' + : null; + if (!code) continue; + issues.push({ + code, + relation: routed.relation, + relationIndex: Number.isInteger(routed.relationIndex) ? routed.relationIndex : fallbackIndex, + segmentIndex, + position, + length, + start, + end, + }); + } + } + return issues; +} + +export function cleanRouteRhythmProblems({ + relations, + endpointIds, + pathFor, + diagramType, + relationCollection, + profile, + profileIsAuthoritative = false, + routeHint = 'move the channel/via point to remove the cramped turn or give the route more corridor space', + interiorSegmentPx = 16, + microSegmentPx = 8, +}) { + if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return []; + const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor }); + return collectRouteRhythmIssues({ routedRelations, interiorSegmentPx, microSegmentPx }).map((hit) => { + const relation = hit.relation || {}; + const relationId = relation.id ? ` id "${relation.id}"` : ''; + const length = Math.round(hit.length * 10) / 10; + const from = hit.start.map((value) => Math.round(value * 10) / 10).join(', '); + const to = hit.end.map((value) => Math.round(value * 10) / 10).join(', '); + const rule = hit.code === 'composition/micro-segment' + ? `is below the ${microSegmentPx}px micro-segment floor` + : `is below the ${interiorSegmentPx}px interior-segment floor`; + const hint = rePlanHint([relation], routeHint); + const message = `[${hit.code}] showcase ${diagramType} ${relationCollection}[${hit.relationIndex}]${relationId} "${relation.from}" -> "${relation.to}" has a ${length}px ${hit.position} segment ${hit.segmentIndex} [${from}] -> [${to}] that ${rule} — ${hint}.`; + recordDiagnostic({ + code: hit.code, + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, hit.relationIndex, relation), + evidence: { + segmentIndex: hit.segmentIndex, + position: hit.position, + lengthPx: length, + minimumPx: hit.code === 'composition/micro-segment' ? microSegmentPx : interiorSegmentPx, + from: hit.start, + to: hit.end, + }, + supportedFixes: [hint], + }); + return message; + }); +} + +export function cleanLabelRouteClearanceProblems({ + relations, + labels, + endpointIds, + pathFor, + diagramType, + relationCollection, + profile, + profileIsAuthoritative = false, + threshold = 4, + routeHint = 'adjust labelAt, labelDx, labelDy, or labelSegment; otherwise adjust the other relationship route/via/channel', +}) { + if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return []; + const routedRelations = collectEligibleRoutedRelations({ relations, endpointIds, pathFor }); + return collectLabelRouteClearance({ labels, routedRelations, threshold }).map((hit) => { + const describe = (relation, relationIndex) => { + const relationId = relation?.id ? ` id "${relation.id}"` : ''; + const relationLabel = relation?.label ? ` label "${relation.label}"` : ''; + return `${relationCollection}[${relationIndex}]${relationId} "${relation?.from}" -> "${relation?.to}"${relationLabel}`; + }; + const clearance = Math.round(hit.clearance * 10) / 10; + const from = hit.start.map((value) => Math.round(value * 10) / 10).join(', '); + const to = hit.end.map((value) => Math.round(value * 10) / 10).join(', '); + const hint = rePlanHint([hit.labelRelation, hit.otherRelation], routeHint); + const message = `[composition/label-route-clearance] showcase ${diagramType} label "${hit.label?.label || hit.labelRelation?.label || ''}" on ${describe(hit.labelRelation, hit.labelRelationIndex)} is ${clearance}px from ${describe(hit.otherRelation, hit.otherRelationIndex)} segment ${hit.segmentIndex} [${from}] -> [${to}] (label rect ${formatRect(hit.rect)}; minimum ${threshold}px) — ${hint}.`; + recordDiagnostic({ + code: 'composition/label-route-clearance', + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, hit.labelRelationIndex, hit.labelRelation), + evidence: { + label: hit.label?.label || hit.labelRelation?.label || '', + otherRelationship: relationshipSubject(diagramType, relationCollection, hit.otherRelationIndex, hit.otherRelation), + segmentIndex: hit.segmentIndex, + clearancePx: clearance, + minimumPx: threshold, + labelRect: hit.rect, + from: hit.start, + to: hit.end, + }, + supportedFixes: [hint], + }); + return message; + }); +} + +// How far [start, start + size) leaves [0, extent), per end of one axis. This +// is the single definition of "inside the canvas" that both the containment +// rule and the repair hints measure against; an unknown extent bounds nothing. +function axisCanvasOverflow(start, size, extent, origin = 0) { + if (!Number.isFinite(extent)) return { start: 0, end: 0 }; + return { start: origin - start, end: start + size - (origin + extent) }; +} + +// THE CANVAS CONTAINMENT RATIONALE (referenced from the other call sites). +// +// The SVG canvas clips whatever leaves the viewBox, so an edge label that +// overhangs an edge ships as truncated text while every post-render check on +// the emitted markup still passes: clipped text is still well-formed markup. +// Renderers with a fixed canvas already bound their nodes, lanes, and legends +// against it; edge label rects were the exception. +// +// Not applied to readable-v2 workflows: that compiler grows its canvas around +// pinned label rects and rejects an authored viewBox that cannot hold the +// result, so the only gap growth cannot close is the origin its own rule +// already guards. Architecture's auto canvas grows the same way, which leaves +// this rule reporting authored viewBoxes and the origin side. +// +// Showcase only: a standard document authored before the rule exists may +// overhang by a few pixels, and failing it there would break compatibility +// instead of repairing a diagram. +export function collectLabelCanvasOverflow({ labels, viewBox, tolerance = 0.5 }) { + // Renderers author origin-zero canvases (meta.viewBox is [width, height] by + // schema), but `check` re-measures foreign artifacts whose SVG viewBox may + // carry a legal non-zero min-x/min-y; those pass all four numbers. + const box = asArray(viewBox); + const [originX, originY, canvasWidth, canvasHeight] = box.length === 4 ? box : [0, 0, box[0], box[1]]; + if (!isFinitePoint(originX, originY, canvasWidth, canvasHeight)) return []; + const hits = []; + for (const [fallbackIndex, label] of asArray(labels).entries()) { + const rect = label?.rect || label; + if (!rect || !isFinitePoint(rect.x, rect.y, rect.width, rect.height)) continue; + // `check` parses foreign markup, so a malformed negative-size rect is + // skipped like the other label collectors do — its flipped interval would + // otherwise read as contained. + if (rect.width < 0 || rect.height < 0) continue; + const horizontal = axisCanvasOverflow(rect.x, rect.width, canvasWidth, originX); + const vertical = axisCanvasOverflow(rect.y, rect.height, canvasHeight, originY); + const overflow = { + left: horizontal.start, + right: horizontal.end, + top: vertical.start, + bottom: vertical.end, + }; + const sides = Object.keys(overflow).filter((side) => overflow[side] > tolerance); + if (!sides.length) continue; + hits.push({ + label, + relation: label?.relation, + relationIndex: Number.isInteger(label?.relationIndex) ? label.relationIndex : fallbackIndex, + rect, + viewBox: [canvasWidth, canvasHeight], + viewBoxOrigin: [originX, originY], + sides, + overflowPx: Object.fromEntries(sides.map((side) => [side, Math.round(overflow[side] * 10) / 10])), + tolerance, + }); + } + return hits; +} + +export function describeLabelCanvasOverflow(hit) { + return hit.sides.map((side) => `${side} edge by ${hit.overflowPx[side]}px`).join(' and '); +} + +export function cleanLabelCanvasContainmentProblems({ + labels, + viewBox, + diagramType, + relationCollection, + profile, + profileIsAuthoritative = false, + routeHint = 'adjust labelAt, labelDx, labelDy, or labelSegment; otherwise enlarge meta.viewBox', + // Label widths are estimated from the text, not measured, so sub-pixel + // overhang is rounding noise rather than a visible truncation. + tolerance = 0.5, +}) { + if (qualityProfileForGate(profile, profileIsAuthoritative) !== 'showcase') return []; + return collectLabelCanvasOverflow({ labels, viewBox, tolerance }).map((hit) => { + const relation = hit.relation; + const relationId = relation?.id ? ` id "${relation.id}"` : ''; + const labelText = hit.label?.label || relation?.label || ''; + const hint = rePlanHint([relation], routeHint); + const message = `[composition/label-canvas-containment] showcase ${diagramType} label "${labelText}" on ${relationCollection}[${hit.relationIndex}]${relationId} "${relation?.from}" -> "${relation?.to}" extends past the ${describeLabelCanvasOverflow(hit)} (label rect ${formatRect(hit.rect)}; viewBox ${hit.viewBox[0]}x${hit.viewBox[1]}) — ${hint}.`; + recordDiagnostic({ + code: 'composition/label-canvas-containment', + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, hit.relationIndex, relation), + evidence: { + label: labelText, + labelRect: { x: hit.rect.x, y: hit.rect.y, width: hit.rect.width, height: hit.rect.height }, + viewBox: hit.viewBox, + overflowPx: hit.overflowPx, + tolerancePx: hit.tolerance, + }, + supportedFixes: [hint], + }); + return message; + }); +} + +function qualityProfileForGate(profile, profileIsAuthoritative) { + return profileIsAuthoritative + ? profile + : process.env.ARCHIFY_QUALITY_PROFILE || profile; +} + +function collectEligibleRoutedRelations({ relations, endpointIds, pathFor }) { + return asArray(relations).map((relation, relationIndex) => { + if (!relation || typeof relation.from !== 'string' || typeof relation.to !== 'string') return null; + if (endpointIds && (!endpointIds.has(relation.from) || !endpointIds.has(relation.to))) return null; + return { relation, relationIndex, points: pathFor(relation)?.points }; + }).filter(Boolean); +} + +function segmentPosition(index, segmentCount) { + if (index === 0) return 'source-stub'; + if (index === segmentCount - 1) return 'target-stub'; + return 'interior'; +} + +// Normalizing a route is pure, and the same route array is normalized again by +// every predicate that inspects it. Keyed by the array itself, so a rebuilt +// route simply gets a new entry. The result's outer array is frozen. Callers +// must also treat input arrays and their coordinate pairs as immutable, since +// edits in place cannot invalidate this identity-based cache. +const NORMALIZED_ROUTE_POINTS = new WeakMap(); + +// Assembling a route from an anchor, corridor points and an anchor is the same +// normalization without the intermediate array, and the result is seeded in the +// cache so later predicates do not normalize it again. +export function joinRoutePoints(start, via, end) { + const normalized = []; + const accept = (point) => { + if (!Array.isArray(point) || point.length !== 2 || !isFinitePoint(point[0], point[1])) return; + const previous = normalized.at(-1); + if (previous + && Math.abs(point[0] - previous[0]) <= 0.0001 + && Math.abs(point[1] - previous[1]) <= 0.0001) return; + while (normalized.length >= 2 && collinearForward(normalized.at(-2), normalized.at(-1), point)) normalized.pop(); + normalized.push(point); + }; + accept(start); + for (const point of Array.isArray(via) ? via : []) accept(point); + accept(end); + Object.freeze(normalized); + NORMALIZED_ROUTE_POINTS.set(normalized, normalized); + return normalized; +} + +export function normalizeRoutePoints(points) { + if (Array.isArray(points)) { + const cached = NORMALIZED_ROUTE_POINTS.get(points); + if (cached) return cached; + } + const normalized = []; + for (const point of Array.isArray(points) ? points : []) { + if (!Array.isArray(point) || point.length !== 2 || !isFinitePoint(point[0], point[1])) continue; + const previous = normalized.at(-1); + if (previous + && Math.abs(point[0] - previous[0]) <= 0.0001 + && Math.abs(point[1] - previous[1]) <= 0.0001) continue; + while (normalized.length >= 2 && collinearForward(normalized.at(-2), normalized.at(-1), point)) normalized.pop(); + normalized.push(point); + } + if (Array.isArray(points)) { + Object.freeze(normalized); + NORMALIZED_ROUTE_POINTS.set(points, normalized); + } + return normalized; +} + +function pointRectDistance(point, rect) { + const dx = Math.max(rect.x - point[0], 0, point[0] - (rect.x + rect.width)); + const dy = Math.max(rect.y - point[1], 0, point[1] - (rect.y + rect.height)); + return Math.hypot(dx, dy); +} + +function pointSegmentDistanceXY(px, py, start, end) { + const dx = end[0] - start[0]; + const dy = end[1] - start[1]; + const lengthSquared = dx * dx + dy * dy; + if (lengthSquared <= 0.0000001) return Math.hypot(px - start[0], py - start[1]); + const t = Math.max(0, Math.min(1, ((px - start[0]) * dx + (py - start[1]) * dy) / lengthSquared)); + return Math.hypot(px - (start[0] + t * dx), py - (start[1] + t * dy)); +} + +function pointSegmentDistance(point, start, end) { + return pointSegmentDistanceXY(point[0], point[1], start, end); +} + +function collinearForward(a, b, c) { + if (Math.abs(crossProduct(a, b, c)) > 0.0001) return false; + return (b[0] - a[0]) * (c[0] - b[0]) + (b[1] - a[1]) * (c[1] - b[1]) >= -0.0001; +} + +export function frameBorderSegments(frame) { + if (!frame || typeof frame !== 'object') return []; + if (frame.shape === 'line') { + const start = frame.start || [frame.x1, frame.y1]; + const end = frame.end || [frame.x2, frame.y2]; + return isFinitePoint(...start, ...end) ? [{ side: 'line', start, end }] : []; + } + if (!isFinitePoint(frame.x, frame.y, frame.width, frame.height) || frame.width <= 0 || frame.height <= 0) return []; + const radius = Math.max(0, Math.min(Number(frame.radius) || 0, frame.width / 2, frame.height / 2)); + const left = frame.x; + const right = frame.x + frame.width; + const top = frame.y; + const bottom = frame.y + frame.height; + return [ + { side: 'top', start: [left + radius, top], end: [right - radius, top] }, + { side: 'right', start: [right, top + radius], end: [right, bottom - radius] }, + { side: 'bottom', start: [right - radius, bottom], end: [left + radius, bottom] }, + { side: 'left', start: [left, bottom - radius], end: [left, top + radius] }, + ].filter(({ start, end }) => Math.hypot(end[0] - start[0], end[1] - start[1]) > 0.0001); +} + +function mergeBorderOverlaps(overlaps, border) { + const horizontal = Math.abs(border.start[1] - border.end[1]) <= 0.0001; + const axis = horizontal ? 0 : 1; + const fixed = horizontal ? border.start[1] : border.start[0]; + const sorted = overlaps.map((overlap) => ({ + low: Math.min(overlap.start[axis], overlap.end[axis]), + high: Math.max(overlap.start[axis], overlap.end[axis]), + })).sort((left, right) => left.low - right.low || left.high - right.high); + const merged = []; + for (const interval of sorted) { + const previous = merged.at(-1); + if (previous && interval.low <= previous.high + 0.0001) previous.high = Math.max(previous.high, interval.high); + else merged.push({ ...interval }); + } + return merged.map((interval) => ({ + ...interval, + length: interval.high - interval.low, + start: horizontal ? [interval.low, fixed] : [fixed, interval.low], + end: horizontal ? [interval.high, fixed] : [fixed, interval.high], + })); +} + +function collinearAxisOverlap(a, b, c, d) { + const epsilon = 0.0001; + const horizontal = Math.abs(a[1] - b[1]) <= epsilon + && Math.abs(c[1] - d[1]) <= epsilon + && Math.abs(a[1] - c[1]) <= epsilon; + const vertical = Math.abs(a[0] - b[0]) <= epsilon + && Math.abs(c[0] - d[0]) <= epsilon + && Math.abs(a[0] - c[0]) <= epsilon; + if (!horizontal && !vertical) return null; + const axis = horizontal ? 0 : 1; + const low = Math.max(Math.min(a[axis], b[axis]), Math.min(c[axis], d[axis])); + const high = Math.min(Math.max(a[axis], b[axis]), Math.max(c[axis], d[axis])); + if (high - low <= epsilon) return null; + const fixed = horizontal ? a[1] : a[0]; + return { + length: high - low, + start: horizontal ? [low, fixed] : [fixed, low], + end: horizontal ? [high, fixed] : [fixed, high], + }; +} + +export function properSegmentIntersection(a, b, c, d) { + const abC = crossProduct(a, b, c); + const abD = crossProduct(a, b, d); + const cdA = crossProduct(c, d, a); + const cdB = crossProduct(c, d, b); + const epsilon = 0.0001; + const opposite = (left, right) => (left > epsilon && right < -epsilon) || (left < -epsilon && right > epsilon); + if (!opposite(abC, abD) || !opposite(cdA, cdB)) return null; + + const denominator = (a[0] - b[0]) * (c[1] - d[1]) - (a[1] - b[1]) * (c[0] - d[0]); + if (Math.abs(denominator) < epsilon) return null; + const ab = a[0] * b[1] - a[1] * b[0]; + const cd = c[0] * d[1] - c[1] * d[0]; + return [ + (ab * (c[0] - d[0]) - (a[0] - b[0]) * cd) / denominator, + (ab * (c[1] - d[1]) - (a[1] - b[1]) * cd) / denominator + ]; +} + +function crossProduct(a, b, c) { + return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); +} + +function pointInBox(point, box) { + return point[0] >= box.x1 && point[0] <= box.x2 && point[1] >= box.y1 && point[1] <= box.y2; +} + +function segmentsIntersect(a, b, c, d) { + const o1 = orientation(a, b, c); + const o2 = orientation(a, b, d); + const o3 = orientation(c, d, a); + const o4 = orientation(c, d, b); + + if (o1 === 0 && onSegment(a, c, b)) return true; + if (o2 === 0 && onSegment(a, d, b)) return true; + if (o3 === 0 && onSegment(c, a, d)) return true; + if (o4 === 0 && onSegment(c, b, d)) return true; + + return o1 !== o2 && o3 !== o4; +} + +function orientation(a, b, c) { + const value = (b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]); + if (Math.abs(value) < 0.0001) return 0; + return value > 0 ? 1 : 2; +} + +function onSegment(a, b, c) { + return ( + b[0] <= Math.max(a[0], c[0]) && + b[0] >= Math.min(a[0], c[0]) && + b[1] <= Math.max(a[1], c[1]) && + b[1] >= Math.min(a[1], c[1]) + ); +} + +export function anchor(rect, side) { + switch (side) { + case 'left': return [rect.x, rect.cy]; + case 'right': return [rect.x + rect.width, rect.cy]; + case 'top': return [rect.cx, rect.y]; + case 'bottom': return [rect.cx, rect.y + rect.height]; + default: + return [rect.x + rect.width, rect.cy]; + } +} + +const PORT_OUTWARD_VECTOR = { + left: [-1, 0], + right: [1, 0], + top: [0, -1], + bottom: [0, 1], +}; + +// Automatic port spreading can put otherwise parallel anchors only a few +// pixels apart. A conventional midpoint dogleg then violates the renderer's +// own 8px/16px route-rhythm floors. Return a full outside-channel route when +// that happens, or null when the normal automatic route remains appropriate. +export function automaticPortRhythmBridge( + start, + end, + fromSide, + toSide, + { endpointStubPx = 24, interiorSegmentPx = 16, accept } = {}, +) { + if (!Array.isArray(start) || !Array.isArray(end) + || start.length !== 2 || end.length !== 2 + || !isFinitePoint(...start, ...end)) return null; + const fromVector = PORT_OUTWARD_VECTOR[fromSide]; + const toVector = PORT_OUTWARD_VECTOR[toSide]; + if (!fromVector || !toVector) return null; + + const opposedFacingGap = ( + (fromSide === 'right' && toSide === 'left' && end[0] > start[0]) + || (fromSide === 'left' && toSide === 'right' && start[0] > end[0]) + ) ? Math.abs(end[0] - start[0]) : ( + (fromSide === 'bottom' && toSide === 'top' && end[1] > start[1]) + || (fromSide === 'top' && toSide === 'bottom' && start[1] > end[1]) + ) ? Math.abs(end[1] - start[1]) : null; + const boundedStubPx = opposedFacingGap != null + && opposedFacingGap - endpointStubPx * 2 < interiorSegmentPx + ? Math.max(8, Math.min(endpointStubPx, (opposedFacingGap - interiorSegmentPx) / 2)) + : null; + const stubDistances = [endpointStubPx]; + if (boundedStubPx != null && boundedStubPx >= 8 && boundedStubPx !== endpointStubPx) { + stubDistances.push(boundedStubPx); + } + const candidates = []; + const verticalSides = new Set(['top', 'bottom']); + const horizontalSides = new Set(['left', 'right']); + + for (const stubPx of stubDistances) { + const startStub = [ + start[0] + fromVector[0] * stubPx, + start[1] + fromVector[1] * stubPx, + ]; + const endStub = [ + end[0] + toVector[0] * stubPx, + end[1] + toVector[1] * stubPx, + ]; + if (verticalSides.has(fromSide) && verticalSides.has(toSide) + && Math.abs(start[0] - end[0]) < interiorSegmentPx) { + for (const channelX of [ + Math.max(start[0], end[0]) + interiorSegmentPx, + Math.min(start[0], end[0]) - interiorSegmentPx, + ]) { + candidates.push([ + start, + startStub, + [channelX, startStub[1]], + [channelX, endStub[1]], + endStub, + end, + ]); + } + } + if (horizontalSides.has(fromSide) && horizontalSides.has(toSide) + && Math.abs(start[1] - end[1]) < interiorSegmentPx) { + for (const channelY of [ + Math.max(start[1], end[1]) + interiorSegmentPx, + Math.min(start[1], end[1]) - interiorSegmentPx, + ]) { + candidates.push([ + start, + startStub, + [startStub[0], channelY], + [endStub[0], channelY], + endStub, + end, + ]); + } + } + } + + return candidates + .map((points) => normalizeRoutePoints(points)) + .find((points) => ( + routeHonorsEndpointSides(points, fromSide, toSide) + && collectRouteRhythmIssues({ routedRelations: [{ points }], interiorSegmentPx }).length === 0 + && (typeof accept !== 'function' || accept(points)) + )) || null; +} + +// Keep conservative auto-routed fan-out/fan-in relationships visually +// distinct without changing authored route controls. The returned map only +// contains endpoints that belong to a shared automatic midpoint anchor. +export function automaticPortSpread(relations, boxes, { gutter = 16, maxSpacing = 14, sideFor, spacingFor } = {}) { + const groups = new Map(); + const spread = new Map(); + + const add = (relation, endpoint, rect, side, counterpart) => { + const key = `${rect.id}\u0000${side}`; + const items = groups.get(key) || []; + items.push({ relation, endpoint, rect, side, counterpart }); + groups.set(key, items); + }; + + for (const relation of asArray(relations)) { + if (!relation || (relation.route && relation.route !== 'auto')) continue; + if (relation.via || relation.channelX !== undefined || relation.channelY !== undefined || relation.labelAt) continue; + const from = boxes.get(relation.from); + const to = boxes.get(relation.to); + if (!from || !to) continue; + const fromSide = chosenSide( + relation.fromSide, + sideFor?.(relation, 'source') || defaultFromSide(from, to), + ); + const toSide = chosenSide( + relation.toSide, + sideFor?.(relation, 'target') || defaultToSide(from, to), + ); + add(relation, 'from', from, fromSide, to); + add(relation, 'to', to, toSide, from); + } + + for (const items of groups.values()) { + if (items.length < 2) continue; + const verticalSide = items[0].side === 'left' || items[0].side === 'right'; + items.sort((a, b) => { + const aCoordinate = verticalSide ? a.counterpart.cy : a.counterpart.cx; + const bCoordinate = verticalSide ? b.counterpart.cy : b.counterpart.cx; + if (aCoordinate !== bCoordinate) return aCoordinate - bCoordinate; + const aKey = `${a.relation.id || ''}\u0000${a.relation.from}\u0000${a.relation.to}\u0000${a.relation.label || ''}`; + const bKey = `${b.relation.id || ''}\u0000${b.relation.from}\u0000${b.relation.to}\u0000${b.relation.label || ''}`; + return aKey < bKey ? -1 : aKey > bKey ? 1 : 0; + }); + + const extent = verticalSide ? items[0].rect.height : items[0].rect.width; + const usable = Math.max(0, extent - gutter * 2); + const spacing = Math.min(maxSpacing, usable / (items.length - 1)); + if (!(spacing > 0)) continue; + + // Width-aware callers reserve the whole group together. Moving a single + // port around the legacy 14px slots can wrongly report a full side while + // its still-unrouted neighbours could have fitted farther apart. + let offsets; + if (spacingFor) { + const gaps = items.slice(1).map((item, index) => Math.max(maxSpacing, + spacingFor(items[index].relation, item.relation))); + const span = gaps.reduce((sum, gap) => sum + gap, 0); + if (span <= usable && gaps.some((gap) => gap > maxSpacing)) { + let offset = -span / 2; + offsets = [offset, ...gaps.map((gap) => (offset += gap))]; + } + } + + for (const [index, item] of items.entries()) { + const offset = offsets?.[index] ?? (index - (items.length - 1) / 2) * spacing; + const point = anchor(item.rect, item.side); + if (verticalSide) point[1] += offset; + else point[0] += offset; + const endpoints = spread.get(item.relation) || {}; + endpoints[item.endpoint] = point; + spread.set(item.relation, endpoints); + } + } + + return spread; +} + +// A centre delta on the horizontal axis does not by itself mean the route +// leaves sideways: a hub above an offset spoke has both a horizontal and a +// larger vertical delta, and the router draws the vertical dogleg. Compare the +// deltas so the inferred side matches the axis the route actually uses. Equal +// deltas select horizontal sides; dx === 0 preserves the previous vertical +// result, while a vertical-dominant nonzero dx selects vertical sides. +export function defaultFromSide(from, to) { + const dx = to.cx - from.cx; + const dy = to.cy - from.cy; + if (dx !== 0 && Math.abs(dx) >= Math.abs(dy)) return dx < 0 ? 'left' : 'right'; + return dy > 0 ? 'bottom' : 'top'; +} + +export function defaultToSide(from, to) { + const dx = to.cx - from.cx; + const dy = to.cy - from.cy; + if (dx !== 0 && Math.abs(dx) >= Math.abs(dy)) return dx < 0 ? 'right' : 'left'; + return dy > 0 ? 'top' : 'bottom'; +} + +// Non-architecture renderers retain their established horizontal-first +// endpoint contract. Architecture opts into dominant-axis inference above so +// a vertical hub-and-spoke route can use perpendicular automatic ports. +export function legacyDefaultFromSide(from, to) { + if (to.cx < from.cx) return 'left'; + if (to.cx > from.cx) return 'right'; + if (to.cy > from.cy) return 'bottom'; + return 'top'; +} + +export function legacyDefaultToSide(from, to) { + if (to.cx < from.cx) return 'right'; + if (to.cx > from.cx) return 'left'; + if (to.cy > from.cy) return 'top'; + return 'bottom'; +} + +export function chosenSide(side, fallback) { + return side && side !== 'auto' ? side : fallback; +} + +export function polylinePath(points) { + return points.map(([x, y], index) => `${index === 0 ? 'M' : 'L'} ${x} ${y}`).join(' '); +} + +export function routePointsValue(points) { + return asArray(points) + .filter((point) => Array.isArray(point) && point.length === 2 && isFinitePoint(...point)) + .map(([x, y]) => `${x},${y}`) + .join(';'); +} + +// Only a direct, explicitly authored diagonal needs an artifact-check exception. +// Nonempty via takes precedence; empty via adds no intermediate geometry. +export function authoredStraightRouteAttrs(relation, points) { + if (relation.route !== 'straight' || relation.via?.length || points.length !== 2) return ''; + const [start, end] = points; + return Math.abs(start[0] - end[0]) > 0.01 && Math.abs(start[1] - end[1]) > 0.01 + ? ' data-composition-route="straight"' + : ''; +} + +export function roundedPath(points, radius) { + if (points.length < 3 || radius <= 0) { + return polylinePath(points); + } + + const commands = [`M ${points[0][0]} ${points[0][1]}`]; + for (let i = 1; i < points.length - 1; i += 1) { + const [px, py] = points[i - 1]; + const [cx, cy] = points[i]; + const [nx, ny] = points[i + 1]; + const prevLen = Math.hypot(cx - px, cy - py); + const nextLen = Math.hypot(nx - cx, ny - cy); + const r = Math.min(radius, prevLen / 2, nextLen / 2); + if (r < 1) { + commands.push(`L ${cx} ${cy}`); + continue; + } + const before = [cx - ((cx - px) / prevLen) * r, cy - ((cy - py) / prevLen) * r]; + const after = [cx + ((nx - cx) / nextLen) * r, cy + ((ny - cy) / nextLen) * r]; + commands.push(`L ${before[0]} ${before[1]}`); + commands.push(`Q ${cx} ${cy} ${after[0]} ${after[1]}`); + } + const [endX, endY] = points[points.length - 1]; + commands.push(`L ${endX} ${endY}`); + return commands.join(' '); +} + +// Shared by edges/flows/transitions: all carry the same optional +// labelAt/labelDx/labelDy/labelSegment knobs. +export function labelPoint(item, points) { + if (item.labelAt) return item.labelAt; + if (points.length === 2) { + return [ + (points[0][0] + points[1][0]) / 2 + (item.labelDx || 0), + points[0][1] - 10 + (item.labelDy || 0) + ]; + } + const segmentIndex = Math.min(points.length - 2, Math.max(0, item.labelSegment ?? 1)); + const a = points[segmentIndex]; + const b = points[segmentIndex + 1]; + return [(a[0] + b[0]) / 2 + (item.labelDx || 0), (a[1] + b[1]) / 2 - 10 + (item.labelDy || 0)]; +} + +export const componentFill = { + frontend: 'c-frontend', + backend: 'c-backend', + database: 'c-database', + cloud: 'c-cloud', + security: 'c-security', + messagebus: 'c-messagebus', + external: 'c-external' +}; + +export const componentText = { + frontend: 't-frontend', + backend: 't-backend', + database: 't-database', + cloud: 't-cloud', + security: 't-security', + messagebus: 't-messagebus', + external: 't-external' +}; + +export const arrowClassMap = { + default: ['a-default', 'arrowhead'], + emphasis: ['a-emphasis', 'arrowhead-emphasis'], + security: ['a-security', 'arrowhead-security'], + dashed: ['a-dashed', 'arrowhead-dashed'] +}; + +// Structural phase/group accents retain their existing semantic colors. +export function variantAccent(variant) { + return variant === 'security' ? 't-security' + : variant === 'emphasis' ? 't-backend' + : variant === 'dashed' ? 't-messagebus' : 't-muted'; +} + +// Relationship labels use the same theme token as their path. Keep this map +// edge-specific: node-kind text colors only coincide with some path colors in +// the classic preset and must not define the relationship's visual meaning. +export function edgeLabelAccent(variant) { + return variant === 'security' + ? 't-edge-security' + : variant === 'emphasis' + ? 't-edge-emphasis' + : variant === 'dashed' + ? 't-edge-dashed' + : 't-edge-default'; +} + +export function formatRect(r) { + return `[${Math.round(r.x)}, ${Math.round(r.y)}, ${Math.round(r.width)}, ${Math.round(r.height)}]`; +} + +function formatValue(n) { + return String(Math.round(n)); +} + +// A label anchor is the text origin, so keeping a suggestion on the canvas +// means moving the whole rect: offset is `rect - anchor` on that axis. +function anchorFitsCanvas(anchorValue, offset, size, extent) { + const overflow = axisCanvasOverflow(anchorValue + offset, size, extent); + return overflow.start <= 0 && overflow.end <= 0; +} + +// Suggestions are emitted as integers, so the real-valued bounds are tightened +// inward with ceil/floor first: every integer between them keeps the whole rect +// on the canvas, which a bound like 616.7 would not. Returns null when the rect +// is wider than the canvas and no anchor can contain it. +function clampAnchorToCanvas(anchorValue, offset, size, extent) { + const rounded = Math.round(anchorValue); + if (!Number.isFinite(extent)) return rounded; + const min = Math.ceil(-offset); + const max = Math.floor(extent - size - offset); + return max < min ? null : Math.min(Math.max(rounded, min), max); +} + +/** + * Actionable hint when an edge label rect hits a node/component box (#7). + * Every suggested value is a replacement for the authored field, not an + * increment, and has to survive being applied to the document: + * - the absolute form is nudged along x so the rect stays on the canvas, and a + * vertical placement that cannot fit is dropped rather than clamped, since + * clamping it back would push the label onto the obstacle it must clear; + * - the relative form is offered only when labelDx/labelDy actually move this + * label (labelPoint returns an authored labelAt as-is) and only when the + * replacements, computed from the document's own labelDx/labelDy, land the + * rect inside the canvas. + * The authored values therefore come from `labelRect.relation`, the authored + * relationship the renderer already attaches to every label record. + * `obstacles` is every box the callers' own overlap loop tests (defaults to + * the named obstacle alone): a placement that merely traded the named obstacle + * for its neighbor would fail the same rule again when applied. + */ +export function suggestLabelObstacleFix(labelRect, lx, ly, obstacle, obstacleKind = 'component', viewBox, obstacles) { + // Renderer canvases are origin-zero [width, height] by schema, but read a + // 4-number [min-x, min-y, width, height] the same way the containment + // collector does — degraded no-ajv runs must not clamp against the origin + // pair as if it were the canvas size. + const box = asArray(viewBox); + const [canvasWidth, canvasHeight] = box.length === 4 ? box.slice(2) : box; + const xOffset = labelRect.x - lx; + const yOffset = labelRect.y - ly; + const anchorX = clampAnchorToCanvas(lx, xOffset, labelRect.width, canvasWidth); + // The placement anchors are derived from the label's own rect, not from the + // obstacle alone: an obstacle-only "above" anchor assumes the 14px + // single-line rect and lands a 27px two-line rect (dataflow classification, + // lifecycle note) back on the obstacle it must clear. The overlap filter + // uses the callers' own detection call, so a surviving hint cannot re-raise + // the problem it repairs. + const blockers = obstacles ? [...obstacles] : [obstacle]; + const placements = [ + { name: 'below', y: Math.round(obstacle.y + obstacle.height + 4 - yOffset) }, + { name: 'above', y: Math.round(obstacle.y - 4 - labelRect.height - yOffset) }, + ].filter(({ y }) => anchorX !== null + && anchorFitsCanvas(y, yOffset, labelRect.height, canvasHeight) + && blockers.every((blocker) => !rectsOverlap( + { x: anchorX + xOffset, y: y + yOffset, width: labelRect.width, height: labelRect.height }, + blocker, + -2, + ))); + const lines = [ + ` label rect: ${formatRect(labelRect)}`, + ` ${obstacleKind} "${obstacle.id}" rect: ${formatRect(obstacle)}`, + ]; + if (!placements.length) { + // Only the vertical slots beside the named obstacle were tried, so an + // unclaimed spot elsewhere may still exist — never assert that none does. + lines.push(anchorX === null + ? ` Suggested fix: the ${Math.round(labelRect.width)}px label rect cannot fit the ${canvasWidth}x${canvasHeight} viewBox at any anchor — shorten the label or enlarge meta.viewBox` + : ` Suggested fix: no placement directly above or below "${obstacle.id}" stays clear inside the ${canvasWidth}x${canvasHeight} viewBox — move the label to an open area with labelAt, shorten the label, move the ${obstacleKind}, or enlarge meta.viewBox`); + return lines.join('\n'); + } + const authored = labelRect.relation || {}; + const authoredDx = Number.isFinite(authored.labelDx) ? authored.labelDx : 0; + const authoredDy = Number.isFinite(authored.labelDy) ? authored.labelDy : 0; + const relativeHint = (y) => { + if (Array.isArray(authored.labelAt)) return null; + // Both replacements are measured from the automatic label point that the + // authored offsets are applied to, and against the unrounded anchor: a + // rounded base would shift the applied label by up to a pixel. + const dx = Math.round(authoredDx + anchorX - lx); + const dy = Math.round(authoredDy + y - ly); + // labelDx is authored as a number, so an authored fraction must not read as + // a needed nudge once the replacement is rounded to an integer. + const movesX = Math.abs(dx - authoredDx) >= 0.5; + const landsAtX = lx - authoredDx + (movesX ? dx : authoredDx); + const landsAtY = ly - authoredDy + dy; + if (!anchorFitsCanvas(landsAtX, xOffset, labelRect.width, canvasWidth)) return null; + if (!anchorFitsCanvas(landsAtY, yOffset, labelRect.height, canvasHeight)) return null; + return movesX + ? `set labelDx ${formatValue(dx)} with labelDy ${formatValue(dy)}` + : `set labelDy ${formatValue(dy)}`; + }; + const hint = placements + .map(({ name, y }) => { + const relative = relativeHint(y); + return `set labelAt [${anchorX}, ${y}]${relative ? ` or ${relative}` : ''} (${name})`; + }) + .join('; or '); + lines.push(` Suggested fix: ${hint}`); + return lines.join('\n'); +} + +/** Hint when two edge labels collide. */ +export function suggestLabelPairFix(a, b) { + return [ + ` "${a.label}" ${formatRect(a)}; "${b.label}" ${formatRect(b)}`, + ' Suggested fix: adjust labelDx/labelDy/labelSegment, or route one relationship through a separate corridor', + ].join('\n'); +} + +/** Hint when two components/nodes are too close. */ +export function suggestComponentSeparation(a, b, minGap = 8) { + const rightX = Math.round(a.x + a.width + minGap); + const belowY = Math.round(a.y + a.height + minGap); + return [ + ` "${a.id}" ${formatRect(a)}; "${b.id}" ${formatRect(b)}`, + ` Suggested fix: move "${b.id}" pos to [${rightX}, ${Math.round(b.y)}] (right of "${a.id}") or [${Math.round(b.x)}, ${belowY}] (below)`, + ].join('\n'); +} diff --git a/vendor/archify/renderers/shared/i18n.mjs b/vendor/archify/renderers/shared/i18n.mjs new file mode 100644 index 0000000..09f3879 --- /dev/null +++ b/vendor/archify/renderers/shared/i18n.mjs @@ -0,0 +1,603 @@ +export const SUPPORTED_LOCALES = ['en', 'zh-CN']; +export const DEFAULT_LOCALE = 'en'; + +const ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; + +export function escapeHtml(value) { + return String(value ?? '').replace(/[&<>"']/g, (character) => ESCAPE_MAP[character]); +} + +// One catalog feeds renderer-time SVG/HTML copy and the selected runtime +// catalog embedded in each standalone artifact. Keeping both built-in locales in one +// tuple makes missing translations impossible to hide behind an English +// fallback during development. +const MESSAGE_PAIRS = { + 'page.title': ['{title} Diagram', '{title}'], + 'diagram.description.architecture': ['An architecture diagram generated by Archify.', '由 Archify 生成的架构图。'], + 'diagram.description.workflow': ['A workflow diagram generated by Archify.', '由 Archify 生成的工作流图。'], + 'diagram.description.sequence': ['A sequence diagram generated by Archify.', '由 Archify 生成的时序图。'], + 'diagram.description.dataflow': ['A data-flow diagram generated by Archify.', '由 Archify 生成的数据流图。'], + 'diagram.description.lifecycle': ['A lifecycle diagram generated by Archify.', '由 Archify 生成的生命周期图。'], + 'node.focus': ['Focus {label}', '聚焦{label}'], + 'node.focus.detail': ['Focus {label}, {detail}', '聚焦{label},{detail}'], + 'node.context.architecture': ['Architecture component', '架构组件'], + 'node.context.workflow': ['Workflow node', '工作流节点'], + 'node.context.sequence': ['Sequence participant', '时序参与者'], + 'node.context.dataflow': ['Data-flow node', '数据流节点'], + 'node.context.lifecycle': ['Lifecycle state', '生命周期状态'], + 'legend.title': ['Legend', '图例'], + + 'legend.architecture.frontend': ['Frontend', '前端'], + 'legend.architecture.backend': ['Backend', '后端'], + 'legend.architecture.database': ['Database', '数据库'], + 'legend.architecture.cloud': ['Cloud', '云服务'], + 'legend.architecture.security': ['Security', '安全'], + 'legend.architecture.messagebus': ['Message bus', '消息总线'], + 'legend.architecture.external': ['External', '外部系统'], + 'legend.workflow.frontend': ['User UI', '用户界面'], + 'legend.workflow.backend': ['Agent logic', 'Agent 逻辑'], + 'legend.workflow.security': ['Policy', '策略'], + 'legend.workflow.messagebus': ['Tool action', '工具操作'], + 'legend.workflow.database': ['Context / trace', '上下文 / 追踪'], + 'legend.workflow.cloud': ['Cloud service', '云服务'], + 'legend.workflow.external': ['External system', '外部系统'], + 'legend.sequence.emphasis': ['main request', '主路径请求'], + 'legend.sequence.return': ['return', '返回'], + 'legend.sequence.security': ['security', '安全'], + 'legend.sequence.dashed': ['async trace', '异步追踪'], + 'legend.sequence.default': ['message', '普通消息'], + 'legend.dataflow.emphasis': ['primary data', '主要数据'], + 'legend.dataflow.security': ['policy / PII', '策略 / PII'], + 'legend.dataflow.dashed': ['async batch', '异步批处理'], + 'legend.dataflow.database': ['data store', '数据存储'], + 'legend.dataflow.default': ['data flow', '数据流'], + 'legend.lifecycle.start': ['initial state', '起点'], + 'legend.lifecycle.active': ['active', '进行中'], + 'legend.lifecycle.waiting': ['waiting', '等待'], + 'legend.lifecycle.decision': ['decision', '决策'], + 'legend.lifecycle.success': ['success', '成功'], + 'legend.lifecycle.failure': ['failure / exit', '失败 / 退出'], + 'legend.lifecycle.neutral': ['neutral', '中性状态'], + 'legend.lifecycle.external': ['external', '外部状态'], + 'legend.lifecycle.final': ['final state', '终态'], + + 'viewer.kind.frontend': ['Frontend', '前端'], + 'viewer.kind.backend': ['Backend', '后端'], + 'viewer.kind.database': ['Database', '数据库'], + 'viewer.kind.cloud': ['Cloud', '云服务'], + 'viewer.kind.security': ['Security', '安全'], + 'viewer.kind.messagebus': ['Message bus', '消息总线'], + 'viewer.kind.external': ['External', '外部系统'], + 'viewer.kind.neutral': ['Neutral', '中性'], + 'viewer.kind.node': ['Node', '节点'], + 'viewer.kind.start': ['Start', '开始'], + 'viewer.kind.active': ['Active', '活动'], + 'viewer.kind.waiting': ['Waiting', '等待'], + 'viewer.kind.decision': ['Decision', '决策'], + 'viewer.kind.success': ['Success', '成功'], + 'viewer.kind.failure': ['Failure', '失败'], + + 'viewer.toolbar.actions': ['Diagram actions', '图表操作'], + 'viewer.theme.toggle.title': ['Toggle theme (T)', '切换主题(T)'], + 'viewer.theme.toggle': ['Toggle color theme', '切换颜色主题'], + 'viewer.theme.dark': ['Dark', '深色'], + 'viewer.theme.light': ['Light', '浅色'], + 'viewer.preset.choose.title': ['Choose visual style (S cycles)', '选择视觉风格(S 循环切换)'], + 'viewer.preset.choose': ['Choose visual style', '选择视觉风格'], + 'viewer.preset.style': ['Style', '风格'], + 'viewer.preset.menu': ['Visual style', '视觉风格'], + 'viewer.preset.identity': ['Visual identity', '视觉表达'], + 'viewer.preset.cycles': ['to cycle', '循环切换'], + 'viewer.preset.classic': ['Classic', '经典'], + 'viewer.preset.classic.short': ['Classic', '经典'], + 'viewer.preset.classic.hint': ['Stable technical default', '稳定的技术默认风格'], + 'viewer.preset.flow': ['Signal Flow', '信号流'], + 'viewer.preset.flow.short': ['Flow', '流动'], + 'viewer.preset.flow.hint': ['Motion-forward presentation', '突出动态流向'], + 'viewer.preset.blueprint': ['Blueprint', '蓝图'], + 'viewer.preset.blueprint.hint': ['Engineering review', '工程评审'], + 'viewer.preset.editorial': ['Editorial', '编辑风格'], + 'viewer.preset.editorial.hint': ['Publication and launch notes', '适合发布与上线说明'], + 'viewer.preset.badge.signalFlow': ['SIGNAL FLOW', '信号流'], + 'viewer.preset.badge.blueprint': ['BLUEPRINT / REV 01', '蓝图 / 修订 01'], + 'viewer.preset.badge.editorial': ['EDITORIAL / FIELD NOTE', '编辑风格 / 现场笔记'], + 'viewer.preset.badge.editorialPlate': ['ARCHIFY / PLATE 04', 'ARCHIFY / 图版 04'], + 'viewer.preset.current': ['Visual style: {style}. Choose visual style', '当前视觉风格:{style}。选择视觉风格'], + 'viewer.motion.live': ['Live', '动态'], + 'viewer.motion.still': ['Still', '静态'], + 'viewer.motion.pause': ['Pause motion', '暂停动效'], + 'viewer.motion.resume': ['Resume motion', '恢复动效'], + 'viewer.motion.reduced': ['Motion paused by reduced-motion preference', '已根据减少动态效果偏好暂停动效'], + 'viewer.motion.hidden': ['Motion paused while this page is hidden', '页面不可见时已暂停动效'], + 'viewer.motion.yielding': ['Pause motion; currently yielding to {owner}', '暂停动效;当前让位于{owner}'], + 'viewer.motion.yielding.title': ['Live preview enabled · yielding to {owner}', '动态预览已启用 · 正在让位于{owner}'], + 'viewer.owner.route': ['Route Probe', '路径探测'], + 'viewer.owner.lens': ['Semantic Lens', '语义透镜'], + 'viewer.owner.relationship': ['Relationship Preview', '关系预览'], + 'viewer.owner.intent': ['Intent Trace', '意图追踪'], + 'viewer.owner.focus': ['semantic focus', '语义聚焦'], + 'viewer.owner.legend': ['legend preview', '图例预览'], + 'viewer.owner.reader': ['reader interaction', '读者交互'], + 'viewer.present.enter': ['Enter presentation stage', '进入演示模式'], + 'viewer.present.enter.title': ['Presentation stage (F)', '演示模式(F)'], + 'viewer.present.exit': ['Exit presentation stage', '退出演示模式'], + 'viewer.present.exit.title': ['Exit presentation stage (F or Escape)', '退出演示模式(F 或 Escape)'], + 'viewer.present.present': ['Present', '演示'], + 'viewer.present.exit.label': ['Exit', '退出'], + + 'viewer.export.button': ['Export', '导出'], + 'viewer.export.button.title': ['Export diagram (E)', '导出图表(E)'], + 'viewer.export.diagram': ['Export diagram', '导出图表'], + 'viewer.export.menu': ['Export', '导出'], + 'viewer.export.subtitle': ['Portable, clean outputs', '便携、整洁的输出'], + 'viewer.export.share': ['Share', '分享'], + 'viewer.export.shareCard': ['Share Card', '分享卡片'], + 'viewer.export.routeShareCard': ['Route Share Card', '路径分享卡片'], + 'viewer.export.reachShareCard': ['Reach Share Card', '可达范围分享卡片'], + 'viewer.export.copyDiagram': ['Copy diagram', '复制图表'], + 'viewer.export.clipboardPng': ['PNG to clipboard', '复制 PNG 到剪贴板'], + 'viewer.export.raster': ['Raster images', '位图'], + 'viewer.export.image': ['Image', '图像'], + 'viewer.export.lossless': ['Lossless image', '无损图像'], + 'viewer.export.compact': ['Compact image', '紧凑图像'], + 'viewer.export.modern': ['Modern image', '现代图像格式'], + 'viewer.export.vectorMotion': ['Vector and motion', '矢量与动效'], + 'viewer.export.vectorMotion.heading': ['Vector & motion', '矢量与动效'], + 'viewer.export.editable': ['Editable vector', '可编辑矢量图'], + 'viewer.export.svg.auto': ['SVG · Auto', 'SVG · 自动'], + 'viewer.export.svg.auto.hint': ['Matches host theme', '匹配宿主主题'], + 'viewer.export.svg.light': ['SVG · Light', 'SVG · 浅色'], + 'viewer.export.svg.light.hint': ['Always light', '始终浅色'], + 'viewer.export.svg.dark': ['SVG · Dark', 'SVG · 深色'], + 'viewer.export.svg.dark.hint': ['Always dark', '始终深色'], + 'viewer.export.motion6s': ['6s motion', '6 秒动效'], + 'viewer.export.unsupported': ['Not supported by this browser', '当前浏览器不支持'], + 'viewer.export.clipboardUnsupported': ['Clipboard image write not supported by this browser', '当前浏览器不支持写入图片剪贴板'], + 'viewer.export.clipboardUnsupported.short': ['Clipboard image write not supported in this browser.', '此浏览器不支持写入图片剪贴板。'], + 'viewer.export.motionUnavailable': ['Motion capture unavailable in this browser', '当前浏览器无法录制动效'], + 'viewer.export.webmUnavailable': ['WebM unavailable in this browser', '当前浏览器不支持 WebM'], + 'viewer.export.failed': ['Export failed: {message}', '导出失败:{message}'], + 'viewer.export.unknownVariant': ['Unknown Share Card variant: {variant}', '未知的分享卡片类型:{variant}'], + 'viewer.export.routeRequired': ['Trace a route before exporting a Route Share Card', '请先追踪路径,再导出路径分享卡片'], + 'viewer.export.reachRequired': ['Trace authored reach before exporting a Reach Share Card', '请先追踪编写可达范围,再导出可达范围分享卡片'], + 'viewer.export.unknown': ['unknown', '未知错误'], + 'viewer.export.routeFailed': ['Route Share Card export failed: {message}', '路径分享卡片导出失败:{message}'], + 'viewer.export.reachFailed': ['Reach Share Card export failed: {message}', '可达范围分享卡片导出失败:{message}'], + 'viewer.export.copyFailed': ['Copy failed: {message}', '复制失败:{message}'], + 'viewer.export.copiedPng': ['Copied PNG to clipboard', '已将 PNG 复制到剪贴板'], + 'viewer.export.downloadedRoute': ['Downloaded Route Share Card', '已下载路径分享卡片'], + 'viewer.export.downloadedReach': ['Downloaded Reach Share Card', '已下载可达范围分享卡片'], + 'viewer.export.downloadedWebm': ['Downloaded WebM', '已下载 WebM'], + 'viewer.export.recording': ['Recording 6 seconds of motion…', '正在录制 6 秒动效…'], + 'viewer.export.card.routeSummary.one': ['Path: {source} → {target} · {count} step', '路径:{source} → {target} · {count} 步'], + 'viewer.export.card.routeSummary.other': ['Path: {source} → {target} · {count} steps', '路径:{source} → {target} · {count} 步'], + 'viewer.export.card.reachSummary': ['Authored {direction} from {origin} · {nodes} · {links} · max {hops}', '从{origin}开始的编写{direction} · {nodes} · {links} · 最深 {hops}'], + 'viewer.export.card.node.one': ['{count} node', '{count} 个节点'], + 'viewer.export.card.node.other': ['{count} nodes', '{count} 个节点'], + 'viewer.export.card.link.one': ['{count} link', '{count} 条连接'], + 'viewer.export.card.link.other': ['{count} links', '{count} 条连接'], + 'viewer.export.card.hop.one': ['{count} hop', '{count} 跳'], + 'viewer.export.card.hop.other': ['{count} hops', '{count} 跳'], + 'viewer.export.card.routeBadge': ['ARCHIFY · ROUTE · {hops}', 'ARCHIFY · 路径 · {hops}'], + 'viewer.export.card.reachBadge': ['ARCHIFY · {direction} REACH', 'ARCHIFY · {direction}可达范围'], + 'viewer.export.direction.upstream': ['Upstream', '上游'], + 'viewer.export.direction.downstream': ['Downstream', '下游'], + 'viewer.export.error.canvasUnavailable': ['Canvas unavailable for {label}', '无法为{label}使用画布'], + 'viewer.export.error.contextUnavailable': ['2D canvas context unavailable for {label}', '无法为{label}创建二维画布上下文'], + 'viewer.export.error.toBlobUnavailable': ['canvas.toBlob unavailable for {label}', '{label}无法使用 canvas.toBlob'], + 'viewer.export.error.toBlobNull': ['canvas.toBlob returned no data for {label}', '{label}的 canvas.toBlob 未返回数据'], + 'viewer.export.error.variantsCombined': ['Share Card variants cannot be combined', '无法同时组合多种分享卡片类型'], + 'viewer.export.error.viewerState': ['Share Card export could not remove temporary viewer state', '分享卡片导出无法移除临时 Viewer 状态'], + 'viewer.export.error.routeState': ['Route Card export could not preserve the resolved route safely', '路径卡片导出无法安全保留已解析路径'], + 'viewer.export.error.reachState': ['Reach Card export could not preserve authored reach safely', '可达范围卡片导出无法安全保留编写的可达范围'], + 'viewer.export.error.webmRequirements': ['WebM motion export requires a trace animation and browser MediaRecorder support', 'WebM 动效导出需要追踪动画及浏览器 MediaRecorder 支持'], + 'viewer.export.error.mediaRecorder': ['MediaRecorder failed', 'MediaRecorder 录制失败'], + 'viewer.export.error.emptyWebm': ['MediaRecorder produced an empty WebM', 'MediaRecorder 生成了空的 WebM'], + 'viewer.export.error.webmBackground': ['SVG background could not be loaded for WebM export', '无法为 WebM 导出加载 SVG 背景'], + + 'viewer.guide.title': ['Explore this system', '探索此系统'], + 'viewer.focus.selectedNodes': ['{count} selected nodes', '已选择 {count} 个节点'], + + 'viewer.guide.eyebrow': ['Diagram guide', '图表指南'], + 'viewer.guide.close': ['Close diagram guide', '关闭图表指南'], + 'viewer.guide.inspecting': ['Inspecting compiled semantics', '正在检查已编译语义'], + 'viewer.guide.actions': ['Diagram exploration actions', '图表探索操作'], + 'viewer.guide.find': ['Find any node', '查找任意节点'], + 'viewer.guide.find.hint': ['Search labels, responsibilities, kinds, and stable IDs.', '搜索标签、职责、类型和稳定 ID。'], + 'viewer.guide.route': ['Trace a route', '追踪路径'], + 'viewer.guide.route.aria': ['Trace a directed route', '追踪有向路径'], + 'viewer.guide.route.hint': ['Ask how two semantic nodes connect in authored direction.', '查看两个语义节点如何按编写方向连接。'], + 'viewer.guide.map': ['See the whole system', '查看完整系统'], + 'viewer.guide.map.hint': ['Open Semantic Radar with a live viewport and stable nodes.', '打开带实时视口和稳定节点的语义雷达。'], + 'viewer.guide.lens': ['Compare semantic kinds', '比较语义类型'], + 'viewer.guide.lens.hint': ['Count roles, reveal their traffic, and compare direct authored links.', '统计角色、显示流量并比较直接编写的连接。'], + 'viewer.guide.present': ['Enter Presentation Stage', '进入演示模式'], + 'viewer.guide.present.hint': ['Give the live diagram the viewport without changing export.', '让实时图表占满视口,同时不改变导出。'], + 'viewer.guide.shortcuts': ['Additional keyboard shortcuts', '其他键盘快捷键'], + 'viewer.guide.shortcut.export': ['Export', '导出'], + 'viewer.guide.shortcut.theme': ['Theme', '主题'], + 'viewer.guide.shortcut.style': ['Style', '风格'], + 'viewer.guide.shortcut.reset': ['Reset', '重置'], + 'viewer.guide.shortcut.zoomIn': ['Zoom in', '放大'], + 'viewer.guide.shortcut.zoomOut': ['Zoom out', '缩小'], + 'viewer.guide.shortcut.close': ['Close', '关闭'], + 'viewer.guide.facts': ['{nodes} · {relationships}', '{nodes} · {relationships}'], + 'viewer.guide.fact.node.one': ['{count} semantic node', '{count} 个语义节点'], + 'viewer.guide.fact.node.other': ['{count} semantic nodes', '{count} 个语义节点'], + 'viewer.guide.fact.relationship.one': ['{count} relationship', '{count} 条关系'], + 'viewer.guide.fact.relationship.other': ['{count} relationships', '{count} 条关系'], + 'viewer.guide.open': ['Open diagram guide', '打开图表指南'], + + 'viewer.finder.title': ['Find a node', '查找节点'], + 'viewer.finder.close': ['Close node finder', '关闭节点查找器'], + 'viewer.finder.placeholder': ['Search labels or IDs', '搜索标签或 ID'], + 'viewer.finder.search': ['Search diagram nodes', '搜索图表节点'], + 'viewer.finder.results': ['Diagram nodes', '图表节点'], + 'viewer.finder.empty': ['No matching nodes', '没有匹配的节点'], + 'viewer.finder.result.focus': ['Focus {label}', '聚焦{label}'], + 'viewer.finder.result.routeStart': ['Choose {label} as route start', '选择{label}作为路径起点'], + 'viewer.finder.result.routeTarget': ['Choose {label} as route destination, {links}', '选择{label}作为路径终点,{links}'], + 'viewer.finder.status.empty': ['No matching nodes', '没有匹配的节点'], + 'viewer.finder.status.count.one': ['{count} matching node', '{count} 个匹配节点'], + 'viewer.finder.status.count.other': ['{count} matching nodes', '{count} 个匹配节点'], + 'viewer.finder.noun.nodes': ['nodes', '个节点'], + 'viewer.finder.link.one': ['{count} link', '{count} 条连接'], + 'viewer.finder.link.other': ['{count} links', '{count} 条连接'], + 'viewer.finder.result.focus.one': ['Focus {label}, {count} related connection', '聚焦{label},{count} 条相关连接'], + 'viewer.finder.result.focus.other': ['Focus {label}, {count} related connections', '聚焦{label},{count} 条相关连接'], + 'viewer.finder.status.filtered': ['{visible} of {available} {noun}', '{visible}/{available} {noun}'], + 'viewer.finder.status.all': ['{available} {noun}', '{available} {noun}'], + + 'viewer.passport.eyebrow': ['Semantic passport', '语义护照'], + 'viewer.passport.metadata': ['Node metadata', '节点元数据'], + 'viewer.passport.evidence': ['Verified source evidence', '已验证的源代码证据'], + 'viewer.passport.verified': ['Verified source', '已验证来源'], + 'viewer.passport.verificationScope': ['Verified against local Git at the pinned revision. Remote access has not been checked.', '已按固定修订版本验证本地 Git 证据,未检查远程访问权限。'], + 'viewer.passport.reach': ['Authored reach', '编写可达范围'], + 'viewer.passport.reach.trace': ['Trace authored reachability', '追踪编写的可达性'], + 'viewer.passport.upstream': ['Upstream', '上游'], + 'viewer.passport.downstream': ['Downstream', '下游'], + 'viewer.passport.upstream.trace': ['Trace upstream authored reachability', '追踪上游编写可达性'], + 'viewer.passport.downstream.trace': ['Trace downstream authored reachability', '追踪下游编写可达性'], + 'viewer.passport.close': ['Close semantic passport', '关闭语义护照'], + 'viewer.passport.move': ['Move semantic passport. Drag, use arrow keys, or press Home to reset.', '移动语义护照。可拖动、使用方向键移动,或按 Home 恢复自动位置。'], + 'viewer.passport.copy': ['Copy link', '复制链接'], + 'viewer.passport.copy.focus': ['Copy link to focused node', '复制聚焦节点的链接'], + 'viewer.passport.relations': ['Relations', '关系'], + 'viewer.passport.relations.show': ['Show connected relationships', '显示关联关系'], + 'viewer.passport.relations.hide': ['Hide connected relationships', '隐藏关联关系'], + 'viewer.passport.relations.list': ['Connected relationships', '关联关系'], + 'viewer.passport.copyRelation': ['Copy relation', '复制关系'], + 'viewer.passport.copyNode': ['Copy node', '复制节点'], + 'viewer.passport.copyPinned': ['Copy link to pinned relationship', '复制固定关系的链接'], + 'viewer.passport.copySource': ['Copy link to source node', '复制来源节点的链接'], + 'viewer.passport.copy.focused.success': ['Focused node link copied', '已复制聚焦节点链接'], + 'viewer.passport.copy.pinned.success': ['Pinned relationship link copied', '已复制固定关系链接'], + 'viewer.passport.copy.focused.failed': ['Could not copy focused node link', '无法复制聚焦节点链接'], + 'viewer.passport.copy.pinned.failed': ['Could not copy pinned relationship link', '无法复制固定关系链接'], + 'viewer.passport.relationship.none': ['No connected relationships', '没有关联关系'], + 'viewer.passport.relationship.count.one': ['{count} relation', '{count} 条关系'], + 'viewer.passport.relationship.count.other': ['{count} relations', '{count} 条关系'], + 'viewer.passport.relationship.show.one': ['Show {count} connected relationship', '显示 {count} 条关联关系'], + 'viewer.passport.relationship.show.other': ['Show {count} connected relationships', '显示 {count} 条关联关系'], + 'viewer.passport.relationship.summary': ['{out} outgoing · {in} incoming{loops}', '{out} 条出向 · {in} 条入向{loops}'], + 'viewer.passport.relationship.loops': [' · {count} loop', ' · {count} 条自环'], + 'viewer.passport.relationship.explorer': ['Direct relationship explorer', '直接关系浏览器'], + 'viewer.passport.relationship.help': ['Use arrow keys to explore relationships. Press Enter or Space to pin details; Escape clears.', '使用方向键浏览关系。按 Enter 或空格键固定详情;按 Escape 清除。'], + 'viewer.passport.relationship.loopsBack': ['loops back', '回环'], + 'viewer.passport.relationship.connectsTo': ['connects to', '连接到'], + 'viewer.passport.relationship.connectsFrom': ['connects from', '连接自'], + 'viewer.passport.relationship.pinned': ['Pinned relationship · {from} → {to} · {label}', '已固定关系 · {from} → {to} · {label}'], + 'viewer.passport.relationship.inspect': ['Inspect relationship {index} of {total}: {from} to {to}, {label}. Press Enter for details.', '检查第 {index}/{total} 条关系:{from} 到 {to},{label}。按 Enter 查看详情。'], + 'viewer.passport.relationship.group.out': ['Outgoing', '出向'], + 'viewer.passport.relationship.group.in': ['Incoming', '入向'], + 'viewer.passport.relationship.group.loop': ['Self loops', '自环'], + 'viewer.passport.relationship.row': ['{group}: {relationship}, {neighbor}', '{group}:{relationship},{neighbor}'], + 'viewer.passport.relationship.direction.out': ['OUT →', '出 →'], + 'viewer.passport.relationship.direction.in': ['← IN', '← 入'], + 'viewer.passport.relationship.direction.loop': ['LOOP', '自环'], + 'viewer.passport.sourceCount.one': ['{count} verified source reference', '{count} 个已验证来源引用'], + 'viewer.passport.sourceCount.other': ['{count} verified source references', '{count} 个已验证来源引用'], + 'viewer.passport.sourceMarker': ['SRC', '来源'], + 'viewer.passport.beacon.one': ['{count} verified source; focus this node to inspect', '{count} 个已验证来源;聚焦此节点以检查'], + 'viewer.passport.beacon.other': ['{count} verified sources; focus this node to inspect', '{count} 个已验证来源;聚焦此节点以检查'], + 'viewer.passport.repository.open': ['Open verified repository revision {revision}', '打开已验证的仓库修订版本 {revision}'], + 'viewer.passport.source.open': ['Open verified source {path} at revision {revision}', '打开修订版本 {revision} 中已验证的来源 {path}'], + 'viewer.passport.source.openLink': ['Open ↗', '打开 ↗'], + 'viewer.passport.reach.upstream.one': ['Trace {count} upstream authored node', '追踪 {count} 个上游编写节点'], + 'viewer.passport.reach.upstream.other': ['Trace {count} upstream authored nodes', '追踪 {count} 个上游编写节点'], + 'viewer.passport.reach.downstream.one': ['Trace {count} downstream authored node', '追踪 {count} 个下游编写节点'], + 'viewer.passport.reach.downstream.other': ['Trace {count} downstream authored nodes', '追踪 {count} 个下游编写节点'], + 'viewer.passport.reach.noUpstream': ['No upstream authored nodes', '没有上游编写节点'], + 'viewer.passport.reach.noDownstream': ['No downstream authored nodes', '没有下游编写节点'], + 'viewer.passport.reach.status': ['{direction} · {nodes} nodes · {links} links · max {hops} hops', '{direction} · {nodes} 个节点 · {links} 条连接 · 最深 {hops} 跳'], + + 'viewer.route.eyebrow': ['Path', '路径'], + 'viewer.route.start': ['Click where the path starts', '点击路径的起点'], + 'viewer.route.start.find': ['Find start', '查找起点'], + 'viewer.route.start.find.aria': ['Find a route start', '查找路径起点'], + 'viewer.route.copy': ['Copy link', '复制链接'], + 'viewer.route.copy.aria': ['Copy link to traced route', '复制已追踪路径的链接'], + 'viewer.route.clear': ['Clear', '清除'], + 'viewer.route.clear.aria': ['Clear route probe', '清除路径探测'], + 'viewer.route.traced': ['Path', '路径'], + 'viewer.route.pickTwo': ['Click two nodes on the diagram', '在图上点两个节点'], + 'viewer.route.pickOne': ['Click a node on the diagram', '在图上点一个节点'], + 'viewer.route.controls': ['Route journey controls', '路径旅程控制'], + 'viewer.route.previous': ['Previous route position', '上一个路径位置'], + 'viewer.route.play': ['Play route journey', '播放路径旅程'], + 'viewer.route.pause': ['Pause route journey', '暂停路径旅程'], + 'viewer.route.replay': ['Replay route journey', '重播路径旅程'], + 'viewer.route.next': ['Next route position', '下一个路径位置'], + 'viewer.route.journey': ['Journey', '旅程'], + 'viewer.route.pause.label': ['Pause', '暂停'], + 'viewer.route.replay.label': ['Replay', '重播'], + 'viewer.route.overview': ['Overview', '总览'], + 'viewer.route.overview.aria': ['Show complete route overview', '显示完整路径总览'], + 'viewer.route.instructions': ['Click a start, then an end. Paths follow the arrows.', '先点起点,再点终点,沿箭头方向找路。'], + 'viewer.route.destination': ['Where does the path from {label} end?', '从{label}出发,走到哪里?'], + 'viewer.route.destination.find': ['Find target', '查找目标'], + 'viewer.route.destination.find.aria': ['Find a reachable route destination', '查找可达的路径目标'], + 'viewer.route.differentDestination': ['Choose a different destination', '选择其他目标'], + 'viewer.route.distinct': ['Pick two different nodes.', '请选两个不同的节点。'], + 'viewer.route.unreachable': ['Cannot reach {label} from here', '走不到{label}'], + 'viewer.route.unreachable.detail': ['{target} cannot be reached from {source} along the arrows. Pick a highlighted node.', '沿箭头方向,从{source}走不到{target}。请选一个高亮节点。'], + 'viewer.route.start.instructions': ['Next, pick the end. Only nodes you can reach will light up.', '接着选终点,能走到的节点会亮起。'], + 'viewer.route.copy.success': ['Traced route link copied', '已复制路径链接'], + 'viewer.route.copy.failed': ['Could not copy traced route link', '无法复制路径链接'], + 'viewer.route.position': ['Route position {index} of {total}: {label}', '路径位置 {index}/{total}:{label}'], + 'viewer.route.step': ['Step {index} of {total} · {phase} · {label}', '第 {index}/{total} 步 · {phase} · {label}'], + 'viewer.route.motionRequired': ['Automatic journey requires Live motion', '自动旅程需要动态模式'], + 'viewer.route.trigger.clear': ['Clear traced route', '清除已追踪路径'], + 'viewer.route.overview.status': ['{nodes} · {hops} · shortest path', '{nodes} · {hops} · 最短路径'], + 'viewer.route.overview.node.one': ['{count} node', '{count} 个节点'], + 'viewer.route.overview.node.other': ['{count} nodes', '{count} 个节点'], + 'viewer.route.overview.hop.one': ['{count} step', '{count} 步'], + 'viewer.route.overview.hop.other': ['{count} steps', '{count} 步'], + 'viewer.route.phase.playing': ['Playing', '播放中'], + 'viewer.route.phase.complete': ['Complete', '已完成'], + 'viewer.route.phase.inspecting': ['Inspecting', '检查中'], + 'viewer.route.destination.count.one': ['{count} reachable node is highlighted. Click it.', '有 {count} 个能走到的节点已高亮,点一下。'], + 'viewer.route.destination.count.other': ['{count} reachable nodes are highlighted. Click one.', '有 {count} 个能走到的节点已高亮,点一个。'], + 'viewer.route.noOutgoing': ['No arrows lead out of this node. Clear it and pick another start.', '这个节点没有向外的箭头。清除后换一个起点。'], + 'viewer.route.result.title': ['{source} to {target}', '{source} 到 {target}'], + 'viewer.route.finder.source.title': ['Choose route start', '选择路径起点'], + 'viewer.route.finder.source.placeholder': ['Search route sources', '搜索路径来源'], + 'viewer.route.finder.source.empty': ['No matching route sources', '没有匹配的路径来源'], + 'viewer.route.finder.source.results': ['Nodes that can start a route', '可作为路径起点的节点'], + 'viewer.route.finder.source.noun': ['route sources', '个路径来源'], + 'viewer.route.finder.source.badge': ['start', '起点'], + 'viewer.route.finder.target.title': ['Destination from {label}', '从{label}出发的目标'], + 'viewer.route.finder.target.placeholder': ['Search reachable destinations', '搜索可达目标'], + 'viewer.route.finder.target.empty': ['No matching reachable destinations', '没有匹配的可达目标'], + 'viewer.route.finder.target.results': ['Reachable route destinations', '可达路径目标'], + 'viewer.route.finder.target.noun': ['reachable destinations', '个可达目标'], + 'viewer.route.hop.one': ['{count} hop', '{count} 跳'], + 'viewer.route.hop.other': ['{count} hops', '{count} 跳'], + + 'viewer.lens.eyebrow': ['Semantic lens', '语义透镜'], + 'viewer.lens.title': ['Compare system roles', '比较系统角色'], + 'viewer.lens.close': ['Close semantic lens', '关闭语义透镜'], + 'viewer.lens.instruction': ['Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.', '最多选择两种语义类型。选择一种可显示其真实流量;选择两种只比较直接编写的关系。'], + 'viewer.lens.kinds': ['Semantic kinds', '语义类型'], + 'viewer.lens.choose': ['Choose a kind to inspect its nodes and touching relationships.', '选择一种类型以检查其节点和相连关系。'], + 'viewer.lens.copy': ['Copy link to semantic lens', '复制语义透镜链接'], + 'viewer.lens.clear': ['Clear semantic lens', '清除语义透镜'], + 'viewer.lens.open': ['Open semantic lens', '打开语义透镜'], + 'viewer.lens.openActive': ['Open active semantic lens', '打开当前语义透镜'], + 'viewer.lens.legend': ['Semantic legend', '语义图例'], + 'viewer.lens.legend.inspect.one': ['Inspect {label}, {count} node', '检查{label},{count} 个节点'], + 'viewer.lens.legend.inspect.other': ['Inspect {label}, {count} nodes', '检查{label},{count} 个节点'], + 'viewer.lens.kind.count.one': ['{label}, {count} node', '{label},{count} 个节点'], + 'viewer.lens.kind.count.other': ['{label}, {count} nodes', '{label},{count} 个节点'], + 'viewer.lens.compare.one': ['{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direct relationship', '{first} → {second}:{forward} · {second} → {first}:{reverse} · 共 {count} 条直接关系'], + 'viewer.lens.compare.other': ['{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direct relationships', '{first} → {second}:{forward} · {second} → {first}:{reverse} · 共 {count} 条直接关系'], + 'viewer.lens.single': ['{nodes} · {relationships} · connected peers remain visible', '{nodes} · {relationships} · 已连接节点保持可见'], + 'viewer.lens.node.one': ['{count} {label} node', '{count} 个{label}节点'], + 'viewer.lens.node.other': ['{count} {label} nodes', '{count} 个{label}节点'], + 'viewer.lens.relationship.one': ['{count} touching relationship', '{count} 条相连关系'], + 'viewer.lens.relationship.other': ['{count} touching relationships', '{count} 条相连关系'], + + 'viewer.radar.title': ['Semantic radar', '语义雷达'], + 'viewer.radar.building': ['Building overview', '正在构建总览'], + 'viewer.radar.openFull': ['Open full semantic radar', '打开完整语义雷达'], + 'viewer.radar.open': ['Open radar', '打开雷达'], + 'viewer.radar.close': ['Close semantic radar', '关闭语义雷达'], + 'viewer.radar.surface': ['Diagram overview. Click a node to focus it, or use arrow keys to pan.', '图表总览。点击节点进行聚焦,或使用方向键平移。'], + 'viewer.radar.click': ['Click node', '点击节点'], + 'viewer.radar.drag': ['Drag to pan', '拖动平移'], + 'viewer.radar.space': ['Semantic radar needs more MAP space.', '语义雷达需要更多地图可见空间。'], + 'viewer.radar.nodes': ['Semantic diagram radar nodes', '语义图表雷达节点'], + 'viewer.radar.focus': ['Focus {label} from Semantic Radar', '从语义雷达聚焦{label}'], + 'viewer.radar.status': ['{count} nodes · {viewport}', '{count} 个节点 · {viewport}'], + 'viewer.radar.fullMap': ['{count} nodes · full map', '{count} 个节点 · 完整地图'], + 'viewer.radar.compacted': ['Radar compacted to avoid covering the Semantic Passport or MAP controls.', '已收紧雷达,避免遮挡语义护照或地图控件。'], + 'viewer.radar.cancelWaiting': ['Cancel semantic radar waiting for more MAP space', '取消等待更多地图空间的语义雷达'], + 'viewer.radar.needsSpace': ['Semantic radar needs more visible MAP space', '语义雷达需要更多可见地图空间'], + 'viewer.radar.viewport.full': ['full map', '完整地图'], + 'viewer.radar.viewport.width': ['{percent}% width', '宽度 {percent}%'], + 'viewer.radar.viewport.scale': ['{percent}% viewport', '视口 {percent}%'], + + 'viewer.nav.controls': ['Diagram view controls', '图表视图控制'], + 'viewer.nav.route': ['Trace a directed route', '追踪有向路径'], + 'viewer.nav.route.title': ['Trace route (R)', '追踪路径(R)'], + 'viewer.nav.route.short': ['PATH', '路径'], + 'viewer.nav.radar': ['Open semantic radar', '打开语义雷达'], + 'viewer.nav.radar.title': ['Semantic radar (M)', '语义雷达(M)'], + 'viewer.nav.radar.short': ['MAP', '地图'], + 'viewer.nav.lens': ['Open semantic lens', '打开语义透镜'], + 'viewer.nav.lens.title': ['Semantic lens (L)', '语义透镜(L)'], + 'viewer.nav.lens.short': ['LENS', '透镜'], + 'viewer.nav.find': ['Find a node', '查找节点'], + 'viewer.nav.find.title': ['Find a node (/)', '查找节点(/)'], + 'viewer.outline.title': ['Node index', '节点索引'], + 'viewer.rail.show': ['Notes & index', '要点与索引'], + 'viewer.rail.controls': ['Side panel', '侧栏'], + 'viewer.rail.collapse': ['Collapse side panel', '收起侧栏'], + 'viewer.rail.bottom': ['Move panel below the diagram', '移到图下方'], + 'viewer.rail.right': ['Move panel beside the diagram', '移到图右侧'], + 'viewer.nav.guide': ['Open diagram guide', '打开图表指南'], + 'viewer.nav.guide.title': ['Diagram guide (?)', '图表指南(?)'], + 'viewer.nav.zoomOut': ['Zoom out', '缩小'], + 'viewer.nav.zoomOut.title': ['Zoom out (-)', '缩小(-)'], + 'viewer.nav.reset': ['Reset diagram view', '重置图表视图'], + 'viewer.nav.reset.title': ['Reset view (0)', '重置视图(0)'], + 'viewer.nav.read': ['READ', '阅读'], + 'viewer.nav.zoomIn': ['Zoom in', '放大'], + 'viewer.nav.zoomIn.title': ['Zoom in (+)', '放大(+)'], + 'viewer.nav.camera': ['{hint}. Reset diagram view', '{hint}。重置图表视图'], + 'viewer.nav.camera.title': ['{semantic}{hint} · reset view (0)', '{semantic}{hint} · 重置视图(0)'], + 'viewer.nav.camera.semantic': ['Semantic camera active · ', '语义相机已启用 · '], + 'viewer.nav.level.map': ['MAP', '概览'], + 'viewer.nav.level.read': ['READ', '阅读'], + 'viewer.nav.level.full': ['FULL', '完整'], + 'viewer.nav.level.auto': ['AUTO', '自动'], + 'viewer.nav.detail.map': ['Zoom in to reveal relationship labels and node context', '放大以显示关系标签和节点上下文'], + 'viewer.nav.detail.read': ['Zoom in again to reveal tags and annotations', '再次放大以显示标签和注释'], + 'viewer.nav.detail.full': ['Full diagram detail', '完整图表详情'], + + 'viewer.intent.summary': ['{label}. {out} outgoing, {in} incoming{loops}. {total} connections. Press Enter for details.', '{label}。{out} 条出向,{in} 条入向{loops}。共 {total} 条连接。按 Enter 查看详情。'], + 'viewer.intent.loops': [', {count} self loop', ',{count} 条自环'], + + 'viewer.common.copied': ['Copied', '已复制'], + 'viewer.common.copyFailed': ['Copy failed', '复制失败'], + 'viewer.common.copyLink': ['Copy link', '复制链接'], + 'viewer.common.clear': ['Clear', '清除'], + 'viewer.common.close': ['Close', '关闭'], +}; + +for (const [key, messages] of Object.entries(MESSAGE_PAIRS)) { + if (messages.length !== SUPPORTED_LOCALES.length || messages.some((message) => typeof message !== 'string')) { + throw new Error(`Incomplete Archify i18n tuple ${JSON.stringify(key)}`); + } +} + +const BUILTIN_CATALOGS = Object.fromEntries(SUPPORTED_LOCALES.map((locale, index) => [ + locale, + Object.fromEntries(Object.entries(MESSAGE_PAIRS).map(([key, pair]) => [key, pair[index]])), +])); + +const EN = BUILTIN_CATALOGS.en; + +const CANONICAL_KEYS = Object.keys(EN); +const PLACEHOLDER_PATTERN = /\{([a-zA-Z0-9_]+)\}/g; + +function extractPlaceholders(message) { + return new Set([...String(message).matchAll(PLACEHOLDER_PATTERN)].map((match) => match[1])); +} + +const CANONICAL_PLACEHOLDERS = Object.fromEntries( + CANONICAL_KEYS.map((key) => [key, extractPlaceholders(EN[key])]), +); + +function placeholdersMatch(expected, actual) { + if (expected.size !== actual.size) return false; + for (const token of expected) if (!actual.has(token)) return false; + return true; +} + +// Runtime catalogs registered by registerLocale(), keyed by whatever locale +// tag the caller supplied (e.g. an agent-authored 'fr'). Kept separate from +// BUILTIN_CATALOGS so a caller can never accidentally shadow a shipped +// catalog with a partial one. +const RUNTIME_CATALOGS = new Map(); + +function catalogFor(locale) { + return RUNTIME_CATALOGS.get(locale) || BUILTIN_CATALOGS[locale]; +} + +// Validates caller-supplied translation data against the canonical (English) +// message-key set. Pure and side-effect free: registerLocale() calls this +// and additionally builds/installs the resolved catalog. +export function validateTranslations(translations = {}) { + const supplied = Object.keys(translations || {}); + const suppliedSet = new Set(supplied); + const missingKeys = CANONICAL_KEYS.filter((key) => !suppliedSet.has(key)); + const unknownKeys = supplied.filter((key) => !Object.hasOwn(CANONICAL_PLACEHOLDERS, key)); + const placeholderMismatches = []; + const usableKeys = []; + for (const key of supplied) { + if (!Object.hasOwn(CANONICAL_PLACEHOLDERS, key)) continue; + const value = translations[key]; + if (typeof value !== 'string' || value.length === 0) { + placeholderMismatches.push({ key, expected: [...CANONICAL_PLACEHOLDERS[key]].sort(), actual: null }); + continue; + } + const actual = extractPlaceholders(value); + if (placeholdersMatch(CANONICAL_PLACEHOLDERS[key], actual)) { + usableKeys.push(key); + } else { + placeholderMismatches.push({ + key, + expected: [...CANONICAL_PLACEHOLDERS[key]].sort(), + actual: [...actual].sort(), + }); + } + } + return { + totalKeys: CANONICAL_KEYS.length, + coveredKeys: usableKeys.length, + coverage: CANONICAL_KEYS.length ? usableKeys.length / CANONICAL_KEYS.length : 1, + missingKeys, + unknownKeys, + placeholderMismatches, + }; +} + +// Registers a fully-resolved catalog for an arbitrary locale tag, built by +// layering validated translations over the English base. A key that is +// missing, non-string, or whose interpolation placeholders don't match the +// canonical set falls back to its English string — partial or malformed +// translation data can never break rendering. Returns the same coverage +// report validateTranslations() would, for the caller to surface as an +// explicit fallback/coverage diagnostic before rendering. +export function registerLocale(locale, translations = {}) { + const report = validateTranslations(translations); + const catalog = { ...EN }; + for (const key of CANONICAL_KEYS) { + const value = translations?.[key]; + if (typeof value !== 'string' || value.length === 0) continue; + if (placeholdersMatch(CANONICAL_PLACEHOLDERS[key], extractPlaceholders(value))) { + catalog[key] = value; + } + } + RUNTIME_CATALOGS.set(locale, catalog); + return { locale, ...report }; +} + +export function resolveLocale(locale) { + return catalogFor(locale) ? locale : DEFAULT_LOCALE; +} + +export function formatMessage(template, values = {}) { + return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => ( + Object.hasOwn(values, key) ? String(values[key]) : match + )); +} + +export function translateMessage(locale, key, values = {}) { + const resolved = resolveLocale(locale); + const catalog = catalogFor(resolved); + if (!Object.hasOwn(catalog, key)) { + throw new Error(`Missing Archify i18n message ${JSON.stringify(key)} for ${resolved}`); + } + return formatMessage(catalog[key], values); +} + +export function translateCount(locale, key, count, values = {}) { + const suffix = count === 1 ? 'one' : 'other'; + return translateMessage(locale, `${key}.${suffix}`, { ...values, count }); +} + +export function viewerCatalog(locale) { + const resolved = resolveLocale(locale); + return Object.fromEntries(Object.entries(catalogFor(resolved)).filter(([key]) => key.startsWith('viewer.'))); +} + +export function localizeTemplate(template, locale) { + return template.replace(/\{\{i18n:([a-zA-Z0-9_.-]+)\}\}/g, (_match, key) => escapeHtml(translateMessage(locale, key))); +} + +export function catalogKeys() { + return [...CANONICAL_KEYS]; +} diff --git a/vendor/archify/renderers/shared/layout-report.mjs b/vendor/archify/renderers/shared/layout-report.mjs new file mode 100644 index 0000000..0993272 --- /dev/null +++ b/vendor/archify/renderers/shared/layout-report.mjs @@ -0,0 +1,42 @@ +/** Serialize computed layout for dry-run / inspect (#9). */ + +export function componentBox(c) { + return { + id: c.id, + type: c.type, + label: c.label, + x: Math.round(c.x), + y: Math.round(c.y), + width: c.width, + height: c.height, + ...(Number.isInteger(c.row) ? { row: c.row } : {}), + ...(Number.isInteger(c.col) ? { col: c.col } : {}), + ...(Array.isArray(c.pos) ? { pos: c.pos.map(Math.round) } : {}), + }; +} + +export function boundaryBox(b) { + return { + kind: b.kind, + label: b.label, + x: Math.round(b.x), + y: Math.round(b.y), + width: Math.round(b.width), + height: Math.round(b.height), + wraps: b.wraps, + }; +} + +export function connectionPath(conn, routed, labelAt) { + return { + from: conn.from, + to: conn.to, + label: conn.label ?? null, + variant: conn.variant ?? 'default', + route: conn.route ?? 'auto', + // These points are repair inputs: rounding a fractional port makes a + // reused waypoint diagonal relative to the actual endpoint. + points: routed.points.map(([x, y]) => [x, y]), + ...(labelAt ? { labelAt: labelAt.map(Math.round) } : {}), + }; +} diff --git a/vendor/archify/renderers/shared/legend.mjs b/vendor/archify/renderers/shared/legend.mjs new file mode 100644 index 0000000..5879de5 --- /dev/null +++ b/vendor/archify/renderers/shared/legend.mjs @@ -0,0 +1,217 @@ +import { throwDiagnosticError } from './diagnostics.mjs'; +import { rectsOverlap, segmentIntersectsRect } from './geometry.mjs'; +import { esc, textUnits } from './utils.mjs'; +import { translateMessage } from './i18n.mjs'; + +const DEFAULT_FONT_SIZE = 8; +const DEFAULT_ITEM_GAP = 22; +const DEFAULT_LINE_GAP = 22; +const DEFAULT_SWATCH_GAP = 8; +const TEXT_ADVANCE_EM = 0.62; +const INTERACTIVE_BADGE_ALLOWANCE = 21; + +export function relationshipLegendObstacles(relations, { pointsFor, labelRectFor } = {}) { + const obstacles = []; + for (const [index, relation] of (Array.isArray(relations) ? relations : []).entries()) { + const points = typeof pointsFor === 'function' ? pointsFor(relation, index) : []; + const finitePoints = (Array.isArray(points) ? points : []).filter((point) => ( + Array.isArray(point) && point.length === 2 && point.every(Number.isFinite) + )); + for (let pointIndex = 0; pointIndex < finitePoints.length - 1; pointIndex += 1) { + obstacles.push({ + kind: 'relationship-segment', + start: finitePoints[pointIndex], + end: finitePoints[pointIndex + 1], + }); + } + const labelRect = typeof labelRectFor === 'function' ? labelRectFor(relation, index) : null; + if (labelRect && [labelRect.x, labelRect.y, labelRect.width, labelRect.height].every(Number.isFinite)) { + obstacles.push({ kind: 'relationship-label', ...labelRect }); + } + } + return obstacles; +} + +export function resolveLegend(config, catalog, presentKinds) { + const mode = config?.mode || 'auto'; + if (mode === 'hidden') return []; + const present = presentKinds instanceof Set ? presentKinds : new Set(presentKinds || []); + const overrides = config?.entries || {}; + + return catalog.flatMap((catalogEntry) => { + const override = overrides[catalogEntry.kind] || {}; + const selectedByMode = mode === 'all' || present.has(catalogEntry.kind); + const visible = override.visible === true || (selectedByMode && override.visible !== false); + if (!visible) return []; + return [{ + ...catalogEntry, + label: override.label || catalogEntry.label, + present: present.has(catalogEntry.kind), + interactive: catalogEntry.interactive !== false && present.has(catalogEntry.kind), + }]; + }); +} + +function measuredEntryWidth(entry, fontSize, swatchGap) { + const swatchWidth = entry.swatchWidth ?? 14; + return Math.ceil( + swatchWidth + + swatchGap + + textUnits(entry.label) * fontSize * TEXT_ADVANCE_EM + + (entry.interactive ? INTERACTIVE_BADGE_ALLOWANCE : 0), + ); +} + +// One pure footprint calculation owns both auto-viewBox sizing and final SVG +// placement. Callers must not maintain a second approximation of legend width +// or row count; that would make generated geometry disagree with validation. +export function legendFootprint(entries, { + width, + fontSize = DEFAULT_FONT_SIZE, + itemGap = DEFAULT_ITEM_GAP, + lineGap = DEFAULT_LINE_GAP, + swatchGap = DEFAULT_SWATCH_GAP, +} = {}) { + if (!entries.length) { + return { measured: [], rows: [], rowCount: 0, minWidth: 0, extraHeight: 0 }; + } + const measured = entries.map((entry) => ({ + ...entry, + width: measuredEntryWidth(entry, fontSize, entry.swatchGap ?? swatchGap), + })); + const rows = [[]]; + let cursor = 0; + for (const entry of measured) { + const row = rows.at(-1); + const required = (row.length ? itemGap : 0) + entry.width; + if (row.length && cursor + required > width) { + rows.push([entry]); + cursor = entry.width; + } else { + row.push(entry); + cursor += required; + } + } + return { + measured, + rows, + rowCount: rows.length, + minWidth: measured.reduce((width, entry) => Math.max(width, entry.width), 0), + extraHeight: (rows.length - 1) * lineGap, + }; +} + +export function measureLegend(entries, { + x, + baselineY, + width, + fontSize = DEFAULT_FONT_SIZE, + itemGap = DEFAULT_ITEM_GAP, + lineGap = DEFAULT_LINE_GAP, + swatchGap = DEFAULT_SWATCH_GAP, + minTitleY = 0, + obstacles = [], + unfit = 'error', + diagramType = 'diagram', +} = {}) { + if (!entries.length) return { entries: [], rowCount: 0, titleY: null }; + const footprint = legendFootprint(entries, { width, fontSize, itemGap, lineGap, swatchGap }); + const tooWide = footprint.measured.find((entry) => entry.width > width); + if (tooWide) { + if (unfit === 'hide') return null; + const message = `[legend/label-too-wide] ${diagramType} legend label for "${tooWide.kind}" needs ${tooWide.width}px but only ${width}px is available.`; + throwDiagnosticError(message, [{ + code: 'legend/label-too-wide', + severity: 'error', + message, + subject: { diagramType, path: `/meta/legend/entries/${tooWide.kind}/label` }, + evidence: { kind: tooWide.kind, measuredWidthPx: tooWide.width, availableWidthPx: width }, + supportedFixes: ['shorten the legend label or use a wider viewBox'], + }]); + } + + const titleY = baselineY - footprint.extraHeight - 20; + const legendTopY = titleY - 10; + if (legendTopY < minTitleY) { + if (unfit === 'hide') return null; + const message = `[legend/vertical-overflow] ${diagramType} legend needs ${footprint.rowCount} rows, which would start at y=${legendTopY} above the available legend band at y=${minTitleY}.`; + throwDiagnosticError(message, [{ + code: 'legend/vertical-overflow', + severity: 'error', + message, + subject: { diagramType, path: '/meta/legend' }, + evidence: { rowCount: footprint.rowCount, requiredTopY: legendTopY, availableTopY: minTitleY }, + supportedFixes: ['shorten legend labels, hide nonessential entries, or use a wider viewBox'], + }]); + } + + const positioned = []; + footprint.rows.forEach((row, rowIndex) => { + let entryX = x; + const baseline = baselineY - (footprint.rowCount - rowIndex - 1) * lineGap; + for (const entry of row) { + positioned.push({ ...entry, x: entryX, baseline, row: rowIndex }); + entryX += entry.width + itemGap; + } + }); + + const legendRects = [ + { kind: 'title', x, y: legendTopY, width: 48, height: 14 }, + ...positioned.map((entry) => ({ + kind: entry.kind, + x: entry.x, + y: entry.baseline - 10, + width: entry.width, + height: 14, + })), + ]; + const collision = legendRects.find((legendRect) => obstacles.some((obstacle) => ( + Array.isArray(obstacle.start) && Array.isArray(obstacle.end) + ? segmentIntersectsRect({ start: obstacle.start, end: obstacle.end }, legendRect) + : rectsOverlap(obstacle, legendRect) + ))); + if (collision) { + if (unfit === 'hide') return null; + const message = `[legend/content-overlap] ${diagramType} legend entry "${collision.kind}" overlaps authored relationship geometry.`; + throwDiagnosticError(message, [{ + code: 'legend/content-overlap', + severity: 'error', + message, + subject: { diagramType, path: '/meta/legend' }, + evidence: { legendKind: collision.kind, legendRect: collision }, + supportedFixes: ['shorten or hide legend entries, use a wider viewBox, or move the authored relationship route/label out of the legend band'], + }]); + } + + return { + entries: positioned, + rowCount: footprint.rowCount, + titleY, + fontSize, + }; +} + +export function renderLegend({ entries, layout, renderSwatch, locale }) { + if (!entries.length) return ''; + const measured = measureLegend(entries, layout); + if (!measured) return ''; + const hasInteractiveEntries = measured.entries.some((entry) => entry.interactive); + const renderedFontSize = measured.fontSize < 8 ? measured.fontSize + 0.5 : measured.fontSize + 2; + const rootAttributes = hasInteractiveEntries ? ' data-legend="" data-legend-bridge=""' : ' data-legend=""'; + const parts = [ + ` `, + ` ${esc(translateMessage(locale, 'legend.title'))}`, + ]; + + for (const entry of measured.entries) { + const interactive = entry.interactive + ? ` data-legend-kind="${esc(entry.kind)}" data-legend-label="${esc(entry.label)}"` + : ''; + parts.push(` `); + parts.push(` ${renderSwatch(entry)}`); + parts.push(` ${esc(entry.label)}`); + parts.push(' '); + } + parts.push(' '); + return parts.join('\n'); +} diff --git a/vendor/archify/renderers/shared/output-path.mjs b/vendor/archify/renderers/shared/output-path.mjs new file mode 100644 index 0000000..372249d --- /dev/null +++ b/vendor/archify/renderers/shared/output-path.mjs @@ -0,0 +1,440 @@ +import path from 'node:path'; +import { + containedBy, + isValidWindowsSmbShareName, + isWindowsIpcShare, + resolvePhysicalLocation, + sameLocation, +} from './path-semantics.mjs'; +import { PortablePathError, validatePortablePath } from './portable-path.mjs'; + +export function canonicalFuturePath(targetPath) { + const resolution = resolvePhysicalLocation(targetPath); + if (resolution.status === 'resolved') { + if (resolution.location.kind === 'existing') return resolution.location.path; + return path.join( + resolution.location.ancestorPath, + ...resolution.location.unresolved, + ); + } + const output = path.resolve(targetPath); + if (resolution.reason.code === 'symlink-cycle') { + throw new OutputPathError(`Output path contains a symbolic-link cycle: "${output}".`, { + code: 'output/symlink-cycle', + message: 'Output path could not be resolved because it contains a symbolic-link cycle.', + subject: { output }, + evidence: { relation: resolution.reason }, + supportedFixes: ['remove the symbolic-link cycle or choose an output path outside it'], + }); + } + throw new OutputPathError('Output path could not be resolved safely.', { + code: 'output/path-resolution-indeterminate', + message: 'Output path could not be resolved safely for the requested filesystem location.', + subject: { output }, + evidence: { relation: resolution.reason }, + supportedFixes: ['use an ordinary local filesystem path that can be resolved safely, then retry'], + }); +} + +export function pathsAlias(leftPath, rightPath) { + const relation = sameLocation(leftPath, rightPath); + if (relation.status === 'match') return true; + if (relation.status === 'different') return false; + if (relation.reason.code === 'ancestor-not-directory') return false; + if (relation.reason.code === 'symlink-cycle') { + const output = path.resolve(relation.reason.side === 'right' ? rightPath : leftPath); + throw new OutputPathError(`Output path contains a symbolic-link cycle: "${output}".`, { + code: 'output/symlink-cycle', + message: 'Output path could not be resolved because it contains a symbolic-link cycle.', + subject: { output }, + evidence: { relation: relation.reason }, + supportedFixes: ['remove the symbolic-link cycle or choose an output path outside it'], + }); + } + throw new OutputPathError('Path identity could not be determined safely.', { + code: 'output/path-identity-indeterminate', + message: 'Path identity could not be determined safely for the requested filesystem location.', + subject: { left: path.resolve(leftPath), right: path.resolve(rightPath) }, + evidence: { relation: relation.reason }, + supportedFixes: ['use ordinary local filesystem paths whose identity can be verified, then retry'], + }); +} + +function pathIsInside(directoryPath, targetPath) { + const relation = containedBy(directoryPath, targetPath); + if (relation.status === 'match') return true; + if (relation.status === 'different') return false; + throw new OutputPathError('Output containment could not be determined safely.', { + code: 'output/containment-indeterminate', + message: 'Output containment could not be determined safely for the requested filesystem location.', + subject: { output: path.resolve(targetPath), cwd: path.resolve(directoryPath) }, + evidence: { relation: relation.reason }, + supportedFixes: ['use an output beneath an ordinary local directory whose identity can be verified'], + }); +} + +function authoredOutputDiagnostic(error, rawOutput) { + const absolute = error?.reason === 'absolute'; + const code = absolute ? 'output/meta-absolute' : 'output/meta-path-syntax'; + const message = absolute + ? 'meta.output must be a relative path resolved from the current working directory.' + : 'meta.output must be a portable POSIX-relative path.'; + return { + code, + message, + subject: { output: rawOutput, path: '/meta/output' }, + evidence: { + reason: error?.reason || 'invalid', + ...(error?.segmentIndex !== undefined ? { segmentIndex: error.segmentIndex } : {}), + ...(error?.segment !== undefined ? { segment: error.segment } : {}), + ...(error?.utf8Bytes !== undefined ? { utf8Bytes: error.utf8Bytes } : {}), + ...(error?.utf16CodeUnits !== undefined ? { utf16CodeUnits: error.utf16CodeUnits } : {}), + ...(error?.limit !== undefined ? { limit: error.limit } : {}), + }, + supportedFixes: ['set meta.output to a portable relative .html path such as reports/diagram.html'], + }; +} + +function nativeOutputDiagnostic(rawOutput, reason, details = {}) { + return { + code: 'output/native-path-syntax', + message: 'The output path is not a valid native filesystem path on this host.', + subject: { output: rawOutput }, + evidence: { reason, ...details }, + supportedFixes: ['choose an ordinary filesystem path without device names, alternate data streams, trailing dots or spaces, or overlong components'], + }; +} + +function throwNativeOutputDiagnostic(rawOutput, reason, details = {}) { + const diagnostic = nativeOutputDiagnostic(rawOutput, reason, details); + throw new OutputPathError(diagnostic.message, diagnostic); +} + +function windowsExtendedTailComponents(rawOutput, tail) { + const authoredComponents = tail.split('\\'); + // Preserve one trailing separator for directory arguments, but never repair + // an empty component inside the raw extended namespace. + if (authoredComponents.slice(0, -1).some((component) => component.length === 0)) { + throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root'); + } + return authoredComponents.filter(Boolean); +} + +function rejectWindowsIpcShare(rawOutput, share) { + if (isWindowsIpcShare(share)) { + throwNativeOutputDiagnostic(rawOutput, 'windows-ipc-namespace', { share }); + } +} + +function validateWindowsNativeComponent( + rawOutput, + component, + componentIndex, + { allowReservedName = false } = {}, +) { + try { + // Prefix the component so a colon is classified as an ADS separator, + // rather than allowing the generic URI detector to claim it first. + validatePortablePath(`native/${component}`, { profile: 'output' }); + } catch (error) { + if (!(error instanceof PortablePathError)) throw error; + if (allowReservedName && error.reason === 'windows-reserved-name') return; + // Native Windows arguments may intentionally name an existing 8.3 alias. + // Portable authored/archive paths reject that ambiguous spelling, while + // native resolution lets the filesystem prove the existing target. + if (error.reason === 'windows-short-name') { + if (component.length <= 255) return; + throwNativeOutputDiagnostic(rawOutput, 'component-too-long', { + component, + componentIndex, + utf16CodeUnits: component.length, + limit: 255, + }); + } + // Native Windows filesystems bound components in UTF-16 code units. The + // stricter UTF-8 bound belongs to portable authored/archive names only. + if (error.reason === 'component-too-long' && error.utf16CodeUnits <= 255) return; + throwNativeOutputDiagnostic(rawOutput, error.reason, { + component, + componentIndex, + ...(error.character !== undefined ? { character: error.character } : {}), + ...(error.utf8Bytes !== undefined ? { utf8Bytes: error.utf8Bytes } : {}), + ...(error.utf16CodeUnits !== undefined ? { utf16CodeUnits: error.utf16CodeUnits } : {}), + ...(error.limit !== undefined ? { limit: error.limit } : {}), + }); + } +} + +function validateWindowsUncRootComponents(rawOutput, server, share) { + rejectWindowsIpcShare(rawOutput, share); + if (!isValidWindowsSmbShareName(share)) { + throwNativeOutputDiagnostic(rawOutput, 'windows-unc-share-name', { + share, + utf16CodeUnits: share.length, + limit: 80, + }); + } + // UNC servers and shares are root components, not DOS file names. Keep the + // server's ordinary native syntax checks while allowing names such as CON. + validateWindowsNativeComponent(rawOutput, server, 0, { allowReservedName: true }); +} + +function windowsExtendedPathComponents(rawOutput) { + // The extended-length namespace deliberately bypasses Win32 normalization. + // Inspect its original spelling so a dot segment cannot retarget a UNC share. + if (rawOutput.includes('/')) { + throwNativeOutputDiagnostic(rawOutput, 'windows-extended-separator', { character: '/' }); + } + if (/^\\\\\?\\(?:GLOBALROOT|Device)(?:\\|$)/iu.test(rawOutput)) { + throwNativeOutputDiagnostic(rawOutput, 'windows-device-namespace'); + } + + const drive = rawOutput.match(/^\\\\\?\\[A-Za-z]:\\/u); + if (drive) { + return windowsExtendedTailComponents(rawOutput, rawOutput.slice(drive[0].length)); + } + + const uncPrefix = rawOutput.match(/^\\\\\?\\UNC\\/iu); + if (uncPrefix) { + const authoredTail = rawOutput.slice(uncPrefix[0].length); + const components = authoredTail.split('\\'); + if (components.length < 2 || components[0].length === 0 || components[1].length === 0) { + throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root'); + } + validateWindowsUncRootComponents(rawOutput, components[0], components[1]); + return windowsExtendedTailComponents(rawOutput, components.slice(2).join('\\')); + } + + throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root'); +} + +function validateWindowsRawUncRoot(rawOutput) { + if (!/^[\\/]{2}/u.test(rawOutput) || /^[\\/]{2}[.?][\\/]/u.test(rawOutput)) return; + // Validate the raw server/share boundary before win32.normalize can collapse + // an empty share or mix the two UNC separator spellings. + const unc = rawOutput.match(/^([\\/])\1([^\\/]+)\1([^\\/]+)(?:[\\/]|$)/u); + if (!unc) { + throwNativeOutputDiagnostic(rawOutput, 'windows-unc-root'); + } + validateWindowsUncRootComponents(rawOutput, unc[2], unc[3]); +} + +function windowsPathComponents(rawOutput, normalized) { + const authoredUncPrefix = /^[\\/]{2}/u.test(rawOutput); + if (/^\\\\\.\\/u.test(normalized)) { + throwNativeOutputDiagnostic(rawOutput, 'windows-device-namespace'); + } + if (/^\\\\\?\\/u.test(normalized)) { + throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root'); + } + if (normalized.startsWith('\\\\')) { + const unc = normalized.match(/^\\\\([^\\]+)\\([^\\]+)(?:\\|$)/u); + if (!unc) throwNativeOutputDiagnostic(rawOutput, 'windows-unc-root'); + return normalized.slice(unc[0].length).split('\\').filter(Boolean); + } + if (authoredUncPrefix) throwNativeOutputDiagnostic(rawOutput, 'windows-unc-root'); + if (normalized.startsWith('\\')) { + throwNativeOutputDiagnostic(rawOutput, 'current-drive-rooted'); + } + const root = path.win32.parse(normalized).root; + return normalized + .slice(root.length) + .split(/[\\/]+/u) + // normalize() retains leading navigation for a relative path. Those dot + // segments are path syntax, not filename components subject to name rules. + .filter((component) => component && component !== '.' && component !== '..'); +} + +/** + * Validate command-line/default output paths using the active host's native + * syntax. Unlike authored portable paths, absolute paths and native separators + * remain supported. File outputs reject a trailing separator before native + * resolution can erase it; directory callers must opt in explicitly. The + * component bound also protects derived sidecars from failing after an + * operation has already started mutating the destination. + */ +export function validateNativeOutputPath( + rawOutput, + { platform = process.platform, kind = 'file' } = {}, +) { + if (kind !== 'file' && kind !== 'directory') { + throw new TypeError(`Unsupported native output path kind: ${JSON.stringify(kind)}`); + } + if (typeof rawOutput !== 'string' || rawOutput.length === 0) { + throwNativeOutputDiagnostic(rawOutput, 'empty'); + } + if (rawOutput.includes('\0')) throwNativeOutputDiagnostic(rawOutput, 'nul-character'); + if (/[\uD800-\uDFFF]/u.test(rawOutput)) { + throwNativeOutputDiagnostic(rawOutput, 'unpaired-surrogate'); + } + const hasTrailingSeparator = platform === 'win32' + ? /[\\/]$/u.test(rawOutput) + : rawOutput.endsWith(path.posix.sep); + if (kind === 'file' && hasTrailingSeparator) { + throwNativeOutputDiagnostic(rawOutput, 'trailing-separator', { kind }); + } + + let normalized; + let components; + if (platform === 'win32') { + if (/^[A-Za-z]:(?:$|[^\\/])/u.test(rawOutput)) { + throwNativeOutputDiagnostic(rawOutput, 'drive-relative'); + } + const extendedPrefix = rawOutput.startsWith('\\\\?\\'); + if (extendedPrefix) { + components = windowsExtendedPathComponents(rawOutput); + } else { + validateWindowsRawUncRoot(rawOutput); + normalized = path.win32.normalize(rawOutput); + components = windowsPathComponents(rawOutput, normalized); + } + for (const [componentIndex, component] of components.entries()) { + validateWindowsNativeComponent(rawOutput, component, componentIndex); + } + } else { + normalized = path.resolve(rawOutput); + const root = path.parse(normalized).root; + components = normalized.slice(root.length).split(path.sep).filter(Boolean); + for (const [componentIndex, component] of components.entries()) { + const utf8Bytes = Buffer.byteLength(component, 'utf8'); + if (utf8Bytes > 255) { + throwNativeOutputDiagnostic(rawOutput, 'component-too-long', { + component, + componentIndex, + utf8Bytes, + limit: 255, + }); + } + } + } + return rawOutput; +} + +/** Validate a CLI directory argument before native resolution can normalize away its raw syntax. */ +export function resolveNativeOutputDirectory( + rawDirectory, + { platform = process.platform, cwd = process.cwd() } = {}, +) { + validateNativeOutputPath(rawDirectory, { platform, kind: 'directory' }); + const pathApi = platform === 'win32' ? path.win32 : path.posix; + return pathApi.resolve(cwd, rawDirectory); +} + +export function validateAuthoredOutputPath(rawOutput, { cwd = process.cwd() } = {}) { + try { + validatePortablePath(rawOutput, { profile: 'output' }); + } catch (error) { + if (!(error instanceof PortablePathError)) throw error; + const diagnostic = authoredOutputDiagnostic(error, rawOutput); + throw new OutputPathError(diagnostic.message, diagnostic); + } + if (path.posix.extname(rawOutput).toLowerCase() !== '.html') { + throw new OutputPathError('meta.output must target an .html file.', { + code: 'output/meta-extension', + message: 'meta.output must target an .html file.', + subject: { output: rawOutput, path: '/meta/output' }, + supportedFixes: ['change meta.output to a portable path ending in .html'], + }); + } + const outputPath = path.resolve(cwd, rawOutput); + if (path.extname(canonicalFuturePath(outputPath)).toLowerCase() !== '.html') { + throw new OutputPathError('meta.output must resolve to an .html file.', { + code: 'output/meta-resolved-extension', + message: 'meta.output must resolve to an .html file after symbolic links are followed.', + subject: { output: rawOutput }, + supportedFixes: ['remove the symbolic-link alias or point it to an .html target inside the current working directory'], + }); + } + if (!pathIsInside(cwd, outputPath)) { + throw new OutputPathError('meta.output must stay inside the current working directory.', { + code: 'output/meta-outside-cwd', + message: 'meta.output must stay inside the current working directory after symbolic links are resolved.', + subject: { output: rawOutput, cwd: path.resolve(cwd) }, + supportedFixes: ['set meta.output to a relative .html path inside the current working directory'], + }); + } + return rawOutput; +} + +export class OutputPathError extends Error { + constructor(message, diagnostic) { + super(message); + this.name = 'OutputPathError'; + this.archifyDiagnostics = [{ + severity: 'error', + subject: {}, + evidence: {}, + supportedFixes: [], + ...diagnostic, + }]; + } +} + +export function resolveOutputPath({ + requestedOutput, + authoredOutput, + defaultOutput, + inputPaths = [], + inputDescription = 'an input', + otherOutputPaths = [], + cwd = process.cwd(), + requiredExtension = '.html', + platform = process.platform, +}) { + if (authoredOutput !== undefined) validateAuthoredOutputPath(authoredOutput, { cwd }); + const source = requestedOutput !== undefined + ? 'cli' + : (authoredOutput !== undefined ? 'meta' : 'default'); + const rawOutput = source === 'cli' + ? requestedOutput + : (source === 'meta' ? authoredOutput : defaultOutput); + if (source !== 'meta') validateNativeOutputPath(rawOutput, { platform, kind: 'file' }); + const outputPath = path.resolve(cwd, rawOutput); + for (const inputPath of inputPaths) { + if (!pathsAlias(outputPath, inputPath)) continue; + throw new OutputPathError(`Output must not replace ${inputDescription}.`, { + code: 'output/input-alias', + message: `Output must not replace ${inputDescription}, including through a symbolic-link or future-path alias.`, + subject: { output: outputPath, input: path.resolve(inputPath) }, + supportedFixes: ['choose an output path that is distinct from every input path'], + }); + } + for (const otherOutputPath of otherOutputPaths) { + if (!pathsAlias(outputPath, otherOutputPath)) continue; + throw new OutputPathError('Output targets must use distinct paths.', { + code: 'output/target-alias', + message: 'Output targets must use distinct paths, including symbolic-link and future-path aliases.', + subject: { output: outputPath, conflictingOutput: path.resolve(otherOutputPath) }, + supportedFixes: ['choose distinct paths for every generated output'], + }); + } + + // Keep explicit CLI directories unrestricted, but reject mistaken file types. + // Alias checks above retain priority when a target would overwrite an input. + if (source === 'cli') { + const resolvedOutput = canonicalFuturePath(outputPath); + const authoredExtension = path.extname(rawOutput).toLowerCase(); + const existingWindowsHtmlAlias = platform === 'win32' + && requiredExtension === '.html' + && authoredExtension === '.htm' + && path.extname(resolvedOutput).toLowerCase() === requiredExtension + && pathsAlias(outputPath, resolvedOutput); + const authoredMatches = authoredExtension === requiredExtension || existingWindowsHtmlAlias; + const resolvedMatches = path.extname(resolvedOutput).toLowerCase() === requiredExtension; + if (!authoredMatches || !resolvedMatches) { + const message = `CLI output must ${authoredMatches ? 'resolve to' : 'target'} a ${requiredExtension} file.`; + throw new OutputPathError(message, { + code: authoredMatches ? 'output/cli-resolved-extension' : 'output/cli-extension', + message, + subject: { output: rawOutput }, + evidence: { resolvedOutput, requiredExtension }, + supportedFixes: [`choose a path ending in ${requiredExtension} whose symbolic-link target also ends in ${requiredExtension}`], + }); + } + } + + return { + outputPath, + source, + }; +} diff --git a/vendor/archify/renderers/shared/path-semantics.mjs b/vendor/archify/renderers/shared/path-semantics.mjs new file mode 100644 index 0000000..59dc039 --- /dev/null +++ b/vendor/archify/renderers/shared/path-semantics.mjs @@ -0,0 +1,1017 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +const MAX_SYMLINK_DEPTH = 64; +const RECENT_PROBE_NAME_LIMIT = 128; +const recentProbeNames = new Set(); +const WINDOWS_SMB_SHARE_FORBIDDEN = /[\u0000-\u001F"\/\\[\]:|<>+=;,?*]/u; + +export function isWindowsIpcShare(share) { + return /^(?:IPC\$|pipe|mailslot)$/iu.test(share); +} + +export function isValidWindowsSmbShareName(share) { + return typeof share === 'string' + && share.length > 0 + && share.length <= 80 + && !WINDOWS_SMB_SHARE_FORBIDDEN.test(share) + && !/[ .]$/u.test(share); +} + +function result(status, code, details = {}) { + return { + status, + reason: { + code, + ...details, + }, + }; +} + +function unknown(code, details = {}) { + return result('unknown', code, details); +} + +function systemFailure(code, error, details = {}) { + return unknown(code, { + ...details, + ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}), + ...(typeof error?.recoveryPath === 'string' ? { recoveryPath: error.recoveryPath } : {}), + ...(typeof error?.restoredAtTarget === 'boolean' + ? { restoredAtTarget: error.restoredAtTarget } + : {}), + }); +} + +function hostPath() { + return process.platform === 'win32' ? path.win32 : path.posix; +} + +function absolutePath(input, side, basePath = process.cwd()) { + if (typeof input !== 'string' || input.length === 0) { + return { + ok: false, + failure: unknown('invalid-path', { side }), + }; + } + + try { + const pathApi = hostPath(); + if (pathApi.isAbsolute(input)) return { ok: true, path: input }; + if (pathApi.parse(input).root) { + return { + ok: false, + failure: unknown('drive-relative-path', { side }), + }; + } + const separator = basePath.endsWith(pathApi.sep) ? '' : pathApi.sep; + return { ok: true, path: `${basePath}${separator}${input}` }; + } catch (error) { + return { + ok: false, + failure: systemFailure('invalid-path', error, { side }), + }; + } +} + +function rootFailure(code, side) { + return { + ok: false, + failure: unknown(code, { side }), + }; +} + +function splitWindowsExtendedTail(tail, side, { internalRelativeLinkTarget = false } = {}) { + const authoredSegments = tail.split('\\'); + // A single final separator is harmless, while an internal empty segment + // would require normalizing a raw extended path before it reaches Windows. + if (!internalRelativeLinkTarget + && authoredSegments.slice(0, -1).some((component) => component.length === 0)) { + return rootFailure('windows-root-invalid', side); + } + const segments = authoredSegments.filter(Boolean); + if (!internalRelativeLinkTarget + && segments.some((component) => component === '.' || component === '..')) { + return rootFailure('windows-root-invalid', side); + } + return { ok: true, segments }; +} + +function splitWindowsAbsolute(absolute, side, { internalRelativeLinkTarget = false } = {}) { + if (/^[\\/]{2}\.[\\/]/u.test(absolute)) { + return rootFailure('windows-namespace-unsupported', side); + } + if (/^[\\/]{2}\?[\\/]/u.test(absolute) && !absolute.startsWith('\\\\?\\')) { + return rootFailure('windows-namespace-unsupported', side); + } + + if (absolute.startsWith('\\\\?\\')) { + if (absolute.includes('/')) return rootFailure('windows-root-invalid', side); + if ( + /^\\\\\?\\(?:GLOBALROOT|Device)(?:\\|$)/iu.test(absolute) + || /^\\\\\?\\Volume\{/iu.test(absolute) + ) { + return rootFailure('windows-namespace-unsupported', side); + } + + const extendedUnc = absolute.match(/^(\\\\\?\\UNC\\([^\\]+)\\([^\\]+))(?:\\(.*))?$/iu); + if (extendedUnc) { + if (isWindowsIpcShare(extendedUnc[3])) { + return rootFailure('windows-namespace-unsupported', side); + } + if (!isValidWindowsSmbShareName(extendedUnc[3])) { + return rootFailure('windows-root-invalid', side); + } + if ([extendedUnc[2], extendedUnc[3]].some((component) => component === '.' || component === '..')) { + return rootFailure('windows-root-invalid', side); + } + const tail = splitWindowsExtendedTail( + extendedUnc[4] || '', + side, + { internalRelativeLinkTarget }, + ); + if (!tail.ok) return tail; + return { + ok: true, + root: `${extendedUnc[1]}\\`, + segments: tail.segments, + }; + } + + const extendedDrive = absolute.match(/^(\\\\\?\\[A-Za-z]:\\)(.*)$/u); + if (extendedDrive) { + const tail = splitWindowsExtendedTail( + extendedDrive[2], + side, + { internalRelativeLinkTarget }, + ); + if (!tail.ok) return tail; + return { + ok: true, + root: extendedDrive[1], + segments: tail.segments, + }; + } + return rootFailure('windows-root-invalid', side); + } + + const unc = absolute.match(/^[\\/]{2}([^\\/]+)[\\/]([^\\/]+)(?:[\\/](.*))?$/u); + if (unc) { + if (isWindowsIpcShare(unc[2])) { + return rootFailure('windows-namespace-unsupported', side); + } + if (!isValidWindowsSmbShareName(unc[2])) { + return rootFailure('windows-root-invalid', side); + } + if ([unc[1], unc[2]].some((component) => component === '.' || component === '..')) { + return rootFailure('windows-root-invalid', side); + } + return { + ok: true, + root: `\\\\${unc[1]}\\${unc[2]}\\`, + segments: (unc[3] || '').split(/[\\/]+/u).filter(Boolean), + }; + } + + const drive = absolute.match(/^([A-Za-z]:)[\\/](.*)$/u); + if (drive) { + return { + ok: true, + root: `${drive[1]}\\`, + segments: drive[2].split(/[\\/]+/u).filter(Boolean), + }; + } + return rootFailure('windows-root-invalid', side); +} + +function splitAbsolute(absolute, side, options) { + if (process.platform === 'win32') return splitWindowsAbsolute(absolute, side, options); + const root = path.posix.parse(absolute).root; + return { + ok: true, + root, + segments: absolute.slice(root.length).split(/\/+/u).filter(Boolean), + }; +} + +function statBigInt(targetPath) { + return fs.statSync(targetPath, { bigint: true }); +} + +function ordinaryWindowsRoot(extendedRoot) { + const drive = extendedRoot.match(/^\\\\\?\\([A-Za-z]:\\)$/u); + if (drive) return drive[1]; + + const unc = extendedRoot.match(/^\\\\\?\\UNC\\([^\\]+)\\([^\\]+)\\$/iu); + return unc ? `\\\\${unc[1]}\\${unc[2]}\\` : null; +} + +function hasStableIdentity(stat) { + return typeof stat?.dev === 'bigint' + && typeof stat?.ino === 'bigint' + && stat.ino !== 0n; +} + +function compareKnownEntries(left, right, operation) { + if (hasStableIdentity(left.stat) && hasStableIdentity(right.stat)) { + const matches = left.stat.dev === right.stat.dev && left.stat.ino === right.stat.ino; + return result(matches ? 'match' : 'different', matches ? 'file-identity-match' : 'file-identity-different', { + operation, + method: 'bigint-stat', + }); + } + + // path-contract-allow: lexical-capability -- Canonical paths are compared inside the identity primitive. + if (left.realPath === right.realPath) { + return result('match', 'native-realpath-match', { + operation, + method: 'native-realpath', + }); + } + + return unknown('file-identity-unavailable', { + operation, + method: 'bigint-stat-and-native-realpath', + }); +} + +function rememberProbeName(name) { + recentProbeNames.add(name); + if (recentProbeNames.size > RECENT_PROBE_NAME_LIMIT) { + recentProbeNames.delete(recentProbeNames.values().next().value); + } +} + +function probeInterference(message, code = 'ARCHIFY_PROBE_IDENTITY_CHANGED', details = {}) { + const error = new Error(message); + error.code = code; + Object.assign(error, details); + return error; +} + +function sameStableIdentity(left, right) { + return hasStableIdentity(left) + && hasStableIdentity(right) + && left.dev === right.dev + && left.ino === right.ino; +} + +function preserveQuarantinedReplacement(quarantinePath, targetPath) { + try { + fs.linkSync(quarantinePath, targetPath); + return { restoredAtTarget: true }; + } catch { + // Preserve the quarantined entry when the original name was concurrently + // occupied or the host cannot create a hard link. Never overwrite either. + return { restoredAtTarget: false }; + } +} + +function removeOwnedProbe(targetPath, expectedStat) { + let currentStat; + try { + currentStat = fs.lstatSync(targetPath, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT') { + return probeInterference('filesystem probe disappeared before cleanup', 'ARCHIFY_PROBE_MISSING'); + } + return error; + } + if (!sameStableIdentity(expectedStat, currentStat)) { + return probeInterference('filesystem probe identity changed before cleanup'); + } + + const quarantineName = `.archify-path-semantics-cleanup-${process.pid}-${crypto.randomBytes(10).toString('hex')}`; + const quarantinePath = path.join(path.dirname(targetPath), quarantineName); + rememberProbeName(quarantineName); + try { + // Move the checked name into a fresh, unpublished quarantine namespace. + // A replacement introduced after the check is moved rather than deleted. + fs.renameSync(targetPath, quarantinePath); + } catch (error) { + return error; + } + + let quarantineStat; + try { + quarantineStat = fs.lstatSync(quarantinePath, { bigint: true }); + } catch (error) { + error.recoveryPath = quarantinePath; + error.restoredAtTarget = false; + return error; + } + if (!sameStableIdentity(expectedStat, quarantineStat)) { + const recovery = preserveQuarantinedReplacement(quarantinePath, targetPath); + return probeInterference( + 'filesystem probe identity changed during cleanup', + 'ARCHIFY_PROBE_IDENTITY_CHANGED', + { recoveryPath: quarantinePath, ...recovery }, + ); + } + + try { + // The quarantine spelling did not exist before the atomic rename and is + // never returned to callers. Legitimate target-path replacements therefore + // remain outside the only namespace we unlink. + fs.unlinkSync(quarantinePath); + return null; + } catch (error) { + error.recoveryPath = quarantinePath; + error.restoredAtTarget = false; + return error; + } +} + +function resolvePlan({ root, segments }, side, depth) { + if (depth > MAX_SYMLINK_DEPTH) { + return { + ok: false, + failure: unknown('symlink-cycle', { side }), + }; + } + + let current; + let currentStat; + try { + current = fs.realpathSync.native(root); + } catch (error) { + // Node on Windows can report EISDIR while resolving a valid extended + // filesystem root (for example \\?\C:\). The namespace was already + // validated by splitWindowsAbsolute, so retain its exact spelling for + // traversal. + if (process.platform === 'win32' + && error?.code === 'EISDIR' + && ordinaryWindowsRoot(root)) { + current = root; + } else { + return { + ok: false, + failure: systemFailure('root-resolution-failed', error, { side }), + }; + } + } + + try { + currentStat = statBigInt(current); + } catch (error) { + // The same Node/Windows edge can occur during stat after realpath succeeds. + // A drive or share root is short enough for its ordinary spelling, while + // `current` remains namespaced so descendants keep long-path semantics. + const ordinaryRoot = process.platform === 'win32' + && error?.code === 'EISDIR' + ? ordinaryWindowsRoot(root) + : null; + if (ordinaryRoot && ordinaryRoot !== current) { + try { + currentStat = statBigInt(ordinaryRoot); + } catch (statError) { + return { + ok: false, + failure: systemFailure('root-resolution-failed', statError, { side }), + }; + } + } else { + return { + ok: false, + failure: systemFailure('root-resolution-failed', error, { side }), + }; + } + } + + for (let index = 0; index < segments.length; index += 1) { + const segment = segments[index]; + if (segment === '.') continue; + if (segment === '..') { + if (!currentStat.isDirectory()) { + return { + ok: false, + failure: unknown('ancestor-not-directory', { side, segmentIndex: index }), + }; + } + try { + current = fs.realpathSync.native(hostPath().dirname(current)); + currentStat = statBigInt(current); + } catch (error) { + return { + ok: false, + failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }), + }; + } + continue; + } + + const candidate = hostPath().join(current, segment); + let candidateStat; + try { + candidateStat = fs.lstatSync(candidate, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT') { + if (!currentStat.isDirectory()) { + return { + ok: false, + failure: unknown('ancestor-not-directory', { side, segmentIndex: index }), + }; + } + const unresolved = segments.slice(index).filter((item) => item !== '.'); + if (unresolved.includes('..')) { + return { + ok: false, + failure: unknown('future-parent-traversal-indeterminate', { + side, + segmentIndex: index, + }), + }; + } + return { + ok: true, + existing: false, + ancestor: { + realPath: current, + stat: currentStat, + }, + unresolved, + }; + } + if (error?.code === 'ENOTDIR') { + return { + ok: false, + failure: systemFailure('ancestor-not-directory', error, { side, segmentIndex: index }), + }; + } + return { + ok: false, + failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }), + }; + } + + if (candidateStat.isSymbolicLink()) { + let linkPlan; + try { + const authoredTarget = fs.readlinkSync(candidate); + const pathApi = hostPath(); + const internalRelativeLinkTarget = !pathApi.isAbsolute(authoredTarget) + && !pathApi.parse(authoredTarget).root; + const parsedTarget = absolutePath(authoredTarget, side, hostPath().dirname(candidate)); + if (!parsedTarget.ok) return parsedTarget; + const target = splitAbsolute(parsedTarget.path, side, { internalRelativeLinkTarget }); + if (!target.ok) return target; + linkPlan = { + root: target.root, + segments: [...target.segments, ...segments.slice(index + 1)], + }; + } catch (error) { + return { + ok: false, + failure: systemFailure('symlink-resolution-failed', error, { side, segmentIndex: index }), + }; + } + return resolvePlan(linkPlan, side, depth + 1); + } + + if (index < segments.length - 1 && !candidateStat.isDirectory()) { + return { + ok: false, + failure: unknown('ancestor-not-directory', { side, segmentIndex: index }), + }; + } + + try { + current = fs.realpathSync.native(candidate); + currentStat = statBigInt(current); + } catch (error) { + return { + ok: false, + failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }), + }; + } + } + + return { + ok: true, + existing: true, + entry: { + realPath: current, + stat: currentStat, + }, + unresolved: [], + }; +} + +function resolveLocation(input, side) { + const parsed = absolutePath(input, side); + if (!parsed.ok) return parsed; + const plan = splitAbsolute(parsed.path, side); + if (!plan.ok) return plan; + return resolvePlan(plan, side, 0); +} + +function semanticNameEnvelope(name) { + const names = new Set([name]); + const pending = [name]; + while (pending.length > 0 && names.size <= 64) { + const current = pending.shift(); + const candidates = [ + current.normalize('NFC'), + current.normalize('NFD'), + current.toLocaleLowerCase('en-US'), + current.toLocaleUpperCase('en-US'), + ]; + for (const candidate of candidates) { + if (names.has(candidate)) continue; + names.add(candidate); + pending.push(candidate); + } + } + return names.size <= 64 ? names : null; +} + +function semanticEnvelopesOverlap(leftName, rightName) { + const left = semanticNameEnvelope(leftName); + const right = semanticNameEnvelope(rightName); + if (!left || !right) return true; + for (const spelling of left) { + if (right.has(spelling)) return true; + } + return false; +} + +function windowsDeviceName(name) { + const stem = name.split('.')[0].replace(/[ .]+$/u, '').toUpperCase(); + return /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9¹²³]|LPT[1-9¹²³])$/u.test(stem); +} + +function futureWindowsNameRisk(leftName, rightName) { + if (process.platform !== 'win32') return null; + const windowsShortName = /~[1-9][0-9]*(?:\.|$)/iu; + if (windowsShortName.test(leftName) || windowsShortName.test(rightName)) { + return unknown('future-short-name-indeterminate', { method: 'windows-8dot3' }); + } + if (windowsDeviceName(leftName) || windowsDeviceName(rightName)) { + return unknown('future-device-name-indeterminate', { method: 'windows-device-name' }); + } + if (/[ .]$/.test(leftName) || /[ .]$/.test(rightName)) { + return unknown('future-trim-name-indeterminate', { method: 'windows-trim-alias' }); + } + return null; +} + +/** + * Resolve one existing or future path through the host filesystem without + * exposing a string key for identity comparisons. + */ +export function resolvePhysicalLocation(targetPath) { + const resolution = resolveLocation(targetPath, 'target'); + if (!resolution.ok) return resolution.failure; + if (resolution.existing) { + return { + status: 'resolved', + location: { + kind: 'existing', + path: resolution.entry.realPath, + }, + reason: { + code: 'existing-native-realpath', + method: 'native-realpath', + }, + }; + } + return { + status: 'resolved', + location: { + kind: 'future', + ancestorPath: resolution.ancestor.realPath, + unresolved: [...resolution.unresolved], + }, + reason: { + code: 'future-physical-ancestor', + method: 'native-realpath', + }, + }; +} + +function probeNameAlias(directoryPath, leftName, rightName) { + if (leftName === rightName) { + return result('match', 'future-name-exact', { method: 'lexical-exact' }); + } + if (recentProbeNames.has(leftName) || recentProbeNames.has(rightName)) { + return result('different', 'internal-probe-name', { method: 'probe-suppression' }); + } + const windowsRisk = futureWindowsNameRisk(leftName, rightName); + if (windowsRisk) return windowsRisk; + if (!semanticEnvelopesOverlap(leftName, rightName)) { + return result('different', 'future-name-distinct', { method: 'semantic-envelope' }); + } + + const prefix = `.archify-path-semantics-${process.pid}-${crypto.randomBytes(10).toString('hex')}-`; + const probeName = `${prefix}${leftName}`; + const lookupName = `${prefix}${rightName}`; + const componentLength = (name) => process.platform === 'win32' + ? name.length + : Buffer.byteLength(name, 'utf8'); + if (componentLength(probeName) > 255 || componentLength(lookupName) > 255) { + return unknown('future-name-probe-unrepresentable', { + method: 'direct-filesystem-probe', + systemCode: 'ENAMETOOLONG', + }); + } + + let probePath; + let probeStat; + let descriptor; + let comparison; + let cleanupError; + try { + // Probe entries must live directly in the directory being queried. On + // Windows case sensitivity is per-directory and a child may not inherit it. + rememberProbeName(probeName); + rememberProbeName(lookupName); + probePath = path.join(directoryPath, probeName); + const lookupPath = path.join(directoryPath, lookupName); + descriptor = fs.openSync(probePath, 'wx', 0o600); + probeStat = fs.fstatSync(descriptor, { bigint: true }); + + let authoredStat; + let lookupStat; + try { + authoredStat = statBigInt(probePath); + lookupStat = statBigInt(lookupPath); + } catch (error) { + if (error?.code === 'ENOENT') { + comparison = result('different', 'future-name-different', { method: 'filesystem-probe' }); + } else { + comparison = systemFailure('future-name-probe-failed', error, { method: 'filesystem-probe' }); + } + } + + if (!comparison) { + const identity = compareKnownEntries( + { realPath: fs.realpathSync.native(probePath), stat: authoredStat }, + { realPath: fs.realpathSync.native(lookupPath), stat: lookupStat }, + 'future-name', + ); + if (identity.status === 'match') { + comparison = result('match', 'future-name-alias', { method: 'filesystem-probe' }); + } else if (identity.status === 'different') { + comparison = result('different', 'future-name-different', { method: 'filesystem-probe' }); + } else { + comparison = identity; + } + } + } catch (error) { + comparison = systemFailure('future-name-probe-failed', error, { method: 'filesystem-probe' }); + } finally { + // Pin the inode until cleanup finishes. Once the final handle is closed, + // an unlinked probe's inode can be reused by a replacement at the same name. + if (probeStat) cleanupError = removeOwnedProbe(probePath, probeStat); + if (descriptor !== undefined) { + try { + fs.closeSync(descriptor); + } catch (error) { + cleanupError = cleanupError ?? error; + } + } + } + + if (cleanupError) { + return systemFailure('future-name-probe-cleanup-failed', cleanupError, { + method: 'filesystem-probe', + ...(comparison ? { comparison: comparison.reason } : {}), + }); + } + return comparison; +} + +function sidecarCaseKey(component) { + // The candidate is accepted only after the containing filesystem confirms + // that the original grapheme and this spelling name the same entry. + return component.toLocaleUpperCase('en-US').toLocaleLowerCase('en-US'); +} + +function transformSidecarComponent(directoryPath, component, transform, phase) { + if (typeof Intl.Segmenter !== 'function') { + return { + ok: false, + failure: unknown('sidecar-unicode-segmentation-unavailable', { + operation: 'sidecar-namespace-key', + }), + }; + } + const segmenter = new Intl.Segmenter('en-US', { granularity: 'grapheme' }); + const comparisons = new Map(); + const output = []; + for (const { segment } of segmenter.segment(component)) { + const candidate = transform(segment); + if (candidate === segment) { + output.push(segment); + continue; + } + const cacheKey = `${segment}\0${candidate}`; + let comparison = comparisons.get(cacheKey); + if (!comparison) { + comparison = probeNameAlias(directoryPath, segment, candidate); + comparisons.set(cacheKey, comparison); + } + if (comparison.status === 'unknown') { + return { + ok: false, + failure: unknown(`sidecar-${phase}-semantics-indeterminate`, { + operation: 'sidecar-namespace-key', + cause: comparison.reason, + }), + }; + } + output.push(comparison.status === 'match' ? candidate : segment); + } + return { + ok: true, + component: output.join(''), + }; +} + +/** + * Produce a stable component for a sidecar namespace in one existing + * directory. The returned string is a collision-domain key, not a canonical + * path and must never be used to decide whether two filesystem entries match. + */ +export function sidecarNamespaceComponentKey(directoryPath, component) { + if (typeof component !== 'string' || component.length === 0 + || component === '.' || component === '..' + || hostPath().basename(component) !== component) { + return unknown('invalid-sidecar-component', { + operation: 'sidecar-namespace-key', + }); + } + + const directory = resolveLocation(directoryPath, 'sidecar-directory'); + if (!directory.ok) return directory.failure; + if (!directory.existing) { + return unknown('sidecar-directory-missing', { + operation: 'sidecar-namespace-key', + }); + } + if (!directory.entry.stat.isDirectory()) { + return unknown('sidecar-parent-not-directory', { + operation: 'sidecar-namespace-key', + }); + } + + const normalized = transformSidecarComponent( + directory.entry.realPath, + component, + (segment) => segment.normalize('NFC'), + 'normalization', + ); + if (!normalized.ok) return normalized.failure; + const cased = transformSidecarComponent( + directory.entry.realPath, + normalized.component, + sidecarCaseKey, + 'case', + ); + if (!cased.ok) return cased.failure; + const renormalized = transformSidecarComponent( + directory.entry.realPath, + cased.component, + (segment) => segment.normalize('NFC'), + 'normalization', + ); + if (!renormalized.ok) return renormalized.failure; + + return { + status: 'resolved', + directoryPath: directory.entry.realPath, + componentKey: renormalized.component, + reason: { + code: 'sidecar-namespace-key', + method: 'directory-filesystem-probe', + }, + }; +} + +function compareUnmaterializedNames(leftName, rightName) { + if (leftName === rightName) { + return result('match', 'future-name-exact', { method: 'lexical-exact' }); + } + const windowsRisk = futureWindowsNameRisk(leftName, rightName); + if (windowsRisk) return windowsRisk; + if (!semanticEnvelopesOverlap(leftName, rightName)) { + return result('different', 'future-name-distinct', { method: 'semantic-envelope' }); + } + return unknown('future-descendant-semantics-indeterminate', { + method: 'unmaterialized-directory', + }); +} + +function compareFutureLocations(left, right) { + const ancestor = compareKnownEntries(left.ancestor, right.ancestor, 'future-ancestor'); + if (ancestor.status !== 'match') return ancestor; + + if (left.unresolved.length !== right.unresolved.length) { + return result('different', 'future-depth-different', { + operation: 'same-location', + leftDepth: left.unresolved.length, + rightDepth: right.unresolved.length, + }); + } + + let probedSegments = 0; + for (let index = 0; index < left.unresolved.length; index += 1) { + const names = index === 0 + ? probeNameAlias( + left.ancestor.realPath, + left.unresolved[index], + right.unresolved[index], + ) + : compareUnmaterializedNames(left.unresolved[index], right.unresolved[index]); + if (names.reason.method === 'filesystem-probe') probedSegments += 1; + if (names.status !== 'match') { + const { code, ...reasonDetails } = names.reason; + return result(names.status, code, { + ...reasonDetails, + operation: 'same-location', + segmentIndex: index, + }); + } + } + + return result('match', probedSegments > 0 ? 'future-location-alias' : 'future-location-exact', { + operation: 'same-location', + method: probedSegments > 0 ? 'filesystem-probe' : 'lexical-exact', + probedSegments, + }); +} + +/** + * Compare two existing filesystem entries by physical identity. + * Missing or unidentifiable entries return `unknown`, never `different`. + */ +export function sameEntry(leftPath, rightPath) { + const left = resolveLocation(leftPath, 'left'); + if (!left.ok) return left.failure; + const right = resolveLocation(rightPath, 'right'); + if (!right.ok) return right.failure; + + if (!left.existing || !right.existing) { + return unknown('entry-missing', { + operation: 'same-entry', + missing: [ + ...(!left.existing ? ['left'] : []), + ...(!right.existing ? ['right'] : []), + ], + }); + } + + return compareKnownEntries(left.entry, right.entry, 'same-entry'); +} + +/** + * Compare two existing or future write targets using the containing filesystem's + * actual name semantics. Existing hard links and resolved symbolic links match. + */ +export function sameLocation(leftPath, rightPath) { + const left = resolveLocation(leftPath, 'left'); + if (!left.ok) return left.failure; + const right = resolveLocation(rightPath, 'right'); + if (!right.ok) return right.failure; + + if (left.existing && right.existing) { + return compareKnownEntries(left.entry, right.entry, 'same-location'); + } + if (left.existing !== right.existing) { + return result('different', 'location-existence-different', { + operation: 'same-location', + existing: left.existing ? 'left' : 'right', + }); + } + return compareFutureLocations(left, right); +} + +/** Compare the physical locations of the two paths' parent directories. */ +export function sameParent(leftPath, rightPath) { + const left = absolutePath(leftPath, 'left'); + if (!left.ok) return left.failure; + const right = absolutePath(rightPath, 'right'); + if (!right.ok) return right.failure; + + return sameLocation(hostPath().dirname(left.path), hostPath().dirname(right.path)); +} + +function parentEntry(entry) { + const parentPath = hostPath().dirname(entry.realPath); + // path-contract-allow: lexical-capability -- Both values are canonical paths captured by this primitive. + if (parentPath === entry.realPath) return null; + try { + const realPath = fs.realpathSync.native(parentPath); + return { + ok: true, + entry: { + realPath, + stat: statBigInt(realPath), + }, + }; + } catch (error) { + return { + ok: false, + failure: systemFailure('ancestor-resolution-failed', error, { + operation: 'contained-by', + }), + }; + } +} + +/** + * Determine physical containment in an existing directory. The comparison is + * inclusive and follows links; the target may exist or name a future location. + */ +export function containedBy(directoryPath, targetPath) { + const directory = resolveLocation(directoryPath, 'directory'); + if (!directory.ok) return directory.failure; + if (directory.existing && !directory.entry.stat.isDirectory()) { + return unknown('container-not-directory', { operation: 'contained-by' }); + } + + const target = resolveLocation(targetPath, 'target'); + if (!target.ok) return target.failure; + + if (!directory.existing) { + if (target.existing) { + return unknown('container-missing', { operation: 'contained-by' }); + } + const ancestor = compareKnownEntries(directory.ancestor, target.ancestor, 'contained-by'); + if (ancestor.status !== 'match') return ancestor; + if (target.unresolved.length < directory.unresolved.length) { + return result('different', 'future-container-prefix-too-deep', { + operation: 'contained-by', + }); + } + for (let index = 0; index < directory.unresolved.length; index += 1) { + const segment = index === 0 + ? probeNameAlias( + directory.ancestor.realPath, + directory.unresolved[index], + target.unresolved[index], + ) + : compareUnmaterializedNames( + directory.unresolved[index], + target.unresolved[index], + ); + if (segment.status === 'match') continue; + const { code, ...reasonDetails } = segment.reason; + return result(segment.status, code, { + ...reasonDetails, + operation: 'contained-by', + segmentIndex: index, + }); + } + return result('match', 'future-container-prefix-match', { + operation: 'contained-by', + target: 'future', + depth: target.unresolved.length - directory.unresolved.length, + }); + } + + let current; + if (!target.existing) { + current = target.ancestor; + } else if (target.entry.stat.isDirectory()) { + current = target.entry; + } else { + const parent = parentEntry(target.entry); + if (!parent?.ok) return parent?.failure ?? unknown('ancestor-resolution-failed', { + operation: 'contained-by', + }); + current = parent.entry; + } + + let depth = 0; + let identityWasUnknown = false; + while (current) { + const comparison = compareKnownEntries(directory.entry, current, 'contained-by'); + if (comparison.status === 'match') { + return result('match', 'physical-ancestor-match', { + operation: 'contained-by', + depth, + target: target.existing ? 'existing' : 'future', + }); + } + if (comparison.status === 'unknown') identityWasUnknown = true; + + const parent = parentEntry(current); + if (parent === null) break; + if (!parent.ok) return parent.failure; + current = parent.entry; + depth += 1; + } + + if (identityWasUnknown) { + return unknown('containment-identity-unavailable', { + operation: 'contained-by', + target: target.existing ? 'existing' : 'future', + }); + } + return result('different', 'physical-ancestor-not-found', { + operation: 'contained-by', + target: target.existing ? 'existing' : 'future', + }); +} diff --git a/vendor/archify/renderers/shared/portable-path.mjs b/vendor/archify/renderers/shared/portable-path.mjs new file mode 100644 index 0000000..e9f8df9 --- /dev/null +++ b/vendor/archify/renderers/shared/portable-path.mjs @@ -0,0 +1,408 @@ +const PORTABLE_PATH_PROFILES = new Set(['output', 'repo', 'archive']); +const WINDOWS_SAFE_PROFILES = new Set(['output', 'archive']); +const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu; +const WINDOWS_SHORT_NAME = /~[1-9][0-9]*(?:\.|$)/iu; +const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/u; +const CONTROL_CHARACTER = /\p{Cc}/u; +const WINDOWS_INVALID_CHARACTER = /[<>"|?*]/u; +const MAX_SEMANTIC_VARIANTS = 32; + +export class PortablePathError extends Error { + constructor(message, { + code, + reason, + value, + profile, + segment, + segmentIndex, + character, + index, + conflictIndex, + conflictValue, + collisionKind, + semantics, + pathPart, + conflictPathPart, + utf8Bytes, + utf16CodeUnits, + limit, + } = {}) { + super(message); + this.name = 'PortablePathError'; + this.code = code; + this.reason = reason; + this.value = value; + this.profile = profile; + if (segment !== undefined) this.segment = segment; + if (segmentIndex !== undefined) this.segmentIndex = segmentIndex; + if (character !== undefined) this.character = character; + if (index !== undefined) this.index = index; + if (conflictIndex !== undefined) this.conflictIndex = conflictIndex; + if (conflictValue !== undefined) this.conflictValue = conflictValue; + if (collisionKind !== undefined) this.collisionKind = collisionKind; + if (semantics !== undefined) this.semantics = semantics; + if (pathPart !== undefined) this.pathPart = pathPart; + if (conflictPathPart !== undefined) this.conflictPathPart = conflictPathPart; + if (utf8Bytes !== undefined) this.utf8Bytes = utf8Bytes; + if (utf16CodeUnits !== undefined) this.utf16CodeUnits = utf16CodeUnits; + if (limit !== undefined) this.limit = limit; + } +} + +function pathError(value, profile, reason, message, details = {}) { + return new PortablePathError(message, { + code: `portable-path/${reason}`, + reason, + value, + profile, + ...details, + }); +} + +function assertProfile(profile) { + if (PORTABLE_PATH_PROFILES.has(profile)) return; + throw pathError( + undefined, + profile, + 'profile', + `Portable path profile must be one of: ${[...PORTABLE_PATH_PROFILES].join(', ')}.`, + ); +} + +export function validatePortablePath(value, options = {}) { + const profile = options?.profile; + assertProfile(profile); + + if (typeof value !== 'string') { + throw pathError(value, profile, 'type', 'Portable path must be a string.'); + } + if (value.length === 0) { + throw pathError(value, profile, 'empty', 'Portable path must not be empty.'); + } + if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith('/') || value.startsWith('\\')) { + throw pathError(value, profile, 'absolute', 'Portable path must be relative.'); + } + if (/^[A-Za-z]:/u.test(value)) { + throw pathError( + value, + profile, + 'drive-relative', + 'Portable path must not use a drive-relative Windows path.', + ); + } + if (URI_SCHEME.test(value)) { + throw pathError(value, profile, 'uri', 'Portable path must not be a URI.'); + } + if (value.includes('\\')) { + throw pathError( + value, + profile, + 'backslash', + 'Portable path must use forward slashes as separators.', + ); + } + + const segments = value.split('/'); + for (const [segmentIndex, segment] of segments.entries()) { + const segmentDetails = { segment, segmentIndex }; + if (segment.length === 0) { + throw pathError( + value, + profile, + 'empty-segment', + 'Portable path must not contain empty segments.', + segmentDetails, + ); + } + if (segment === '.' || segment === '..') { + throw pathError( + value, + profile, + 'dot-segment', + 'Portable path must not contain dot segments.', + segmentDetails, + ); + } + const controlMatch = segment.match(CONTROL_CHARACTER); + if (controlMatch) { + throw pathError( + value, + profile, + 'control', + 'Portable path must not contain control characters.', + { ...segmentDetails, character: controlMatch[0] }, + ); + } + const surrogateMatch = segment.match(/[\uD800-\uDFFF]/u); + if (surrogateMatch) { + throw pathError( + value, + profile, + 'unpaired-surrogate', + 'Portable path must not contain unpaired UTF-16 surrogates.', + { ...segmentDetails, character: surrogateMatch[0] }, + ); + } + + if (!WINDOWS_SAFE_PROFILES.has(profile)) continue; + if (segment.includes(':')) { + throw pathError( + value, + profile, + 'windows-ads', + 'Portable path must not select a Windows alternate data stream.', + { ...segmentDetails, character: ':' }, + ); + } + const invalidMatch = segment.match(WINDOWS_INVALID_CHARACTER); + if (invalidMatch) { + throw pathError( + value, + profile, + 'windows-invalid-character', + 'Portable path contains a character that is invalid in Windows file names.', + { ...segmentDetails, character: invalidMatch[0] }, + ); + } + if (/[. ]$/u.test(segment)) { + throw pathError( + value, + profile, + 'windows-trailing-dot-space', + 'Portable path segments must not end with a dot or space.', + segmentDetails, + ); + } + if (WINDOWS_RESERVED_NAME.test(segment)) { + throw pathError( + value, + profile, + 'windows-reserved-name', + 'Portable path must not use a reserved Windows device name.', + segmentDetails, + ); + } + if (WINDOWS_SHORT_NAME.test(segment)) { + throw pathError( + value, + profile, + 'windows-short-name', + 'Portable path must not use a Windows 8.3 short-name shape.', + segmentDetails, + ); + } + const utf8Bytes = Buffer.byteLength(segment, 'utf8'); + const utf16CodeUnits = segment.length; + if (utf8Bytes > 255 || utf16CodeUnits > 255) { + throw pathError( + value, + profile, + 'component-too-long', + 'Portable path segments must fit both UTF-8 and UTF-16 filesystem component limits.', + { + ...segmentDetails, + utf8Bytes, + utf16CodeUnits, + limit: 255, + }, + ); + } + } + + return value; +} + +function collisionError(values, profile, index, conflictIndex, reason, details = {}) { + return new PortablePathError( + `Portable paths at indexes ${conflictIndex} and ${index} collide under portable filesystem semantics.`, + { + code: 'portable-path/collision', + reason, + value: values[index], + profile, + index, + conflictIndex, + conflictValue: values[conflictIndex], + ...details, + }, + ); +} + +function createSemanticIndex() { + return new Map(); +} + +function semanticEnvelope(value, profile, index) { + const variants = [value]; + const seen = new Set(variants); + const transforms = [ + (candidate) => candidate.normalize('NFC'), + (candidate) => candidate.normalize('NFD'), + // path-contract-allow: portable-logical-path -- Archive names require conservative case-collision closure. + (candidate) => candidate.toLocaleLowerCase('en-US'), + // path-contract-allow: portable-logical-path -- Archive names require conservative case-collision closure. + (candidate) => candidate.toLocaleUpperCase('en-US'), + ]; + + for (let cursor = 0; cursor < variants.length; cursor += 1) { + for (const transform of transforms) { + const transformed = transform(variants[cursor]); + if (seen.has(transformed)) continue; + if (variants.length >= MAX_SEMANTIC_VARIANTS) { + throw pathError( + value, + profile, + 'semantic-expansion', + 'Portable path semantic comparison exceeded its bounded Unicode expansion.', + { index, limit: MAX_SEMANTIC_VARIANTS }, + ); + } + seen.add(transformed); + variants.push(transformed); + } + } + + return variants; +} + +function classifySemanticCollision(value, conflictValue) { + if (value === conflictValue) return 'exact'; + if (value.normalize('NFC') === conflictValue.normalize('NFC')) return 'normalization'; + + const directLowerMatch = value.toLocaleLowerCase('en-US') + === conflictValue.toLocaleLowerCase('en-US'); + const directUpperMatch = value.toLocaleUpperCase('en-US') + === conflictValue.toLocaleUpperCase('en-US'); + return directLowerMatch || directUpperMatch ? 'case' : 'case-and-normalization'; +} + +function findSemanticCollision(semanticIndex, value, profile, sourceIndex) { + const variants = semanticEnvelope(value, profile, sourceIndex); + for (const variant of variants) { + const entry = semanticIndex.get(variant); + if (entry) { + return { + entry, + variants, + semantics: classifySemanticCollision(value, entry.value), + }; + } + } + return { entry: null, variants, semantics: null }; +} + +function rememberSemanticEntry(semanticIndex, entry, variants) { + for (const variant of variants) { + if (!semanticIndex.has(variant)) semanticIndex.set(variant, entry); + } +} + +export function validatePortablePathSet(values, options = {}) { + const profile = options?.profile; + assertProfile(profile); + if (!Array.isArray(values)) { + throw pathError(values, profile, 'set-type', 'Portable path set must be an array.'); + } + + const leafEntries = createSemanticIndex(); + const directoryEntries = createSemanticIndex(); + + for (const [index, value] of values.entries()) { + try { + validatePortablePath(value, { profile }); + } catch (error) { + if (error instanceof PortablePathError && error.index === undefined) error.index = index; + throw error; + } + + const leafCollision = findSemanticCollision(leafEntries, value, profile, index); + if (leafCollision.entry) { + const conflictIndex = leafCollision.entry.index; + throw collisionError( + values, + profile, + index, + conflictIndex, + leafCollision.semantics === 'exact' ? 'duplicate' : leafCollision.semantics, + { + collisionKind: 'entry', + semantics: leafCollision.semantics, + pathPart: value, + conflictPathPart: leafCollision.entry.value, + }, + ); + } + + const segments = value.split('/'); + const prefixes = segments.slice(0, -1).map((_, prefixIndex) => ( + segments.slice(0, prefixIndex + 1).join('/') + )); + for (const prefix of prefixes) { + const directoryCollision = findSemanticCollision(directoryEntries, prefix, profile, index); + if (directoryCollision.entry && directoryCollision.semantics !== 'exact') { + throw collisionError( + values, + profile, + index, + directoryCollision.entry.index, + `directory-${directoryCollision.semantics}`, + { + collisionKind: 'directory-spelling', + semantics: directoryCollision.semantics, + pathPart: prefix, + conflictPathPart: directoryCollision.entry.value, + }, + ); + } + + const fileCollision = findSemanticCollision(leafEntries, prefix, profile, index); + if (fileCollision.entry) { + throw collisionError( + values, + profile, + index, + fileCollision.entry.index, + fileCollision.semantics === 'exact' + ? 'tree-file' + : `tree-file-${fileCollision.semantics}`, + { + collisionKind: 'tree-file', + semantics: fileCollision.semantics, + pathPart: prefix, + conflictPathPart: fileCollision.entry.value, + }, + ); + } + } + + const directoryCollision = findSemanticCollision(directoryEntries, value, profile, index); + if (directoryCollision.entry) { + throw collisionError( + values, + profile, + index, + directoryCollision.entry.index, + directoryCollision.semantics === 'exact' + ? 'tree-file' + : `tree-file-${directoryCollision.semantics}`, + { + collisionKind: 'tree-file', + semantics: directoryCollision.semantics, + pathPart: value, + conflictPathPart: directoryCollision.entry.value, + }, + ); + } + + rememberSemanticEntry(leafEntries, { index, value }, leafCollision.variants); + for (const prefix of prefixes) { + rememberSemanticEntry( + directoryEntries, + { index, value: prefix }, + semanticEnvelope(prefix, profile, index), + ); + } + } + + return values; +} diff --git a/vendor/archify/renderers/shared/repository-evidence.mjs b/vendor/archify/renderers/shared/repository-evidence.mjs new file mode 100644 index 0000000..33898c0 --- /dev/null +++ b/vendor/archify/renderers/shared/repository-evidence.mjs @@ -0,0 +1,352 @@ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { throwDiagnosticError, withDiagnosticRecordingSuppressed } from './diagnostics.mjs'; +import { sameEntry } from './path-semantics.mjs'; +import { parseRepositoryRemote, redactRepositoryRemote, repositorySourceHref } from './repository-location.mjs'; + +const FULL_SHA_RE = /^[a-f0-9]{40}$/i; +const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/; +const MAX_SOURCE_BYTES = 16 * 1024 * 1024; + +function evidenceFailure(code, message, { subject = {}, evidence = {}, supportedFixes = [] } = {}) { + throwDiagnosticError(message, [{ + code, + severity: 'error', + message, + subject: { surface: 'repository-evidence', ...subject }, + evidence, + supportedFixes, + }]); +} + +function runGit(repoRoot, args) { + // 固定 SHA 的来源必须读取原始对象,不能使用本地 replacement refs 的替换内容。 + const result = spawnSync('git', ['--no-replace-objects', '-C', repoRoot, ...args], { + encoding: 'utf8', + maxBuffer: MAX_SOURCE_BYTES, + }); + if (result.error) evidenceFailure('repository-evidence/git-unavailable', `Could not run Git: ${result.error.message}`, { + evidence: { reason: result.error.message }, + supportedFixes: ['install Git and ensure it is available on PATH'], + }); + return result; +} + +// Check types in one session, then read only blobs whose cited lines need +// verification. Path-only references never require loading the file contents. +function prefetchBlobs(repoRoot, objectNeedsContent) { + const blobs = readBatchObjects(repoRoot, [...objectNeedsContent.keys()], false); + if (!blobs) return null; + const readable = [...objectNeedsContent].filter(([object, needsContent]) => { + const blob = blobs.get(object); + return needsContent && blob?.type === 'blob' && blob.size <= MAX_SOURCE_BYTES; + }).map(([object]) => object); + const contents = readBatchObjects(repoRoot, readable, true); + if (contents) for (const [object, blob] of contents) blobs.set(object, blob); + // Failed or oversized reads fall back in source order to the original + // per-file path, preserving its size limit and diagnostic behavior. + return blobs; +} + +function readBatchObjects(repoRoot, objects, includeContent) { + if (!objects.length) return new Map(); + const mode = includeContent ? '--batch' : '--batch-check'; + const result = spawnSync('git', ['--no-replace-objects', '-C', repoRoot, 'cat-file', mode], { + input: objects.join('\n') + '\n', + maxBuffer: 64 * 1024 * 1024, + }); + if (result.error || result.status !== 0 || !Buffer.isBuffer(result.stdout)) return null; + const buffer = result.stdout; + const blobs = new Map(); + let cursor = 0; + for (const object of objects) { + const newline = buffer.indexOf(0x0a, cursor); + if (newline < 0) return null; + const header = buffer.toString('utf8', cursor, newline); + cursor = newline + 1; + if (header.endsWith(' missing')) { + blobs.set(object, { missing: true }); + continue; + } + const parts = header.split(' '); + const size = Number(parts[2]); + if (parts.length !== 3 || !Number.isSafeInteger(size) || size < 0) return null; + const blob = { type: parts[1], size }; + if (includeContent) { + if (cursor + size >= buffer.length || buffer[cursor + size] !== 0x0a) return null; + blob.content = buffer.toString('utf8', cursor, cursor + size); + cursor += size + 1; + } + blobs.set(object, blob); + } + return blobs; +} + +function gitValue(repoRoot, args, failure) { + const result = runGit(repoRoot, args); + if (result.status !== 0) evidenceFailure('repository-evidence/git-command', failure, { + evidence: { gitArgs: args, exitCode: result.status }, + supportedFixes: ['use the intended local Git repository and verify its origin and revision'], + }); + return result.stdout.trim(); +} + +function verifiedSourcePath(value, where) { + const sourcePath = String(value || ''); + // path-contract-allow: git-path -- Git tree entries use repository-relative POSIX syntax. + if (!sourcePath || sourcePath.startsWith('/') || sourcePath.includes('\\') || CONTROL_CHARACTER_RE.test(sourcePath)) { + evidenceFailure('repository-evidence/path-invalid', `${where} must be a repo-relative POSIX path.`, { + subject: { path: where }, + evidence: { authoredPath: sourcePath }, + supportedFixes: ['use a repository-relative path with forward slashes'], + }); + } + const segments = sourcePath.split('/'); + if (segments.some((segment) => !segment || segment === '.' || segment === '..') || segments[0] === '.git') { + evidenceFailure('repository-evidence/path-escape', `${where} must stay inside the repository and may not address .git.`, { + subject: { path: where }, + evidence: { authoredPath: sourcePath }, + supportedFixes: ['remove empty, dot, parent, or .git path segments'], + }); + } + return segments.join('/'); +} + +function sourceLineCount(content) { + if (!content.length) return 0; + const lines = content.split(/\r\n|\n|\r/); + return lines.length - (/(?:\r\n|\n|\r)$/.test(content) ? 1 : 0); +} + +// Every diagram type carries its nodes under a different property name, and +// source evidence is authored on those nodes. One table keeps the verification +// below identical for all five types instead of branching per type: the only +// per-type fact is which array to read and which JSON pointer to quote back. +const EVIDENCE_NODE_COLLECTIONS = { + architecture: 'components', + workflow: 'nodes', + sequence: 'participants', + dataflow: 'nodes', + lifecycle: 'states', +}; + +function evidenceNodes(diagramType, diagram) { + const collection = EVIDENCE_NODE_COLLECTIONS[diagramType]; + if (!collection) return null; + return { collection, nodes: Array.isArray(diagram?.[collection]) ? diagram[collection] : [] }; +} + +export function hasRepositoryEvidence(diagramType, diagram) { + const authored = evidenceNodes(diagramType, diagram); + if (!authored) return false; + return Boolean(diagram?.meta?.repository) || authored.nodes.some((node) => Array.isArray(node?.sources) && node.sources.length); +} + +export function verifyRepositoryEvidence(diagramType, diagram, repoRootInput) { + if (!hasRepositoryEvidence(diagramType, diagram)) return null; + const { collection, nodes: authoredNodes } = evidenceNodes(diagramType, diagram); + + const repository = diagram.meta?.repository; + if (!repository) evidenceFailure('repository-evidence/repository-required', 'Repository evidence requires /meta/repository.', { + subject: { path: '/meta/repository', diagramType, collection }, + supportedFixes: [`add the pinned repository metadata or remove /${collection} sources`], + }); + if (!FULL_SHA_RE.test(repository.revision || '')) { + evidenceFailure('repository-evidence/revision-invalid', '/meta/repository/revision must be a full 40-character commit SHA.', { + subject: { path: '/meta/repository/revision' }, + evidence: { revision: repository.revision }, + supportedFixes: ['pin one full 40-character commit SHA'], + }); + } + const location = parseRepositoryRemote(repository.url, { authored: true }); + if (!location) { + // A filesystem path is the common authoring mistake: the field carries the + // remote origin identity, which `git remote get-url origin` reports. + const filesystemPath = /^(?:[\\/]|~|\.{1,2}(?:[\\/]|$)|[A-Za-z]:[\\/])/.test(String(repository.url ?? '')); + evidenceFailure('repository-evidence/url-invalid', '/meta/repository/url must be a credential-free HTTP(S) or Git SSH repository address without query, fragment, or dot segments.', { + subject: { path: '/meta/repository/url' }, + evidence: filesystemPath ? { authoredValueLooksLike: 'local filesystem path; the expected value is the remote origin address' } : {}, + supportedFixes: ['run `git remote get-url origin` inside --repo-root and declare that credential-free address', 'use link_mode: local-only for internal repositories'], + }); + } + const linkMode = repository.link_mode ?? 'web'; + if (!['web', 'local-only'].includes(linkMode)) evidenceFailure('repository-evidence/link-mode-invalid', 'Repository link_mode must be web or local-only.'); + if (repository.provider !== undefined && (!['github', 'gitee'].includes(repository.provider) || repository.provider !== location.provider)) { + evidenceFailure('repository-evidence/provider-invalid', 'Repository provider must match its supported public host (github.com or gitee.com).', { + subject: { path: '/meta/repository/provider' }, + supportedFixes: ['use the matching provider or omit provider and select link_mode: local-only'], + }); + } + if (linkMode === 'web' && (!location.provider || location.protocol !== 'https:' || location.endpoint !== 'standard' || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(location.path))) { + evidenceFailure('repository-evidence/links-unsupported', 'Web source links require a canonical GitHub or Gitee HTTPS owner/repository URL.', { + subject: { path: '/meta/repository/url' }, + supportedFixes: ['use a canonical GitHub or Gitee URL, or select link_mode: local-only to retain local verification without web links'], + }); + } + if (!repoRootInput) { + evidenceFailure('repository-evidence/root-required', 'This diagram declares source evidence. Pass --repo-root so Archify can verify it before rendering.', { + subject: { path: '/meta/repository' }, + supportedFixes: ['pass --repo-root with the matching local Git checkout'], + }); + } + + const requestedRoot = path.resolve(repoRootInput); + let realRoot; + try { + realRoot = fs.realpathSync(requestedRoot); + } catch (error) { + evidenceFailure('repository-evidence/root-unreadable', `Could not resolve evidence repository root "${requestedRoot}": ${error.message}`, { + subject: { repoRoot: requestedRoot }, + evidence: { reason: error.message }, + supportedFixes: ['pass one readable local repository directory'], + }); + } + const gitRoot = gitValue(realRoot, ['rev-parse', '--show-toplevel'], `Evidence root "${realRoot}" is not a Git repository.`); + const rootIdentity = sameEntry(realRoot, gitRoot); + if (rootIdentity.status === 'unknown') { + evidenceFailure('repository-evidence/root-identity-indeterminate', 'Could not determine whether the evidence root is the Git top-level directory.', { + subject: { repoRoot: realRoot }, + evidence: { gitTopLevel: gitRoot, relation: rootIdentity.reason }, + supportedFixes: ['pass the readable Git top-level directory using its canonical filesystem path'], + }); + } + if (rootIdentity.status === 'different') { + evidenceFailure('repository-evidence/root-not-top-level', `Evidence root must be the Git top-level directory: ${gitRoot}`, { + subject: { repoRoot: realRoot }, + evidence: { gitTopLevel: gitRoot }, + supportedFixes: [`pass --repo-root ${gitRoot}`], + }); + } + const origin = gitValue(realRoot, ['remote', 'get-url', 'origin'], 'Evidence repository must have an origin remote.'); + if (parseRepositoryRemote(origin)?.identity !== location.identity) { + const safeOrigin = redactRepositoryRemote(origin); + evidenceFailure('repository-evidence/origin-mismatch', `Evidence repository origin ${JSON.stringify(safeOrigin)} does not match ${JSON.stringify(repository.url)}.`, { + subject: { repoRoot: realRoot }, + evidence: { localOrigin: safeOrigin, authoredRepository: repository.url }, + supportedFixes: ['use the matching local checkout or correct the authored repository URL'], + }); + } + + const revision = repository.revision.toLowerCase(); + const commit = runGit(realRoot, ['cat-file', '-e', `${revision}^{commit}`]); + if (commit.status !== 0) { + evidenceFailure('repository-evidence/revision-unavailable', `Evidence revision ${revision} is not available in the local repository.`, { + subject: { repoRoot: realRoot }, + evidence: { revision }, + supportedFixes: ['fetch the pinned commit or pin an available full commit SHA'], + }); + } + + // The batch is an optimization only: every path, line-range, file and line + // check still runs in source order in the verification loop below, so a + // citation the batch cannot answer for never reorders the first diagnostic. + const citedObjects = new Map(); + for (const [nodeIndex, node] of authoredNodes.entries()) { + if (!Array.isArray(node.sources) || node.sources.length === 0) continue; + for (const [sourceIndex, authored] of node.sources.entries()) { + const at = `/${collection}/${nodeIndex}/sources/${sourceIndex}`; + let sourcePath; + try { + sourcePath = withDiagnosticRecordingSuppressed(() => verifiedSourcePath(authored.path, `${at}/path`)); + } catch { + continue; + } + const object = `${revision}:${sourcePath}`; + citedObjects.set(object, citedObjects.get(object) || Boolean(authored.line)); + } + } + const prefetchedBlobs = prefetchBlobs(realRoot, citedObjects); + + const nodes = Object.create(null); + let referenceCount = 0; + for (const [nodeIndex, node] of authoredNodes.entries()) { + if (!Array.isArray(node.sources) || node.sources.length === 0) continue; + // `componentId` shipped with the architecture-only path; keep it beside the + // type-neutral `nodeId` so existing agent handling stays valid. + const nodeSubject = collection === 'components' + ? { diagramType, collection, nodeId: node.id, componentId: node.id } + : { diagramType, collection, nodeId: node.id }; + const verified = []; + for (const [sourceIndex, authored] of node.sources.entries()) { + const at = `/${collection}/${nodeIndex}/sources/${sourceIndex}`; + const where = `${at}/path`; + const source = { + path: verifiedSourcePath(authored.path, where), + ...(authored.line ? { line: authored.line } : {}), + ...(authored.end_line ? { endLine: authored.end_line } : {}), + ...(authored.label ? { label: authored.label } : {}), + }; + if (source.endLine && !source.line) { + evidenceFailure('repository-evidence/line-required', `${at}/end_line requires line.`, { + subject: { path: `${at}/end_line`, ...nodeSubject }, + supportedFixes: ['add line or remove end_line'], + }); + } + if (source.endLine && source.endLine < source.line) { + evidenceFailure('repository-evidence/line-range-invalid', `${at}/end_line must be greater than or equal to line.`, { + subject: { path: at, ...nodeSubject }, + evidence: { line: source.line, endLine: source.endLine }, + supportedFixes: ['use an end_line greater than or equal to line'], + }); + } + const object = `${revision}:${source.path}`; + const prefetched = prefetchedBlobs ? prefetchedBlobs.get(object) : undefined; + const objectIsBlob = prefetched + ? !prefetched.missing && prefetched.type === 'blob' + : (() => { + const type = runGit(realRoot, ['cat-file', '-t', object]); + return type.status === 0 && type.stdout.trim() === 'blob'; + })(); + if (!objectIsBlob) { + evidenceFailure('repository-evidence/file-missing', `${where} does not identify a file at revision ${revision}.`, { + subject: { path: where, ...nodeSubject }, + evidence: { sourcePath: source.path, revision }, + supportedFixes: ['use a file path that exists at the pinned revision'], + }); + } + if (source.line) { + const content = prefetched && Object.hasOwn(prefetched, 'content') + ? { status: 0, stdout: prefetched.content } + : runGit(realRoot, ['show', object]); + if (content.status !== 0) evidenceFailure('repository-evidence/file-unreadable', `${where} could not be read at revision ${revision}.`, { + subject: { path: where, ...nodeSubject }, + evidence: { sourcePath: source.path, revision }, + supportedFixes: ['verify the pinned blob is readable in the local checkout'], + }); + const lineCount = sourceLineCount(content.stdout); + const requestedLine = source.endLine || source.line; + if (requestedLine > lineCount) { + evidenceFailure('repository-evidence/line-out-of-range', `${at} requests line ${requestedLine}, but ${source.path} has ${lineCount} lines at revision ${revision}.`, { + subject: { path: at, ...nodeSubject }, + evidence: { sourcePath: source.path, requestedLine, lineCount, revision }, + supportedFixes: ['use a line range that exists at the pinned revision'], + }); + } + } + verified.push({ ...source, ...(linkMode === 'web' ? { href: repositorySourceHref(location.provider, location.url, revision, source) } : {}) }); + referenceCount += 1; + } + nodes[node.id] = verified; + } + if (referenceCount === 0) { + evidenceFailure('repository-evidence/source-required', `/meta/repository requires at least one /${collection} source reference.`, { + subject: { path: '/meta/repository', diagramType, collection }, + supportedFixes: [`add at least one verified /${collection} source or remove repository metadata`], + }); + } + + return { + schemaVersion: 1, + verified: true, + repository: { + url: location.url, + revision, + shortRevision: revision.slice(0, 7), + label: location.provider === 'github' ? location.path : location.url.replace(/^(?:https?:\/\/|ssh:\/\/git@|git@)/, ''), + ...(linkMode === 'web' ? { href: `${location.url}/tree/${revision}` } : { linkMode }), + }, + referenceCount, + nodes, + }; +} diff --git a/vendor/archify/renderers/shared/repository-location.mjs b/vendor/archify/renderers/shared/repository-location.mjs new file mode 100644 index 0000000..aa6ee82 --- /dev/null +++ b/vendor/archify/renderers/shared/repository-location.mjs @@ -0,0 +1,59 @@ +// Repository identity and forge links are independent of local Git object checks. +// This module never contacts a remote server or reads the user's SSH config. +export function parseRepositoryRemote(value, { authored = false } = {}) { + if (typeof value !== 'string') return null; + const raw = authored ? value : value.trim(); + if (!raw || /[\s\\\u0000-\u001f\u007f?#]/.test(raw)) return null; + const scp = raw.match(/^git@([^/:]+):(.+)$/); + const scpAbsolute = Boolean(scp && scp[2].startsWith('/')); + const expanded = scp ? `ssh://git@${scp[1]}/${scp[2].replace(/^\//, '')}` : raw; + const match = expanded.match(/^(https?|ssh):\/\/([^/]+)\/(.+)$/i); + if (!match) return null; + // Validate the original path before URL parsing can collapse dot segments. + let segments; + try { + segments = match[3].replace(/\/$/, '').split('/'); + // Git passes SCP paths literally; percent escapes are decoded only in URIs. + if (!scp) segments = segments.map(decodeURIComponent); + } + catch { return null; } + if (segments.some((part) => !part || part === '.' || part === '..' || /[/\\\s\u0000-\u001f\u007f?#]/.test(part))) return null; + let url; + try { url = new URL(expanded); } catch { return null; } + const protocol = url.protocol; + if (protocol === 'ssh:' && (url.username !== 'git' || url.password)) return null; + if (authored && protocol !== 'ssh:' && (url.username || url.password)) return null; + const hostname = url.hostname.toLowerCase(); + if (!hostname) return null; + const provider = hostname === 'github.com' ? 'github' : hostname === 'gitee.com' ? 'gitee' : null; + const last = segments.length - 1; + if (provider) segments[last] = segments[last].replace(provider === 'github' ? /\.git$/i : /\.git$/, ''); + if (!segments[last] || segments[last] === '.' || segments[last] === '..') return null; + const repositoryPath = segments.join('/'); + // Only known forges map HTTPS and SSH to one repository namespace. Other + // hosts retain transport, port and remote-relative/absolute path semantics. + const port = url.port || (protocol === 'ssh:' ? '22' : protocol === 'https:' ? '443' : '80'); + const endpoint = provider && ((protocol === 'https:' && port === '443') || (protocol === 'ssh:' && port === '22')) + ? 'standard' : `${protocol}${port}`; + const pathKind = provider ? 'repository' : scp && !scpAbsolute ? 'relative' : 'absolute'; + // path-contract-allow: url-path -- GitHub repository names are case-insensitive URL identities. + const identityPath = provider === 'github' ? repositoryPath.toLowerCase() : repositoryPath; + const encodedPath = segments.map(encodeURIComponent).join('/'); + const canonicalUrl = scp ? `git@${hostname}:${scpAbsolute ? '/' : ''}${repositoryPath}` + : `${protocol}//${protocol === 'ssh:' ? 'git@' : ''}${url.host}/${encodedPath}`; + return { identity: JSON.stringify([hostname, endpoint, pathKind, identityPath]), url: canonicalUrl, provider, protocol, path: repositoryPath, endpoint }; +} + +export function redactRepositoryRemote(value) { + return String(value || '') + .replace(/^((?:https?|ssh):\/\/)[^/]*@/i, '$1REDACTED@') + .replace(/[?#].*$/s, '?REDACTED'); +} + +export function repositorySourceHref(provider, url, revision, source) { + const encodedPath = source.path.split('/').map(encodeURIComponent).join('/'); + const end = source.endLine && source.endLine !== source.line + ? `-${provider === 'github' ? 'L' : ''}${source.endLine}` : ''; + const fragment = source.line ? `#L${source.line}${end}` : ''; + return `${url}/blob/${revision}/${encodedPath}${fragment}`; +} diff --git a/vendor/archify/renderers/shared/route-quality.mjs b/vendor/archify/renderers/shared/route-quality.mjs new file mode 100644 index 0000000..a79779f --- /dev/null +++ b/vendor/archify/renderers/shared/route-quality.mjs @@ -0,0 +1,614 @@ +import { recordDiagnostic } from './diagnostics.mjs'; +import { + asArray, + isFinitePoint, + normalizeRoutePoints, + properSegmentIntersection, + segmentIntersectsRect, +} from './geometry.mjs'; + +const DEFAULTS = Object.freeze({ + clearance: 2, + minimumDetourRatio: 2.5, + minimumExcessLengthPx: 200, + minimumEmptyExcursionPx: 96, + maximumObstacleCount: 80, + sharedCorridorMinimumPx: 32, +}); + +const OUTWARD = Object.freeze({ + left: [-1, 0], + right: [1, 0], + top: [0, -1], + bottom: [0, 1], +}); + +function rounded(value) { + return Math.round(value * 100) / 100; +} + +function pointKey(point) { + return `${point[0]}\u0000${point[1]}`; +} + +class MinHeap { + constructor() { + this.entries = []; + } + + push(key, distance) { + const entry = { key, distance }; + this.entries.push(entry); + let index = this.entries.length - 1; + while (index > 0) { + const parent = Math.floor((index - 1) / 2); + if (this.entries[parent].distance <= distance) break; + this.entries[index] = this.entries[parent]; + index = parent; + } + this.entries[index] = entry; + } + + pop() { + if (!this.entries.length) return null; + const first = this.entries[0]; + const last = this.entries.pop(); + if (!this.entries.length) return first; + let index = 0; + while (true) { + const left = index * 2 + 1; + const right = left + 1; + if (left >= this.entries.length) break; + const child = right < this.entries.length + && this.entries[right].distance < this.entries[left].distance ? right : left; + if (this.entries[child].distance >= last.distance) break; + this.entries[index] = this.entries[child]; + index = child; + } + this.entries[index] = last; + return first; + } +} + +function orthogonalLength(points) { + let total = 0; + for (let index = 0; index < points.length - 1; index += 1) { + const [x1, y1] = points[index]; + const [x2, y2] = points[index + 1]; + if (x1 !== x2 && y1 !== y2) return null; + total += Math.abs(x2 - x1) + Math.abs(y2 - y1); + } + return total; +} + +function inferredSide(points, endpoint) { + if (points.length < 2) return null; + const start = endpoint === 'source' ? points[0] : points.at(-2); + const end = endpoint === 'source' ? points[1] : points.at(-1); + const dx = end[0] - start[0]; + const dy = end[1] - start[1]; + if (endpoint === 'source') { + if (dx > 0 && dy === 0) return 'right'; + if (dx < 0 && dy === 0) return 'left'; + if (dy > 0 && dx === 0) return 'bottom'; + if (dy < 0 && dx === 0) return 'top'; + } else { + if (dx > 0 && dy === 0) return 'left'; + if (dx < 0 && dy === 0) return 'right'; + if (dy > 0 && dx === 0) return 'top'; + if (dy < 0 && dx === 0) return 'bottom'; + } + return null; +} + +function moveOutward(point, side, distance) { + const [dx, dy] = OUTWARD[side] || [0, 0]; + return [point[0] + dx * distance, point[1] + dy * distance]; +} + +function expandedRect(rect, clearance) { + return { + id: rect.id, + x: rect.x - clearance, + y: rect.y - clearance, + width: rect.width + clearance * 2, + height: rect.height + clearance * 2, + }; +} + +function boundsForRects(rects) { + const usable = [...rects].filter((rect) => ( + rect && isFinitePoint(rect.x, rect.y, rect.width, rect.height) + && rect.width >= 0 && rect.height >= 0 + )); + if (!usable.length) return null; + const left = Math.min(...usable.map((rect) => rect.x)); + const top = Math.min(...usable.map((rect) => rect.y)); + const right = Math.max(...usable.map((rect) => rect.x + rect.width)); + const bottom = Math.max(...usable.map((rect) => rect.y + rect.height)); + return { left, top, right, bottom, width: right - left, height: bottom - top }; +} + +function boundsForPoints(points) { + if (!points.length) return null; + const xs = points.map(([x]) => x); + const ys = points.map(([, y]) => y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + return { left, top, right, bottom, width: right - left, height: bottom - top }; +} + +function outsideExcursion(routeBounds, contentBounds) { + if (!routeBounds || !contentBounds) return null; + const sides = { + left: Math.max(0, contentBounds.left - routeBounds.left), + top: Math.max(0, contentBounds.top - routeBounds.top), + right: Math.max(0, routeBounds.right - contentBounds.right), + bottom: Math.max(0, routeBounds.bottom - contentBounds.bottom), + }; + return { ...sides, maximum: Math.max(...Object.values(sides)) }; +} + +function pointDistanceFromRect(point, rect) { + const dx = Math.max(rect.x - point[0], 0, point[0] - (rect.x + rect.width)); + const dy = Math.max(rect.y - point[1], 0, point[1] - (rect.y + rect.height)); + return dx + dy; +} + +function emptyControlPointClearance(points, contentRects) { + const controls = points.slice(1, -1); + const rects = [...contentRects].filter((rect) => ( + rect && isFinitePoint(rect.x, rect.y, rect.width, rect.height) + && rect.width >= 0 && rect.height >= 0 + )); + if (!controls.length || !rects.length) return null; + const distances = controls.map((point) => Math.min( + ...rects.map((rect) => pointDistanceFromRect(point, rect)), + )); + const maximum = Math.max(...distances); + return { maximum, point: controls[distances.indexOf(maximum)] }; +} + +function pointBlocked(point, obstacles) { + return obstacles.some((rect) => ( + point[0] >= rect.x && point[0] <= rect.x + rect.width + && point[1] >= rect.y && point[1] <= rect.y + rect.height + )); +} + +function segmentBlocked(start, end, obstacles) { + return obstacles.some((rect) => segmentIntersectsRect({ start, end }, rect)); +} + +function segmentConflictsWithAvoided(start, end, avoidedSegments, minimumOverlapPx, allowCrossings) { + return avoidedSegments.some((segment) => ( + (!allowCrossings && (properSegmentIntersection(start, end, segment.start, segment.end) + || orthogonalTouchOnAvoidedInterior(start, end, segment.start, segment.end))) + || collinearOverlap(start, end, segment.start, segment.end) >= minimumOverlapPx + )); +} + +function orthogonalTouchOnAvoidedInterior(start, end, avoidedStart, avoidedEnd) { + const epsilon = 0.0001; + const candidateHorizontal = Math.abs(start[1] - end[1]) <= epsilon; + const candidateVertical = Math.abs(start[0] - end[0]) <= epsilon; + const avoidedHorizontal = Math.abs(avoidedStart[1] - avoidedEnd[1]) <= epsilon; + const avoidedVertical = Math.abs(avoidedStart[0] - avoidedEnd[0]) <= epsilon; + if (candidateHorizontal && avoidedVertical) { + const x = avoidedStart[0]; + const y = start[1]; + return x >= Math.min(start[0], end[0]) - epsilon + && x <= Math.max(start[0], end[0]) + epsilon + && y > Math.min(avoidedStart[1], avoidedEnd[1]) + epsilon + && y < Math.max(avoidedStart[1], avoidedEnd[1]) - epsilon; + } + if (candidateVertical && avoidedHorizontal) { + const x = start[0]; + const y = avoidedStart[1]; + return y >= Math.min(start[1], end[1]) - epsilon + && y <= Math.max(start[1], end[1]) + epsilon + && x > Math.min(avoidedStart[0], avoidedEnd[0]) + epsilon + && x < Math.max(avoidedStart[0], avoidedEnd[0]) - epsilon; + } + return false; +} + +function pointOnSegmentInterior(point, start, end) { + const epsilon = 0.0001; + const cross = (end[0] - start[0]) * (point[1] - start[1]) + - (end[1] - start[1]) * (point[0] - start[0]); + if (Math.abs(cross) > epsilon) return false; + const dot = (point[0] - start[0]) * (point[0] - end[0]) + + (point[1] - start[1]) * (point[1] - end[1]); + return dot < -epsilon; +} + +function pointOnAvoidedInterior(point, avoidedSegments) { + return avoidedSegments.some((segment) => ( + pointOnSegmentInterior(point, segment.start, segment.end) + )); +} + +function writeGridMetrics(metrics, patch) { + if (!metrics || typeof metrics !== 'object') return; + Object.assign(metrics, patch); +} + +export function shortestOrthogonalGridRoute({ + start, + end, + points, + obstacles, + fromSide, + toSide, + clearance, + maximumObstacleCount, + endpointStubPx = clearance + 2, + maximumGridNodes = Infinity, + avoidedSegments = [], + allowAvoidedCrossings = false, + minimumAvoidedOverlapPx = 8, + routeSeparationPx = 8, + minimumSegmentPx = 8, + borderSegments = [], + bendPenaltyPx = 0, + metrics, +}) { + writeGridMetrics(metrics, { + status: 'initializing', + maximumGridNodes, + obstacleCount: 0, + avoidedSegmentCount: 0, + coordinateCount: 0, + candidateNodeCount: 0, + usableNodeCount: 0, + graphEdgeCount: 0, + visitedNodeCount: 0, + }); + if (!OUTWARD[fromSide] || !OUTWARD[toSide]) { + writeGridMetrics(metrics, { status: 'unsupported-endpoint-side' }); + return null; + } + const startStub = moveOutward(start, fromSide, endpointStubPx); + const endStub = moveOutward(end, toSide, endpointStubPx); + // The graph may legally leave the initial endpoint bounds to find a clear + // corridor. Keep every bounded obstacle and occupied relationship visible + // to that search; filtering them against the initial box lets a detour walk + // straight through geometry that only becomes relevant after it leaves the + // box. The explicit obstacle/node budgets below keep this deterministic. + const expanded = [...obstacles] + .filter((rect) => rect && isFinitePoint(rect.x, rect.y, rect.width, rect.height)) + .map((rect) => expandedRect(rect, clearance)); + const relevantAvoidedSegments = [...avoidedSegments] + .filter((segment) => segment?.start && segment?.end); + // Frame borders may be crossed perpendicularly but never borrowed as a + // corridor: the composition gate rejects any collinear run along them. + const relevantBorderSegments = [...borderSegments] + .filter((segment) => segment?.start && segment?.end); + writeGridMetrics(metrics, { + obstacleCount: expanded.length, + avoidedSegmentCount: relevantAvoidedSegments.length, + }); + if (expanded.length > maximumObstacleCount) { + writeGridMetrics(metrics, { status: 'obstacle-budget-exceeded' }); + return null; + } + + const xs = new Set([startStub[0], endStub[0], ...points.map(([x]) => x)]); + const ys = new Set([startStub[1], endStub[1], ...points.map(([, y]) => y)]); + for (const rect of expanded) { + xs.add(rect.x - 1); + xs.add(rect.x + rect.width + 1); + ys.add(rect.y - 1); + ys.add(rect.y + rect.height + 1); + } + for (const segment of relevantAvoidedSegments) { + const [segmentStart, segmentEnd] = [segment.start, segment.end]; + xs.add(segmentStart[0]); + xs.add(segmentEnd[0]); + ys.add(segmentStart[1]); + ys.add(segmentEnd[1]); + if (Math.abs(segmentStart[0] - segmentEnd[0]) <= 0.0001) { + xs.add(segmentStart[0] - routeSeparationPx); + xs.add(segmentStart[0] + routeSeparationPx); + } + if (Math.abs(segmentStart[1] - segmentEnd[1]) <= 0.0001) { + ys.add(segmentStart[1] - routeSeparationPx); + ys.add(segmentStart[1] + routeSeparationPx); + } + } + for (const segment of relevantBorderSegments) { + if (Math.abs(segment.start[0] - segment.end[0]) <= 0.0001) { + xs.add(segment.start[0] - routeSeparationPx); + xs.add(segment.start[0] + routeSeparationPx); + } + if (Math.abs(segment.start[1] - segment.end[1]) <= 0.0001) { + ys.add(segment.start[1] - routeSeparationPx); + ys.add(segment.start[1] + routeSeparationPx); + } + } + // Grid lines closer than a readable segment would let the search emit a + // micro jog between two obstacle edges; keep the endpoint stubs and coalesce + // the rest so every turn the route can take is at least one segment long. + const coalesce = (values, keep) => values.sort((a, b) => a - b).filter((value, index, sorted) => ( + index === 0 || keep.has(value) || value - sorted[index - 1] >= minimumSegmentPx + )); + const orderedX = coalesce([...xs], new Set([startStub[0], endStub[0]])); + const orderedY = coalesce([...ys], new Set([startStub[1], endStub[1]])); + const candidateNodeCount = orderedX.length * orderedY.length; + writeGridMetrics(metrics, { + coordinateCount: orderedX.length + orderedY.length, + candidateNodeCount, + }); + if (candidateNodeCount > maximumGridNodes) { + writeGridMetrics(metrics, { status: 'node-budget-exceeded' }); + return null; + } + const nodes = new Map(); + for (const x of orderedX) { + for (const y of orderedY) { + const point = [x, y]; + if (!pointBlocked(point, expanded) + && (allowAvoidedCrossings || !pointOnAvoidedInterior(point, relevantAvoidedSegments))) { + nodes.set(pointKey(point), point); + } + } + } + writeGridMetrics(metrics, { usableNodeCount: nodes.size }); + if (!nodes.has(pointKey(startStub)) || !nodes.has(pointKey(endStub))) { + writeGridMetrics(metrics, { status: 'endpoint-blocked' }); + return null; + } + + const adjacency = new Map([...nodes.keys()].map((key) => [key, []])); + let graphEdgeCount = 0; + const connectLine = (line, axis) => { + for (let index = 0; index < line.length - 1; index += 1) { + const left = line[index]; + const right = line[index + 1]; + if (segmentBlocked(left, right, expanded)) continue; + if (segmentConflictsWithAvoided( + left, + right, + relevantAvoidedSegments, + minimumAvoidedOverlapPx, + allowAvoidedCrossings, + )) continue; + if (relevantBorderSegments.some((segment) => ( + collinearOverlap(left, right, segment.start, segment.end) > 0.0001 + ))) continue; + const distance = Math.abs(right[0] - left[0]) + Math.abs(right[1] - left[1]); + const leftKey = pointKey(left); + const rightKey = pointKey(right); + adjacency.get(leftKey).push([rightKey, distance, axis === 'h' ? 'R' : 'D']); + adjacency.get(rightKey).push([leftKey, distance, axis === 'h' ? 'L' : 'U']); + graphEdgeCount += 1; + } + }; + for (const y of orderedY) { + connectLine(orderedX.map((x) => nodes.get(pointKey([x, y]))).filter(Boolean), 'h'); + } + for (const x of orderedX) { + connectLine(orderedY.map((y) => nodes.get(pointKey([x, y]))).filter(Boolean), 'v'); + } + writeGridMetrics(metrics, { graphEdgeCount }); + + // The search state carries the incoming direction so a turn can cost extra + // and a reversal is never taken: the pure shortest path hugs every obstacle + // corner with a staircase of short jogs, while a bend-penalised one takes + // the same corridor in a few long strokes. The first stub already leaves + // the endpoint along its side and the last one arrives along the end side. + const directionOf = ([dx, dy]) => (dx > 0 ? 'R' : dx < 0 ? 'L' : dy > 0 ? 'D' : 'U'); + const opposite = { R: 'L', L: 'R', D: 'U', U: 'D' }; + const stateKey = (key, direction) => `${key}|${direction}`; + const sourceAxis = directionOf(OUTWARD[fromSide]); + const targetAxis = opposite[directionOf(OUTWARD[toSide])]; + const source = pointKey(startStub); + const target = pointKey(endStub); + const sourceState = stateKey(source, sourceAxis); + const distances = new Map([[sourceState, 0]]); + const previous = new Map(); + const queue = new MinHeap(); + queue.push(sourceState, 0); + let visitedNodeCount = 0; + let targetState = null; + while (queue.entries.length) { + const next = queue.pop(); + const current = next.key; + const currentDistance = next.distance; + if (currentDistance !== distances.get(current)) continue; + visitedNodeCount += 1; + const [currentNode, currentAxis] = current.split('|'); + if (currentNode === target) { + // Arriving on the wrong axis costs one final turn onto the end stub. + const arrival = currentDistance + (currentAxis === targetAxis ? 0 : bendPenaltyPx); + if (targetState == null || arrival < targetState.distance) { + targetState = { key: current, distance: arrival }; + } + if (currentAxis === targetAxis || bendPenaltyPx === 0) break; + continue; + } + if (targetState && currentDistance >= targetState.distance) break; + for (const [neighbor, weight, axis] of adjacency.get(currentNode) || []) { + if (axis === opposite[currentAxis]) continue; + const candidate = currentDistance + weight + (axis === currentAxis ? 0 : bendPenaltyPx); + const neighborState = stateKey(neighbor, axis); + if (candidate >= (distances.get(neighborState) ?? Infinity)) continue; + distances.set(neighborState, candidate); + previous.set(neighborState, current); + queue.push(neighborState, candidate); + } + } + writeGridMetrics(metrics, { visitedNodeCount }); + if (!targetState) { + writeGridMetrics(metrics, { status: 'no-route' }); + return null; + } + const reversed = []; + for (let key = targetState.key; key; key = previous.get(key)) { + reversed.push(nodes.get(key.split('|')[0])); + if (key === sourceState) break; + } + if (pointKey(reversed.at(-1)) !== source) { + writeGridMetrics(metrics, { status: 'broken-predecessor-chain' }); + return null; + } + const shortestPoints = normalizeRoutePoints([start, ...reversed.reverse(), end]); + writeGridMetrics(metrics, { status: 'routed' }); + return { + points: shortestPoints, + length: orthogonalLength(shortestPoints), + obstacleCount: expanded.length, + }; +} + +function collinearOverlap(leftStart, leftEnd, rightStart, rightEnd) { + if (leftStart[0] === leftEnd[0] && rightStart[0] === rightEnd[0] + && leftStart[0] === rightStart[0]) { + return Math.max(0, Math.min(Math.max(leftStart[1], leftEnd[1]), Math.max(rightStart[1], rightEnd[1])) + - Math.max(Math.min(leftStart[1], leftEnd[1]), Math.min(rightStart[1], rightEnd[1]))); + } + if (leftStart[1] === leftEnd[1] && rightStart[1] === rightEnd[1] + && leftStart[1] === rightStart[1]) { + return Math.max(0, Math.min(Math.max(leftStart[0], leftEnd[0]), Math.max(rightStart[0], rightEnd[0])) + - Math.max(Math.min(leftStart[0], leftEnd[0]), Math.min(rightStart[0], rightEnd[0]))); + } + return 0; +} + +function segmentOutsideContent(start, end, contentBounds) { + if (!contentBounds) return false; + const midpoint = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]; + return midpoint[0] < contentBounds.left || midpoint[0] > contentBounds.right + || midpoint[1] < contentBounds.top || midpoint[1] > contentBounds.bottom; +} + +function sharesOuterCorridor({ relation, relations, pathFor, points, contentBounds, minimumOverlap }) { + for (const other of asArray(relations)) { + if (!other || other === relation) continue; + const related = relation.from === other.from || relation.from === other.to + || relation.to === other.from || relation.to === other.to; + if (!related) continue; + const otherPoints = normalizeRoutePoints(pathFor(other)?.points || []); + for (let left = 0; left < points.length - 1; left += 1) { + if (!segmentOutsideContent(points[left], points[left + 1], contentBounds)) continue; + for (let right = 0; right < otherPoints.length - 1; right += 1) { + if (collinearOverlap(points[left], points[left + 1], otherPoints[right], otherPoints[right + 1]) >= minimumOverlap) { + return true; + } + } + } + } + return false; +} + +function relationshipSubject(diagramType, relationCollection, relationIndex, relation) { + return { + diagramType, + collection: relationCollection, + index: relationIndex, + ...(relation.id ? { id: relation.id } : {}), + from: relation.from, + to: relation.to, + }; +} + +/** + * Reject conspicuous authored detours without penalizing routes whose length is + * explained by opaque-node avoidance or a related shared outer corridor. + */ +export function cleanRouteDetourProblems({ + relations, + obstacles, + contentRects = obstacles, + endpointIds, + pathFor, + fromSideFor, + toSideFor, + diagramType, + relationCollection, + profile, + thresholds = {}, +}) { + if (profile !== 'showcase') return []; + const policy = { ...DEFAULTS, ...thresholds }; + const obstacleList = [...obstacles]; + const contentBounds = boundsForRects(contentRects); + const problems = []; + for (const [relationIndex, relation] of asArray(relations).entries()) { + if (!relation || !endpointIds?.has(relation.from) || !endpointIds?.has(relation.to)) continue; + if (!Array.isArray(relation.via) || relation.via.length === 0) continue; + const points = normalizeRoutePoints(pathFor(relation)?.points || []); + if (points.length < 3 || !points.every((point) => Array.isArray(point) && isFinitePoint(...point))) continue; + const actualLength = orthogonalLength(points); + if (!Number.isFinite(actualLength)) continue; + const start = points[0]; + const end = points.at(-1); + const manhattan = Math.abs(end[0] - start[0]) + Math.abs(end[1] - start[1]); + if (actualLength < manhattan * policy.minimumDetourRatio + || actualLength - manhattan < policy.minimumExcessLengthPx) continue; + const routeBounds = boundsForPoints(points); + const excursion = outsideExcursion(routeBounds, contentBounds); + const emptyClearance = emptyControlPointClearance(points, obstacleList); + if (Math.max(excursion?.maximum || 0, emptyClearance?.maximum || 0) + < policy.minimumEmptyExcursionPx) continue; + if (sharesOuterCorridor({ + relation, + relations, + pathFor, + points, + contentBounds, + minimumOverlap: policy.sharedCorridorMinimumPx, + })) continue; + + const fromSide = fromSideFor?.(relation) || inferredSide(points, 'source'); + const toSide = toSideFor?.(relation) || inferredSide(points, 'target'); + const shortest = shortestOrthogonalGridRoute({ + start, + end, + points, + obstacles: obstacleList, + fromSide, + toSide, + clearance: policy.clearance, + maximumObstacleCount: policy.maximumObstacleCount, + }); + if (!shortest || !Number.isFinite(shortest.length) || shortest.length <= 0) continue; + const detourRatio = actualLength / shortest.length; + const excessLength = actualLength - shortest.length; + if (detourRatio < policy.minimumDetourRatio || excessLength < policy.minimumExcessLengthPx) continue; + + const relationId = relation.id ? ` id "${relation.id}"` : ''; + const message = `[composition/excessive-route-detour] ${diagramType} ${relationCollection}[${relationIndex}]${relationId} "${relation.from}" -> "${relation.to}" travels ${Math.round(actualLength)}px, ${rounded(detourRatio)}x the ${Math.round(shortest.length)}px shortest obstacle-clearing orthogonal route, and reaches ${Math.round(excursion.maximum)}px beyond the content bounds — remove the distant via corridor or move it close to the connected content.`; + const supportedFix = 'remove the distant via points and retry automatic routing, or keep the endpoint sides and move the via corridor near the connected nodes while preserving labels and direction'; + recordDiagnostic({ + code: 'composition/excessive-route-detour', + severity: 'error', + message, + subject: relationshipSubject(diagramType, relationCollection, relationIndex, relation), + evidence: { + points, + actualLengthPx: rounded(actualLength), + shortestLegalPoints: shortest.points, + shortestLegalLengthPx: rounded(shortest.length), + detourRatio: rounded(detourRatio), + excessLengthPx: rounded(excessLength), + routeBounds, + contentBounds, + emptyExcursionPx: excursion, + emptyControlPointClearancePx: emptyClearance, + obstacleCount: shortest.obstacleCount, + thresholds: { + minimumDetourRatio: policy.minimumDetourRatio, + minimumExcessLengthPx: policy.minimumExcessLengthPx, + minimumEmptyExcursionPx: policy.minimumEmptyExcursionPx, + }, + }, + supportedFixes: [supportedFix], + }); + problems.push(message); + } + return problems; +} diff --git a/vendor/archify/renderers/shared/sidecar-path.mjs b/vendor/archify/renderers/shared/sidecar-path.mjs new file mode 100644 index 0000000..32e7360 --- /dev/null +++ b/vendor/archify/renderers/shared/sidecar-path.mjs @@ -0,0 +1,38 @@ +import { createHash } from 'node:crypto'; + +const SIDECAR_STEM_NAMESPACE = /\.~archify-[0-9a-f]{64}$/iu; + +export function sidecarStemNeedsBounding(stem, suffixes) { + const fits = (value) => value.length <= 255 && Buffer.byteLength(value, 'utf8') <= 255; + return !suffixes.every((suffix) => fits(`${stem}${suffix}`)); +} + +export function boundedSidecarStem(stem, suffixes, { force = false, hashDomain } = {}) { + if (!force && !sidecarStemNeedsBounding(stem, suffixes) + && !SIDECAR_STEM_NAMESPACE.test(stem)) return stem; + const hashInput = hashDomain ? `${hashDomain}\0${stem}` : stem; + const marker = `.~archify-${createHash('sha256').update(hashInput).digest('hex')}`; + const codePoints = [...stem]; + while (codePoints.length + && sidecarStemNeedsBounding(`${codePoints.join('')}${marker}`, suffixes)) { + codePoints.pop(); + } + return `${codePoints.join('')}${marker}`; +} + +export function sidecarStemFromComponent(component) { + if (component.endsWith('.html')) { + return { + stem: component.slice(0, -'.html'.length), + options: undefined, + }; + } + return { + stem: component, + options: { force: true, hashDomain: 'full-component' }, + }; +} + +export function isBoundedSidecarStem(stem) { + return SIDECAR_STEM_NAMESPACE.test(stem); +} diff --git a/vendor/archify/renderers/shared/spatial-grid.mjs b/vendor/archify/renderers/shared/spatial-grid.mjs new file mode 100644 index 0000000..d2eb439 --- /dev/null +++ b/vendor/archify/renderers/shared/spatial-grid.mjs @@ -0,0 +1,66 @@ +/** Uniform spatial grid for "which items can this box reach" queries (#8). */ + +// A cell box wider than this is not worth walking: the query returns every +// inserted item instead, which stays a superset of what the box reaches. +const DEFAULT_MAX_CELLS = 4096; + +export function createSpatialGrid(cellSize, { maxCells = DEFAULT_MAX_CELLS } = {}) { + const buckets = new Map(); + const items = []; + // Items whose own box cannot be walked are candidates for every query. + const overflow = []; + const keyOf = (x, y) => x + ':' + y; + const rangeOf = (box) => ({ + x0: Math.floor(box.minX / cellSize), x1: Math.floor(box.maxX / cellSize), + y0: Math.floor(box.minY / cellSize), y1: Math.floor(box.maxY / cellSize), + }); + // Finite coordinates can still land outside the safe-integer range, where + // incrementing an index no longer advances it; and a legal coordinate can + // name more cells than the grid is worth. Both cases stay out of the buckets + // and are answered by the item list, so they never hang or allocate. + const walkable = (range) => Number.isSafeInteger(range.x0) && Number.isSafeInteger(range.x1) + && Number.isSafeInteger(range.y0) && Number.isSafeInteger(range.y1) + && (range.x1 - range.x0 + 1) * (range.y1 - range.y0 + 1) <= maxCells; + + return { + insert(box, item) { + items.push(item); + const range = rangeOf(box); + if (!walkable(range)) { + overflow.push(item); + return; + } + for (let x = range.x0; x <= range.x1; x += 1) { + for (let y = range.y0; y <= range.y1; y += 1) { + const key = keyOf(x, y); + let bucket = buckets.get(key); + if (!bucket) { bucket = []; buckets.set(key, bucket); } + bucket.push(item); + } + } + }, + query(box) { + const range = rangeOf(box); + if (!walkable(range)) return items.slice(); + const seen = new Set(); + const found = []; + for (let x = range.x0; x <= range.x1; x += 1) { + for (let y = range.y0; y <= range.y1; y += 1) { + const bucket = buckets.get(keyOf(x, y)); + if (!bucket) continue; + for (const item of bucket) { + if (seen.has(item)) continue; + seen.add(item); + found.push(item); + } + } + } + for (const item of overflow) { + if (seen.has(item)) continue; + seen.add(item); + found.push(item); + } + return found; + }, + }; +} diff --git a/vendor/archify/renderers/shared/text-fit.mjs b/vendor/archify/renderers/shared/text-fit.mjs new file mode 100644 index 0000000..860356a --- /dev/null +++ b/vendor/archify/renderers/shared/text-fit.mjs @@ -0,0 +1,105 @@ +// Single-line node text fitting, shared by every renderer. +// +// Node text (`label`, `sublabel`, `tag`) renders as one element with +// text-anchor="middle" and is never wrapped. Left unmeasured, an over-long +// value silently spills across its neighbours while validation still reports +// a clean receipt — the failure mode this module exists to close. +// +// Two halves, always used together: +// - fittedNodeFontSize shrinks the text toward a legible minimum at render +// time, so ordinary overruns simply get smaller instead of overlapping. +// - minimumNodeTextWidth reports the width the text still needs once it has +// shrunk as far as it may, so validation can reject what shrinking cannot +// save. +// +// The geometry constants below are shared; the per-field `preferred` and +// `minimum` font sizes are not, because renderers set node text at different +// sizes (architecture sublabels are 9px, the rest are 7px). + +import { textUnits, SEMANTIC_SIGIL_INSET, SEMANTIC_SIGIL_SIZE, SEMANTIC_SIGIL_FOOTPRINT, SOURCE_BADGE_FOOTPRINT } from './utils.mjs'; + +// widthFactor: px of advance width per text unit, per px of font size. +// horizontalPadding: total px reserved inside the box so text never touches +// the border. +export const nodeTextFit = { + widthFactor: 0.6, + horizontalPadding: 8, +}; + +// Largest font size at or below `preferred` that fits `text` inside `width`, +// floored at `minimum` — below that the text is no longer legible and the +// caller should be reporting a problem instead. +export function fittedNodeFontSize(text, width, preferred, minimum) { + const units = Math.max(1, textUnits(text)); + const available = Math.max(1, width - nodeTextFit.horizontalPadding); + const fitted = Math.min(preferred, available / (units * nodeTextFit.widthFactor)); + return Math.max(minimum, Math.floor(fitted * 10) / 10); +} + +// Width `text` occupies at its legible minimum. Compare against +// `width - nodeTextFit.horizontalPadding` to decide whether shrink-to-fit can +// rescue it. +export function minimumNodeTextWidth(text, minimum) { + return textUnits(text) * minimum * nodeTextFit.widthFactor; +} + +// Available text width inside a box of `width`. +export function availableNodeTextWidth(width) { + return width - nodeTextFit.horizontalPadding; +} + +// Adapted from Souptik Chakraborty's #220: shift only labels which reach a +// corner icon. Unlike the original hard gate, a narrow valid node uses a +// separate text row; its authored bounds and acceptance remain unchanged. +export function nodeLabelLayout({ width, height, rows, side = 'left', brand = false, source = false, step = '' }) { + const result = { x: width / 2, ys: rows.map(row => row.y), sigilY: SEMANTIC_SIGIL_INSET, sigilSize: SEMANTIC_SIGIL_SIZE }; + const labelWidth = minimumNodeTextWidth(rows[0].text, rows[0].font); + const stepEnd = step ? (side === 'left' ? 23 : 10) + minimumNodeTextWidth(step, 8) + 3 : 0; + const left = Math.max(side === 'left' ? SEMANTIC_SIGIL_FOOTPRINT + 2 : 4, stepEnd); + const right = width - (brand ? 26 : side === 'right' ? SEMANTIC_SIGIL_FOOTPRINT + 2 : 4) + - (source ? SOURCE_BADGE_FOOTPRINT : 0); + if (result.x - labelWidth / 2 >= left && result.x + labelWidth / 2 <= right) return result; + if (labelWidth <= right - left) { + // Round away from the icon, retaining the node centre whenever possible. + result.x = Math.min(Math.floor((right - labelWidth / 2) * 10) / 10, + Math.max(result.x, Math.ceil((left + labelWidth / 2) * 10) / 10)); + return result; + } + // Keep the existing font sizes and put the text below the decoration rail. + // Conservative ascent/descent bounds also protect CJK and fallback fonts. + let bottom = Math.max(brand ? 22 : SEMANTIC_SIGIL_FOOTPRINT, source ? 19 : 0); + const ys = rows.map(row => { + const y = Math.max(row.y, Math.ceil((bottom + 2 + row.font * 1.2) * 10) / 10); + bottom = y + row.font * 0.3; + return y; + }); + if (bottom <= height - 2) { + result.ys = ys; + return result; + } + if (source) { + // A source badge adds a second decoration on the right. On short boxes, + // restoring the original rows would put the title back under that badge. + // Try compact leading before giving up the dedicated text rail. Retain + // every font size and the authored box; only this crowded fallback packs + // the rows, with a full em above each baseline and 0.3 em below it. + let compactBottom = Math.max(brand ? 22 : SEMANTIC_SIGIL_FOOTPRINT, 19) + 1; + const compactYs = rows.map(row => { + const y = Math.ceil((compactBottom + 1 + row.font) * 10) / 10; + compactBottom = y + row.font * 0.3; + return y; + }); + // The compact fallback may also use the otherwise reserved bottom + // padding; the entire descent still stays inside the fixed box. + if (compactBottom <= height - 0.5) { + result.ys = compactYs; + return result; + } + } + // A deliberately short fixed box may have no spare row. Preserve its text + // and geometry, and fit only the decorative sigil in the space above it. + result.sigilY = 1; + result.sigilSize = Math.max(1, Math.min(SEMANTIC_SIGIL_SIZE, + Math.floor(rows[0].y - rows[0].font * 1.2 - 3))); + return result; +} diff --git a/vendor/archify/renderers/shared/utils.mjs b/vendor/archify/renderers/shared/utils.mjs new file mode 100644 index 0000000..46e78cb --- /dev/null +++ b/vendor/archify/renderers/shared/utils.mjs @@ -0,0 +1,254 @@ +import { + escapeHtml as esc, + localizeTemplate, + resolveLocale, + translateMessage, + viewerCatalog, +} from './i18n.mjs'; + +export { esc }; + +export function renderDefinitions() { + return ` + + + + + + + + + + + + + + + + + `; +} + +const SIGIL_TONE = { + frontend: 'frontend', + start: 'frontend', + backend: 'backend', + active: 'frontend', + database: 'database', + success: 'backend', + cloud: 'cloud', + waiting: 'cloud', + security: 'security', + decision: 'database', + failure: 'security', + messagebus: 'messagebus', + external: 'external', + neutral: 'external', +}; + +const SIGIL_SHAPE = { + calendar: ``, + clock: ``, + person: ``, + briefcase: ``, + flag: ``, + moon: ``, + frontend: ` + + + `, + backend: ``, + database: ` + `, + cloud: ``, + security: ` + `, + messagebus: ` + + + `, + external: ` + `, + start: ` + `, + active: ``, + waiting: ``, + success: ` + `, + failure: ` + `, + neutral: ` + `, +}; + +// A quiet, renderer-owned corner symbol (type default or authored icon). It is +// SVG content rather than a +// viewer overlay, so it survives canonical export while adding no focus target, +// accessible name, layout box, or interaction state of its own. +// Shared with label clearance so the reserved rail matches the actual icon. +export const SEMANTIC_SIGIL_INSET = 6; +export const SEMANTIC_SIGIL_SIZE = 11; +export const SEMANTIC_SIGIL_FOOTPRINT = SEMANTIC_SIGIL_INSET + SEMANTIC_SIGIL_SIZE; +// The viewer installs a runtime "sources" beacon on the node's top-right +// rail, just left of the brand mark. Layout must reserve the same footprint +// so labels never sit under the badge. +export const SOURCE_BADGE_FOOTPRINT = 38; + +export function renderSemanticSigil(kind, { x, y, size = SEMANTIC_SIGIL_SIZE, icon } = {}) { + if (icon === 'none') return ''; + const selected = icon ?? kind; + const normalized = Object.hasOwn(SIGIL_SHAPE, selected) ? selected : 'neutral'; + const tone = SIGIL_TONE[kind] || 'external'; + const scale = size / 16; + return ``; +} + +export function renderCards(cards) { + const list = Array.isArray(cards) ? cards : []; + return ` +
    +${list.map((card) => `
    +
    +
    +

    ${esc(card.title)}

    +
    +
      +${card.items.map((item) => `
    • ${esc(item)}
    • `).join('\n')} +
    +
    `).join('\n\n')} +
    `; +} + +const SVG_SLOT_RE = / [\s\S]*? /; +const CARDS_SLOT_RE = / [\s\S]*? /; +const SUBTITLE_SLOT_RE = /^([ \t]*)

    \[Subtitle description\]<\/p>[ \t]*(\r?\n)?/m; +const SOURCE_EVIDENCE_PLACEHOLDER = ' '; +const I18N_PLACEHOLDER = ' '; + +function serializeScriptJson(value) { + return JSON.stringify(value) + .replaceAll('<', '\\u003c') + .replaceAll('>', '\\u003e') + .replaceAll('&', '\\u0026'); +} + +const TEMPLATE_PLACEHOLDERS = [ + '', + '[PROJECT NAME] Architecture Diagram', + '

    [PROJECT NAME] Architecture

    ', + I18N_PLACEHOLDER, +]; + +export function applyTemplate(template, { + title, + subtitle, + svg, + cards, + locale, + visualPreset = 'classic', + sourceEvidence = null, +}) { + if (!SVG_SLOT_RE.test(template)) { + throw new Error('applyTemplate: template missing ARCHIFY:SVG_SLOT sentinel'); + } + if (!CARDS_SLOT_RE.test(template)) { + throw new Error('applyTemplate: template missing ARCHIFY:CARDS_SLOT sentinel'); + } + if (!SUBTITLE_SLOT_RE.test(template)) { + throw new Error('applyTemplate: template missing subtitle placeholder'); + } + for (const ph of TEMPLATE_PLACEHOLDERS) { + if (!template.includes(ph)) { + throw new Error(`applyTemplate: template missing placeholder ${JSON.stringify(ph)}`); + } + } + // Keep existing custom templates compatible when evidence is not requested. + // Silently dropping verified evidence would be misleading, so the new slot + // becomes mandatory only for the opt-in evidence path. + if (sourceEvidence && !template.includes(SOURCE_EVIDENCE_PLACEHOLDER)) { + throw new Error(`applyTemplate: repository evidence requires placeholder ${JSON.stringify(SOURCE_EVIDENCE_PLACEHOLDER)}`); + } + // Function replacers: a literal `$&`, `$'`, `$\`` or `$$` in titles, labels, + // or rendered SVG must not be interpreted as a replacement pattern. + const sourceEvidenceJson = serializeScriptJson(sourceEvidence); + const resolvedLocale = resolveLocale(locale); + const i18nJson = serializeScriptJson({ locale: resolvedLocale, messages: viewerCatalog(resolvedLocale) }); + const renderedSubtitle = typeof subtitle === 'string' && subtitle.trim() + ? `

    ${esc(subtitle)}

    ` + : ''; + const i18nData = ` `; + return localizeTemplate(template, resolvedLocale) + .replace(I18N_PLACEHOLDER, () => i18nData) + .replace(TEMPLATE_PLACEHOLDERS[0], () => ``) + .replace(TEMPLATE_PLACEHOLDERS[1], () => `${esc(translateMessage(resolvedLocale, 'page.title', { title }))}`) + .replace(TEMPLATE_PLACEHOLDERS[2], () => `

    ${esc(title)}

    `) + .replace(SUBTITLE_SLOT_RE, (_match, indent, newline = '') => renderedSubtitle + ? `${indent}${renderedSubtitle}${newline}` + : '') + .replace(SVG_SLOT_RE, () => svg) + .replace(CARDS_SLOT_RE, () => cards) + .replace(SOURCE_EVIDENCE_PLACEHOLDER, () => sourceEvidence + ? ` ` + : ''); +} + +// CJK and other wide/fullwidth glyphs render at roughly twice the advance +// width of ASCII in the monospace stacks the template uses. Keep halfwidth +// forms (notably U+FF61–U+FF9F Katakana) out of this set. The explicit ranges +// also cover vertical punctuation and supplementary East Asian scripts that +// literal glyph ranges made difficult to audit. +// Code points that take two columns of advance width: East Asian Wide and +// Fullwidth per UAX #11, tracking Unicode 17.0. That takes in the BMP symbols +// carrying emoji presentation (U+2705, U+2B50, U+26A1, U+231B, ...), which +// render at the same square advance as the supplementary-plane emoji already +// listed here, and Hangul Jamo Extended-A. Two boundary calls worth naming: +// Unicode 16.0 reclassified the trigrams (U+2630-U+2637) and the monogram / +// digram symbols (U+268A-U+268F) from Neutral to Wide, so both are in; and +// Hangul Jamo Extended-A stops at U+A97C, its last assigned jamo, because +// U+A97D-U+A97F are unassigned, and unassigned code points outside the CJK +// ranges UAX #11 names default to Neutral rather than Wide. Spelled out as +// ranges because V8 has no \p{East_Asian_Width=W} property escape. +const FULLWIDTH_RE = /[\u1100-\u115F\u231A-\u231B\u2329-\u232A\u23E9-\u23EC\u23F0\u23F3\u25FD-\u25FE\u2614-\u2615\u2630-\u2637\u2648-\u2653\u267F\u268A-\u268F\u2693\u26A1\u26AA-\u26AB\u26BD-\u26BE\u26C4-\u26C5\u26CE\u26D4\u26EA\u26F2-\u26F3\u26F5\u26FA\u26FD\u2705\u270A-\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B-\u2B1C\u2B50\u2B55\u2E80-\uA4CF\uA960-\uA97C\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF01-\uFF60\uFFE0-\uFFE6\u{16FE0}-\u{18DFF}\u{1AFF0}-\u{1AFFF}\u{1B000}-\u{1B2FF}\u{1F000}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u; + +// Variation selectors contribute no separate unit. This is a conservative +// width estimate, not a measurement of the selected glyph: its actual advance +// depends on the font and presentation (Unicode UAX #11). +// VS16 requests emoji presentation, so reserve two units for the sequence. +// VS15 retains the base's width estimate; forcing every text-presentation +// sequence to one unit undercounts wide bases, including CJK characters whose +// font ignores that selector. Neutral bases remain one unit. Some selected +// text glyphs can be narrower than this estimate; prefer extra space to overflow. +const VARIATION_SELECTOR_FIRST = 0xfe00; +const VARIATION_SELECTOR_LAST = 0xfe0f; +const VARIATION_SELECTOR_EMOJI = 0xfe0f; + +// Width measurement is pure and the same labels are measured many times per +// compile, so the unit count is memoized by its input string. +const TEXT_UNITS_CACHE = new Map(); +const MAX_TEXT_UNITS_CACHE_ENTRIES = 4096; +const MAX_CACHED_TEXT_LENGTH = 1024; + +export function textUnits(text) { + const cacheKey = String(text ?? ''); + const cachedUnits = TEXT_UNITS_CACHE.get(cacheKey); + if (cachedUnits !== undefined) return cachedUnits; + const chars = Array.from(cacheKey); + let units = 0; + for (let i = 0; i < chars.length; i += 1) { + const codePoint = chars[i].codePointAt(0); + if (codePoint >= VARIATION_SELECTOR_FIRST && codePoint <= VARIATION_SELECTOR_LAST) continue; + const next = i + 1 < chars.length ? chars[i + 1].codePointAt(0) : -1; + if (next === VARIATION_SELECTOR_EMOJI) units += 2; + else units += FULLWIDTH_RE.test(chars[i]) ? 2 : 1; + } + // Keep repeated in-process compiles bounded, including unusually long labels. + if (cacheKey.length <= MAX_CACHED_TEXT_LENGTH) { + if (TEXT_UNITS_CACHE.size >= MAX_TEXT_UNITS_CACHE_ENTRIES) { + TEXT_UNITS_CACHE.delete(TEXT_UNITS_CACHE.keys().next().value); + } + TEXT_UNITS_CACHE.set(cacheKey, units); + } + return units; +} diff --git a/vendor/archify/renderers/shared/validator.mjs b/vendor/archify/renderers/shared/validator.mjs new file mode 100644 index 0000000..7664657 --- /dev/null +++ b/vendor/archify/renderers/shared/validator.mjs @@ -0,0 +1,86 @@ +import * as validators from './generated-validators.mjs'; +import { throwDiagnosticError } from './diagnostics.mjs'; + +// "/nodes/3/label" reads much better as "/nodes/3 (id: "router") /label" for the +// LLM fixing the JSON; resolve the nearest enclosing element's id or label. +function annotatedPath(instancePath, data) { + if (!instancePath) return { path: '/', identity: null }; + let node = data; + let hint = null; + for (const seg of instancePath.split('/').slice(1)) { + if (node == null || typeof node !== 'object') break; + node = node[/^\d+$/.test(seg) ? Number(seg) : seg]; + if (node && typeof node === 'object' && !Array.isArray(node)) { + const tag = node.id ?? node.label; + if (tag != null) hint = String(tag); + } + } + return { path: instancePath, identity: hint }; +} + +function annotatePath(instancePath, data) { + const annotated = annotatedPath(instancePath, data); + return annotated.identity != null + ? `${annotated.path} (id/label: ${JSON.stringify(annotated.identity)})` + : annotated.path; +} + +function formatErrors(errors, data) { + return errors.map((e) => { + const where = annotatePath(e.instancePath, data); + const detail = e.params && Object.keys(e.params).length + ? ' ' + JSON.stringify(e.params) + : ''; + return ` ${where} ${e.message}${detail}`; + }).join('\n'); +} + +export function validateSchema(diagramType, data) { + const validate = validators[diagramType]; + if (!validate) { + throw new Error(`validateSchema: unknown diagram type "${diagramType}"`); + } + if (!validate(data)) { + const diagnostics = validate.errors.map((error) => { + const annotated = annotatedPath(error.instancePath, data); + const subject = { + diagramType, + path: annotated.path, + ...(annotated.identity != null ? { identity: String(annotated.identity) } : {}), + }; + const evidence = { + keyword: error.keyword, + expected: error.schema, + ...error.params, + }; + const supportedFixes = { + additionalProperties: [`remove unsupported property ${JSON.stringify(error.params?.additionalProperty)}`], + required: [`add required property ${JSON.stringify(error.params?.missingProperty)}`], + type: [`use ${JSON.stringify(error.params?.type)} at ${annotated.path}`], + enum: [`choose one of ${JSON.stringify(error.params?.allowedValues || [])}`], + pattern: [`match the required pattern ${JSON.stringify(error.params?.pattern)}`], + minimum: [`use a value ${error.params?.comparison || '>='} ${error.params?.limit}`], + maximum: [`use a value ${error.params?.comparison || '<='} ${error.params?.limit}`], + minItems: [`provide at least ${error.params?.limit} item(s)`], + maxItems: [`provide at most ${error.params?.limit} item(s)`], + minLength: [`provide at least ${error.params?.limit} character(s)`], + maxLength: [`provide at most ${error.params?.limit} character(s)`], + }[error.keyword] || []; + const detail = error.params && Object.keys(error.params).length + ? ` ${JSON.stringify(error.params)}` + : ''; + return { + code: `schema/${error.keyword}`, + severity: 'error', + message: `${annotatePath(error.instancePath, data)} ${error.message}${detail}`, + subject, + evidence, + supportedFixes, + }; + }); + throwDiagnosticError( + `${diagramType} schema validation failed:\n${formatErrors(validate.errors, data)}`, + diagnostics, + ); + } +} diff --git a/vendor/archify/renderers/workflow/README.md b/vendor/archify/renderers/workflow/README.md new file mode 100644 index 0000000..9fb7229 --- /dev/null +++ b/vendor/archify/renderers/workflow/README.md @@ -0,0 +1,301 @@ +# Workflow Renderer + +Render `diagram_type: "workflow"` JSON files into the standard Archify HTML +template. + +```bash +node archify/renderers/workflow/render-workflow.mjs input.workflow.json output.html +``` + +The renderer validates input against `archify/schemas/workflow.schema.json` +with the bundled standalone validator. No dependency installation is required. + +If `output.html` is omitted, the renderer uses the required `meta.output` value +from the JSON file. + +After rendering, run the artifact checker: + +```bash +node archify/scripts/check-render-output.mjs output.html +``` + +It catches final-SVG issues that are easiest to see in a browser: non-finite +SVG values, accidental two-point diagonal arrows, and arrows crossing the +legend. + +## Input + +Workflow JSON files must set: + +```json +{ + "schema_version": 2, + "diagram_type": "workflow", + "meta": { + "title": "Agent Tool Call Workflow", + "output": "agent-tool-call.html" + }, + "lanes": [], + "phases": [], + "groups": [], + "mainPath": [], + "nodes": [], + "edges": [], + "cards": [] +} +``` + +Use `schema_version: 2` for new workflows. Its readable layout compiler treats +every `col` as a logical rank in `0..5` and derives geometry from the measured +document. `schema_version: 1` remains the fixed legacy contract for existing +sources; valid v1 output is preserved byte-for-byte and never silently +reinterpreted as v2. + +Omit `meta.viewBox` for the common v2 case so the compiler can use intrinsic +measured bounds. In v1, the omitted width remains fixed at 720 and height is +derived from lane count. A complete worked example lives at +`archify/examples/agent-tool-call.workflow.json`; its `schema_version` selects +the applicable contract. + +The schema lives at: + +```text +archify/schemas/workflow.schema.json +``` + +## Migration and layout receipt + +Migrate an existing v1 source into a separate v2 file: + +```bash +node archify/bin/archify.mjs migrate workflow old.json new.json --to-schema 2 --json +``` + +Running the command again with its schema-v2 output as the new source is an +idempotent verification pass: the destination bytes and geometry stay unchanged. + +If a legacy v1 source is blocked solely because `meta.output` is missing or no +longer portable, supply its replacement for the separate v2 destination: + +```bash +node archify/bin/archify.mjs migrate workflow old.json new.json --to-schema 2 --output reports/workflow.html --json +``` + +`--output` must itself be a portable POSIX-relative `.html` path. It updates +only the verified destination candidate; the source bytes remain unchanged and +all non-output schema and compiler diagnostics still block migration. + +The command never overwrites the source by default. It maps absolute +`via[*][0]`, `labelAt[0]`, and `channelX` values from legacy to solved rank +space, preserves y coordinates unless a reported vertical constraint needs +author input, expands an explicit viewBox only for an unambiguous containment +repair, and writes the destination only after v2 compilation and artifact +checks pass. Ambiguous explicit pins fail without producing the destination. + +Inspect the stable author-facing v2 plan with: + +```bash +node archify/bin/archify.mjs validate workflow input.workflow.json --layout-json +``` + +The receipt reports the selected contract, measured `viewBox` and +`requiredViewBox`, solved columns, nodes, edges, labels, and causal diagnostics. +It deliberately omits solver iterations and candidate scores. + +## Legend + +The default legend derives component kinds from `nodes[].type`. Supported +`meta.legend.entries` keys, in stable order, are `frontend`, `backend`, +`security`, `messagebus`, `database`, `cloud`, and `external`. Labels and +visibility may be overridden through the shared legend contract; only kinds +backed by rendered nodes receive Semantic Legend controls. + +## Layout contracts + +### Fixed v1 + +| Constant | Value | +|----------|-------| +| viewBox | default `[720, auto]` — auto height = 52 + lanes×104 + (lanes−1)×20 + 124 | +| Lane frame | x 40, width 640, height 104, gap 20; first lane top at y 52 | +| Lane title strip | top 30px of each lane; node boxes must stay below it | +| Column centers (`col` 0–5) | x = 88, 220, 300, 430, 500, 625 | +| Phase headers | Optional `phases[]` render above the first lane, spanning `fromCol..toCol` | +| Lane groups | Optional `groups[]` frame parallel work or branch work inside one lane | +| Exception lanes | Set `lane.variant: "exception"` for retry, denial, fallback, or failure paths | +| Main path lint | Optional `mainPath[]` checks that happy-path steps have matching edges and do not move backward | +| Default node | 92×52 (height 68 when `tag` is set) | +| Node spacing | ≥8px between nodes in the same lane | +| Edge length | straight segments must span ≥28px | +| Legend row | y = lane bottom + 44; viewBox height must be ≥ legend y + 18 | + +Column-center gaps are 132 / 80 / 130 / 70 / 125 px: columns 1↔2 (80px) and +3↔4 (70px) cannot both hold default-width 92px nodes in the same lane. Such an +invalid v1 source receives one causal `workflow/column-capacity` diagnostic and +a verified migration-to-v2 repair; v1 never falls through to adaptive layout. + +### Readable v2 + +| Invariant | Contract | +|----------|----------| +| Logical columns | `col` is an integer in `0..5`; pixel centers are measured output | +| Adjacent-rank baseline | 120px center distance before document-specific constraints | +| Same-lane node clearance | ≥8px when vertical node intervals overlap | +| Facing direct edge | clear gap ≥`max(28px, measured label mask width + 8px)` | +| Automatic route rhythm | direct segment ≥28px; endpoint stub ≥8px; interior turn segment ≥16px | +| Implicit viewBox | intrinsic content bounds plus contract padding | +| Explicit viewBox | containment capacity; too-small input reports exact `requiredViewBox` and contributors | +| Lane measurement | A same-column vertical stack (two or more distinct `yOffset` values) opts an implicit, unpinned workflow into per-lane measurement. Explicit `meta.viewBox`, `via`, `labelAt`, `channelX`, or `channelY`, and workflows without a stack retain shared-height v2 geometry for compatibility. | + +The compiler applies constraints only to actual related or overlapping +same-lane nodes, so a wide node in an unrelated lane does not expand every +rank. Legacy centers are a soft preference after correctness constraints, not +a geometry promise. Phase and group frames derive from the solved rank bands. +Automatic routes are normalized once and the same final scene drives +validation and SVG serialization. Long automatic labels compare direct-gutter +growth with a legal channel instead of widening every downstream rank. Measured +multi-row legends participate in intrinsic height and explicit viewBox +capacity. + +The Issue #250 shape already has a v2 representation without an authored lane +size. Keep the three stages in one grouped lane, omit `meta.viewBox`, and center +their offsets around zero: + +```json +{ + "schema_version": 2, + "diagram_type": "workflow", + "meta": { "title": "stack", "output": "stack.html" }, + "lanes": [{ "id": "cage", "label": "one cage" }], + "groups": [{ "id": "g", "label": "cage", "lane": "cage", "fromCol": 1, "toCol": 3 }], + "nodes": [ + { "id": "a", "lane": "cage", "col": 2, "type": "security", "label": "stageA", "yOffset": -90 }, + { "id": "b", "lane": "cage", "col": 2, "type": "security", "label": "stageB", "yOffset": 0 }, + { "id": "c", "lane": "cage", "col": 2, "type": "security", "label": "stageC", "yOffset": 90 } + ], + "edges": [ + { "from": "a", "to": "b", "fromSide": "bottom", "toSide": "top" }, + { "from": "b", "to": "c", "fromSide": "bottom", "toSide": "top" } + ] +} +``` + +An implicit readable-v2 vertical stack whose measured lane height exceeds the +104px baseline opts into the desktop Viewer's height budget. This decision +comes from compiled geometry, not an authored sizing field. The Viewer changes only +the outer reader width so the complete lane remains on screen; canonical SVG +geometry and explicit `meta.viewBox` workflows retain their authored contracts. +When necessary, the Viewer may scale below the intrinsic 1:1 width only as far +as the 6px projected node-text floor. If the complete workflow still cannot fit +at that readable scale, `visual-check` reports the remaining viewport overflow +instead of clipping or introducing an internal scroller. + +Authored `via`, `labelAt`, `channelX`, and `channelY` are absolute hard pins in +v2; an infeasible pin returns `workflow/explicit-pin-conflict` rather than being +silently moved. `fromSide` and `toSide` remain direction constraints. A route +preset restricts the automatic candidate family but is not itself an absolute +coordinate pin. When either endpoint side is omitted, the v2 compiler chooses +a feasible side; an authored side restricts that endpoint to the named port. + +## Design Rules + +- Use lanes for ownership or runtime boundaries. +- Use phase headers for high-level story beats such as Intake, Plan, Execute, and Report. +- Use groups for parallel checks, branch handling, or bounded work within a lane; every group must contain at least one node. +- For sequential stages stacked inside one container, use one v2 lane and one group, keep the stages in one column, and omit `meta.viewBox`. `yOffset` is relative to the center of the lane's content area, so center a three-stage stack with `-90 / 0 / 90` rather than `0 / 90 / 180`. With compiler-owned routes and canvas, the compiler expands only that lane. +- Use `lane.variant: "exception"` for human wait, denial, retry, fallback, and failure lanes instead of mixing those paths into the happy path. +- Set `mainPath` when the diagram has a clear happy path; the renderer validates that consecutive ids have matching edges and move left-to-right. +- Place nodes with lane IDs and `col` indexes in `0..5`, not raw SVG coordinates. +- Preserve semantic edge labels. Readable v2 allocates measured label clearance; + when a label does not fit, repair the reported capacity or route constraint + instead of deleting meaning. +- Use labels for decisions, approvals, protocols, async traces, return paths, + and any other relationship meaning not fully implied by its endpoints. +- Prefer route presets — `drop` (bend between lanes; `bias` 0–1 picks where), + `outside-right`, `return-left`, `bottom-channel`, and `up-channel` — before + using raw `via` points. `straight` and the default `auto` cover the rest. +- Keep workflow examples compact enough to render well in narrow chat/browser + previews. + +### Optional semantic checks + +Layout validation cannot infer domain truth from labels or cards. When source +evidence establishes roots, terminals, mandatory direct relationships, or +mandatory directed reachability, encode those facts in `semanticChecks`: + +```json +"semanticChecks": { + "allowedRoots": ["request", "resource_catalog"], + "allowedTerminals": ["reply", "audit_log"], + "requiredEdges": [ + { "from": "dispatch", "to": "dispatch_ledger" } + ], + "requiredPaths": [ + { "from": "event_ledger", "to": "runtime_host" } + ] +} +``` + +When `allowedRoots` or `allowedTerminals` is present, it is the complete allow +list for zero-incoming or zero-outgoing nodes respectively. `requiredEdges` +requires one exact authored direction; `requiredPaths` permits intermediate +nodes but follows authored edge direction. These checks run before layout, do +not alter SVG or receipt bytes, and must not be weakened merely to resolve a +route or composition diagnostic. Omit fields whose domain facts are unknown. + +Schema violations exit non-zero with path-prefixed messages annotated with the +element's id or label. The renderer additionally fails when it can detect +layout problems, including node overlap, nodes outside their lanes, invalid +phase/group column ranges, empty groups, broken `mainPath` steps, unknown edge +targets, labels colliding with nodes or other labels, labels wider than their +node, legends outside the viewBox, or straight arrows that are too short to +read cleanly. The shared Clean Flow Gate also rejects edges crossing unrelated +nodes with 2px clearance; lanes, phases, and groups remain intentional +pass-through containers. Text width is estimated CJK-aware: fullwidth glyphs +count as two units. + +Diagnostics are causal: a rank-capacity failure suppresses derivative short +edge, endpoint-direction, and label-overlap findings. Every +`supportedFixes[]` entry is verified by replanning the proposed edit, and a +diagnostic never proposes removing a semantic label when label presence does +not cause the failed invariant. + +Set `meta.quality_profile` to `showcase` for polished delivery. Unrelated proper +X crossings then fail with `composition/proper-crossing`; default `standard` +keeps them as artifact-receipt warnings. Collinear lane corridors are outside +the proper-X rule, but a separate gate warns in `standard` and fails in +`showcase` when unrelated edges overlap for at least 8px. V1 keeps its authored +shared-endpoint contract. V2 also checks shared endpoints, including explicitly +controlled routes: long or mixed-style trunks, counterflow, proper interior X +crossings and overlapping independent arrowheads are not exempt. Pins are +preserved, not silently repaired; unresolved v2 corridor/arrowhead collisions +warn in `standard` and fail in `showcase`. + +V2 permits a same-direction shared terminal stub of at most 24 SVG units only +when both relationships share the actual source or target port, effective +variant, stroke width and role. A nonterminal overlap is never such a stub; +forward-collinear waypoints do not split a long trunk into permitted pieces. +Compatible short merges may share their terminal arrowhead. Automatic routes +consider separate ports, reserve absolute routes at contested nodes, and prefer +clear paths over shorter ambiguous ones. Crowded automatic corridors can use a +bounded local adjustment without moving nodes or changing explicit coordinates. + +The SVG carries `data-layout-contract="readable-v2"` and each edge's role so +artifact checks apply the same classification to actual visible path geometry, +not stale composition-point metadata. These internal output attributes do not +add authoring schema fields. Other diagram types retain their existing rules. +V2 proper-crossing diagnostics retain the relationship IDs, intersection point, +and supported fixes in both compiler/layout-JSON receipts and final HTML checks; +`standard` reports warnings while `showcase` rejects the crossing. Both analyses +merge forward-collinear waypoints without rewriting authored paths; real bends +and reversals remain endpoint touches rather than being merged into an X. + +The older per-edge `data-composition-routing="workflow-v2-auto"` marker remains +for compatibility with first-round exported HTML that has no root layout +contract and with older artifact checkers. Marker-only artifacts retain their +narrower automatic-pair crossing/counterflow policy; the root `readable-v2` +contract is authoritative when present and also checks explicit routes. Do not +remove the marker-only path as dead code without retiring that export format. +Showcase also +rejects any route segment below 8px and any interior turn segment below 16px; +ordinary 8–15px endpoint stubs remain valid for fixed lane gaps. diff --git a/vendor/archify/renderers/workflow/render-workflow.mjs b/vendor/archify/renderers/workflow/render-workflow.mjs new file mode 100644 index 0000000..329558a --- /dev/null +++ b/vendor/archify/renderers/workflow/render-workflow.mjs @@ -0,0 +1,37 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadDiagramWithBrandMarks, writeDiagram } from '../shared/cli.mjs'; +import { throwDiagnosticError } from '../shared/diagnostics.mjs'; +import { compileWorkflow } from './workflow-compiler.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const { diagram: workflow, template, outPath, sourceEvidence } = await loadDiagramWithBrandMarks({ + rendererDir: __dirname, + diagramType: 'workflow', + defaultExample: 'agent-tool-call.workflow.json' +}); + +const compiled = compileWorkflow({ + workflow, + qualityProfile: process.env.ARCHIFY_QUALITY_PROFILE || workflow.meta?.quality_profile, + sourceEvidence, +}); + +const layoutJson = process.argv.includes('--layout-json'); + +if (layoutJson) { + process.stdout.write(`${JSON.stringify(compiled.receipt, null, 2)}\n`); + if (!compiled.ok) process.exitCode = 1; +} else if (!compiled.ok) { + throwDiagnosticError(compiled.error || 'Workflow compilation failed.', compiled.diagnostics); +} else { + writeDiagram({ + outPath, + template, + diagramType: 'workflow', + meta: workflow.meta, + svg: compiled.svg, + cards: workflow.cards, + sourceEvidence, + }); +} diff --git a/vendor/archify/renderers/workflow/workflow-compiler.mjs b/vendor/archify/renderers/workflow/workflow-compiler.mjs new file mode 100644 index 0000000..b3b3b42 --- /dev/null +++ b/vendor/archify/renderers/workflow/workflow-compiler.mjs @@ -0,0 +1,4913 @@ +import { createSpatialGrid } from '../shared/spatial-grid.mjs'; +import { esc, renderDefinitions, renderSemanticSigil, textUnits } from '../shared/utils.mjs'; +import { + animateAttr, + focusEdgeAttrs, + focusNodeAttrs, + focusNodeTitle, + svgAccessibleText, + svgRootAttrs, + validateCrossCollectionContracts, +} from '../shared/cli.mjs'; +import { + throwDiagnosticError, + throwDiagnosticProblems, + withDiagnosticRecordingSuppressed, +} from '../shared/diagnostics.mjs'; +import { validateSchema } from '../shared/validator.mjs'; +import { + legendFootprint, + measureLegend, + relationshipLegendObstacles, + resolveLegend, + renderLegend as renderResolvedLegend, +} from '../shared/legend.mjs'; +import { availableNodeTextWidth, fittedNodeFontSize, minimumNodeTextWidth, nodeLabelLayout } from '../shared/text-fit.mjs'; +import { brandLabelFitWidth, brandMarkFor, brandMetadataFor, brandTopRailProblem, renderBrandMark } from '../shared/brand-marks.mjs'; +import { translateMessage as i18nText } from '../shared/i18n.mjs'; +import { + createMappedWorkflowCandidate, + intrinsicWorkflow, + planningWorkflow, +} from './workflow-migration-geometry.mjs'; +import { + joinRoutePoints, + asArray, + isFinitePoint, + rectsOverlap, + segmentIntersectsRect, + segmentRectClearance, + cleanEndpointSideProblems, + cleanFlowProblems, + cleanCrossingProblems, + cleanAmbiguousCorridorProblems, + cleanBorderRunProblems, + cleanRouteRhythmProblems, + cleanLabelRouteClearanceProblems, + cleanLabelCanvasContainmentProblems, + collectAmbiguousCorridors, + collectArrowheadCollisions, + collectLabelRouteClearance, + collectBorderRuns, + forwardCollinearAnalysisSegments, + sourceSegmentIndexAtPoint, + suggestLabelObstacleFix, + suggestLabelPairFix, + anchor, + automaticPortSpread, + legacyDefaultFromSide as defaultFromSide, + legacyDefaultToSide as defaultToSide, + chosenSide, + normalizeRoutePoints, + routeHonorsEndpointSides, + polylinePath, + routePointsValue, + labelPoint, + componentFill, + componentText, + arrowClassMap, + variantAccent, + edgeLabelAccent +} from '../shared/geometry.mjs'; + +const LEGACY_COLUMN_CENTERS = Object.freeze([88, 220, 300, 430, 500, 625]); +const READABLE_CANDIDATE_COST_PRIORITY = Object.freeze([ + 'properCrossingCount', + 'sharedCorridorPx', + 'automaticForwardReversePx', + 'labelRouteClearanceDeficit', + 'interiorPreferred28Deficit', + 'bendCount', + 'stretchMilli', + 'canvasGrowthPx', + 'portDisplacementMilli', + 'legacyCoordinateDisplacement', + 'stableCandidateOrdinal', +]); +const MAX_READABLE_LAYOUT_FEEDBACK_ROUNDS = 3; +const GROUP_FRAME_TOP_INSET = 8; +const GROUP_FRAME_BOTTOM_INSET = 4; +const GROUP_LABEL_BASELINE_OFFSET = -2; +const GROUP_LABEL_MASK_ASCENT = 10; +const GROUP_LABEL_MASK_H = 14; +const GROUP_NODE_INSET = 4; + +class WorkflowLayoutFeedback extends Error { + constructor(request) { + super(`Workflow layout requires ${request.kind} feedback.`); + this.name = 'WorkflowLayoutFeedback'; + this.request = request; + } +} + +function createLegacyLayout() { + return { + contract: 'fixed-v1', + laneX: 40, + laneY: 52, + laneW: 640, + laneH: 104, + laneGap: 20, + laneTitleH: 30, + colXs: [...LEGACY_COLUMN_CENTERS], + nodeW: 92, + nodeH: 52, + defaultViewBoxWidth: 720, + }; +} + +function hasAbsoluteWorkflowPins(workflow) { + return asArray(workflow.edges).some((edge) => ( + Array.isArray(edge.via) + || Array.isArray(edge.labelAt) + || edge.channelX !== undefined + || edge.channelY !== undefined + )); +} + +function hasVerticalStack(workflow) { + const offsetsByLaneAndColumn = new Map(); + for (const node of asArray(workflow.nodes)) { + if (!Number.isInteger(node.col)) continue; + const key = `${node.lane}\u0000${node.col}`; + const offsets = offsetsByLaneAndColumn.get(key) || new Set(); + offsets.add(Number(node.yOffset) || 0); + if (offsets.size > 1) return true; + offsetsByLaneAndColumn.set(key, offsets); + } + return false; +} + +function usesIndependentLaneMeasurement(workflow) { + return hasVerticalStack(workflow) + && !workflow.meta?.viewBox + && !hasAbsoluteWorkflowPins(workflow); +} + +function authoredNodeWidth(node) { + return Number.isFinite(node?.width) ? node.width : 92; +} + +function nodeWidthContributor(node) { + return `node ${node.id} width ${authoredNodeWidth(node)}px`; +} + +function authoredNodeHeight(node) { + if (Number.isFinite(node?.height)) return node.height; + return node?.tag ? 68 : 52; +} + +function workflowLabelWidth(label) { + return Math.max(30, textUnits(label) * 4.8 + 10); +} + +function readableGroupBounds(workflow, group, colXs) { + if (!Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol) + || group.fromCol < 0 || group.fromCol > group.toCol || group.toCol >= colXs.length) { + return { x: 0, width: 0, cx: 0 }; + } + const start = colXs[group.fromCol] - 50; + const end = colXs[group.toCol] + 50; + const naturalWidth = end - start; + const minimumWidth = textUnits(group.label) * 5.6 + 20; + let width = Math.max(naturalWidth, minimumWidth); + let left = group.fromCol === group.toCol && width > naturalWidth + ? start + : (start + end - width) / 2; + let right = left + width; + for (const node of asArray(workflow.nodes)) { + if (node.lane !== group.lane + || !Number.isInteger(node.col) + || node.col < group.fromCol + || node.col > group.toCol + || node.col < 0 + || node.col >= colXs.length) continue; + const halfWidth = authoredNodeWidth(node) / 2; + left = Math.min(left, colXs[node.col] - halfWidth - GROUP_NODE_INSET); + right = Math.max(right, colXs[node.col] + halfWidth + GROUP_NODE_INSET); + } + width = right - left; + return { x: left, width, cx: left + width / 2 }; +} + +function verticalIntervalsOverlap(a, b, clearance = 0) { + const aCenter = Number(a?.yOffset) || 0; + const bCenter = Number(b?.yOffset) || 0; + return Math.abs(aCenter - bCenter) + < authoredNodeHeight(a) / 2 + authoredNodeHeight(b) / 2 + clearance; +} + +function createReadableLayout(workflow, layoutFeedback = {}) { + const columnCount = 6; + const baselinePitch = 120; + const columnStart = 94; + const maxLayoutIterations = 3; + const channelDetourBudgetPx = 4 * 28; + const constraints = []; + const feedbackConstraints = []; + const channelLabelEdgeKeys = new Set(); + const widthContributors = new Set(); + const heightContributors = new Set(); + const nodes = asArray(workflow.nodes); + const nodesById = new Map(nodes.map((node) => [node.id, node])); + + for (let col = 0; col < columnCount - 1; col += 1) { + constraints.push({ from: col, to: col + 1, minimum: baselinePitch }); + } + for (const [key, minimum] of Object.entries(layoutFeedback.rankGapMinimums || {}).sort()) { + const [from, to] = key.split(':').map(Number); + constraints.push({ + from, + to, + minimum, + contributors: layoutFeedback.rankGapContributors?.[key] + || [`rank ${from}→${to} route clearance`], + }); + } + + for (let leftIndex = 0; leftIndex < nodes.length; leftIndex += 1) { + for (let rightIndex = leftIndex + 1; rightIndex < nodes.length; rightIndex += 1) { + const leftNode = nodes[leftIndex]; + const rightNode = nodes[rightIndex]; + if (leftNode.lane !== rightNode.lane || leftNode.col === rightNode.col) continue; + if (!verticalIntervalsOverlap(leftNode, rightNode, 8)) continue; + const fromNode = leftNode.col < rightNode.col ? leftNode : rightNode; + const toNode = fromNode === leftNode ? rightNode : leftNode; + constraints.push({ + from: fromNode.col, + to: toNode.col, + minimum: authoredNodeWidth(fromNode) / 2 + 8 + authoredNodeWidth(toNode) / 2, + contributors: [ + `rank ${fromNode.col}→${toNode.col} node width clearance`, + nodeWidthContributor(fromNode), + nodeWidthContributor(toNode), + ], + }); + } + } + + for (const edge of asArray(workflow.edges)) { + const fromNode = nodesById.get(edge.from); + const toNode = nodesById.get(edge.to); + if (!fromNode || !toNode || fromNode.lane !== toNode.lane || fromNode.col === toNode.col) continue; + if (edge.via || edge.channelX !== undefined || edge.channelY !== undefined + || !['auto', 'straight'].includes(edge.route || 'auto')) continue; + if ((Number(fromNode.yOffset) || 0) !== (Number(toNode.yOffset) || 0)) continue; + const earlier = fromNode.col < toNode.col ? fromNode : toNode; + const later = earlier === fromNode ? toNode : fromNode; + const labeledDirectClearance = edge.label && !edge.labelAt + ? Math.max(28, workflowLabelWidth(edge.label) + 8) + : 28; + const directLabelExpansionCost = Math.max(0, labeledDirectClearance - 28); + const canUseAutomaticLabelChannel = edge.label + && !edge.labelAt + && (edge.route || 'auto') === 'auto' + && !edge.fromSide + && !edge.toSide + && edge.channelX === undefined + && edge.channelY === undefined; + const preferLabelChannel = canUseAutomaticLabelChannel + && directLabelExpansionCost > channelDetourBudgetPx; + if (preferLabelChannel) channelLabelEdgeKeys.add(stableValueKey(edge)); + constraints.push({ + from: earlier.col, + to: later.col, + minimum: authoredNodeWidth(earlier) / 2 + 28 + authoredNodeWidth(later) / 2, + contributors: [ + `rank ${earlier.col}→${later.col} direct clearance`, + `rank ${earlier.col}→${later.col} node width clearance`, + nodeWidthContributor(earlier), + nodeWidthContributor(later), + ], + }); + if (!preferLabelChannel && labeledDirectClearance > 28) { + const labelConstraintMinimum = authoredNodeWidth(earlier) / 2 + + labeledDirectClearance + + authoredNodeWidth(later) / 2; + feedbackConstraints.push({ + from: earlier.col, + to: later.col, + minimum: labelConstraintMinimum, + contributors: [ + `rank ${earlier.col}→${later.col} direct clearance`, + `edge ${workflowEdgeName(edge)} label mask`, + nodeWidthContributor(earlier), + nodeWidthContributor(later), + ], + }); + } + } + + for (const phase of asArray(workflow.phases)) { + if (!Number.isInteger(phase.fromCol) || !Number.isInteger(phase.toCol) + || phase.fromCol < 0 || phase.fromCol > phase.toCol || phase.toCol >= columnCount) continue; + const minimumWidth = textUnits(phase.label) * 5.6 + 8; + if (phase.fromCol === phase.toCol) { + if (phase.toCol < columnCount - 1) { + constraints.push({ + from: phase.toCol, + to: phase.toCol + 1, + minimum: baselinePitch + Math.max(0, minimumWidth - 92), + contributors: [`phase ${phase.id || phase.label} label span`], + }); + } + continue; + } + constraints.push({ + from: phase.fromCol, + to: phase.toCol, + minimum: Math.max(0, minimumWidth - 92), + contributors: [`phase ${phase.id || phase.label} label span`], + }); + } + + for (const group of asArray(workflow.groups)) { + if (!Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol) + || group.fromCol < 0 || group.fromCol > group.toCol || group.toCol >= columnCount) continue; + const minimumWidth = textUnits(group.label) * 5.6 + 20; + if (group.fromCol === group.toCol) { + if (group.toCol < columnCount - 1) { + constraints.push({ + from: group.toCol, + to: group.toCol + 1, + minimum: baselinePitch + Math.max(0, minimumWidth - 100), + contributors: [`group ${group.id || group.label} label span`], + }); + } + continue; + } + constraints.push({ + from: group.fromCol, + to: group.toCol, + minimum: Math.max(0, minimumWidth - 100), + contributors: [`group ${group.id || group.label} label span`], + }); + } + + let activeConstraints = [...constraints]; + let colXs; + let colProvenance; + for (let iteration = 0; iteration < maxLayoutIterations; iteration += 1) { + colXs = Array.from({ length: columnCount }, (_, col) => columnStart + col * baselinePitch); + colProvenance = Array.from({ length: columnCount }, () => new Set()); + const orderedConstraints = activeConstraints + .filter(({ from, to, minimum }) => ( + Number.isInteger(from) && Number.isInteger(to) + && from >= 0 && from < to && to < columnCount + && Number.isFinite(minimum) + )) + .sort((a, b) => a.to - b.to || a.from - b.from || a.minimum - b.minimum); + for (let to = 1; to < columnCount; to += 1) { + for (const constraint of orderedConstraints) { + if (constraint.to !== to) continue; + const candidate = colXs[constraint.from] + constraint.minimum; + const candidateProvenance = new Set([ + ...colProvenance[constraint.from], + ...asArray(constraint.contributors), + ]); + if (candidate > colXs[to] + 0.0001) { + colXs[to] = candidate; + colProvenance[to] = candidateProvenance; + } else if (Math.abs(candidate - colXs[to]) <= 0.0001 + && candidate > columnStart + to * baselinePitch + 0.0001) { + for (const contributor of candidateProvenance) colProvenance[to].add(contributor); + } + } + } + if (iteration > 0 || !feedbackConstraints.length) break; + activeConstraints = [...activeConstraints, ...feedbackConstraints]; + } + + const firstRankNodes = nodes.filter((node) => node.col === 0); + const firstExtent = firstRankNodes.reduce( + (maximum, node) => Math.max(maximum, authoredNodeWidth(node) / 2), + 46, + ); + const leftInset = 8; + const leftShift = Math.max(0, 40 + leftInset + firstExtent - colXs[0]); + if (leftShift) { + for (let col = 0; col < colXs.length; col += 1) colXs[col] += leftShift; + for (const node of firstRankNodes) { + if (Math.abs(authoredNodeWidth(node) / 2 - firstExtent) > 0.0001) continue; + for (const provenance of colProvenance) provenance.add(nodeWidthContributor(node)); + } + } + + const unpinnedTopEndpointIds = new Set(); + for (const edge of asArray(workflow.edges)) { + const preservesHorizontalPins = Array.isArray(edge.via) || edge.channelX !== undefined; + if (preservesHorizontalPins) continue; + if (edge.fromSide === 'top') unpinnedTopEndpointIds.add(edge.from); + if (edge.toSide === 'top') unpinnedTopEndpointIds.add(edge.to); + } + const laneOrder = new Map(asArray(workflow.lanes).map((lane, index) => [lane.id, index])); + let laneHeaderShift = 0; + const laneHeaderShiftContributors = new Set(); + for (const nodeId of unpinnedTopEndpointIds) { + const node = nodesById.get(nodeId); + if (!node || !Number.isInteger(node.col) || node.col < 0 || node.col >= columnCount) continue; + const lanePosition = laneOrder.get(node.lane); + const lane = asArray(workflow.lanes)[lanePosition]; + if (!lane) continue; + const prefix = lane.variant === 'exception' + ? 'EX' + : String(lanePosition + 1).padStart(2, '0'); + const laneHeaderRight = 40 + 14 + textUnits(`${prefix} / ${lane.label}`) * 6.2; + const requiredShift = laneHeaderRight + 2 - colXs[node.col]; + if (requiredShift > laneHeaderShift + 0.0001) { + laneHeaderShift = requiredShift; + laneHeaderShiftContributors.clear(); + laneHeaderShiftContributors.add(`lane ${lane.id} label width`); + } else if (requiredShift > 0 && Math.abs(requiredShift - laneHeaderShift) <= 0.0001) { + laneHeaderShiftContributors.add(`lane ${lane.id} label width`); + } + } + if (laneHeaderShift > 0) { + for (let col = 0; col < colXs.length; col += 1) colXs[col] += laneHeaderShift; + for (const provenance of colProvenance) { + for (const contributor of laneHeaderShiftContributors) provenance.add(contributor); + } + } + + let measuredContentLeftShift = asArray(workflow.edges).reduce((maximum, edge) => { + if (!channelLabelEdgeKeys.has(stableValueKey(edge))) return maximum; + const fromNode = nodesById.get(edge.from); + const toNode = nodesById.get(edge.to); + if (!fromNode || !toNode) return maximum; + const labelCenter = (colXs[fromNode.col] + colXs[toNode.col]) / 2; + const labelLeft = labelCenter - workflowLabelWidth(edge.label) / 2; + return Math.max(maximum, 16 - labelLeft); + }, 0); + for (const phase of asArray(workflow.phases)) { + if (!Number.isInteger(phase.fromCol) || !Number.isInteger(phase.toCol)) continue; + const width = Math.max( + colXs[phase.toCol] - colXs[phase.fromCol] + 92, + textUnits(phase.label) * 5.6 + 8, + ); + const left = phase.fromCol === phase.toCol + ? colXs[phase.fromCol] - 46 + : (colXs[phase.fromCol] + colXs[phase.toCol] - width) / 2; + measuredContentLeftShift = Math.max(measuredContentLeftShift, 16 - left); + } + for (const group of asArray(workflow.groups)) { + if (!Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol)) continue; + const bounds = readableGroupBounds(workflow, group, colXs); + measuredContentLeftShift = Math.max(measuredContentLeftShift, 44 - bounds.x); + } + if (measuredContentLeftShift > 0) { + for (let col = 0; col < colXs.length; col += 1) colXs[col] += measuredContentLeftShift; + } + + let rightmost = colXs.at(-1) + 50; + let rightmostContributors = new Set(colProvenance.at(-1)); + for (const node of nodes) { + if (!Number.isInteger(node.col) || node.col < 0 || node.col >= columnCount) continue; + const nodeRight = colXs[node.col] + authoredNodeWidth(node) / 2; + const nodeContributors = new Set([ + ...colProvenance[node.col], + nodeWidthContributor(node), + ]); + if (nodeRight > rightmost + 0.0001) { + rightmost = nodeRight; + rightmostContributors = nodeContributors; + } else if (Math.abs(nodeRight - rightmost) <= 0.0001) { + for (const contributor of nodeContributors) rightmostContributors.add(contributor); + } + } + for (const group of asArray(workflow.groups)) { + if (!Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol)) continue; + const bounds = readableGroupBounds(workflow, group, colXs); + const groupRight = bounds.x + bounds.width; + const groupContributors = new Set([ + ...colProvenance[group.fromCol], + ...colProvenance[group.toCol], + `group ${group.id || group.label} label span`, + ...nodes + .filter((node) => node.lane === group.lane + && node.col >= group.fromCol && node.col <= group.toCol) + .map(nodeWidthContributor), + ]); + if (groupRight > rightmost + 0.0001) { + rightmost = groupRight; + rightmostContributors = groupContributors; + } else if (Math.abs(groupRight - rightmost) <= 0.0001) { + for (const contributor of groupContributors) rightmostContributors.add(contributor); + } + } + const widestLaneLabel = asArray(workflow.lanes).reduce((widest, lane, index) => { + const width = textUnits(`${String(index + 1).padStart(2, '0')} / ${lane.label}`) * 6.2 + 30; + return width > widest.width ? { width, lane } : widest; + }, { width: 0, lane: null }); + const laneLabelWidth = widestLaneLabel.width; + const rightmostLaneWidth = Math.ceil(rightmost - 40 + 8); + const laneW = Math.max( + 640, + rightmostLaneWidth, + Math.ceil(laneLabelWidth), + ); + if (laneW > 640) { + if (rightmostLaneWidth === laneW) { + for (const contributor of rightmostContributors) widthContributors.add(contributor); + } + if (Math.ceil(laneLabelWidth) === laneW && widestLaneLabel.lane) { + widthContributors.add(`lane ${widestLaneLabel.lane.id || widestLaneLabel.lane.label} label width`); + } + } + const verticalExtent = (laneId) => { + let maximum = 0; + const contributors = new Set(); + for (const node of nodes) { + if (laneId !== undefined && node.lane !== laneId) continue; + const yOffset = Number(node.yOffset) || 0; + const extent = authoredNodeHeight(node) / 2 + Math.abs(yOffset); + const contributor = `node ${node.id} height ${authoredNodeHeight(node)}px${yOffset ? ` with yOffset ${yOffset}px` : ''}`; + if (extent > maximum + 0.0001) { + maximum = extent; + contributors.clear(); + contributors.add(contributor); + } else if (Math.abs(extent - maximum) <= 0.0001) { + contributors.add(contributor); + } + } + return { maximum, contributors }; + }; + const sharedVerticalExtent = verticalExtent(); + const laneH = 30 + Math.max(74, Math.ceil(sharedVerticalExtent.maximum * 2 + 8)); + const independentLaneMeasurement = usesIndependentLaneMeasurement(workflow); + const laneBaseHeights = asArray(workflow.lanes).map((lane) => { + if (!independentLaneMeasurement) return laneH; + const ownVerticalExtent = verticalExtent(lane.id); + const height = 30 + Math.max(74, Math.ceil(ownVerticalExtent.maximum * 2 + 8)); + if (height > 104) { + for (const contributor of ownVerticalExtent.contributors) heightContributors.add(contributor); + } + return height; + }); + if (!independentLaneMeasurement && laneH > 104) { + for (const contributor of sharedVerticalExtent.contributors) heightContributors.add(contributor); + } + const groupsByLane = new Map(); + for (const group of asArray(workflow.groups)) { + groupsByLane.set(group.lane, [...(groupsByLane.get(group.lane) || []), group]); + } + const groupLaneReserves = asArray(workflow.lanes).map((lane, laneIndex) => { + const baseContentH = laneBaseHeights[laneIndex] - 30; + let header = 0; + let footer = 0; + for (const group of groupsByLane.get(lane.id) || []) { + const bounds = readableGroupBounds(workflow, group, colXs); + const labelLeft = bounds.x + 10; + const labelRight = labelLeft + textUnits(group.label) * 5.6; + for (const node of nodes) { + if (node.lane !== group.lane + || !Number.isInteger(node.col) + || node.col < group.fromCol + || node.col > group.toCol + || node.col < 0 + || node.col >= colXs.length) continue; + const halfWidth = authoredNodeWidth(node) / 2; + const nodeLeft = colXs[node.col] - halfWidth; + const nodeRight = colXs[node.col] + halfWidth; + const overlapsLabel = nodeRight > labelLeft && nodeLeft < labelRight; + const topOffset = (baseContentH - authoredNodeHeight(node)) / 2 + + (Number(node.yOffset) || 0); + const minimumTopOffset = overlapsLabel ? 11 : 9; + header = Math.max(header, Math.ceil(minimumTopOffset - topOffset)); + const bottomMargin = baseContentH - GROUP_FRAME_BOTTOM_INSET + - topOffset - authoredNodeHeight(node); + footer = Math.max(footer, Math.ceil(1 - bottomMargin)); + } + } + return { header: Math.max(0, header), footer: Math.max(0, footer) }; + }); + const groupHeaderHeights = groupLaneReserves.map(({ header }) => header); + const groupFooterHeights = groupLaneReserves.map(({ footer }) => footer); + const laneHeights = groupLaneReserves.map(({ header, footer }, index) => ( + laneBaseHeights[index] + header + footer + )); + const laneGap = Math.max(20, Math.ceil(layoutFeedback.laneGapMin || 0)); + for (const [index, reserve] of groupHeaderHeights.entries()) { + if (!reserve) continue; + const lane = asArray(workflow.lanes)[index]; + heightContributors.add(`lane ${lane.id || lane.label} group label clearance ${reserve}px`); + } + for (const [index, reserve] of groupFooterHeights.entries()) { + if (!reserve) continue; + const lane = asArray(workflow.lanes)[index]; + heightContributors.add(`lane ${lane.id || lane.label} group frame containment ${reserve}px`); + } + if (laneGap > 20) { + for (const contributor of asArray(layoutFeedback.laneGapContributors)) { + heightContributors.add(contributor); + } + } + const requiredWidth = 40 + laneW + 16; + + return { + contract: 'readable-v2', + laneX: 40, + laneY: 52, + laneW, + laneH, + laneHeights, + laneGap, + laneTitleH: 30, + groupHeaderHeights, + groupFooterHeights, + colXs, + nodeW: 92, + nodeH: 52, + defaultViewBoxWidth: requiredWidth, + channelLabelEdgeKeys, + widthContributors: [...widthContributors].sort(stableCompare), + heightContributors: [...heightContributors].sort(stableCompare), + }; +} + +function compilerFailure(contract, diagnostics, error = diagnostics.map(({ message }) => message).join('\n')) { + return { + ok: false, + error, + diagnostics, + receipt: { contract, diagnostics }, + }; +} + +function workflowEdgeName(edge) { + return edge.id || `${edge.from}->${edge.to}`; +} + +function stableText(value) { + return value == null ? '' : String(value); +} + +function stableCompare(left, right) { + const a = stableText(left); + const b = stableText(right); + return a < b ? -1 : a > b ? 1 : 0; +} + +function stableValueKey(value) { + if (Array.isArray(value)) return `[${value.map(stableValueKey).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort(stableCompare).map((key) => `${JSON.stringify(key)}:${stableValueKey(value[key])}`).join(',')}}`; + } + return JSON.stringify(value); +} + +function cloneWorkflow(value) { + return JSON.parse(JSON.stringify(value)); +} + +function canonicalReadableWorkflow(workflow) { + if (workflow.schema_version !== 2) return workflow; + const laneOrder = new Map(asArray(workflow.lanes).map((lane, index) => [lane.id, index])); + const nodes = [...asArray(workflow.nodes)].sort((left, right) => ( + (laneOrder.get(left.lane) ?? Number.MAX_SAFE_INTEGER) - (laneOrder.get(right.lane) ?? Number.MAX_SAFE_INTEGER) + || left.col - right.col + || stableCompare(left.id, right.id) + )); + const edges = [...asArray(workflow.edges)].sort((left, right) => ( + stableCompare(left.id, right.id) + || stableCompare(left.from, right.from) + || stableCompare(left.to, right.to) + || stableCompare(left.label, right.label) + || stableCompare(left.route, right.route) + || stableCompare(stableValueKey(left), stableValueKey(right)) + )); + const phases = workflow.phases === undefined ? undefined : [...asArray(workflow.phases)].sort((left, right) => ( + left.fromCol - right.fromCol || left.toCol - right.toCol + || stableCompare(left.id, right.id) + )); + const groups = workflow.groups === undefined ? undefined : [...asArray(workflow.groups)].sort((left, right) => ( + (laneOrder.get(left.lane) ?? Number.MAX_SAFE_INTEGER) - (laneOrder.get(right.lane) ?? Number.MAX_SAFE_INTEGER) + || left.fromCol - right.fromCol || left.toCol - right.toCol + || stableCompare(left.id, right.id) + )); + return { + ...workflow, + nodes, + edges, + ...(phases ? { phases } : {}), + ...(groups ? { groups } : {}), + }; +} + +function semanticContractDiagnostics(workflow) { + const checks = workflow.semanticChecks; + if (!checks) return []; + + const nodeIds = new Set(asArray(workflow.nodes).map((node) => node.id)); + const incoming = new Map([...nodeIds].map((id) => [id, 0])); + const outgoing = new Map([...nodeIds].map((id) => [id, 0])); + const adjacency = new Map([...nodeIds].map((id) => [id, new Set()])); + for (const edge of asArray(workflow.edges)) { + if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) continue; + outgoing.set(edge.from, outgoing.get(edge.from) + 1); + incoming.set(edge.to, incoming.get(edge.to) + 1); + adjacency.get(edge.from).add(edge.to); + } + + const diagnostics = []; + const diagnostic = (code, message, subject, evidence, supportedFixes) => ({ + code, + severity: 'error', + message, + subject: { diagramType: 'workflow', ...subject }, + evidence, + supportedFixes, + }); + const referencedNodes = [ + ...asArray(checks.allowedRoots).map((id, index) => ({ id, path: `/semanticChecks/allowedRoots/${index}` })), + ...asArray(checks.allowedTerminals).map((id, index) => ({ id, path: `/semanticChecks/allowedTerminals/${index}` })), + ...asArray(checks.requiredEdges).flatMap((relation, index) => [ + { id: relation.from, path: `/semanticChecks/requiredEdges/${index}/from` }, + { id: relation.to, path: `/semanticChecks/requiredEdges/${index}/to` }, + ]), + ...asArray(checks.requiredPaths).flatMap((relation, index) => [ + { id: relation.from, path: `/semanticChecks/requiredPaths/${index}/from` }, + { id: relation.to, path: `/semanticChecks/requiredPaths/${index}/to` }, + ]), + ]; + for (const { id, path } of referencedNodes) { + if (nodeIds.has(id)) continue; + diagnostics.push(diagnostic( + 'workflow/semantic-node-reference', + `Workflow semantic contract references unknown node "${id}" at ${path}.`, + { node: id, path }, + { knownNodes: [...nodeIds] }, + [`replace "${id}" with an existing node id`, 'add the missing node before compiling'], + )); + } + if (diagnostics.length) return diagnostics; + + if (checks.allowedRoots !== undefined) { + const allowed = new Set(checks.allowedRoots); + for (const [node, count] of incoming) { + if (count > 0 || allowed.has(node)) continue; + diagnostics.push(diagnostic( + 'workflow/unexpected-root', + `Workflow node "${node}" has no incoming edge and is not declared in semanticChecks.allowedRoots.`, + { node, path: '/semanticChecks/allowedRoots' }, + { incomingEdges: 0, allowedRoots: [...allowed] }, + [`add the missing incoming edge to "${node}"`, `declare "${node}" in semanticChecks.allowedRoots if it is an intentional source`], + )); + } + } + + if (checks.allowedTerminals !== undefined) { + const allowed = new Set(checks.allowedTerminals); + for (const [node, count] of outgoing) { + if (count > 0 || allowed.has(node)) continue; + diagnostics.push(diagnostic( + 'workflow/unexpected-terminal', + `Workflow node "${node}" has no outgoing edge and is not declared in semanticChecks.allowedTerminals.`, + { node, path: '/semanticChecks/allowedTerminals' }, + { outgoingEdges: 0, allowedTerminals: [...allowed] }, + [`add the missing outgoing edge from "${node}"`, `declare "${node}" in semanticChecks.allowedTerminals if it is an intentional sink`], + )); + } + } + + const authoredEdges = new Set(asArray(workflow.edges).map((edge) => `${edge.from}\u0000${edge.to}`)); + for (const [index, relation] of asArray(checks.requiredEdges).entries()) { + if (authoredEdges.has(`${relation.from}\u0000${relation.to}`)) continue; + diagnostics.push(diagnostic( + 'workflow/required-edge', + `Workflow semantic contract requires edge "${relation.from}" -> "${relation.to}", but no authored edge matches it.`, + { from: relation.from, to: relation.to, path: `/semanticChecks/requiredEdges/${index}` }, + { authoredEdgeCount: asArray(workflow.edges).length }, + [`add an edge from "${relation.from}" to "${relation.to}" without deleting the semantic requirement`], + )); + } + + function reachable(from, to) { + const visited = new Set([from]); + const pending = [from]; + while (pending.length) { + const current = pending.shift(); + if (current === to) return true; + for (const next of adjacency.get(current) || []) { + if (visited.has(next)) continue; + visited.add(next); + pending.push(next); + } + } + return false; + } + + for (const [index, relation] of asArray(checks.requiredPaths).entries()) { + if (reachable(relation.from, relation.to)) continue; + diagnostics.push(diagnostic( + 'workflow/required-path', + `Workflow semantic contract requires a directed path from "${relation.from}" to "${relation.to}", but none exists.`, + { from: relation.from, to: relation.to, path: `/semanticChecks/requiredPaths/${index}` }, + { reachableNodes: [...new Set([relation.from, ...(adjacency.get(relation.from) || [])])] }, + [`restore a directed path from "${relation.from}" to "${relation.to}" without weakening the semantic requirement`], + )); + } + + return diagnostics; +} + +// Legend footprint and canvas width: one phase of the workflow compile, lifted +// out of compileWorkflowInternal so the caller reads as a sequence of steps. +function resolveWorkflowLegendFootprint(workflow, layout) { + const LEGEND_CATALOG = [ + 'frontend', + 'backend', + 'security', + 'messagebus', + 'database', + 'cloud', + 'external', + ].map((kind) => ({ kind, label: i18nText(workflow.meta.locale, `legend.workflow.${kind}`) })); + const presentLegendKinds = new Set(asArray(workflow.nodes).map((node) => node.type)); + const workflowLegendEntries = resolveLegend( + workflow.meta?.legend, + LEGEND_CATALOG, + presentLegendKinds, + ); + const legendFootprintOptions = { fontSize: 7, itemGap: 7 }; + const oneRowLegendFootprint = legendFootprint(workflowLegendEntries, { + ...legendFootprintOptions, + width: Number.MAX_SAFE_INTEGER, + }); + const minimumCanvasWidth = workflow.schema_version === 2 + ? Math.max(layout.defaultViewBoxWidth, oneRowLegendFootprint.minWidth + 40) + : layout.defaultViewBoxWidth; + const legendPackingWidth = Math.max( + 1, + (workflow.schema_version === 2 + ? minimumCanvasWidth + : (workflow.meta?.viewBox?.[0] ?? minimumCanvasWidth)) - 40, + ); + const packedLegendFootprint = legendFootprint(workflowLegendEntries, { + ...legendFootprintOptions, + width: legendPackingWidth, + }); + const legendExtraHeight = workflow.schema_version === 2 + ? packedLegendFootprint.extraHeight + : 0; + return { + workflowLegendEntries, + presentLegendKinds, + legendPackingWidth, + packedLegendFootprint, + legendExtraHeight, + minimumCanvasWidth, + }; +} + +// Lane geometry: the canvas height, the lane lookups and the small position +// helpers every later phase reads. Lifted out of compileWorkflowInternal so the +// body reads as phases instead of one script. +function createWorkflowLaneGeometry(workflow, layout, legendExtraHeight, minimumCanvasWidth) { + // Content is 680px wide (laneX + laneW); auto height fits the lanes plus legend. + const autoHeight = layout.laneY + + (layout.laneHeights?.reduce((total, height) => total + height, 0) + ?? (workflow.lanes?.length || 1) * layout.laneH) + + ((workflow.lanes?.length || 1) - 1) * layout.laneGap + + 124 + + legendExtraHeight; + const initialViewBox = workflow.meta?.viewBox || [minimumCanvasWidth, autoHeight]; + + + const laneIndex = new Map(asArray(workflow.lanes).map((lane, index) => [lane.id, index])); + const laneLabels = new Map(asArray(workflow.lanes).map((lane) => [lane.id, lane.label])); + + function nodeContext(node) { + const group = asArray(workflow.groups).find((candidate) => ( + candidate.lane === node.lane && node.col >= candidate.fromCol && node.col <= candidate.toCol + )); + const phase = asArray(workflow.phases).find((candidate) => ( + node.col >= candidate.fromCol && node.col <= candidate.toCol + )); + return [laneLabels.get(node.lane), group?.label, phase?.label].filter(Boolean).join(' › ') + || i18nText(workflow.meta.locale, 'node.context.workflow'); + } + + function laneHeight(idOrIndex) { + const index = typeof idOrIndex === 'number' ? idOrIndex : laneIndex.get(idOrIndex); + return layout.laneHeights?.[index] ?? layout.laneH; + } + + function laneGroupHeaderH(idOrIndex) { + const index = typeof idOrIndex === 'number' ? idOrIndex : laneIndex.get(idOrIndex); + return layout.groupHeaderHeights?.[index] ?? 0; + } + + function laneGroupFooterH(idOrIndex) { + const index = typeof idOrIndex === 'number' ? idOrIndex : laneIndex.get(idOrIndex); + return layout.groupFooterHeights?.[index] ?? 0; + } + + function laneTop(id) { + const index = laneIndex.get(id); + const precedingHeight = asArray(workflow.lanes).slice(0, index) + .reduce((total, _lane, lanePosition) => total + laneHeight(lanePosition), 0); + return layout.laneY + precedingHeight + index * layout.laneGap; + } + + function lastLaneBottom() { + return layout.laneY + + asArray(workflow.lanes).reduce((total, _lane, index) => total + laneHeight(index), 0) + + (workflow.lanes.length - 1) * layout.laneGap; + } + + function legendY() { + return lastLaneBottom() + 44 + legendExtraHeight; + } + + return { + autoHeight, + initialViewBox, + laneIndex, + laneLabels, + nodeContext, + laneHeight, + laneGroupHeaderH, + laneGroupFooterH, + laneTop, + lastLaneBottom, + legendY, + }; +} + +// Node measurement: the measured node map and the text-fit sizes every later +// phase reads. Kept together so the compiler body reads as phases. +function measureWorkflowNodes(workflow, layout, laneGeometry) { + const { laneHeight, laneGroupHeaderH, laneGroupFooterH, laneTop } = laneGeometry; + function measureNode(node) { + const width = node.width || layout.nodeW; + const height = node.height || (node.tag ? 68 : layout.nodeH); + const cx = layout.colXs[node.col]; + const groupHeaderH = laneGroupHeaderH(node.lane); + const contentH = laneHeight(node.lane) - layout.laneTitleH + - groupHeaderH - laneGroupFooterH(node.lane); + const y = laneTop(node.lane) + layout.laneTitleH + groupHeaderH + + (contentH - height) / 2 + (node.yOffset || 0); + return { + ...node, + width, + height, + x: cx - width / 2, + y, + cx, + cy: y + height / 2 + }; + } + + // Font sizes for this renderer's node text; the fitting geometry is shared. + const nodeTextFit = { + labelPreferred: 11, + labelMinimum: 9, + sublabelPreferred: 8, + sublabelMinimum: 6, + tagPreferred: 7, + tagMinimum: 6, + }; + + const nodes = new Map(asArray(workflow.nodes).map((node) => [node.id, measureNode(node)])); + + return { measureNode, nodeTextFit, nodes }; +} + +// Step indexes: where each edge and node sits on the main path. Lifted out so +// the compiler body keeps only the phases that read them. +function createWorkflowStepIndexes(workflow) { + const mainPathSteps = new Map(asArray(workflow.mainPath).map((id, index) => [id, index])); + const edgeSteps = new Map(asArray(workflow.edges).map((edge, index) => { + const fromStep = mainPathSteps.get(edge.from); + const toStep = mainPathSteps.get(edge.to); + const mainStep = Number.isInteger(fromStep) && toStep === fromStep + 1 ? fromStep : null; + return [edge, mainStep ?? asArray(workflow.mainPath).length + index]; + })); + + function nodeStep(node) { + return mainPathSteps.get(node.id) ?? asArray(workflow.mainPath).length + asArray(workflow.nodes).findIndex((item) => item.id === node.id); + } + + return { mainPathSteps, edgeSteps, nodeStep }; +} + +// Legacy capacity repair: the v1 column-capacity gate and the three fix +// verifiers it uses. They share the measured nodes, the layout and the fix +// acceptor, so they are built together and the body only calls the gate. +function createLegacyCapacityRepair({ + workflow, + nodes, + layout, + discoverFixes, + resolvedQualityProfile, + authoredQualityProfile, + nodeTextFit, + acceptsFix, +}) { + function verifiedLegacyAlternative(edge, from, to, requiredClearance) { + const occupied = [...nodes.values()].filter((node) => node.lane === to.lane && node.id !== to.id); + const candidates = layout.colXs.map((center, col) => ({ center, col })) + .filter(({ col }) => col !== to.col) + .sort((a, b) => Math.abs(a.col - to.col) - Math.abs(b.col - to.col) || a.col - b.col); + for (const candidate of candidates) { + const candidateRect = { ...to, col: candidate.col, cx: candidate.center, x: candidate.center - to.width / 2 }; + if (occupied.some((node) => rectsOverlap(candidateRect, node, 8))) continue; + const centerDistance = Math.abs(candidate.center - from.cx); + const signedClearance = centerDistance - from.width / 2 - to.width / 2; + if (signedClearance < requiredClearance) continue; + if (acceptsFix((document) => { + document.nodes.find((node) => node.id === to.id).col = candidate.col; + })) return candidate.col; + } + return null; + } + + function readableMigrationProvidesCapacity(from, to, requiredClearance) { + const readable = createReadableLayout({ ...workflow, schema_version: 2 }); + const centerDistance = Math.abs(readable.colXs[to.col] - readable.colXs[from.col]); + if (centerDistance - from.width / 2 - to.width / 2 < requiredClearance) return false; + if (!discoverFixes) return false; + + return withDiagnosticRecordingSuppressed(() => { + const migrationQualityProfile = authoredQualityProfile; + let planned = compileWorkflowWithFeedback({ + workflow: intrinsicWorkflow(workflow), + qualityProfile: migrationQualityProfile, + discoverFixes: false, + }); + if (!planned.ok) { + planned = compileWorkflowWithFeedback({ + workflow: planningWorkflow(workflow), + qualityProfile: migrationQualityProfile, + discoverFixes: false, + }); + } + if (!planned.ok || !Array.isArray(planned.receipt?.columns)) return false; + + let candidate; + try { + candidate = createMappedWorkflowCandidate( + workflow, + LEGACY_COLUMN_CENTERS, + planned.receipt.columns, + ).document; + } catch { + return false; + } + let compiled = compileWorkflowWithFeedback({ + workflow: candidate, + qualityProfile: migrationQualityProfile, + discoverFixes: false, + }); + const requiredViewBox = compiled.diagnostics?.length + && compiled.diagnostics.every(({ code }) => code === 'workflow/viewbox-capacity') + ? compiled.diagnostics.find(({ evidence }) => Array.isArray(evidence?.requiredViewBox)) + ?.evidence.requiredViewBox + : null; + if (!compiled.ok && Array.isArray(candidate.meta?.viewBox) && requiredViewBox) { + candidate.meta.viewBox = [ + Math.max(candidate.meta.viewBox[0], requiredViewBox[0]), + Math.max(candidate.meta.viewBox[1], requiredViewBox[1]), + ]; + compiled = compileWorkflowWithFeedback({ + workflow: candidate, + qualityProfile: migrationQualityProfile, + discoverFixes: false, + }); + } + return compiled.ok; + }); + } + + function verifiedReducedWidths(from, to, requiredClearance) { + const widthBudget = 2 * (Math.abs(to.cx - from.cx) - requiredClearance); + if (widthBudget < 64) return null; + const widths = [from.width, to.width]; + let excess = widths[0] + widths[1] - widthBudget; + for (const index of widths[0] >= widths[1] ? [0, 1] : [1, 0]) { + const reduction = Math.min(excess, widths[index] - 32); + widths[index] -= reduction; + excess -= reduction; + } + if (excess > 0.0001) return null; + const candidates = [from, to]; + const labelsFit = candidates.every((node, index) => ( + textUnits(node.label) * 6.8 <= widths[index] + 6 + && (!node.sublabel || minimumNodeTextWidth(node.sublabel, nodeTextFit.sublabelMinimum) <= availableNodeTextWidth(widths[index])) + && (!node.tag || minimumNodeTextWidth(node.tag, nodeTextFit.tagMinimum) <= availableNodeTextWidth(widths[index])) + )); + if (!labelsFit) return null; + const serializedWidths = widths.map((width) => Math.floor((width + 1e-9) * 100) / 100); + const signedClearance = Math.abs(to.cx - from.cx) + - serializedWidths[0] / 2 - serializedWidths[1] / 2; + if (signedClearance + 0.0001 < requiredClearance) return null; + const accepted = acceptsFix((document) => { + document.nodes.find((node) => node.id === from.id).width = serializedWidths[0]; + document.nodes.find((node) => node.id === to.id).width = serializedWidths[1]; + }); + return accepted ? serializedWidths : null; + } + + function enforceLegacyColumnCapacity() { + if (workflow.schema_version !== 1) return; + for (const edge of workflow.edges) { + const from = nodes.get(edge.from); + const to = nodes.get(edge.to); + if (!from || !to || from.lane !== to.lane || from.col === to.col) continue; + if (!verticalIntervalsOverlap(from, to, 8)) continue; + const centerDistance = Math.abs(to.cx - from.cx); + const actualSignedClearance = centerDistance - from.width / 2 - to.width / 2; + const direct = !edge.via && ['auto', 'straight'].includes(edge.route || 'auto') + && Math.abs(from.cy - to.cy) < 0.0001; + const requiredDirectClearance = direct ? 28 : 8; + if (actualSignedClearance >= requiredDirectClearance) continue; + const alternative = verifiedLegacyAlternative(edge, from, to, requiredDirectClearance); + const reducedWidths = verifiedReducedWidths(from, to, requiredDirectClearance); + const capacity = actualSignedClearance < 0 + ? `overlap by ${Math.abs(Math.round(actualSignedClearance))}px` + : `leave only ${Math.round(actualSignedClearance)}px of direct clearance`; + const message = `Workflow columns ${from.col}→${to.col} place nodes "${from.id}" and "${to.id}" so they ${capacity} under the fixed-v1 layout.`; + const supportedFixes = []; + if (readableMigrationProvidesCapacity(from, to, requiredDirectClearance)) { + supportedFixes.push('migrate this workflow to schema_version 2'); + } + if (alternative !== null) supportedFixes.push(`move node "${to.id}" to verified free column ${alternative}`); + if (reducedWidths) { + supportedFixes.push(`set node widths "${from.id}"=${Math.round(reducedWidths[0] * 100) / 100}px and "${to.id}"=${Math.round(reducedWidths[1] * 100) / 100}px`); + } + throwDiagnosticError(message, [{ + code: 'workflow/column-capacity', + severity: 'error', + message, + subject: { + diagramType: 'workflow', + edge: edge.id ?? null, + from: edge.from, + to: edge.to, + fromCol: from.col, + toCol: to.col, + }, + evidence: { + centerDistancePx: centerDistance, + nodeWidthsPx: [from.width, to.width], + actualSignedClearancePx: actualSignedClearance, + requiredDirectClearancePx: requiredDirectClearance, + }, + supportedFixes, + suppresses: [ + 'workflow/short-edge', + 'clean-flow/endpoint-side-direction', + 'workflow/label-node-overlap', + ], + }]); + } + } + return { enforceLegacyColumnCapacity }; +} + +// Automatic side selection: which sides an automatic one-bend route leaves and +// enters. Uses the shared side cache and the readable candidate set, so both are +// passed in rather than captured. +function createAutomaticSideSelection({ + workflow, + readableSideCache, + readableAutomaticCandidateSet, + compareCost, + oneBendCrossLaneVia, +}) { + function legacyAutomaticOneBendSides(edge, from, to) { + const automaticRoute = !edge.via && (!edge.route || edge.route === 'auto'); + const automaticFrom = !edge.fromSide || edge.fromSide === 'auto'; + const automaticTo = !edge.toSide || edge.toSide === 'auto'; + if (!automaticRoute || !automaticFrom || !automaticTo || from.lane === to.lane) return null; + if (from.cx === to.cx || from.cy === to.cy) return null; + const verticalFrom = to.cy < from.cy ? 'top' : 'bottom'; + const horizontalTo = to.cx < from.cx ? 'right' : 'left'; + const horizontalFrom = to.cx < from.cx ? 'left' : 'right'; + const verticalTo = to.cy < from.cy ? 'bottom' : 'top'; + const candidates = [ + { fromSide: verticalFrom, toSide: horizontalTo }, + { fromSide: horizontalFrom, toSide: verticalTo }, + ]; + + return candidates.find(({ fromSide, toSide }) => { + const start = anchor(from, fromSide); + const end = anchor(to, toSide); + return oneBendCrossLaneVia(edge, start, end, fromSide, toSide); + }) || null; + } + + function readableAutomaticSides(edge, from, to) { + const automaticRoute = !edge.via + && edge.channelX === undefined + && edge.channelY === undefined + && (!edge.route || edge.route === 'auto'); + const authoredFrom = edge.fromSide && edge.fromSide !== 'auto' ? edge.fromSide : null; + const authoredTo = edge.toSide && edge.toSide !== 'auto' ? edge.toSide : null; + if (!automaticRoute || (authoredFrom && authoredTo)) return null; + if (readableSideCache.has(edge)) return readableSideCache.get(edge); + + const preferred = []; + const legacyPreferred = legacyAutomaticOneBendSides(edge, from, to); + if (legacyPreferred) preferred.push(legacyPreferred); + preferred.push({ + fromSide: authoredFrom || defaultFromSide(from, to), + toSide: authoredTo || defaultToSide(from, to), + }); + const sideOrder = ['right', 'bottom', 'left', 'top']; + for (const fromSide of authoredFrom ? [authoredFrom] : sideOrder) { + for (const toSide of authoredTo ? [authoredTo] : sideOrder) { + preferred.push({ fromSide, toSide }); + } + } + + const seen = new Set(); + const sidePairs = []; + for (const candidate of preferred) { + if (authoredFrom && candidate.fromSide !== authoredFrom) continue; + if (authoredTo && candidate.toSide !== authoredTo) continue; + const key = `${candidate.fromSide}:${candidate.toSide}`; + if (seen.has(key)) continue; + seen.add(key); + sidePairs.push(candidate); + } + + const naturalFromSide = authoredFrom || defaultFromSide(from, to); + const naturalToSide = authoredTo || defaultToSide(from, to); + const planFor = (candidate, pairOrdinal) => { + const start = anchor(from, candidate.fromSide); + const end = anchor(to, candidate.toSide); + return { + start, + end, + planned: readableAutomaticCandidateSet( + edge, + from, + to, + start, + end, + candidate.fromSide, + candidate.toSide, + { + ordinalOffset: pairOrdinal * 9, + naturalFromSide, + naturalToSide, + }, + ), + }; + }; + + const primary = sidePairs[0]; + if (primary) { + const { planned } = planFor(primary, 0); + if (planned.candidates.length) { + readableSideCache.set(edge, primary); + return primary; + } + } + + const candidates = []; + for (const [pairOrdinal, candidate] of sidePairs.entries()) { + const { planned } = planFor(candidate, pairOrdinal); + candidates.push(...planned.candidates.map((route) => ({ ...route, ...candidate }))); + } + candidates.sort((left, right) => compareCost(left.cost, right.cost)); + if (candidates.length) { + const selected = { + fromSide: candidates[0].fromSide, + toSide: candidates[0].toSide, + }; + readableSideCache.set(edge, selected); + return selected; + } + readableSideCache.set(edge, null); + return null; + } + + function automaticOneBendSides(edge, from, to) { + return workflow.schema_version === 2 + ? readableAutomaticSides(edge, from, to) + : legacyAutomaticOneBendSides(edge, from, to); + } + return { automaticOneBendSides }; +} + +function compileWorkflowInternal({ + workflow: inputWorkflow, + qualityProfile, + sourceEvidence, + discoverFixes = true, + layoutFeedback = {}, +} = {}) { + if (!inputWorkflow || typeof inputWorkflow !== 'object' || Array.isArray(inputWorkflow)) { + const diagnostics = [{ + code: 'workflow/input-contract', + severity: 'error', + message: 'compileWorkflow requires one parsed workflow document object.', + subject: { diagramType: 'workflow', path: '/' }, + evidence: {}, + supportedFixes: [], + }]; + return compilerFailure('fixed-v1', diagnostics, diagnostics[0].message); + } + const resolvedQualityProfile = qualityProfile || inputWorkflow.meta?.quality_profile; + const authoredQualityProfile = inputWorkflow.meta?.quality_profile; + const qualityResolvedWorkflow = resolvedQualityProfile && resolvedQualityProfile !== inputWorkflow.meta?.quality_profile + ? { ...inputWorkflow, meta: { ...inputWorkflow.meta, quality_profile: resolvedQualityProfile } } + : inputWorkflow; + let inputDiagnostics = []; + try { + validateSchema('workflow', qualityResolvedWorkflow); + validateCrossCollectionContracts('workflow', qualityResolvedWorkflow); + } catch (error) { + inputDiagnostics = Array.isArray(error?.archifyDiagnostics) + ? error.archifyDiagnostics.map((diagnostic) => ({ + ...diagnostic, + supportedFixes: [], + })) + : [{ + code: 'workflow/input-contract', + severity: 'error', + message: 'Workflow schema validation failed unexpectedly.', + subject: { diagramType: 'workflow', path: '/' }, + evidence: { reason: error?.message || String(error) }, + supportedFixes: [], + }]; + } + if (inputDiagnostics.length) { + return compilerFailure( + inputWorkflow.schema_version === 2 ? 'readable-v2' : 'fixed-v1', + inputDiagnostics, + ); + } + const workflow = canonicalReadableWorkflow(qualityResolvedWorkflow); + const semanticDiagnostics = semanticContractDiagnostics(workflow); + if (semanticDiagnostics.length) { + return compilerFailure( + workflow.schema_version === 2 ? 'readable-v2' : 'fixed-v1', + semanticDiagnostics, + ); + } + const sourceIndexes = { + lanes: new Map(asArray(qualityResolvedWorkflow.lanes).map((lane, index) => [lane, index])), + nodes: new Map(asArray(qualityResolvedWorkflow.nodes).map((node, index) => [node, index])), + edges: new Map(asArray(qualityResolvedWorkflow.edges).map((edge, index) => [edge, index])), + }; + const layout = workflow.schema_version === 2 + ? createReadableLayout(workflow, layoutFeedback) + : createLegacyLayout(); + +const { + workflowLegendEntries, + presentLegendKinds, + legendPackingWidth, + packedLegendFootprint, + legendExtraHeight, + minimumCanvasWidth, +} = resolveWorkflowLegendFootprint(workflow, layout); + +const laneGeometry = createWorkflowLaneGeometry(workflow, layout, legendExtraHeight, minimumCanvasWidth); +const { + autoHeight, + laneIndex, + laneLabels, + nodeContext, + laneHeight, + laneGroupHeaderH, + laneGroupFooterH, + laneTop, + lastLaneBottom, + legendY, +} = laneGeometry; +let viewBox = laneGeometry.initialViewBox; +let requiredViewBox = [...viewBox]; + +function workflowLegendLayout(obstacles = []) { + return { + x: 20, + baselineY: legendY(), + width: workflow.schema_version === 2 ? legendPackingWidth : viewBox[0] - 40, + fontSize: 7, + itemGap: 7, + minTitleY: lastLaneBottom() + 8, + obstacles, + unfit: workflow.meta?.legend === undefined ? 'hide' : 'error', + diagramType: 'workflow', + }; +} + +function workflowLegendRects() { + if (!workflowLegendEntries.length) return []; + const measured = measureLegend(workflowLegendEntries, workflowLegendLayout()); + if (!measured) return []; + return [ + { kind: 'title', x: 20, y: measured.titleY - 10, width: 48, height: 14 }, + ...measured.entries.map((entry) => ({ + kind: entry.kind, + x: entry.x, + y: entry.baseline - 10, + width: entry.width, + height: 14, + })), + ]; +} + +const { measureNode, nodeTextFit, nodes } = measureWorkflowNodes(workflow, layout, laneGeometry); + +// Obstacles of the routing search are queried through a uniform grid instead of +// scanned: a candidate can only fail an obstacle its own box can reach. +const OBSTACLE_CELL = 160; + +const obstacleGrid = createSpatialGrid(OBSTACLE_CELL); +let rightmostNodeEdge = 0; +for (const node of nodes.values()) { + obstacleGrid.insert(rectToBounds(node), { kind: 'node', node, bounds: rectToBounds(node) }); + rightmostNodeEdge = Math.max(rightmostNodeEdge, node.x + node.width); +} +let rightmostRoutedEdge = Number.NEGATIVE_INFINITY; + +// Routed endpoints per node, so the port search reads only the routes that touch +// the node instead of walking every routed path. +const nodeRoutes = new Map(); + +function workflowCompositionFrames() { + const frames = []; + for (const [index, lane] of asArray(workflow.lanes).entries()) { + const y = laneTop(lane.id); + const height = laneHeight(index); + frames.push({ id: `lane-${index}`, label: lane.label, kind: 'lane', x: layout.laneX, y, width: layout.laneW, height, radius: 10 }); + if (lane.variant === 'exception') { + frames.push({ id: `lane-${index}-exception`, label: `${lane.label} exception`, kind: 'exception-lane', x: layout.laneX + 6, y: y + 6, width: layout.laneW - 12, height: height - 12, radius: 8 }); + } + } + for (const [index, group] of asArray(workflow.groups).entries()) { + const span = groupSpan(group); + frames.push({ + id: `group-${index}`, + label: group.label, + kind: 'group', + x: span.x, + y: laneTop(group.lane) + layout.laneTitleH + GROUP_FRAME_TOP_INSET, + width: span.width, + height: workflow.schema_version === 2 + ? laneHeight(group.lane) - layout.laneTitleH + - GROUP_FRAME_TOP_INSET - GROUP_FRAME_BOTTOM_INSET + : layout.laneH - layout.laneTitleH - 16, + radius: 9, + }); + } + return frames; +} + +function workflowSceneLabelObstacles() { + const obstacles = []; + for (const [index, lane] of asArray(workflow.lanes).entries()) { + const prefix = lane.variant === 'exception' ? 'EX' : String(index + 1).padStart(2, '0'); + const label = `${prefix} / ${lane.label}`; + obstacles.push({ + kind: 'lane-header', + id: lane.id, + x: layout.laneX + 14, + y: laneTop(lane.id) + 12, + width: textUnits(label) * 6.2, + height: 14, + }); + } + for (const phase of asArray(workflow.phases)) { + if (!Number.isInteger(phase.fromCol) || !Number.isInteger(phase.toCol) + || phase.fromCol < 0 || phase.toCol >= layout.colXs.length || phase.fromCol > phase.toCol) continue; + const span = phaseSpan(phase); + obstacles.push({ + kind: 'phase-header', + id: phase.id ?? null, + x: span.x, + y: 27, + width: span.width, + height: 16, + }); + } + for (const group of asArray(workflow.groups)) { + if (!laneIndex.has(group.lane) + || !Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol) + || group.fromCol < 0 || group.toCol >= layout.colXs.length || group.fromCol > group.toCol) continue; + const span = groupSpan(group); + const frameY = laneTop(group.lane) + layout.laneTitleH + GROUP_FRAME_TOP_INSET; + const labelBaseline = frameY + GROUP_LABEL_BASELINE_OFFSET; + obstacles.push({ + kind: 'group-label', + id: group.id ?? null, + x: span.x + 10, + y: labelBaseline - GROUP_LABEL_MASK_ASCENT, + width: textUnits(group.label) * 5.6, + height: GROUP_LABEL_MASK_H, + }); + } + return obstacles; +} + +const { mainPathSteps, edgeSteps, nodeStep } = createWorkflowStepIndexes(workflow); +const edgeIndexByEdge = new Map(asArray(workflow.edges).map((edge, index) => [edge, index])); + + function acceptsFix(mutator) { + if (!discoverFixes) return false; + const candidate = cloneWorkflow(workflow); + mutator(candidate); + return withDiagnosticRecordingSuppressed(() => compileWorkflowWithFeedback({ + workflow: candidate, + qualityProfile: resolvedQualityProfile, + discoverFixes: false, + }).ok); + } + +const { enforceLegacyColumnCapacity } = createLegacyCapacityRepair({ + workflow, + nodes, + layout, + discoverFixes, + resolvedQualityProfile, + authoredQualityProfile, + nodeTextFit, + acceptsFix, +}); + +function verifiedEdgeFix(edge, message, mutator) { + const edgeIndex = workflow.edges.indexOf(edge); + if (edgeIndex < 0) return null; + const accepted = acceptsFix((document) => mutator(document.edges[edgeIndex], document)); + return accepted ? message : null; +} + +function verifiedAutomaticRouteFix(edge, { clearSides = false } = {}) { + const edgeName = workflowEdgeName(edge); + return verifiedEdgeFix( + edge, + clearSides + ? `remove explicit route geometry and endpoint sides from edge "${edgeName}" so readable-v2 can use its verified automatic candidate` + : `remove explicit route geometry from edge "${edgeName}" so readable-v2 can use its verified automatic candidate`, + (candidate) => { + delete candidate.via; + delete candidate.channelX; + delete candidate.channelY; + delete candidate.route; + if (clearSides) { + delete candidate.fromSide; + delete candidate.toSide; + } + }, + ); +} + +function authoredPinEvidence(edge, field) { + const authoredEdgeIndex = sourceIndexes.edges.get(edge); + const value = Array.isArray(edge[field]) + ? edge[field].map((item) => (Array.isArray(item) ? [...item] : item)) + : edge[field]; + return { + edge: workflowEdgeName(edge), + field, + ...(Number.isInteger(authoredEdgeIndex) ? { path: `/edges/${authoredEdgeIndex}/${field}` } : {}), + value, + }; +} + +function combinations(values, size, start = 0, prefix = [], output = []) { + if (prefix.length === size) { + output.push([...prefix]); + return output; + } + for (let index = start; index <= values.length - (size - prefix.length); index += 1) { + prefix.push(values[index]); + combinations(values, size, index + 1, prefix, output); + prefix.pop(); + } + return output; +} + +function verifiedPinRemovalAlternatives(edge, fields, reason) { + if (!discoverFixes) return { removalSets: [], supportedFixes: [] }; + const edgeIndex = workflow.edges.indexOf(edge); + if (edgeIndex < 0) return { removalSets: [], supportedFixes: [] }; + const uniqueFields = [...new Set(fields.filter((field) => edge[field] !== undefined))]; + for (let size = 1; size <= uniqueFields.length; size += 1) { + const removalSets = combinations(uniqueFields, size).filter((fieldSet) => ( + acceptsFix((document) => { + for (const field of fieldSet) delete document.edges[edgeIndex][field]; + }) + )); + if (!removalSets.length) continue; + const edgeName = workflowEdgeName(edge); + return { + removalSets, + supportedFixes: removalSets.map((fieldSet) => ( + `remove ${fieldSet.join(' and ')} from edge "${edgeName}" ${reason}` + )), + }; + } + return { removalSets: [], supportedFixes: [] }; +} + +function conflictPinsFromRemovalSets(edge, removalSets, fallbackFields = []) { + const fields = removalSets.length + ? [...new Set(removalSets.flat())] + : [...new Set(fallbackFields)]; + return fields.map((field) => authoredPinEvidence(edge, field)); +} + +function authoredRouteAssertionFields(edge) { + return [ + ...(Array.isArray(edge?.via) ? ['via'] : []), + ...(edge?.channelX !== undefined ? ['channelX'] : []), + ...(edge?.channelY !== undefined ? ['channelY'] : []), + ...(edge?.route && edge.route !== 'auto' ? ['route'] : []), + ...(edge?.fromSide && edge.fromSide !== 'auto' ? ['fromSide'] : []), + ...(edge?.toSide && edge.toSide !== 'auto' ? ['toSide'] : []), + ]; +} + +function hasAuthoredRouteAssertions(edge) { + return authoredRouteAssertionFields(edge).length > 0; +} + +function verifiedPinReferenceAlternatives(candidateRefs, reason) { + const seenRefs = new Set(); + const refs = candidateRefs.filter(({ edge, edgeIndex, field }) => { + if (edgeIndex < 0 || edge?.[field] === undefined) return false; + const key = `${edgeIndex}:${field}`; + if (seenRefs.has(key)) return false; + seenRefs.add(key); + return true; + }); + const fallbackPins = refs.map(({ edge, field }) => authoredPinEvidence(edge, field)); + if (!discoverFixes) { + return { + removalSets: [], conflictingRefs: refs, conflictingPins: fallbackPins, repairs: [], supportedFixes: [], + }; + } + + for (let size = 1; size <= refs.length; size += 1) { + const removalSets = combinations(refs, size).filter((removalSet) => ( + acceptsFix((document) => { + for (const { edgeIndex, field } of removalSet) delete document.edges[edgeIndex][field]; + }) + )); + if (!removalSets.length) continue; + const conflictingRefs = []; + const conflictingPins = []; + const seenPins = new Set(); + for (const removalSet of removalSets) { + for (const { edge, field } of removalSet) { + const key = `${workflow.edges.indexOf(edge)}:${field}`; + if (seenPins.has(key)) continue; + seenPins.add(key); + conflictingRefs.push({ edge, edgeIndex: workflow.edges.indexOf(edge), field }); + conflictingPins.push(authoredPinEvidence(edge, field)); + } + } + const repairs = removalSets.map((removalSet) => { + const grouped = []; + for (const ref of removalSet) { + let group = grouped.find(({ edge }) => edge === ref.edge); + if (!group) { + group = { edge: ref.edge, fields: [] }; + grouped.push(group); + } + group.fields.push(ref.field); + } + const removals = grouped.map(({ edge, fields }) => ( + `remove ${fields.join(' and ')} from edge "${workflowEdgeName(edge)}"` + )); + return { removalSet, message: `${removals.join(' and ')} ${reason}` }; + }); + return { + removalSets, + conflictingRefs, + conflictingPins, + repairs, + supportedFixes: repairs.map(({ message }) => message), + }; + } + return { + removalSets: [], conflictingRefs: refs, conflictingPins: fallbackPins, repairs: [], supportedFixes: [], + }; +} + +function verifiedRoutePairPinAlternatives(leftEdge, rightEdge, reason) { + const refs = [leftEdge, rightEdge].flatMap((edge) => { + const edgeIndex = workflow.edges.indexOf(edge); + return authoredRouteAssertionFields(edge).map((field) => ({ edge, edgeIndex, field })); + }); + return verifiedPinReferenceAlternatives(refs, reason); +} + +function verifiedLabelRoutePinAlternatives(labelEdge, routeEdge) { + const refs = []; + const labelEdgeIndex = workflow.edges.indexOf(labelEdge); + if (Array.isArray(labelEdge?.labelAt)) { + refs.push({ edge: labelEdge, edgeIndex: labelEdgeIndex, field: 'labelAt' }); + } + const routeEdgeIndex = workflow.edges.indexOf(routeEdge); + for (const field of authoredRouteAssertionFields(routeEdge)) { + refs.push({ edge: routeEdge, edgeIndex: routeEdgeIndex, field }); + } + return verifiedPinReferenceAlternatives( + refs, + Array.isArray(labelEdge?.labelAt) + ? 'so readable-v2 can replan the remaining authored label-route pins' + : 'so readable-v2 can replan the remaining authored route assertions', + ); +} + +function verifiedLabelPairPinAlternatives(leftEdge, rightEdge) { + return verifiedPinReferenceAlternatives( + [leftEdge, rightEdge].flatMap((edge) => ( + Array.isArray(edge?.labelAt) + ? [{ edge, edgeIndex: workflow.edges.indexOf(edge), field: 'labelAt' }] + : [] + )), + 'so readable-v2 can replan the remaining authored label pins', + ); +} + +function verifiedRepairsWithLabelNudges(alternatives) { + return alternatives.repairs.flatMap(({ removalSet, message }) => { + if (removalSet.length !== 1 || removalSet[0].field !== 'labelAt') return [message]; + const nudges = verifiedLabelAtAlternatives(removalSet[0].edge); + return nudges.length ? nudges : [message]; + }); +} + +function throwExplicitPinConflict(edge, invariant, evidence, supportedFixes = []) { + const message = `Workflow edge "${workflowEdgeName(edge)}" has explicit geometry that violates ${invariant}.`; + const [onlyPin] = asArray(evidence?.conflictingPins); + const authoredEdgeIndex = sourceIndexes.edges.get(edge); + const pinPath = asArray(evidence?.conflictingPins).length === 1 + && Number.isInteger(authoredEdgeIndex) + && onlyPin?.field + ? onlyPin.path || `/edges/${authoredEdgeIndex}/${onlyPin.field}` + : null; + throwDiagnosticError(message, [{ + code: 'workflow/explicit-pin-conflict', + severity: 'error', + message, + subject: { + diagramType: 'workflow', + edge: edge.id ?? null, + from: edge.from, + to: edge.to, + ...(pinPath ? { path: pinPath } : {}), + }, + evidence: { invariant, ...evidence }, + supportedFixes: supportedFixes.filter(Boolean), + }]); +} + +function hasAbsoluteRoutePins(edge) { + return Array.isArray(edge?.via) + || edge?.channelX !== undefined + || edge?.channelY !== undefined; +} + +function presentRouteGeometryFields(edge) { + return [ + ...(Array.isArray(edge?.via) ? ['via'] : []), + ...(edge?.channelX !== undefined ? ['channelX'] : []), + ...(edge?.channelY !== undefined ? ['channelY'] : []), + ]; +} + +function verifiedRouteGeometryPinAlternatives( + edge, + reason = 'so readable-v2 can replan the remaining explicit route assertions', +) { + const edgeIndex = workflow.edges.indexOf(edge); + return verifiedPinReferenceAlternatives( + authoredRouteAssertionFields(edge).map((field) => ({ edge, edgeIndex, field })), + reason, + ); +} + +function properOrthogonalIntersection(leftStart, leftEnd, rightStart, rightEnd) { + const leftOrientation = segmentOrientation(leftStart, leftEnd); + const rightOrientation = segmentOrientation(rightStart, rightEnd); + if (leftOrientation === rightOrientation + || leftOrientation === 'diagonal' + || rightOrientation === 'diagonal') return null; + const horizontalStart = leftOrientation === 'horizontal' ? leftStart : rightStart; + const horizontalEnd = leftOrientation === 'horizontal' ? leftEnd : rightEnd; + const verticalStart = leftOrientation === 'vertical' ? leftStart : rightStart; + const verticalEnd = leftOrientation === 'vertical' ? leftEnd : rightEnd; + const point = [verticalStart[0], horizontalStart[1]]; + const epsilon = 0.0001; + const insideHorizontal = point[0] > Math.min(horizontalStart[0], horizontalEnd[0]) + epsilon + && point[0] < Math.max(horizontalStart[0], horizontalEnd[0]) - epsilon; + const insideVertical = point[1] > Math.min(verticalStart[1], verticalEnd[1]) + epsilon + && point[1] < Math.max(verticalStart[1], verticalEnd[1]) - epsilon; + return insideHorizontal && insideVertical ? point : null; +} + +function verifiedLabelAtAlternatives(edge) { + if (!Array.isArray(edge.labelAt)) return []; + const [x, y] = edge.labelAt; + return [ + [0, 24], [0, -24], [24, 0], [-24, 0], + [0, 48], [0, -48], [48, 0], [-48, 0], + ].map(([dx, dy]) => { + const next = [x + dx, y + dy]; + return verifiedEdgeFix( + edge, + `set labelAt on edge "${workflowEdgeName(edge)}" to [${next[0]}, ${next[1]}]`, + (candidate) => { candidate.labelAt = next; }, + ); + }).filter(Boolean); +} + +function verifiedLabelAtNudge(edge) { + const [alternative] = verifiedLabelAtAlternatives(edge); + if (alternative) return alternative; + return verifiedEdgeFix( + edge, + `remove labelAt from edge "${workflowEdgeName(edge)}" so readable-v2 can use verified automatic label placement`, + (candidate) => { delete candidate.labelAt; }, + ); +} + +function throwReadableLabelRoutePinConflict(hit, routePoints = null) { + const labelEdge = hit.labelRelation; + const routeEdge = hit.otherRelation; + const labelPinned = Array.isArray(labelEdge?.labelAt); + const routePinned = hasAuthoredRouteAssertions(routeEdge); + if (!labelPinned && !routePinned) return false; + const alternatives = verifiedLabelRoutePinAlternatives(labelEdge, routeEdge); + const actualRoutePoints = routePoints + || pathCache.get(routeEdge)?.points + || pathFor(routeEdge).points; + const diagnosticEdge = alternatives.conflictingRefs[0]?.edge + || (labelPinned ? labelEdge : routeEdge); + throwExplicitPinConflict(diagnosticEdge, 'explicit label-route clearance', { + conflictingPins: alternatives.conflictingPins, + ...(labelPinned ? { labelAt: [...labelEdge.labelAt] } : {}), + labelRect: { + x: hit.rect.x, + y: hit.rect.y, + width: hit.rect.width, + height: hit.rect.height, + }, + collidedRoute: { + edge: routeEdge.id || `${routeEdge.from}->${routeEdge.to}`, + from: routeEdge.from, + to: routeEdge.to, + points: actualRoutePoints.map((point) => [...point]), + }, + routeSegmentIndex: hit.segmentIndex, + routeSegment: { from: [...hit.start], to: [...hit.end] }, + clearancePx: Math.round(hit.clearance * 10) / 10, + minimumPx: hit.threshold, + }, [ + ...verifiedRepairsWithLabelNudges(alternatives), + ]); + return true; +} + +function throwReadableLabelLabelPinConflict(left, right) { + const leftEdge = left.relation; + const rightEdge = right.relation; + const pinnedEdges = [leftEdge, rightEdge].filter((edge) => Array.isArray(edge?.labelAt)); + if (!pinnedEdges.length) return false; + const alternatives = verifiedLabelPairPinAlternatives(leftEdge, rightEdge); + const causalLabelEdges = [...new Set(alternatives.conflictingRefs.map(({ edge }) => edge))]; + const diagnosticEdge = causalLabelEdges[0] || pinnedEdges[0]; + throwExplicitPinConflict(diagnosticEdge, 'explicit label-label clearance', { + conflictingPins: alternatives.conflictingPins, + labelRects: [left, right].map((rect) => ({ + edge: rect.relation.id ?? null, + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + })), + minimumGapPx: -2, + }, verifiedRepairsWithLabelNudges(alternatives)); + return true; +} + +function classifyFailedAutomaticCandidatePins(edge, rawCandidates) { + const relationIndex = workflow.edges.indexOf(edge); + const priorRoutes = [...pathCache.entries()] + .filter(([otherEdge]) => otherEdge !== edge) + .map(([relation, routed]) => ({ + relation, + relationIndex: workflow.edges.indexOf(relation), + points: routed.points, + })); + if (!priorRoutes.length) return; + const priorLabels = priorRoutes.map(({ relation, relationIndex }) => ( + labelRectFor(relation, relationIndex) + )).filter(Boolean); + + for (const { points } of rawCandidates) { + const candidateRect = candidateLabelRect(edge, points); + const candidateLabel = candidateRect + ? { ...candidateRect, relation: edge, relationIndex, label: edge.label } + : null; + if (candidateLabel) { + const priorLabel = priorLabels.find((otherLabel) => ( + rectsOverlap(candidateLabel, otherLabel, -2) + && (Array.isArray(edge.labelAt) || Array.isArray(otherLabel.relation?.labelAt)) + )); + if (priorLabel) throwReadableLabelLabelPinConflict(candidateLabel, priorLabel); + + const labelRouteHit = collectLabelRouteClearance({ + labels: [candidateLabel], + routedRelations: priorRoutes, + threshold: 4, + }).find((hit) => ( + Array.isArray(edge.labelAt) || hasAbsoluteRoutePins(hit.otherRelation) + )); + if (labelRouteHit) { + const collidedRoute = priorRoutes.find(({ relation }) => relation === labelRouteHit.otherRelation); + throwReadableLabelRoutePinConflict(labelRouteHit, collidedRoute?.points); + } + } + + const reverseHit = collectLabelRouteClearance({ + labels: priorLabels, + routedRelations: [{ relation: edge, relationIndex, points }], + threshold: 4, + }).find((hit) => Array.isArray(hit.labelRelation?.labelAt)); + if (reverseHit) throwReadableLabelRoutePinConflict(reverseHit, points); + } +} + +function validateReadablePairwisePinConflicts() { + const labels = workflow.edges.map((edge, relationIndex) => ( + labelRectFor(edge, relationIndex) + )).filter(Boolean); + const routedRelations = workflow.edges.map((edge, relationIndex) => ( + nodes.has(edge.from) && nodes.has(edge.to) + ? { relation: edge, relationIndex, points: pathFor(edge).points } + : null + )).filter(Boolean); + + const labelRouteHit = collectLabelRouteClearance({ + labels, + routedRelations, + threshold: 4, + }).find((hit) => ( + Array.isArray(hit.labelRelation?.labelAt) || hasAuthoredRouteAssertions(hit.otherRelation) + )); + if (labelRouteHit) throwReadableLabelRoutePinConflict(labelRouteHit); + + for (let leftIndex = 0; leftIndex < labels.length; leftIndex += 1) { + for (let rightIndex = leftIndex + 1; rightIndex < labels.length; rightIndex += 1) { + const left = labels[leftIndex]; + const right = labels[rightIndex]; + if (!rectsOverlap(left, right, -2)) continue; + throwReadableLabelLabelPinConflict(left, right); + } + } + + const requestedProfile = workflow.meta?.quality_profile; + if (requestedProfile !== 'showcase') return; + for (let leftIndex = 0; leftIndex < routedRelations.length; leftIndex += 1) { + const left = routedRelations[leftIndex]; + for (let rightIndex = leftIndex + 1; rightIndex < routedRelations.length; rightIndex += 1) { + const right = routedRelations[rightIndex]; + const leftPinned = hasAuthoredRouteAssertions(left.relation); + const rightPinned = hasAuthoredRouteAssertions(right.relation); + if (!leftPinned && !rightPinned) continue; + if ([left.relation.from, left.relation.to].some((id) => ( + id === right.relation.from || id === right.relation.to + ))) continue; + const leftAnalysis = forwardCollinearAnalysisSegments(left.points); + const rightAnalysis = forwardCollinearAnalysisSegments(right.points); + for (const leftSegment of leftAnalysis) { + for (const rightSegment of rightAnalysis) { + const point = properOrthogonalIntersection( + leftSegment.start, + leftSegment.end, + rightSegment.start, + rightSegment.end, + ); + if (!point) continue; + const leftSourceIndex = sourceSegmentIndexAtPoint(leftSegment, point); + const rightSourceIndex = sourceSegmentIndexAtPoint(rightSegment, point); + const leftSource = { + from: left.points[leftSourceIndex], + to: left.points[leftSourceIndex + 1], + }; + const rightSource = { + from: right.points[rightSourceIndex], + to: right.points[rightSourceIndex + 1], + }; + const alternatives = verifiedRoutePairPinAlternatives( + left.relation, + right.relation, + 'so readable-v2 can replan the remaining authored route assertions', + ); + const diagnosticEdge = leftPinned ? left.relation : right.relation; + throwExplicitPinConflict(diagnosticEdge, 'explicit route-route crossing', { + conflictingPins: alternatives.conflictingPins, + point, + segmentIndex: leftSourceIndex, + otherSegmentIndex: rightSourceIndex, + routeSegments: [ + { edge: left.relation.id ?? null, from: [...leftSegment.start], to: [...leftSegment.end] }, + { edge: right.relation.id ?? null, from: [...rightSegment.start], to: [...rightSegment.end] }, + ], + sourceRouteSegments: [ + { edge: left.relation.id ?? null, from: [...leftSource.from], to: [...leftSource.to] }, + { edge: right.relation.id ?? null, from: [...rightSource.from], to: [...rightSource.to] }, + ], + }, alternatives.supportedFixes); + } + } + } + } + + const corridorHit = collectAmbiguousCorridors({ + routedRelations, + minOverlapPx: 8, + }).find((hit) => ( + hasAuthoredRouteAssertions(hit.left.relation) + || hasAuthoredRouteAssertions(hit.right.relation) + )); + if (corridorHit) { + const leftSegment = { + from: corridorHit.left.points[corridorHit.leftSegment], + to: corridorHit.left.points[corridorHit.leftSegment + 1], + }; + const rightSegment = { + from: corridorHit.right.points[corridorHit.rightSegment], + to: corridorHit.right.points[corridorHit.rightSegment + 1], + }; + const leftPinned = hasAuthoredRouteAssertions(corridorHit.left.relation); + const alternatives = verifiedRoutePairPinAlternatives( + corridorHit.left.relation, + corridorHit.right.relation, + 'so readable-v2 can replan the remaining authored route assertions', + ); + const diagnosticEdge = leftPinned ? corridorHit.left.relation : corridorHit.right.relation; + throwExplicitPinConflict(diagnosticEdge, 'explicit route-route corridor clearance', { + conflictingPins: alternatives.conflictingPins, + segmentIndex: corridorHit.leftSegment, + otherSegmentIndex: corridorHit.rightSegment, + routeSegments: [ + { + edge: corridorHit.left.relation.id ?? null, + from: [...leftSegment.from], + to: [...leftSegment.to], + }, + { + edge: corridorHit.right.relation.id ?? null, + from: [...rightSegment.from], + to: [...rightSegment.to], + }, + ], + overlapStart: [...corridorHit.overlapStart], + overlapEnd: [...corridorHit.overlapEnd], + overlapLengthPx: corridorHit.overlapLength, + minimumClearancePx: 8, + }, alternatives.supportedFixes); + } +} + +const READABLE_PRESET_PIN_FIELDS = Object.freeze({ + straight: [], + drop: ['channelY'], + 'outside-right': ['channelX'], + 'return-left': ['channelX'], + 'bottom-channel': ['channelY'], + 'up-channel': ['channelY'], +}); + +function presentChannelPins(edge) { + return ['channelX', 'channelY'].filter((field) => edge[field] !== undefined); +} + +function validateReadableRouteControls(edge) { + const channelPins = presentChannelPins(edge); + const preset = edge.route || 'auto'; + if (preset === 'auto') return; + const allowedPins = new Set(READABLE_PRESET_PIN_FIELDS[preset] || []); + const conflictingPins = channelPins.filter((field) => !allowedPins.has(field)); + if (!conflictingPins.length) return; + const edgeIndex = workflow.edges.indexOf(edge); + const alternatives = verifiedPinReferenceAlternatives([ + { edge, edgeIndex, field: 'route' }, + ...conflictingPins.map((field) => ({ edge, edgeIndex, field })), + ], 'and keep the remaining verified route assertions'); + throwExplicitPinConflict(edge, 'route preset compatibility', { + route: preset, + allowedPins: [...allowedPins], + conflictingPins: alternatives.conflictingPins, + }, alternatives.supportedFixes); +} + +function segmentOrientation(start, end) { + if (Math.abs(start[0] - end[0]) <= 0.0001) return 'vertical'; + if (Math.abs(start[1] - end[1]) <= 0.0001) return 'horizontal'; + return 'diagonal'; +} + +function routeSegments(points) { + return points.slice(0, -1).map((start, index) => ({ + start, + end: points[index + 1], + orientation: segmentOrientation(start, points[index + 1]), + })); +} + +function endpointSideIsHonored(points, side, endpoint) { + if (!side || side === 'auto' || points.length < 2) return true; + const source = endpoint === 'source'; + const from = source ? points[0] : points.at(-2); + const to = source ? points[1] : points.at(-1); + const dx = to[0] - from[0]; + const dy = to[1] - from[1]; + if (source) { + if (side === 'right') return dx > 0 && Math.abs(dy) <= 0.0001; + if (side === 'left') return dx < 0 && Math.abs(dy) <= 0.0001; + if (side === 'bottom') return dy > 0 && Math.abs(dx) <= 0.0001; + if (side === 'top') return dy < 0 && Math.abs(dx) <= 0.0001; + return false; + } + if (side === 'right') return dx < 0 && Math.abs(dy) <= 0.0001; + if (side === 'left') return dx > 0 && Math.abs(dy) <= 0.0001; + if (side === 'bottom') return dy < 0 && Math.abs(dx) <= 0.0001; + if (side === 'top') return dy > 0 && Math.abs(dx) <= 0.0001; + return false; +} + +function corridorTopologyMatches(points, axis, coordinate) { + const collapsed = normalizeRoutePoints(points.map((point) => [...point])); + const start = collapsed[0]; + const end = collapsed.at(-1); + const via = axis === 'x' + ? [[coordinate, start[1]], [coordinate, end[1]]] + : [[start[0], coordinate], [end[0], coordinate]]; + const expected = joinRoutePoints(start, via, end); + const actualPattern = routeSegments(collapsed).map(({ orientation }) => orientation); + const expectedPattern = routeSegments(expected).map(({ orientation }) => orientation); + return actualPattern.length === expectedPattern.length + && actualPattern.every((orientation, index) => orientation === expectedPattern[index]) + && routeContainsChannelPin( + collapsed, + axis === 'x' ? 'channelX' : 'channelY', + coordinate, + ); +} + +function routeMatchesPresetFamily(preset, points, from, to) { + const collapsed = normalizeRoutePoints(points.map((point) => [...point])); + const segments = routeSegments(collapsed); + if (preset === 'straight') return collapsed.length === 2; + if (preset === 'drop') { + if (from.lane === to.lane) return false; + if (collapsed.length === 2 && segments[0]?.orientation === 'vertical') return true; + const upper = from.cy <= to.cy ? from : to; + const lower = upper === from ? to : from; + return segments.some(({ start, orientation }) => ( + orientation === 'horizontal' + && start[1] >= upper.y + upper.height - 0.0001 + && start[1] <= lower.y + 0.0001 + && corridorTopologyMatches(points, 'y', start[1]) + )); + } + if (preset === 'outside-right' || preset === 'return-left') { + const boundary = preset === 'outside-right' + ? Math.max(from.x + from.width, to.x + to.width) + : Math.min(from.x, to.x); + return segments.some(({ start, orientation }) => ( + orientation === 'vertical' + && (preset === 'outside-right' + ? start[0] > boundary + 0.0001 + : start[0] < boundary - 0.0001) + && corridorTopologyMatches(points, 'x', start[0]) + )); + } + if (preset === 'bottom-channel' || preset === 'up-channel') { + const boundary = preset === 'bottom-channel' + ? Math.max(from.y + from.height, to.y + to.height) + : Math.min(from.y, to.y); + return segments.some(({ start, orientation }) => ( + orientation === 'horizontal' + && (preset === 'bottom-channel' + ? start[1] > boundary + 0.0001 + : start[1] < boundary - 0.0001) + && corridorTopologyMatches(points, 'y', start[1]) + )); + } + return false; +} + +function routeContainsChannelPin(points, field, value) { + return points.slice(0, -1).some((start, index) => { + const end = points[index + 1]; + if (field === 'channelX') { + return start[0] === value + && end[0] === value + && Math.abs(end[1] - start[1]) > 0.0001; + } + return start[1] === value + && end[1] === value + && Math.abs(end[0] - start[0]) > 0.0001; + }); +} + +function validateReadablePinnedGeometry() { + if (workflow.schema_version !== 2) return; + // Reserve absolute geometry at contested nodes before automatic routing, + // independently of IDs. Unrelated routes retain their diagnostic ordering. + for (const edge of workflow.edges) { + if (!hasAbsoluteRoutePins(edge) || !nodes.has(edge.from) || !nodes.has(edge.to)) continue; + if (!workflow.edges.some(other => !hasAbsoluteRoutePins(other) + && [edge.from, edge.to].some(id => id === other.from || id === other.to))) continue; + validateReadableRouteControls(edge); + pathFor(edge); + } + for (const edge of workflow.edges) { + if (!nodes.has(edge.from) || !nodes.has(edge.to)) continue; + validateReadableRouteControls(edge); + const edgeName = workflowEdgeName(edge); + const edgeIndex = sourceIndexes.edges.get(edge); + if (Array.isArray(edge.labelAt)) { + const rect = labelRectFor(edge, workflow.edges.indexOf(edge)); + if (rect && (rect.x < 0 || rect.y < 0)) { + throwExplicitPinConflict(edge, 'viewBox-origin containment', { + conflictingPins: [{ + edge: edgeName, + field: 'labelAt', + path: `/edges/${edgeIndex}/labelAt`, + value: [...edge.labelAt], + }], + offendingRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + minimumCoordinate: 0, + }, [verifiedLabelAtNudge(edge)]); + } + } + const negativeViaIndex = asArray(edge.via).findIndex(([x, y]) => x < 0 || y < 0); + const negativeRoutePin = negativeViaIndex >= 0 + ? { + field: 'via', + path: `/edges/${edgeIndex}/via/${negativeViaIndex}`, + value: [...edge.via[negativeViaIndex]], + } + : edge.channelX < 0 + ? { field: 'channelX', path: `/edges/${edgeIndex}/channelX`, value: edge.channelX } + : edge.channelY < 0 + ? { field: 'channelY', path: `/edges/${edgeIndex}/channelY`, value: edge.channelY } + : null; + if (negativeRoutePin) { + throwExplicitPinConflict(edge, 'viewBox-origin containment', { + conflictingPins: [{ edge: edgeName, ...negativeRoutePin }], + minimumCoordinate: 0, + }, [ + verifiedAutomaticRouteFix(edge), + verifiedAutomaticRouteFix(edge, { clearSides: true }), + ]); + } + const hasPinnedRoute = Array.isArray(edge.via) + || edge.channelX !== undefined + || edge.channelY !== undefined; + const points = pathFor(edge).points; + if (hasPinnedRoute) { + const invalidPointIndex = points.findIndex((point) => ( + !Array.isArray(point) || point.length !== 2 || !isFinitePoint(...point) + )); + if (invalidPointIndex !== -1) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'finite route coordinates', { + conflictingPins: alternatives.conflictingPins, + pointIndex: invalidPointIndex, + point: points[invalidPointIndex], + }, alternatives.supportedFixes); + } + for (let segmentIndex = 0; segmentIndex < points.length - 1; segmentIndex += 1) { + const start = points[segmentIndex]; + const end = points[segmentIndex + 1]; + const dx = Math.abs(end[0] - start[0]); + const dy = Math.abs(end[1] - start[1]); + if (dx <= 0.0001 && dy <= 0.0001) { + const duplicateFix = Array.isArray(edge.via) && edge.via.length + ? verifiedEdgeFix( + edge, + `remove duplicate via[${Math.min(segmentIndex, edge.via.length - 1)}] and keep the remaining authored pins unchanged`, + (candidate) => candidate.via.splice(Math.min(segmentIndex, candidate.via.length - 1), 1), + ) + : verifiedAutomaticRouteFix(edge); + const alternatives = Array.isArray(edge.via) + ? null + : verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'non-zero route segments', { + conflictingPins: alternatives?.conflictingPins + || [authoredPinEvidence(edge, 'via')], + segmentIndex, + from: start, + to: end, + }, alternatives?.supportedFixes || [duplicateFix]); + } + if (dx > 0.0001 && dy > 0.0001) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'orthogonal route segments', { + conflictingPins: alternatives.conflictingPins, + segmentIndex, + from: start, + to: end, + }, alternatives.supportedFixes); + } + const endpoint = segmentIndex === 0 || segmentIndex === points.length - 2; + const minimumPx = points.length === 2 ? 28 : endpoint ? 8 : 16; + const lengthPx = dx + dy; + if (lengthPx + 0.0001 < minimumPx) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, endpoint ? '8px endpoint stub clearance' : '16px interior turn clearance', { + conflictingPins: alternatives.conflictingPins, + segmentIndex, + position: segmentIndex === 0 ? 'source-stub' : segmentIndex === points.length - 2 ? 'target-stub' : 'interior', + from: start, + to: end, + lengthPx, + minimumPx, + }, alternatives.supportedFixes); + } + } + const { fromSide, toSide } = edgeSides(edge); + if (Array.isArray(edge.via)) { + const missingChannelPins = presentChannelPins(edge).filter((field) => ( + !routeContainsChannelPin(points, field, edge[field]) + )); + if (missingChannelPins.length) { + const candidateFields = ['via', ...missingChannelPins]; + const alternatives = verifiedPinRemovalAlternatives( + edge, + candidateFields, + 'and replan the remaining explicit route assertions', + ); + throwExplicitPinConflict(edge, 'channel pin preservation', { + route: edge.route || 'auto', + conflictingPins: conflictPinsFromRemovalSets( + edge, + alternatives.removalSets, + candidateFields, + ), + points: points.map((point) => [...point]), + }, alternatives.supportedFixes); + } + } + if (edge.route + && edge.route !== 'auto' + && !routeMatchesPresetFamily( + edge.route, + points, + nodes.get(edge.from), + nodes.get(edge.to), + )) { + const authoredEdgeIndex = workflow.edges.indexOf(edge); + const alternatives = verifiedPinReferenceAlternatives([ + { edge, edgeIndex: authoredEdgeIndex, field: 'route' }, + ...presentRouteGeometryFields(edge) + .map((field) => ({ edge, edgeIndex: authoredEdgeIndex, field })), + ], 'and keep the remaining verified route assertions'); + throwExplicitPinConflict(edge, 'route preset compatibility', { + route: edge.route, + conflictingPins: alternatives.conflictingPins, + points: points.map((point) => [...point]), + }, alternatives.supportedFixes); + } + if (!routeHonorsEndpointSides(points, fromSide, toSide)) { + const mismatchedSideFields = [ + ...(edge.fromSide && edge.fromSide !== 'auto' + && !endpointSideIsHonored(points, fromSide, 'source') ? ['fromSide'] : []), + ...(edge.toSide && edge.toSide !== 'auto' + && !endpointSideIsHonored(points, toSide, 'target') ? ['toSide'] : []), + ]; + const candidateFields = [ + ...mismatchedSideFields, + ...presentRouteGeometryFields(edge), + ]; + const alternatives = verifiedPinRemovalAlternatives( + edge, + candidateFields, + 'and replan the remaining explicit pins', + ); + throwExplicitPinConflict(edge, 'perpendicular endpoint-side direction', { + conflictingPins: conflictPinsFromRemovalSets( + edge, + alternatives.removalSets, + candidateFields, + ), + points: points.map((point) => [...point]), + fromSide, + toSide, + }, alternatives.supportedFixes); + } + const nodeCollision = firstRouteNodeCollision(edge, points); + if (nodeCollision) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'node clearance', { + conflictingPins: alternatives.conflictingPins, + ...nodeCollision, + }, alternatives.supportedFixes); + } + const legendObstacle = workflowLegendRects().find((rect) => points.slice(0, -1).some((point, index) => ( + segmentIntersectsRect({ start: point, end: points[index + 1] }, rect) + ))); + if (legendObstacle) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'legend clearance', { + conflictingPins: alternatives.conflictingPins, + points: points.map((point) => [...point]), + legendObstacle, + }, alternatives.supportedFixes); + } + const compositionObstacle = workflowSceneLabelObstacles().find((rect) => ( + points.slice(0, -1).some((point, index) => ( + segmentIntersectsRect({ start: point, end: points[index + 1] }, rect) + )) + )); + if (compositionObstacle) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'lane/phase/group label clearance', { + conflictingPins: alternatives.conflictingPins, + points: points.map((point) => [...point]), + compositionObstacle, + }, alternatives.supportedFixes); + } + const [frameRun] = collectBorderRuns({ + routedRelations: [{ points }], + frames: workflowCompositionFrames(), + }); + if (frameRun) { + const alternatives = verifiedRouteGeometryPinAlternatives(edge); + throwExplicitPinConflict(edge, 'structural-frame border clearance', { + conflictingPins: alternatives.conflictingPins, + points: points.map((point) => [...point]), + frame: frameRun.frame?.id ?? frameRun.frameIndex, + side: frameRun.side, + overlapLengthPx: frameRun.overlapLength, + }, alternatives.supportedFixes); + } + } + + if (edge.labelAt) { + const rect = labelRectFor(edge, workflow.edges.indexOf(edge)); + const obstacle = rect && [...nodes.values()].find((node) => rectsOverlap(rect, node, -2)); + if (obstacle) { + throwExplicitPinConflict(edge, 'edge-label node clearance', { + conflictingPins: [authoredPinEvidence(edge, 'labelAt')], + labelAt: [...edge.labelAt], + labelRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + obstacleNode: obstacle.id, + }, [verifiedEdgeFix( + edge, + 'remove labelAt so readable-v2 can use its verified automatic label placement', + (candidate) => { delete candidate.labelAt; }, + )]); + } + const legendObstacle = rect && workflowLegendRects().find((legendRect) => ( + rectsOverlap(rect, legendRect) + )); + if (legendObstacle) { + throwExplicitPinConflict(edge, 'edge-label legend clearance', { + conflictingPins: [authoredPinEvidence(edge, 'labelAt')], + labelAt: [...edge.labelAt], + labelRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + legendObstacle, + }, [verifiedEdgeFix( + edge, + 'remove labelAt so readable-v2 can use its verified automatic label placement', + (candidate) => { delete candidate.labelAt; }, + )]); + } + const compositionObstacle = rect && workflowSceneLabelObstacles().find((candidate) => ( + rectsOverlap(rect, candidate) + )); + if (compositionObstacle) { + throwExplicitPinConflict(edge, 'edge-label lane/phase/group clearance', { + conflictingPins: [authoredPinEvidence(edge, 'labelAt')], + labelAt: [...edge.labelAt], + labelRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + compositionObstacle, + }, [verifiedEdgeFix( + edge, + `remove labelAt from edge "${workflowEdgeName(edge)}" so readable-v2 can use its verified automatic label placement`, + (candidate) => { delete candidate.labelAt; }, + )]); + } + } + } + validateReadablePairwisePinConflicts(); +} + +function validateWorkflow() { + const problems = []; + if (workflow.schema_version !== 1 && workflow.schema_version !== 2) { + problems.push('Workflow files must set "schema_version" to 1 or 2.'); + } + if (workflow.diagram_type !== 'workflow') { + problems.push(`Unsupported diagram_type "${workflow.diagram_type}". Expected "workflow".`); + } + if (!workflow.meta || !workflow.meta.title) { + problems.push('Workflow files must include meta.title.'); + } + if (!Array.isArray(workflow.lanes) || !workflow.lanes.length) { + problems.push('Workflow files must include at least one lane.'); + } + if (!Array.isArray(workflow.nodes)) { + problems.push('Workflow files must include a nodes array.'); + } + if (!Array.isArray(workflow.edges)) { + problems.push('Workflow files must include an edges array.'); + } + if (workflow.phases !== undefined && !Array.isArray(workflow.phases)) { + problems.push('Workflow "phases" must be an array.'); + } + if (workflow.groups !== undefined && !Array.isArray(workflow.groups)) { + problems.push('Workflow "groups" must be an array.'); + } + if (workflow.mainPath !== undefined && !Array.isArray(workflow.mainPath)) { + problems.push('Workflow "mainPath" must be an array of node ids.'); + } + if (workflow.cards !== undefined && !Array.isArray(workflow.cards)) { + problems.push('Workflow "cards" must be an array.'); + } + if (problems.length) { + throwDiagnosticProblems('Workflow layout validation failed', problems, { + subject: { diagramType: 'workflow' }, + }); + } + + enforceLegacyColumnCapacity(); + + const laneIds = new Set(workflow.lanes.map((lane) => lane.id)); + if (laneIds.size !== workflow.lanes.length) { + problems.push('Lane ids must be unique.'); + } + if (nodes.size !== workflow.nodes.length) { + problems.push('Node ids must be unique.'); + } + const phaseIds = new Set(asArray(workflow.phases).map((phase) => phase.id)); + if (phaseIds.size !== asArray(workflow.phases).length) { + problems.push('Phase ids must be unique.'); + } + const groupIds = new Set(asArray(workflow.groups).map((group) => group.id)); + if (groupIds.size !== asArray(workflow.groups).length) { + problems.push('Group ids must be unique.'); + } + + for (const node of nodes.values()) { + if (!laneIds.has(node.lane)) { + problems.push(`Node "${node.id}" uses unknown lane "${node.lane}".`); + continue; + } + if (!Number.isInteger(node.col) || node.col < 0 || node.col >= layout.colXs.length) { + problems.push(`Node "${node.id}" uses column ${node.col}, but valid columns are integers 0..${layout.colXs.length - 1}.`); + continue; + } + if (!isFinitePoint(node.x, node.y, node.cx, node.cy)) { + problems.push(`Node "${node.id}" produced non-finite coordinates — check col, width, height, and yOffset are numbers.`); + continue; + } + const estLabelW = textUnits(node.label) * 6.8; + if (estLabelW > node.width + 6) { + problems.push(`Label "${node.label}" (~${Math.round(estLabelW)}px) is wider than node "${node.id}" (${node.width}px) — shorten the label or increase node.width.`); + } + const brandRailProblem = brandTopRailProblem(node, node.width, nodeTextFit.labelMinimum); + if (brandRailProblem) problems.push(brandRailProblem); + const availableTextW = availableNodeTextWidth(node.width); + for (const [field, value, minimum] of [ + ['Sublabel', node.sublabel, nodeTextFit.sublabelMinimum], + ['Tag', node.tag, nodeTextFit.tagMinimum], + ]) { + if (!value) continue; + const minimumW = minimumNodeTextWidth(value, minimum); + if (minimumW > availableTextW) { + problems.push(`${field} "${value}" needs ~${Math.ceil(minimumW)}px at the ${minimum}px legible minimum, but node "${node.id}" provides ${availableTextW}px — shorten the ${field.toLowerCase()} or increase node.width.`); + } + } + + const top = laneTop(node.lane); + const contentTop = top + layout.laneTitleH + laneGroupHeaderH(node.lane); + const laneRight = layout.laneX + layout.laneW; + if (node.x < layout.laneX || node.x + node.width > laneRight) { + problems.push(`Node "${node.id}" exceeds the horizontal bounds of lane "${node.lane}".`); + } + if (node.y < contentTop || node.y + node.height > top + laneHeight(node.lane)) { + problems.push(`Node "${node.id}" collides with the title or boundary of lane "${node.lane}".`); + } + } + + const phaseRanges = []; + for (const phase of asArray(workflow.phases)) { + if (!Number.isInteger(phase.fromCol) || !Number.isInteger(phase.toCol)) { + problems.push(`Phase "${phase.id}" must use integer fromCol/toCol values.`); + continue; + } + if (phase.fromCol < 0 || phase.toCol >= layout.colXs.length || phase.fromCol > phase.toCol) { + problems.push(`Phase "${phase.id}" uses invalid columns ${phase.fromCol}..${phase.toCol}; use an ordered range within 0..${layout.colXs.length - 1}.`); + } else { + phaseRanges.push(phase); + } + const estLabelW = textUnits(phase.label) * 5.6; + const width = phaseSpan(phase).width; + if (estLabelW > width + 8) { + problems.push(`Phase label "${phase.label}" (~${Math.round(estLabelW)}px) is wider than its ${Math.round(width)}px span — shorten the label or widen the phase range.`); + } + } + phaseRanges.sort((a, b) => a.fromCol - b.fromCol || a.toCol - b.toCol); + for (let i = 0; i < phaseRanges.length; i += 1) { + for (let j = i + 1; j < phaseRanges.length; j += 1) { + const earlier = phaseRanges[i]; + const later = phaseRanges[j]; + if (later.fromCol > earlier.toCol) break; + problems.push(`Phase "${later.id}" (${later.fromCol}..${later.toCol}) overlaps phase "${earlier.id}" (${earlier.fromCol}..${earlier.toCol}) — start at col ${earlier.toCol + 1} or later, or end the earlier phase at col ${later.fromCol - 1}.`); + } + } + + for (const group of asArray(workflow.groups)) { + if (!laneIds.has(group.lane)) { + problems.push(`Group "${group.id}" uses unknown lane "${group.lane}".`); + continue; + } + if (!Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol)) { + problems.push(`Group "${group.id}" must use integer fromCol/toCol values.`); + continue; + } + if (group.fromCol < 0 || group.toCol >= layout.colXs.length || group.fromCol > group.toCol) { + problems.push(`Group "${group.id}" uses invalid columns ${group.fromCol}..${group.toCol}; use an ordered range within 0..${layout.colXs.length - 1}.`); + } + const contained = [...nodes.values()].some((node) => node.lane === group.lane && node.col >= group.fromCol && node.col <= group.toCol); + if (!contained) { + problems.push(`Group "${group.id}" does not contain any nodes — align its lane/columns with the parallel or branch work it frames.`); + } + } + + const byLane = new Map(); + for (const node of nodes.values()) { + byLane.set(node.lane, [...(byLane.get(node.lane) || []), node]); + } + for (const [lane, laneNodes] of byLane) { + for (let i = 0; i < laneNodes.length; i += 1) { + for (let j = i + 1; j < laneNodes.length; j += 1) { + if (rectsOverlap(laneNodes[i], laneNodes[j], 8)) { + problems.push(`Nodes "${laneNodes[i].id}" and "${laneNodes[j].id}" are less than 8px apart in lane "${lane}" — move one to another col, adjust yOffset, or reduce width/height.`); + } + } + } + } + + for (const edge of workflow.edges) { + if (!nodes.has(edge.from)) problems.push(`Edge "${edge.label || edge.from}" references unknown source "${edge.from}".`); + if (!nodes.has(edge.to)) problems.push(`Edge "${edge.label || edge.to}" references unknown target "${edge.to}".`); + if (nodes.has(edge.from) && nodes.has(edge.to)) { + const routed = pathFor(edge); + if (routed.points.length === 2) { + const [start, end] = routed.points; + const segmentLength = Math.hypot(end[0] - start[0], end[1] - start[1]); + if (segmentLength < 28) { + problems.push(`Edge "${edge.from}" -> "${edge.to}" is too short (${Math.round(segmentLength)}px; minimum 28px) — move the nodes farther apart or use a verified orthogonal route with readable clearance.`); + } + } + } + } + + problems.push(...cleanEndpointSideProblems({ + relations: workflow.edges, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + fromSideFor: (edge) => edgeSides(edge).fromSide, + toSideFor: (edge) => edgeSides(edge).toSide, + routeHint: 'keep automatic routing, or choose fromSide/toSide and via points whose first and final segments cross node borders perpendicularly', + })); + problems.push(...cleanFlowProblems({ + relations: workflow.edges, + obstacles: nodes.values(), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + obstacleKind: 'node', + routeHint: 'adjust fromSide/toSide, set route/via or channel coordinates, or move the node to a clearer lane/column' + })); + problems.push(...cleanCrossingProblems({ + relations: workflow.edges, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + profile: workflow.meta?.quality_profile, + profileIsAuthoritative: true, + mergeForwardCollinearWaypoints: workflow.schema_version === 2, + includeSharedEndpoints: () => workflow.schema_version === 2, + warnInStandard: workflow.schema_version === 2, + ...(workflow.schema_version === 2 ? { onDiagnostic: diagnostic => workflowDiagnostics.push(diagnostic) } : {}), + routeHint: 'adjust route/via, bias, or channel coordinates so the edges use separate lane corridors' + })); + problems.push(...cleanAmbiguousCorridorProblems({ + relations: workflow.edges, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + profile: workflow.meta?.quality_profile, + profileIsAuthoritative: true, + includeSharedEndpoints: () => workflow.schema_version === 2, + allowShortWorkflowTrunks: workflow.schema_version === 2, + onDiagnostic: (diagnostic) => workflowDiagnostics.push(diagnostic), + routeHint: 'adjust route/via, bias, or channel coordinates so unrelated edges do not visually merge' + })); + if (workflow.schema_version === 2) { + const routedRelations = workflow.edges.map((relation, relationIndex) => ({ + relation: { ...relation, width: relation.width || (relation.variant === 'emphasis' ? 1.8 : 1.4) }, + relationIndex, points: pathFor(relation).points, + })); + for (const hit of collectArrowheadCollisions({ routedRelations, allowShortWorkflowTrunks: true })) { + const severity = resolvedQualityProfile === 'showcase' ? 'error' : 'warning'; + const name = entry => entry.relation.id || `${entry.relation.from}->${entry.relation.to}`; + const message = `[composition/arrowhead-collision] workflow arrows "${name(hit.left)}" and "${name(hit.right)}" overlap at their destination (clearance ${hit.distance}px; minimum ${hit.minimum}px).`; + workflowDiagnostics.push({ + code: 'composition/arrowhead-collision', severity, message, + subject: { diagramType: 'workflow', edge: name(hit.left), from: hit.left.relation.from, to: hit.left.relation.to }, + evidence: { otherEdge: name(hit.right), distancePx: hit.distance, minimumPx: hit.minimum }, + supportedFixes: ['use separate destination sides or ports with clearance for both arrow markers'], + }); + if (severity === 'error') problems.push(message); + } + } + problems.push(...cleanBorderRunProblems({ + relations: workflow.edges, + endpointIds: new Set(nodes.keys()), + frames: workflowCompositionFrames(), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + profile: workflow.meta?.quality_profile, + profileIsAuthoritative: true, + routeHint: 'adjust route/via, bias, or channel coordinates so the edge crosses the lane or group perpendicularly instead of following its border' + })); + problems.push(...cleanRouteRhythmProblems({ + relations: workflow.edges, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + profile: workflow.meta?.quality_profile, + profileIsAuthoritative: true, + routeHint: 'adjust route/via, bias, or channel coordinates so each turn has a readable run-up' + })); + + if (Array.isArray(workflow.mainPath)) { + for (const id of workflow.mainPath) { + if (!nodes.has(id)) { + problems.push(`mainPath references unknown node "${id}".`); + } + } + for (let i = 0; i < workflow.mainPath.length - 1; i += 1) { + const fromId = workflow.mainPath[i]; + const toId = workflow.mainPath[i + 1]; + const from = nodes.get(fromId); + const to = nodes.get(toId); + if (!from || !to) continue; + const linked = workflow.edges.some((edge) => edge.from === fromId && edge.to === toId); + if (!linked) { + problems.push(`mainPath step "${fromId}" -> "${toId}" has no matching edge — add the edge or remove the pair from mainPath.`); + } + if (to.col < from.col) { + problems.push(`mainPath step "${fromId}" -> "${toId}" moves backward from col ${from.col} to ${to.col} — use a return edge outside mainPath for loops.`); + } + } + } + + const labelRects = []; + for (const [edgeIndex, edge] of workflow.edges.entries()) { + const labelRect = labelRectFor(edge, edgeIndex); + if (labelRect) labelRects.push(labelRect); + } + for (const rect of labelRects) { + for (const node of nodes.values()) { + if (rectsOverlap(rect, node, -2)) { + problems.push(`Label "${rect.label}" overlaps node "${node.id}" — adjust labelDx/labelDy/labelSegment or set labelAt.\n${suggestLabelObstacleFix(rect, rect.lx, rect.ly, node, 'node', viewBox, nodes.values())}`); + } + } + } + for (let i = 0; i < labelRects.length; i += 1) { + for (let j = i + 1; j < labelRects.length; j += 1) { + if (rectsOverlap(labelRects[i], labelRects[j], -2)) { + problems.push(`Labels "${labelRects[i].label}" and "${labelRects[j].label}" overlap — adjust labelDx/labelDy/labelSegment or route one relationship through a separate corridor.\n${suggestLabelPairFix(labelRects[i], labelRects[j])}`); + } + } + } + problems.push(...cleanLabelRouteClearanceProblems({ + relations: workflow.edges, + labels: labelRects, + endpointIds: new Set(nodes.keys()), + pathFor, + diagramType: 'workflow', + relationCollection: 'edges', + profile: workflow.meta?.quality_profile, + profileIsAuthoritative: true, + })); + + if (workflow.schema_version === 1) { + if (viewBox[0] < layout.laneX + layout.laneW + 16) { + problems.push(`viewBox width ${viewBox[0]} clips the ${layout.laneW}px lanes — set meta.viewBox[0] to at least ${layout.laneX + layout.laneW + 16}.`); + } + if (legendY() + 18 > viewBox[1]) { + problems.push(`Legend exceeds viewBox height ${viewBox[1]} — set meta.viewBox[1] to at least ${legendY() + 18}.`); + } + // v1 only; see collectLabelCanvasOverflow in shared/geometry.mjs. + problems.push(...cleanLabelCanvasContainmentProblems({ + labels: labelRects, + viewBox, + diagramType: 'workflow', + relationCollection: 'edges', + profile: workflow.meta?.quality_profile, + profileIsAuthoritative: true, + })); + } + + if (problems.length) { + throwDiagnosticProblems('Workflow layout validation failed', problems, { + subject: { diagramType: 'workflow' }, + diagnostics: workflowDiagnostics, + }); + } +} + +function validateReadableInputsBeforeRouting() { + if (workflow.schema_version !== 2) return; + const fail = (diagnostic) => throwDiagnosticError(diagnostic.message, [diagnostic]); + const unusedId = (base, used) => { + for (let suffix = 2; ; suffix += 1) { + const candidate = `${base}-${suffix}`; + if (!used.has(candidate)) return candidate; + } + }; + const authoredLanes = [...workflow.lanes].sort((left, right) => ( + sourceIndexes.lanes.get(left) - sourceIndexes.lanes.get(right) + )); + const authoredNodes = [...workflow.nodes].sort((left, right) => ( + sourceIndexes.nodes.get(left) - sourceIndexes.nodes.get(right) + )); + const authoredEdges = [...workflow.edges].sort((left, right) => ( + sourceIndexes.edges.get(left) - sourceIndexes.edges.get(right) + )); + + const firstLaneIndex = new Map(); + for (const lane of authoredLanes) { + const laneIndex = sourceIndexes.lanes.get(lane); + if (firstLaneIndex.has(lane.id)) { + const message = `Workflow lane id "${lane.id}" is duplicated.`; + const replacement = unusedId(lane.id, new Set(workflow.lanes.map(({ id }) => id))); + const canonicalLaneIndex = workflow.lanes.indexOf(lane); + const supportedFixes = acceptsFix((document) => { + document.lanes[canonicalLaneIndex].id = replacement; + }) ? [`rename /lanes/${laneIndex}/id to verified unique id "${replacement}"`] : []; + fail({ + code: 'workflow/duplicate-lane-id', + severity: 'error', + message, + subject: { diagramType: 'workflow', lane: lane.id, path: `/lanes/${laneIndex}/id` }, + evidence: { + duplicateLaneId: lane.id, + firstPath: `/lanes/${firstLaneIndex.get(lane.id)}/id`, + duplicatePath: `/lanes/${laneIndex}/id`, + }, + supportedFixes, + }); + } + firstLaneIndex.set(lane.id, laneIndex); + } + + const firstNodeIndex = new Map(); + for (const node of authoredNodes) { + const nodeIndex = sourceIndexes.nodes.get(node); + if (firstNodeIndex.has(node.id)) { + const message = `Workflow node id "${node.id}" is duplicated.`; + const replacement = unusedId(node.id, new Set(workflow.nodes.map(({ id }) => id))); + const canonicalNodeIndex = workflow.nodes.indexOf(node); + const supportedFixes = acceptsFix((document) => { + document.nodes[canonicalNodeIndex].id = replacement; + }) ? [`rename /nodes/${nodeIndex}/id to verified unique id "${replacement}"`] : []; + fail({ + code: 'workflow/duplicate-node-id', + severity: 'error', + message, + subject: { diagramType: 'workflow', node: node.id, path: `/nodes/${nodeIndex}/id` }, + evidence: { + duplicateNodeId: node.id, + firstPath: `/nodes/${firstNodeIndex.get(node.id)}/id`, + duplicatePath: `/nodes/${nodeIndex}/id`, + }, + supportedFixes, + }); + } + firstNodeIndex.set(node.id, nodeIndex); + } + + const availableNodeIds = [...nodes.keys()].sort(stableCompare); + for (const edge of authoredEdges) { + const edgeIndex = sourceIndexes.edges.get(edge); + for (const [field, endpoint] of [['from', 'source'], ['to', 'target']]) { + if (nodes.has(edge[field])) continue; + const message = `Workflow edge "${workflowEdgeName(edge)}" references unknown ${endpoint} "${edge[field]}".`; + const canonicalEdgeIndex = workflow.edges.indexOf(edge); + const supportedFixes = availableNodeIds.flatMap((nodeId) => ( + acceptsFix((document) => { + document.edges[canonicalEdgeIndex][field] = nodeId; + }) + ? [`set /edges/${edgeIndex}/${field} to verified node id "${nodeId}"`] + : [] + )); + fail({ + code: 'workflow/unknown-edge-endpoint', + severity: 'error', + message, + subject: { + diagramType: 'workflow', + edge: edge.id ?? null, + path: `/edges/${edgeIndex}/${field}`, + from: edge.from, + to: edge.to, + }, + evidence: { + endpoint, + unknownNodeId: edge[field], + availableNodeIds, + }, + supportedFixes, + }); + } + } + const laneIds = new Set(workflow.lanes.map((lane) => lane.id)); + const availableLaneIds = [...laneIds].sort(stableCompare); + const nodeSourceIndexes = new Map(authoredNodes.map((node) => [ + node.id, + sourceIndexes.nodes.get(node), + ])); + + const byLane = new Map(); + for (const authoredNode of authoredNodes) { + const nodeIndex = sourceIndexes.nodes.get(authoredNode); + const node = nodes.get(authoredNode.id); + if (!laneIds.has(node.lane)) { + const message = `Workflow node "${node.id}" uses unknown lane "${node.lane}".`; + const canonicalNodeIndex = workflow.nodes.findIndex((candidate) => candidate.id === node.id); + const supportedFixes = availableLaneIds.flatMap((laneId) => ( + acceptsFix((document) => { + document.nodes[canonicalNodeIndex].lane = laneId; + }) + ? [`set /nodes/${nodeIndex}/lane to verified lane id "${laneId}"`] + : [] + )); + fail({ + code: 'workflow/unknown-node-lane', + severity: 'error', + message, + subject: { diagramType: 'workflow', node: node.id, path: `/nodes/${nodeIndex}/lane` }, + evidence: { unknownLaneId: node.lane, availableLaneIds }, + supportedFixes, + }); + } + if (!Number.isInteger(node.col) || node.col < 0 || node.col >= layout.colXs.length) { + const message = `Workflow node "${node.id}" uses column ${node.col}, but valid columns are integers 0..${layout.colXs.length - 1}.`; + const canonicalNodeIndex = workflow.nodes.findIndex((candidate) => candidate.id === node.id); + const supportedFixes = layout.colXs.flatMap((_x, col) => ( + acceptsFix((document) => { + document.nodes[canonicalNodeIndex].col = col; + }) + ? [`set /nodes/${nodeIndex}/col to verified column ${col}`] + : [] + )); + fail({ + code: 'workflow/invalid-node-column', + severity: 'error', + message, + subject: { diagramType: 'workflow', node: node.id, path: `/nodes/${nodeIndex}/col` }, + evidence: { actualColumn: node.col, minimumColumn: 0, maximumColumn: layout.colXs.length - 1 }, + supportedFixes, + }); + } + if (!isFinitePoint(node.x, node.y, node.cx, node.cy)) { + const message = `Workflow node "${node.id}" produced non-finite coordinates.`; + fail({ + code: 'workflow/non-finite-node-geometry', + severity: 'error', + message, + subject: { diagramType: 'workflow', node: node.id, path: `/nodes/${nodeIndex}` }, + evidence: { + measuredRect: { x: node.x, y: node.y, width: node.width, height: node.height }, + authored: { + col: authoredNode.col, + width: authoredNode.width ?? null, + height: authoredNode.height ?? null, + yOffset: authoredNode.yOffset ?? null, + }, + }, + supportedFixes: [], + }); + } + byLane.set(node.lane, [...(byLane.get(node.lane) || []), node]); + } + // Every overlapping pair is reported at once. Failing on the first pair + // made an author who stacked two groups of nodes fix one, rerun, and only + // then learn about the other; the draft already contained both. + const overlaps = []; + for (const [lane, laneNodes] of byLane) { + for (let left = 0; left < laneNodes.length; left += 1) { + for (let right = left + 1; right < laneNodes.length; right += 1) { + if (rectsOverlap(laneNodes[left], laneNodes[right], 8)) { + const leftNode = laneNodes[left]; + const rightNode = laneNodes[right]; + const rightIndex = nodeSourceIndexes.get(rightNode.id); + const canonicalNodeIndex = workflow.nodes.findIndex((candidate) => ( + candidate.id === rightNode.id + )); + const supportedFixes = layout.colXs.flatMap((_x, col) => { + if (col === rightNode.col) return []; + return acceptsFix((document) => { + document.nodes[canonicalNodeIndex].col = col; + }) + ? [`set /nodes/${rightIndex}/col to verified free column ${col}`] + : []; + }); + const message = `Workflow nodes "${leftNode.id}" and "${rightNode.id}" are less than 8px apart in lane "${lane}".`; + overlaps.push({ + code: 'workflow/node-overlap', + severity: 'error', + message, + subject: { diagramType: 'workflow', node: rightNode.id, path: `/nodes/${rightIndex}` }, + evidence: { + lane, + minimumClearancePx: 8, + nodes: [ + { id: leftNode.id, rect: { x: leftNode.x, y: leftNode.y, width: leftNode.width, height: leftNode.height } }, + { id: rightNode.id, rect: { x: rightNode.x, y: rightNode.y, width: rightNode.width, height: rightNode.height } }, + ], + }, + supportedFixes, + }); + } + } + } + } + if (overlaps.length) { + throwDiagnosticError(overlaps.length === 1 + ? overlaps[0].message + : `Workflow node overlap:\n- ${overlaps.map((entry) => entry.message).join('\n- ')}`, overlaps); + } +} + +function gapYBetween(fromLane, toLane, bias = 0.5) { + const a = laneTop(fromLane) + laneHeight(fromLane); + const b = laneTop(toLane); + return a + (b - a) * bias; +} + +function spanForCols(fromCol, toCol, pad = 46, minimumWidth = 0) { + const start = layout.colXs[fromCol] - pad; + const end = layout.colXs[toCol] + pad; + const width = Math.max(end - start, minimumWidth); + if (fromCol === toCol && width > end - start) { + return { x: start, width, cx: start + width / 2 }; + } + const cx = (start + end) / 2; + return { x: cx - width / 2, width, cx }; +} + +function phaseSpan(phase) { + return spanForCols( + phase.fromCol, + phase.toCol, + 46, + workflow.schema_version === 2 ? textUnits(phase.label) * 5.6 + 8 : 0, + ); +} + +function groupSpan(group) { + if (workflow.schema_version === 2) { + return readableGroupBounds(workflow, group, layout.colXs); + } + return spanForCols( + group.fromCol, + group.toCol, + 50, + 0, + ); +} + +function sameLaneAutoVia(start, end) { + if (start[0] === end[0] || start[1] === end[1]) return []; + const midX = (start[0] + end[0]) / 2; + return [[midX, start[1]], [midX, end[1]]]; +} + +function routeBounds(points) { + let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; + for (const point of points) { + if (point[0] < minX) minX = point[0]; + if (point[0] > maxX) maxX = point[0]; + if (point[1] < minY) minY = point[1]; + if (point[1] > maxY) maxY = point[1]; + } + return { minX, minY, maxX, maxY }; +} + +function rectToBounds(rect) { + return { minX: rect.x, minY: rect.y, maxX: rect.x + rect.width, maxY: rect.y + rect.height }; +} + +function boundsOverlap(a, b, margin) { + return a.minX <= b.maxX + margin && b.minX <= a.maxX + margin + && a.minY <= b.maxY + margin && b.minY <= a.maxY + margin; +} + +function routeClearsUnrelatedNodes(edge, points, clearance = 2) { + const endpointIds = new Set([edge.from, edge.to]); + const routeExtent = routeBounds(points); + const queryBox = { + minX: routeExtent.minX - clearance, minY: routeExtent.minY - clearance, + maxX: routeExtent.maxX + clearance, maxY: routeExtent.maxY + clearance, + }; + for (const item of obstacleGrid.query(queryBox)) { + if (item.kind !== 'node') continue; + const node = item.node; + if (endpointIds.has(node.id)) continue; + if (!boundsOverlap(routeExtent, rectToBounds(node), clearance)) continue; + for (let index = 0; index < points.length - 1; index += 1) { + if (segmentIntersectsRect({ start: points[index], end: points[index + 1] }, node, clearance)) { + return false; + } + } + } + return true; +} + +function firstRouteNodeCollision(edge, points) { + const lastSegment = points.length - 2; + for (const node of nodes.values()) { + const endpointRole = node.id === edge.from + ? 'source-endpoint' + : node.id === edge.to ? 'target-endpoint' : 'unrelated'; + for (let segmentIndex = 0; segmentIndex <= lastSegment; segmentIndex += 1) { + if (endpointRole === 'source-endpoint' && segmentIndex === 0) continue; + if (endpointRole === 'target-endpoint' && segmentIndex === lastSegment) continue; + const clearancePx = endpointRole === 'unrelated' ? 2 : 0; + const from = points[segmentIndex]; + const to = points[segmentIndex + 1]; + if (segmentIntersectsRect({ start: from, end: to }, node, clearancePx)) { + return { + obstacleNode: node.id, + obstacleRole: endpointRole, + segmentIndex, + from: [...from], + to: [...to], + clearancePx, + }; + } + } + } + return null; +} + +function oneBendCrossLaneVia(edge, start, end, fromSide, toSide) { + const fromVertical = fromSide === 'top' || fromSide === 'bottom'; + const toVertical = toSide === 'top' || toSide === 'bottom'; + if (fromVertical === toVertical) return null; + + const corner = fromVertical ? [start[0], end[1]] : [end[0], start[1]]; + const points = normalizeRoutePoints([start, corner, end]); + if (points.length !== 3 || !routeHonorsEndpointSides(points, fromSide, toSide)) return null; + + const segmentsAreReadable = points.slice(0, -1).every((point, index) => ( + Math.hypot( + points[index + 1][0] - point[0], + points[index + 1][1] - point[1], + ) >= 8 + )); + if (!segmentsAreReadable || !routeClearsUnrelatedNodes(edge, points)) return null; + return points.slice(1, -1); +} + +const pathCache = new Map(); +const readableSideCache = new Map(); +const workflowDiagnostics = []; + +const { automaticOneBendSides } = createAutomaticSideSelection({ + workflow, + readableSideCache, + readableAutomaticCandidateSet, + compareCost, + oneBendCrossLaneVia, +}); + +const OUTWARD_SIDE_VECTOR = Object.freeze({ + left: [-1, 0], + right: [1, 0], + top: [0, -1], + bottom: [0, 1], +}); + +function outwardStub(point, side, distance = 16) { + const [dx, dy] = OUTWARD_SIDE_VECTOR[side] || [0, 0]; + return [point[0] + dx * distance, point[1] + dy * distance]; +} + +function orthogonalRoute(points) { + return points.every((point, index) => { + if (!Array.isArray(point) || point.length !== 2 || !isFinitePoint(...point)) return false; + if (index === 0) return true; + const previous = points[index - 1]; + const dx = Math.abs(point[0] - previous[0]); + const dy = Math.abs(point[1] - previous[1]); + return (dx <= 0.0001) !== (dy <= 0.0001); + }); +} + +function routeClearsEndpointNodes(points, from, to) { + const lastSegment = points.length - 2; + for (let index = 0; index <= lastSegment; index += 1) { + const segment = { start: points[index], end: points[index + 1] }; + if (index > 0 && segmentIntersectsRect(segment, from)) return false; + if (index < lastSegment && segmentIntersectsRect(segment, to)) return false; + } + return true; +} + +function routeMeetsHardRhythm(points) { + if (points.length === 2) { + return Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]) + 0.0001 >= 28; + } + return points.slice(0, -1).every((point, index) => { + const length = Math.abs(points[index + 1][0] - point[0]) + Math.abs(points[index + 1][1] - point[1]); + const endpoint = index === 0 || index === points.length - 2; + return length + 0.0001 >= (endpoint ? 8 : 16); + }); +} + +function routeLabelClearsNodes(edge, points) { + if (!edge.label || edge.labelAt) return true; + const [lx, ly] = workflowEdgeLabelPoint(edge, points); + const width = workflowLabelWidth(edge.label); + const rect = { x: lx - width / 2, y: ly - 10, width, height: 14 }; + // The comparison shrinks the rect by two pixels, so only nodes inside the + // shrunk box can fail it. + const queryBox = { + minX: rect.x + 2, minY: rect.y + 2, + maxX: rect.x + rect.width - 2, maxY: rect.y + rect.height - 2, + }; + for (const item of obstacleGrid.query(queryBox)) { + if (item.kind !== 'node') continue; + if (rectsOverlap(rect, item.node, -2)) return false; + } + return true; +} + +function candidateLabelRect(edge, points) { + if (!edge.label) return null; + const [lx, ly] = workflowEdgeLabelPoint(edge, points); + const width = workflowLabelWidth(edge.label); + return { x: lx - width / 2, y: ly - 10, width, height: 14 }; +} + +function labelRouteClearanceDeficit(edge, points, threshold = 8) { + const candidateLabel = candidateLabelRect(edge, points); + const candidateExtent = routeBounds(points); + if (candidateLabel) { + const labelExtent = rectToBounds(candidateLabel); + candidateExtent.minX = Math.min(candidateExtent.minX, labelExtent.minX); + candidateExtent.minY = Math.min(candidateExtent.minY, labelExtent.minY); + candidateExtent.maxX = Math.max(candidateExtent.maxX, labelExtent.maxX); + candidateExtent.maxY = Math.max(candidateExtent.maxY, labelExtent.maxY); + } + const queryBox = { + minX: candidateExtent.minX - threshold, minY: candidateExtent.minY - threshold, + maxX: candidateExtent.maxX + threshold, maxY: candidateExtent.maxY + threshold, + }; + // Accumulate in the order the routes were registered, which is the order the + // previous full scan used, so the floating-point sum is identical. + const items = obstacleGrid.query(queryBox) + .filter((item) => item.kind === 'route' || item.kind === 'label') + .sort((left, right) => left.sequence - right.sequence || (left.kind === right.kind ? 0 : left.kind === 'route' ? -1 : 1)); + let deficit = 0; + for (const item of items) { + if (item.kind === 'route') { + if (!candidateLabel) continue; + const otherPoints = item.routed.points; + for (let index = 0; index < otherPoints.length - 1; index += 1) { + const clearance = segmentRectClearance({ + start: otherPoints[index], + end: otherPoints[index + 1], + }, candidateLabel); + if (clearance != null) deficit += Math.max(0, threshold - clearance); + } + } else if (candidateLabel || !edge.label) { + for (let index = 0; index < points.length - 1; index += 1) { + const clearance = segmentRectClearance({ + start: points[index], + end: points[index + 1], + }, item.rect); + if (clearance != null) deficit += Math.max(0, threshold - clearance); + } + } + } + return deficit; +} + +function routeClearsPlacedLabels(edge, points) { + const candidateLabel = candidateLabelRect(edge, points); + const candidateExtent = routeBounds(points); + const queryBox = { + minX: candidateExtent.minX - 4, minY: candidateExtent.minY - 4, + maxX: candidateExtent.maxX + 4, maxY: candidateExtent.maxY + 4, + }; + if (candidateLabel) { + queryBox.minX = Math.min(queryBox.minX, candidateLabel.x - 4); + queryBox.minY = Math.min(queryBox.minY, candidateLabel.y - 4); + queryBox.maxX = Math.max(queryBox.maxX, candidateLabel.x + candidateLabel.width + 4); + queryBox.maxY = Math.max(queryBox.maxY, candidateLabel.y + candidateLabel.height + 4); + } + for (const item of obstacleGrid.query(queryBox)) { + // A label may sit far from the route it annotates, so both boxes are queried. + if (item.kind === 'label') { + if (candidateLabel && rectsOverlap(candidateLabel, item.rect, -2)) return false; + const rect = item.rect; + // Either axis further than the minimum clearance means no segment can reach it. + if (Math.max(rect.x - candidateExtent.maxX, candidateExtent.minX - (rect.x + rect.width)) >= 4 + || Math.max(rect.y - candidateExtent.maxY, candidateExtent.minY - (rect.y + rect.height)) >= 4) continue; + for (let index = 0; index < points.length - 1; index += 1) { + const clearance = segmentRectClearance({ + start: points[index], + end: points[index + 1], + }, rect); + if (clearance != null && clearance + 0.0001 < 4) return false; + } + } else if (item.kind === 'route' && candidateLabel) { + const otherBounds = item.bounds; + if (Math.max(candidateLabel.x - otherBounds.maxX, otherBounds.minX - (candidateLabel.x + candidateLabel.width)) >= 4 + || Math.max(candidateLabel.y - otherBounds.maxY, otherBounds.minY - (candidateLabel.y + candidateLabel.height)) >= 4) continue; + const otherPoints = item.routed.points; + for (let index = 0; index < otherPoints.length - 1; index += 1) { + const clearance = segmentRectClearance({ + start: otherPoints[index], + end: otherPoints[index + 1], + }, candidateLabel); + if (clearance != null && clearance + 0.0001 < 4) return false; + } + } + } + return true; +} + +function routeClearsLegend(edge, points) { + if (!workflowLegendEntries.length) return true; + const legendRects = workflowLegendRects(); + for (const rect of legendRects) { + for (let index = 0; index < points.length - 1; index += 1) { + if (segmentIntersectsRect({ start: points[index], end: points[index + 1] }, rect)) return false; + } + const label = candidateLabelRect(edge, points); + if (label && rectsOverlap(label, rect)) return false; + } + return true; +} + +// Scene label obstacles are a pure function of the document and the layout. +let cachedSceneLabelObstacles = null; +function sceneLabelObstacles() { + if (!cachedSceneLabelObstacles) { + cachedSceneLabelObstacles = workflowSceneLabelObstacles(); + for (const obstacle of cachedSceneLabelObstacles) { + obstacleGrid.insert(rectToBounds(obstacle), { kind: 'scene', obstacle }); + } + } + return cachedSceneLabelObstacles; +} + +function routeClearsSceneLabelObstacles(edge, points) { + // Asking for the cached list also publishes it to the grid. + sceneLabelObstacles(); + const label = candidateLabelRect(edge, points); + const extent = routeBounds(points); + const queryBox = { + minX: extent.minX, minY: extent.minY, maxX: extent.maxX, maxY: extent.maxY, + }; + if (label) { + queryBox.minX = Math.min(queryBox.minX, label.x); + queryBox.minY = Math.min(queryBox.minY, label.y); + queryBox.maxX = Math.max(queryBox.maxX, label.x + label.width); + queryBox.maxY = Math.max(queryBox.maxY, label.y + label.height); + } + for (const item of obstacleGrid.query(queryBox)) { + if (item.kind !== 'scene') continue; + for (let index = 0; index < points.length - 1; index += 1) { + if (segmentIntersectsRect({ start: points[index], end: points[index + 1] }, item.obstacle)) { + return false; + } + } + if (label && rectsOverlap(label, item.obstacle)) return false; + } + return true; +} + +// Composition frames are pure as well; only a frame that reaches the candidate +// can share a border line with it. +let cachedCompositionFrames = null; +function compositionFrames() { + if (!cachedCompositionFrames) cachedCompositionFrames = workflowCompositionFrames(); + return cachedCompositionFrames; +} + +function routeClearsFrameBorders(points) { + const extent = routeBounds(points); + const nearFrames = compositionFrames().filter((frame) => boundsOverlap(rectToBounds(frame), extent, 0)); + if (!nearFrames.length) return true; + return collectBorderRuns({ + routedRelations: [{ points }], + frames: nearFrames, + }).length === 0; +} + +function routeExtentCoordinates(edge, points) { + const coordinates = [...points]; + if (!edge.labelAt) { + const label = candidateLabelRect(edge, points); + if (label) { + coordinates.push([label.x, label.y], [label.x + label.width, label.y + label.height]); + } + } + return coordinates; +} + +function routeFitsCanvasOrigin(edge, points) { + return routeExtentCoordinates(edge, points).every(([x, y]) => x >= 0 && y >= 0); +} + +// Predicates are pure, so their order does not change the result; they are +// ordered by "cheap and selective first" so the expensive clearance work runs +// on fewer candidates. +function readableCandidateIsFeasible(edge, points, from, to, fromSide, toSide) { + return points.length >= 2 + && orthogonalRoute(points) + && routeMeetsHardRhythm(points) + && routeHonorsEndpointSides(points, fromSide, toSide) + && routeClearsEndpointNodes(points, from, to) + && routeLabelClearsNodes(edge, points) + && routeClearsSceneLabelObstacles(edge, points) + && routeClearsUnrelatedNodes(edge, points) + && routeClearsPlacedLabels(edge, points) + && routeFitsCanvasOrigin(edge, points) + && routeClearsFrameBorders(points) + && routeClearsLegend(edge, points); +} + +function corridorViaY(start, end, fromSide, toSide, y) { + const startStub = outwardStub(start, fromSide); + const endStub = outwardStub(end, toSide); + return [startStub, [startStub[0], y], [endStub[0], y], endStub]; +} + +function corridorViaX(start, end, fromSide, toSide, x) { + const startStub = outwardStub(start, fromSide); + const endStub = outwardStub(end, toSide); + return [startStub, [x, startStub[1]], [x, endStub[1]], endStub]; +} + +function properAxisCrossing(a, b, c, d) { + const firstHorizontal = Math.abs(a[1] - b[1]) <= 0.0001; + const secondHorizontal = Math.abs(c[1] - d[1]) <= 0.0001; + if (firstHorizontal === secondHorizontal) return false; + const horizontal = firstHorizontal ? [a, b] : [c, d]; + const vertical = firstHorizontal ? [c, d] : [a, b]; + const x = vertical[0][0]; + const y = horizontal[0][1]; + return x > Math.min(horizontal[0][0], horizontal[1][0]) + 0.0001 + && x < Math.max(horizontal[0][0], horizontal[1][0]) - 0.0001 + && y > Math.min(vertical[0][1], vertical[1][1]) + 0.0001 + && y < Math.max(vertical[0][1], vertical[1][1]) - 0.0001; +} + +function independentAutomaticRoute(edge) { + return workflow.schema_version === 2 + && !edge.via && edge.channelX === undefined && edge.channelY === undefined + && !edge.labelAt + && (!edge.route || edge.route === 'auto') + && (!edge.fromSide || edge.fromSide === 'auto') + && (!edge.toSide || edge.toSide === 'auto'); +} + +function routeInteractionMetrics(edge, points) { + let properCrossingCount = 0; + let sharedCorridorPx = 0; + const extent = routeBounds(points); + const nearbyRoutes = obstacleGrid.query({ + minX: extent.minX - 8, minY: extent.minY - 8, + maxX: extent.maxX + 8, maxY: extent.maxY + 8, + }) + .filter((item) => item.kind === 'route') + .sort((left, right) => left.sequence - right.sequence); + for (const item of nearbyRoutes) { + const otherEdge = item.edge; + const routed = item.routed; + const sharedEndpoint = [edge.from, edge.to].some((id) => id === otherEdge.from || id === otherEdge.to); + if (sharedEndpoint && workflow.schema_version !== 2) continue; + sharedCorridorPx += collectAmbiguousCorridors({ + routedRelations: [{ relation: edge, points }, { relation: otherEdge, points: routed.points }], + includeSharedEndpoints: () => workflow.schema_version === 2, + allowShortWorkflowTrunks: workflow.schema_version === 2, + }).reduce((total, hit) => total + hit.overlapLength, 0); + for (let left = 0; left < points.length - 1; left += 1) { + for (let right = 0; right < routed.points.length - 1; right += 1) { + if (properAxisCrossing(points[left], points[left + 1], routed.points[right], routed.points[right + 1])) { + properCrossingCount += 1; + } + } + } + } + return { properCrossingCount, sharedCorridorPx }; +} + +function automaticForwardReversePx(edge, points) { + const from = nodes.get(edge.from); + const to = nodes.get(edge.to); + if (!from || !to || ['return', 'error'].includes(edge.role) || to.col <= from.col) return 0; + return points.slice(0, -1).reduce((total, point, index) => ( + total + Math.max(0, point[0] - points[index + 1][0]) + ), 0); +} + +function readableCandidateCost( + edge, + points, + ordinal, + naturalFromSide, + naturalToSide, +) { + const interaction = routeInteractionMetrics(edge, points); + const segmentLengths = points.slice(0, -1).map((point, index) => ( + Math.abs(points[index + 1][0] - point[0]) + Math.abs(points[index + 1][1] - point[1]) + )); + const routeLength = segmentLengths.reduce((total, length) => total + length, 0); + const directLength = Math.abs(points.at(-1)[0] - points[0][0]) + Math.abs(points.at(-1)[1] - points[0][1]); + const interiorPreferred28Deficit = segmentLengths.slice(1, -1) + .reduce((total, length) => total + Math.max(0, 28 - length), 0); + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + for (const [x, y] of points) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + const canvasGrowthPx = Math.max(0, -minX) + + Math.max(0, maxX - minimumCanvasWidth) + + Math.max(0, -minY) + + Math.max(0, maxY - autoHeight); + const from = nodes.get(edge.from); + const to = nodes.get(edge.to); + const naturalStart = anchor(from, naturalFromSide); + const naturalEnd = anchor(to, naturalToSide); + const portDisplacementPx = Math.abs(points[0][0] - naturalStart[0]) + + Math.abs(points[0][1] - naturalStart[1]) + + Math.abs(points.at(-1)[0] - naturalEnd[0]) + + Math.abs(points.at(-1)[1] - naturalEnd[1]); + const legacyCoordinateDisplacement = Math.abs(from.cx - LEGACY_COLUMN_CENTERS[from.col]) + + Math.abs(to.cx - LEGACY_COLUMN_CENTERS[to.col]); + return { + automaticForwardReversePx: automaticForwardReversePx(edge, points), + properCrossingCount: interaction.properCrossingCount, + sharedCorridorPx: interaction.sharedCorridorPx, + labelRouteClearanceDeficit: labelRouteClearanceDeficit(edge, points), + interiorPreferred28Deficit, + bendCount: Math.max(0, points.length - 2), + stretchMilli: Math.round((directLength > 0 ? routeLength / directLength : 1) * 1000), + canvasGrowthPx, + portDisplacementMilli: Math.round(portDisplacementPx * 1000), + legacyCoordinateDisplacement, + stableCandidateOrdinal: ordinal, + }; +} + +function compareCost(left, right) { + for (const dimension of READABLE_CANDIDATE_COST_PRIORITY) { + if ((left[dimension] || 0) !== (right[dimension] || 0)) { + return (left[dimension] || 0) - (right[dimension] || 0); + } + } + return 0; +} + +function readableAutomaticCandidateSet( + edge, + from, + to, + start, + end, + fromSide, + toSide, + { + ordinalOffset = 0, + naturalFromSide = fromSide, + naturalToSide = toSide, + } = {}, +) { + const midX = (start[0] + end[0]) / 2; + const laneGapY = from.lane === to.lane + ? laneTop(from.lane) - 16 + : gapYBetween(from.lane, to.lane, edge.bias ?? 0.5); + const topY = Math.max(8, Math.min(laneTop(from.lane), laneTop(to.lane)) - 16); + const bottomY = Math.max( + laneTop(from.lane) + laneHeight(from.lane), + laneTop(to.lane) + laneHeight(to.lane), + ) + 16; + const outsideLeft = layout.laneX - 20; + const outsideRight = layout.laneX + layout.laneW + 12; + const rawCandidates = [ + { family: 'facing-straight', via: [] }, + { family: 'horizontal-then-vertical', via: [[end[0], start[1]]] }, + { family: 'vertical-then-horizontal', via: [[start[0], end[1]]] }, + { family: 'lane-gap-corridor', via: corridorViaY(start, end, fromSide, toSide, laneGapY) }, + { family: 'column-gap-corridor', via: corridorViaX(start, end, fromSide, toSide, midX) }, + { family: 'outside-left', via: corridorViaX(start, end, fromSide, toSide, outsideLeft) }, + { family: 'outside-right', via: corridorViaX(start, end, fromSide, toSide, outsideRight) }, + { family: 'top-corridor', via: corridorViaY(start, end, fromSide, toSide, topY) }, + { family: 'bottom-corridor', via: corridorViaY(start, end, fromSide, toSide, bottomY) }, + ]; + const candidates = rawCandidates.map((candidate, ordinal) => ({ + ...candidate, + ordinal: ordinalOffset + ordinal, + points: joinRoutePoints(start, candidate.via, end), + })).filter(({ points }) => ( + readableCandidateIsFeasible(edge, points, from, to, fromSide, toSide) + )).map((candidate) => ({ + ...candidate, + cost: readableCandidateCost( + edge, + candidate.points, + candidate.ordinal, + naturalFromSide, + naturalToSide, + ), + })).sort((left, right) => compareCost(left.cost, right.cost)); + return { rawCandidates, candidates, outsideRight }; +} + +function readableAutomaticVia(edge, from, to, start, end, fromSide, toSide) { + const { rawCandidates, candidates, outsideRight } = readableAutomaticCandidateSet( + edge, + from, + to, + start, + end, + fromSide, + toSide, + ); + + if (candidates.length) return candidates[0].points.slice(1, -1); + const outsideRightCandidate = rawCandidates.find(({ family }) => family === 'outside-right'); + if (outsideRightCandidate) { + const currentPoints = normalizeRoutePoints([start, ...outsideRightCandidate.via, end]); + const labelRect = candidateLabelRect(edge, currentPoints); + let outsideRightMinX = outsideRight; + if (labelRect) { + // Only obstacles whose box can reach the label rect can push the corridor. + const labelQuery = { + minX: labelRect.x - 4, minY: labelRect.y - 4, + maxX: labelRect.x + labelRect.width + 4, maxY: labelRect.y + labelRect.height + 4, + }; + for (const item of obstacleGrid.query(labelQuery)) { + if (item.kind === 'node') { + if (!rectsOverlap(labelRect, item.node, -2)) continue; + const rightwardLabelDeficit = item.node.x + item.node.width - 2 - labelRect.x; + if (rightwardLabelDeficit > 0) { + outsideRightMinX = Math.max(outsideRightMinX, outsideRight + rightwardLabelDeficit * 2); + } + } else if (item.kind === 'label') { + if (!rectsOverlap(labelRect, item.rect, -2)) continue; + const rightwardLabelDeficit = item.rect.x + item.rect.width - 2 - labelRect.x; + if (rightwardLabelDeficit > 0) { + outsideRightMinX = Math.max(outsideRightMinX, outsideRight + rightwardLabelDeficit * 2); + } + } else if (item.kind === 'route') { + const otherPoints = item.routed.points; + for (let index = 0; index < otherPoints.length - 1; index += 1) { + const clearance = segmentRectClearance({ + start: otherPoints[index], + end: otherPoints[index + 1], + }, labelRect); + if (clearance == null || clearance + 0.0001 >= 4) continue; + const rightwardLabelDeficit = Math.max(otherPoints[index][0], otherPoints[index + 1][0]) + + 4 - labelRect.x; + if (rightwardLabelDeficit > 0) { + outsideRightMinX = Math.max(outsideRightMinX, outsideRight + rightwardLabelDeficit * 2); + } + } + } + } + } + outsideRightMinX = Math.ceil(outsideRightMinX * 1000) / 1000; + const rightmostPlacedX = Math.max(outsideRight, rightmostNodeEdge, rightmostRoutedEdge); + let probeGrowth = Math.max( + 32, + labelRect?.width ?? 0, + rightmostPlacedX + 16 - outsideRightMinX, + ); + let lastInfeasibleX = outsideRight; + for (let probe = 0; probe < 7; probe += 1) { + if (outsideRightMinX > outsideRight + 0.0001) { + const expandedPoints = normalizeRoutePoints([ + start, + ...corridorViaX(start, end, fromSide, toSide, outsideRightMinX), + end, + ]); + if (readableCandidateIsFeasible(edge, expandedPoints, from, to, fromSide, toSide)) { + let feasibleX = outsideRightMinX; + let feasiblePoints = expandedPoints; + let infeasibleX = lastInfeasibleX; + for (let refinement = 0; + refinement < 53 && feasibleX - infeasibleX > 0.001; + refinement += 1) { + const midpointX = Math.ceil(((infeasibleX + feasibleX) / 2) * 1000) / 1000; + if (midpointX >= feasibleX - 0.0001) break; + const midpointPoints = normalizeRoutePoints([ + start, + ...corridorViaX(start, end, fromSide, toSide, midpointX), + end, + ]); + if (readableCandidateIsFeasible( + edge, + midpointPoints, + from, + to, + fromSide, + toSide, + )) { + feasibleX = midpointX; + feasiblePoints = midpointPoints; + } else { + infeasibleX = midpointX; + } + } + return feasiblePoints.slice(1, -1); + } + lastInfeasibleX = outsideRightMinX; + } + outsideRightMinX = Math.ceil((outsideRightMinX + probeGrowth) * 1000) / 1000; + probeGrowth *= 2; + } + } + const hasRelevantAbsolutePin = Array.isArray(edge.labelAt) + || [...pathCache.keys()].some((otherEdge) => ( + Array.isArray(otherEdge.labelAt) || hasAbsoluteRoutePins(otherEdge) + )); + if (hasRelevantAbsolutePin) classifyFailedAutomaticCandidatePins(edge, rawCandidates); + const horizontallyFacing = ( + fromSide === 'right' && toSide === 'left' && end[0] > start[0] + ) || ( + fromSide === 'left' && toSide === 'right' && start[0] > end[0] + ); + if (horizontallyFacing && from.col !== to.col) { + const fromCol = Math.min(from.col, to.col); + const toCol = Math.max(from.col, to.col); + const requiredRankGap = from.width / 2 + 32 + to.width / 2; + const actualRankGap = layout.colXs[toCol] - layout.colXs[fromCol]; + if (actualRankGap + 0.0001 < requiredRankGap) { + throw new WorkflowLayoutFeedback({ + kind: 'rank-gap-minimum', + fromCol, + toCol, + minimum: Math.ceil(requiredRankGap * 1000) / 1000, + edge: edge.id ?? null, + from: edge.from, + to: edge.to, + attemptedCandidateFamilies: rawCandidates.map(({ family }) => family), + candidateCount: rawCandidates.length, + }); + } + } + if (from.lane !== to.lane && layout.laneGap < 32) { + throw new WorkflowLayoutFeedback({ + kind: 'lane-gap-minimum', + minimum: 32, + edge: edge.id ?? null, + from: edge.from, + to: edge.to, + attemptedCandidateFamilies: rawCandidates.map(({ family }) => family), + candidateCount: rawCandidates.length, + }); + } + const message = `Workflow edge "${workflowEdgeName(edge)}" has no feasible readable-v2 automatic route.`; + throwDiagnosticError(message, [{ + code: 'workflow/solver-budget-exhausted', + severity: 'error', + message, + subject: { + diagramType: 'workflow', + edge: edge.id ?? null, + from: edge.from, + to: edge.to, + }, + evidence: { + attemptedCandidateFamilies: rawCandidates.map(({ family }) => family), + candidateCount: rawCandidates.length, + }, + supportedFixes: [], + }]); +} + +function readablePresetVia(edge, from, to, start, end, fromSide, toSide) { + const preset = edge.route; + let via; + switch (preset) { + case 'straight': + via = []; + break; + case 'drop': { + const y = gapYBetween(from.lane, to.lane, edge.bias ?? 0.5); + via = [[start[0], y], [end[0], y]]; + break; + } + case 'outside-right': { + const x = layout.laneX + layout.laneW + 12; + via = [[x, start[1]], [x, end[1]]]; + break; + } + case 'return-left': { + const x = Math.min(from.x, to.x) - 28; + via = [[x, start[1]], [x, end[1]]]; + break; + } + case 'bottom-channel': { + const y = Math.max(from.y + from.height, to.y + to.height) + 32; + via = [[start[0], y], [end[0], y]]; + break; + } + case 'up-channel': { + const y = Math.min(from.y, to.y) - 28; + via = [[start[0], y], [end[0], y]]; + break; + } + default: + return readableAutomaticVia(edge, from, to, start, end, fromSide, toSide); + } + const points = joinRoutePoints(start, via, end); + if (readableCandidateIsFeasible(edge, points, from, to, fromSide, toSide) + && routeMatchesPresetFamily(preset, points, from, to)) { + return points.slice(1, -1); + } + const message = `Workflow edge "${workflowEdgeName(edge)}" cannot satisfy route preset "${preset}" under readable-v2 constraints (minimum 8px endpoint stubs, 16px interior turns, and 28px direct clearance).`; + const edgeIndex = workflow.edges.indexOf(edge); + const edgeName = workflowEdgeName(edge); + const supportedFixes = []; + for (const candidatePreset of ['straight', 'drop', 'outside-right', 'return-left', 'bottom-channel', 'up-channel']) { + if (candidatePreset === preset) continue; + if (acceptsFix((document) => { + document.edges[edgeIndex].route = candidatePreset; + })) { + supportedFixes.push(`set edge "${edgeName}" route to verified preset "${candidatePreset}"`); + } + } + if (acceptsFix((document) => { + delete document.edges[edgeIndex].route; + })) { + supportedFixes.push(`remove route from edge "${edgeName}" so readable-v2 can use its verified automatic candidate`); + } + throwDiagnosticError(message, [{ + code: 'workflow/route-preset-conflict', + severity: 'error', + message, + subject: { + diagramType: 'workflow', + edge: edge.id ?? null, + from: edge.from, + to: edge.to, + route: preset, + }, + evidence: { + attemptedCandidateFamily: preset, + points, + fromSide, + toSide, + requiredEndpointStubPx: 8, + requiredInteriorSegmentPx: 16, + requiredDirectClearancePx: 28, + }, + supportedFixes, + }]); +} + +function routeVia( + edge, + from, + to, + start, + end, + fromSide, + toSide, + { validateReadablePreset = true } = {}, +) { + if (edge.via) return edge.via; + const hasCoordinatePins = edge.channelX !== undefined || edge.channelY !== undefined; + if (workflow.schema_version === 2 + && edge.route + && edge.route !== 'auto' + && !hasCoordinatePins + && validateReadablePreset) { + return readablePresetVia(edge, from, to, start, end, fromSide, toSide); + } + switch (edge.route || 'auto') { + case 'straight': + return []; + case 'drop': { + const y = edge.channelY ?? gapYBetween(from.lane, to.lane, edge.bias ?? 0.5); + return [[start[0], y], [end[0], y]]; + } + case 'outside-right': { + const x = edge.channelX ?? layout.laneX + layout.laneW + 12; + return [[x, start[1]], [x, end[1]]]; + } + case 'return-left': { + const x = edge.channelX ?? Math.min(from.x, to.x) - 28; + return [[x, start[1]], [x, end[1]]]; + } + case 'bottom-channel': { + const y = edge.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 32; + return [[start[0], y], [end[0], y]]; + } + case 'up-channel': { + const y = edge.channelY ?? Math.min(from.y, to.y) - 28; + return [[start[0], y], [end[0], y]]; + } + case 'auto': + default: { + if (workflow.schema_version === 2) { + if (edge.channelX !== undefined && edge.channelY !== undefined) { + return [[edge.channelX, start[1]], [edge.channelX, edge.channelY], [end[0], edge.channelY]]; + } + if (edge.channelX !== undefined) return [[edge.channelX, start[1]], [edge.channelX, end[1]]]; + if (edge.channelY !== undefined) return [[start[0], edge.channelY], [end[0], edge.channelY]]; + return readableAutomaticVia(edge, from, to, start, end, fromSide, toSide); + } + if (from.lane === to.lane) return sameLaneAutoVia(start, end); + const oneBendVia = oneBendCrossLaneVia(edge, start, end, fromSide, toSide); + if (oneBendVia) return oneBendVia; + const y = gapYBetween(from.lane, to.lane, edge.bias ?? 0.5); + return [[start[0], y], [end[0], y]]; + } + } +} + +function workflowEdgeLabelPoint(edge, points) { + if (workflow.schema_version === 1) { + if (edge.labelAt || Number.isInteger(edge.labelSegment) || points.length !== 3) { + return labelPoint(edge, points); + } + const segmentLengths = [0, 1].map((index) => Math.hypot( + points[index + 1][0] - points[index][0], + points[index + 1][1] - points[index][1], + )); + const labelSegment = segmentLengths[0] >= segmentLengths[1] ? 0 : 1; + const point = labelPoint({ ...edge, labelSegment }, points); + if (points[labelSegment][0] === points[labelSegment + 1][0]) point[1] += 10; + return point; + } + if (!edge.labelAt && !Number.isInteger(edge.labelSegment) + && edge.labelDx === undefined && edge.labelDy === undefined + && points.length === 2 && points[0][0] === points[1][0]) { + // Center implicit v2 vertical labels in the corridor. Authored offsets + // retain their existing source-relative anchor, including explicit zero. + return [points[0][0], (points[0][1] + points[1][1]) / 2]; + } + if (edge.labelAt || Number.isInteger(edge.labelSegment) || points.length <= 2) { + return labelPoint(edge, points); + } + const segments = points.slice(0, -1).map((point, index) => ({ + index, + horizontal: Math.abs(points[index + 1][1] - point[1]) <= 0.0001, + length: Math.hypot( + points[index + 1][0] - point[0], + points[index + 1][1] - point[1], + ), + })).sort((left, right) => ( + Number(right.horizontal) - Number(left.horizontal) + || right.length - left.length + || left.index - right.index + )); + const labelSegment = segments[0]?.index ?? 0; + const point = labelPoint({ ...edge, labelSegment }, points); + if (points[labelSegment][0] === points[labelSegment + 1][0]) point[1] += 10; + return point; +} + +function edgeSides(edge) { + const from = nodes.get(edge.from); + const to = nodes.get(edge.to); + const resolved = workflow.schema_version === 2 ? readableSideCache.get(edge) : null; + if (resolved) return resolved; + const oneBendSides = automaticOneBendSides(edge, from, to); + if (oneBendSides) return oneBendSides; + if (workflow.schema_version === 2 + && layout.channelLabelEdgeKeys?.has(stableValueKey(edge)) + && !edge.fromSide + && !edge.toSide) { + return { fromSide: 'top', toSide: 'top' }; + } + return { + fromSide: chosenSide(edge.fromSide, defaultFromSide(from, to)), + toSide: chosenSide(edge.toSide, defaultToSide(from, to)), + }; +} + +const automaticPorts = automaticPortSpread(workflow.edges, nodes, { + sideFor: (edge, endpoint) => edgeSides(edge)[endpoint === 'source' ? 'fromSide' : 'toSide'], +}); + +function automaticPortCandidates(edge, node, side, preferred, counterpart) { + if (!independentAutomaticRoute(edge)) return [preferred]; + const verticalSide = side === 'left' || side === 'right'; + const axis = verticalSide ? 1 : 0; + const center = anchor(node, side); + const occupied = []; + for (const [other, routed] of nodeRoutes.get(node.id) ?? []) { + for (const endpoint of ['from', 'to']) { + if (other[endpoint] !== node.id) continue; + const point = endpoint === 'from' ? routed.points[0] : routed.points.at(-1); + const ownWidth = edge.width || (edge.variant === 'emphasis' ? 1.8 : 1.4); + const otherWidth = other.width || (other.variant === 'emphasis' ? 1.8 : 1.4); + if (Math.abs(point[1 - axis] - center[1 - axis]) < 0.0001) { + occupied.push({ coordinate: point[axis], clearance: Math.max(12, 3.5 * (ownWidth + otherWidth)) }); + } + } + } + const clear = (point) => occupied.every(({ coordinate, clearance }) => Math.abs(coordinate - point[axis]) >= clearance - 0.0001); + if (clear(preferred)) return [preferred]; + const candidates = []; + const extent = verticalSide ? node.height : node.width; + const direction = (verticalSide ? counterpart.cy : counterpart.cx) >= center[axis] ? 1 : -1; + for (let offset = 0; offset <= extent / 2 - 16; offset += 12) { + for (const sign of [direction, -direction]) { + const point = [...center]; + point[axis] += sign * offset; + if (clear(point) && !candidates.some((candidate) => candidate[axis] === point[axis])) candidates.push(point); + } + if (candidates.length >= 2) return candidates.slice(0, 2); + } + return candidates.length ? candidates : [preferred]; +} + +function readableAutomaticRoute(edge, from, to, primarySides, primaryPorts) { + const authoredFrom = edge.fromSide && edge.fromSide !== 'auto' ? edge.fromSide : null; + const authoredTo = edge.toSide && edge.toSide !== 'auto' ? edge.toSide : null; + const sideOrder = ['right', 'bottom', 'left', 'top']; + const sidePairs = [primarySides]; + for (const fromSide of authoredFrom ? [authoredFrom] : sideOrder) { + for (const toSide of authoredTo ? [authoredTo] : sideOrder) { + sidePairs.push({ fromSide, toSide }); + } + } + + const naturalFromSide = authoredFrom || defaultFromSide(from, to); + const naturalToSide = authoredTo || defaultToSide(from, to); + const seen = new Set(); + const plans = []; + const feedback = []; + let firstFailure = null; + // Spreading ports for provisional sides can offset an otherwise clear + // facing pair. Keep the aligned route in the same feasibility/cost contest. + const alignedStart = automaticPortCandidates(edge, from, naturalFromSide, anchor(from, naturalFromSide), to)[0]; + const alignedEnd = automaticPortCandidates(edge, to, naturalToSide, anchor(to, naturalToSide), from)[0]; + const alignedPoints = [alignedStart, alignedEnd]; + if (to.col > from.col && !['return', 'error'].includes(edge.role) + && !edge.fromSide && !edge.toSide && !edge.labelAt + && readableCandidateIsFeasible(edge, alignedPoints, from, to, naturalFromSide, naturalToSide)) { + plans.push({ + family: 'aligned-facing', + points: alignedPoints, + fromSide: naturalFromSide, + toSide: naturalToSide, + cost: readableCandidateCost(edge, alignedPoints, -1, naturalFromSide, naturalToSide), + }); + } + for (const candidateSides of sidePairs) { + if (authoredFrom && candidateSides.fromSide !== authoredFrom) continue; + if (authoredTo && candidateSides.toSide !== authoredTo) continue; + const key = `${candidateSides.fromSide}:${candidateSides.toSide}`; + if (seen.has(key)) continue; + const pairOrdinal = seen.size; + seen.add(key); + const primary = pairOrdinal === 0; + const preferredStart = primaryPorts?.from && primary + ? primaryPorts.from + : anchor(from, candidateSides.fromSide); + const preferredEnd = primaryPorts?.to && primary + ? primaryPorts.to + : anchor(to, candidateSides.toSide); + const starts = automaticPortCandidates(edge, from, candidateSides.fromSide, preferredStart, to); + const ends = automaticPortCandidates(edge, to, candidateSides.toSide, preferredEnd, from); + const [start] = starts; + const [end] = ends; + const planned = readableAutomaticCandidateSet( + edge, + from, + to, + start, + end, + candidateSides.fromSide, + candidateSides.toSide, + { + ordinalOffset: pairOrdinal * 9, + naturalFromSide, + naturalToSide, + }, + ); + plans.push(...planned.candidates.map((candidate) => ({ + ...candidate, + ...candidateSides, + }))); + let portOrdinal = 0; + for (const alternativeStart of starts) { + for (const alternativeEnd of ends) { + if (alternativeStart === start && alternativeEnd === end) continue; + portOrdinal += 1; + const alternative = readableAutomaticCandidateSet(edge, from, to, alternativeStart, alternativeEnd, + candidateSides.fromSide, candidateSides.toSide, { + ordinalOffset: pairOrdinal * 9 + portOrdinal * 144, + naturalFromSide, + naturalToSide, + }); + plans.push(...alternative.candidates.map((candidate) => ({ ...candidate, ...candidateSides }))); + if (!alternative.candidates.length) { + try { + const via = withDiagnosticRecordingSuppressed(() => readableAutomaticVia( + edge, from, to, alternativeStart, alternativeEnd, candidateSides.fromSide, candidateSides.toSide, + )); + const points = joinRoutePoints(alternativeStart, via, alternativeEnd); + plans.push({ + points, ...candidateSides, + cost: readableCandidateCost(edge, points, pairOrdinal * 9 + portOrdinal * 144 + 6, naturalFromSide, naturalToSide), + }); + } catch (error) { + if (error instanceof WorkflowLayoutFeedback) feedback.push({ error, pairOrdinal }); + else if (!firstFailure) firstFailure = error; + } + } + } + } + if (planned.candidates.length) continue; + try { + const expandedVia = withDiagnosticRecordingSuppressed(() => readableAutomaticVia( + edge, + from, + to, + start, + end, + candidateSides.fromSide, + candidateSides.toSide, + )); + const expandedPoints = joinRoutePoints(start, expandedVia, end); + const outsideRightOrdinal = planned.rawCandidates.findIndex(({ family }) => ( + family === 'outside-right' + )); + const ordinal = pairOrdinal * 9 + Math.max(0, outsideRightOrdinal); + plans.push({ + family: 'outside-right', + ordinal, + points: expandedPoints, + cost: readableCandidateCost( + edge, + expandedPoints, + ordinal, + naturalFromSide, + naturalToSide, + ), + ...candidateSides, + }); + } catch (error) { + if (error instanceof WorkflowLayoutFeedback) { + feedback.push({ error, pairOrdinal }); + } else if (!firstFailure) { + firstFailure = error; + } + } + } + + plans.sort((left, right) => compareCost(left.cost, right.cost)); + if (plans.length) { + const repairSeeds = plans[0].cost.sharedCorridorPx > 0 + ? plans.filter((plan, index) => plans.findIndex(other => ( + other.fromSide === plan.fromSide && other.toSide === plan.toSide + && String(other.points[0]) === String(plan.points[0]) + && String(other.points.at(-1)) === String(plan.points.at(-1)) + )) === index).slice(0, 8) : []; + // A crowded fan-out can exhaust the fixed corridor coordinates even with + // distinct ports. Repair only interior segments of this automatic route; + // One pass over at most eight seeds and four offsets bounds the search. + for (const selected of repairSeeds) { + if (selected.cost.sharedCorridorPx === 0) continue; + const alternatives = []; + for (let segment = 1; segment < selected.points.length - 2; segment += 1) { + const a = selected.points[segment], b = selected.points[segment + 1]; + const axis = a[0] === b[0] ? 0 : 1; + for (const offset of [-16, 16, -32, 32]) { + const points = selected.points.map(point => [...point]); + points[segment][axis] += offset; + points[segment + 1][axis] += offset; + if (!readableCandidateIsFeasible(edge, points, from, to, selected.fromSide, selected.toSide)) continue; + const cost = readableCandidateCost(edge, points, selected.cost.stableCandidateOrdinal, naturalFromSide, naturalToSide); + if (compareCost(cost, selected.cost) < 0) alternatives.push({ ...selected, points, cost }); + } + } + alternatives.sort((a, b) => compareCost(a.cost, b.cost)); + if (alternatives.length) plans.push(alternatives[0]); + } + plans.sort((a, b) => compareCost(a.cost, b.cost)); + const selected = plans[0]; + return { + points: selected.points, + fromSide: selected.fromSide, + toSide: selected.toSide, + }; + } + + const feedbackPriority = { + 'rank-gap-minimum': 0, + 'lane-gap-minimum': 1, + }; + feedback.sort((left, right) => ( + (feedbackPriority[left.error.request?.kind] ?? 99) + - (feedbackPriority[right.error.request?.kind] ?? 99) + || left.pairOrdinal - right.pairOrdinal + )); + if (feedback.length) throw feedback[0].error; + const authoredSideFields = [ + ...(authoredFrom ? ['fromSide'] : []), + ...(authoredTo ? ['toSide'] : []), + ]; + if (authoredSideFields.length) { + const alternatives = verifiedPinRemovalAlternatives( + edge, + authoredSideFields, + 'so readable-v2 can replan the remaining endpoint-side pins', + ); + const sourceAnchor = primaryPorts?.from || anchor(from, primarySides.fromSide); + const targetAnchor = primaryPorts?.to || anchor(to, primarySides.toSide); + const attemptedEvidence = firstFailure?.archifyDiagnostics?.[0]?.evidence || {}; + throwExplicitPinConflict(edge, 'readable route feasibility with authored endpoint sides', { + conflictingPins: conflictPinsFromRemovalSets( + edge, + alternatives.removalSets, + authoredSideFields, + ), + actualCoordinates: { + sourceAnchor: [...sourceAnchor], + targetAnchor: [...targetAnchor], + }, + fromSide: primarySides.fromSide, + toSide: primarySides.toSide, + ...(attemptedEvidence.attemptedCandidateFamilies + ? { attemptedCandidateFamilies: attemptedEvidence.attemptedCandidateFamilies } + : {}), + ...(attemptedEvidence.candidateCount !== undefined + ? { candidateCount: attemptedEvidence.candidateCount } + : {}), + }, alternatives.supportedFixes); + } + if (firstFailure) throw firstFailure; + throw new Error('readable-v2 automatic route enumeration produced no result'); +} + +function isReadableControlledRoute(edge) { + return workflow.schema_version === 2 && ( + Array.isArray(edge.via) + || edge.channelX !== undefined + || edge.channelY !== undefined + || (edge.route && edge.route !== 'auto') + ); +} + +function readableControlledRoute(edge, from, to) { + const authoredFrom = edge.fromSide && edge.fromSide !== 'auto' ? edge.fromSide : null; + const authoredTo = edge.toSide && edge.toSide !== 'auto' ? edge.toSide : null; + const naturalFromSide = authoredFrom || defaultFromSide(from, to); + const naturalToSide = authoredTo || defaultToSide(from, to); + const sideOrder = ['right', 'bottom', 'left', 'top']; + const preferredPairs = [{ + fromSide: naturalFromSide, + toSide: naturalToSide, + }]; + for (const fromSide of authoredFrom ? [authoredFrom] : sideOrder) { + for (const toSide of authoredTo ? [authoredTo] : sideOrder) { + preferredPairs.push({ fromSide, toSide }); + } + } + + const seen = new Set(); + const sidePairs = preferredPairs.filter(({ fromSide, toSide }) => { + if (authoredFrom && fromSide !== authoredFrom) return false; + if (authoredTo && toSide !== authoredTo) return false; + const key = `${fromSide}:${toSide}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + const hasAbsoluteRoutePins = Array.isArray(edge.via) + || edge.channelX !== undefined + || edge.channelY !== undefined; + const candidates = []; + const diagnosticCandidates = []; + const materializedCandidates = []; + for (const [ordinal, { fromSide, toSide }] of sidePairs.entries()) { + const start = anchor(from, fromSide); + const end = anchor(to, toSide); + const via = routeVia( + edge, + from, + to, + start, + end, + fromSide, + toSide, + { validateReadablePreset: false }, + ); + const authoredPoints = [start, ...via, end]; + const points = hasAbsoluteRoutePins + ? authoredPoints + : normalizeRoutePoints(authoredPoints); + const materialized = { points, fromSide, toSide, ordinal }; + materializedCandidates.push(materialized); + if (points.length >= 2 + && points.every((point) => ( + Array.isArray(point) && point.length === 2 && isFinitePoint(...point) + )) + && routeHonorsEndpointSides(points, fromSide, toSide)) { + diagnosticCandidates.push(materialized); + } + if (!readableCandidateIsFeasible(edge, points, from, to, fromSide, toSide)) continue; + if (edge.route && edge.route !== 'auto' && !routeMatchesPresetFamily( + edge.route, + points, + from, + to, + )) continue; + if (presentChannelPins(edge).some((field) => ( + !routeContainsChannelPin(points, field, edge[field]) + ))) continue; + candidates.push({ + points, + fromSide, + toSide, + cost: readableCandidateCost( + edge, + points, + ordinal, + naturalFromSide, + naturalToSide, + ), + }); + } + candidates.sort((left, right) => compareCost(left.cost, right.cost)); + if (candidates.length) return candidates[0]; + + // Absolute geometry is authoritative even when it is invalid. Preserve the + // best endpoint-side inference so validation can diagnose the authored + // segment or preset that actually failed instead of silently falling back to + // default sides and changing the route's meaning. + if (hasAbsoluteRoutePins) { + return diagnosticCandidates[0] || materializedCandidates[0] || null; + } + + // Preset-only routes retain their dedicated typed conflict (and verified + // alternative search) when exhaustive side inference found no valid plan. + const fallback = materializedCandidates[0]; + if (!fallback) return null; + const fallbackVia = readablePresetVia( + edge, + from, + to, + fallback.points[0], + fallback.points.at(-1), + fallback.fromSide, + fallback.toSide, + ); + return { + ...fallback, + points: normalizeRoutePoints([ + fallback.points[0], + ...fallbackVia, + fallback.points.at(-1), + ]), + }; +} + +// Registering a routed path also publishes it (and its label box) to the grid. +let obstacleSequence = 0; +function registerRouted(edge, routed) { + pathCache.set(edge, routed); + const sequence = obstacleSequence++; + const routeExtent = routeBounds(routed.points); + obstacleGrid.insert(routeExtent, { kind: 'route', edge, routed, sequence, bounds: routeExtent }); + for (const [x] of routed.points) rightmostRoutedEdge = Math.max(rightmostRoutedEdge, x); + for (const end of ['from', 'to']) { + const nodeId = edge[end]; + if (nodeId === undefined || nodeId === null) continue; + let byEdge = nodeRoutes.get(nodeId); + if (!byEdge) { byEdge = new Map(); nodeRoutes.set(nodeId, byEdge); } + byEdge.set(edge, routed); + } + const index = edgeIndexByEdge.get(edge); + const label = index === undefined ? null : labelRectFor(edge, index); + if (label) { + obstacleGrid.insert(rectToBounds(label), { kind: 'label', edge, rect: label, sequence }); + rightmostRoutedEdge = Math.max(rightmostRoutedEdge, label.x + label.width); + } + return routed; +} + +function pathFor(edge) { + if (pathCache.has(edge)) return pathCache.get(edge); + const from = nodes.get(edge.from); + const to = nodes.get(edge.to); + if (isReadableControlledRoute(edge)) { + const planned = readableControlledRoute(edge, from, to); + if (planned) { + readableSideCache.set(edge, { + fromSide: planned.fromSide, + toSide: planned.toSide, + }); + const routed = { d: polylinePath(planned.points), points: planned.points }; + return registerRouted(edge, routed); + } + } + const ports = automaticPorts.get(edge); + const { fromSide, toSide } = edgeSides(edge); + const readableAutomatic = workflow.schema_version === 2 + && !Array.isArray(edge.via) + && edge.channelX === undefined + && edge.channelY === undefined + && (!edge.route || edge.route === 'auto'); + if (readableAutomatic) { + const planned = readableAutomaticRoute( + edge, + from, + to, + { fromSide, toSide }, + ports, + ); + readableSideCache.set(edge, { + fromSide: planned.fromSide, + toSide: planned.toSide, + }); + const routed = { d: polylinePath(planned.points), points: planned.points }; + return registerRouted(edge, routed); + } + const start = ports?.from || anchor(from, fromSide); + const end = ports?.to || anchor(to, toSide); + const authoredPoints = [start, ...routeVia(edge, from, to, start, end, fromSide, toSide), end]; + const hasAbsoluteRoutePins = Array.isArray(edge.via) + || edge.channelX !== undefined + || edge.channelY !== undefined; + const points = workflow.schema_version === 2 && !hasAbsoluteRoutePins + ? normalizeRoutePoints(authoredPoints) + : authoredPoints; + const routed = { d: polylinePath(points), points }; + return registerRouted(edge, routed); +} + +// The label rectangle follows from the routed path, which is fixed once it is +// cached; keying on that object means a re-routed edge gets a fresh rectangle. +const LABEL_RECT_CACHE = new WeakMap(); + +function labelRectFor(edge, relationIndex) { + if (!edge.label || !nodes.has(edge.from) || !nodes.has(edge.to)) return null; + const routed = pathFor(edge); + const cached = LABEL_RECT_CACHE.get(routed); + if (cached) return cached; + const [lx, ly] = workflowEdgeLabelPoint(edge, routed.points); + const width = workflowLabelWidth(edge.label); + const rect = { + relation: edge, + relationIndex, + label: edge.label, + x: lx - width / 2, + y: ly - 10, + width, + height: 14, + lx, + ly, + }; + LABEL_RECT_CACHE.set(routed, rect); + return rect; +} + +function measuredContentBounds() { + let left = layout.laneX; + let top = 27; + let right = layout.laneX + layout.laneW; + let bottom = legendY() + 18; + const owners = { + left: 'workflow lanes', + top: asArray(workflow.phases).length ? 'phase header band' : 'workflow top padding', + right: 'workflow lanes', + bottom: workflowLegendEntries.length ? 'legend' : 'workflow lanes and bottom padding', + }; + const includePoint = ([x, y], contributor) => { + if (x < left) { + left = x; + owners.left = contributor; + } + if (y < top) { + top = y; + owners.top = contributor; + } + if (x > right) { + right = x; + owners.right = contributor; + } + if (y > bottom) { + bottom = y; + owners.bottom = contributor; + } + }; + const includeRect = (rect, contributor) => { + includePoint([rect.x, rect.y], contributor); + includePoint([rect.x + rect.width, rect.y + rect.height], contributor); + }; + + for (const node of nodes.values()) includeRect(node, `node ${node.id}`); + for (const [index, edge] of workflow.edges.entries()) { + if (!nodes.has(edge.from) || !nodes.has(edge.to)) continue; + for (const point of pathFor(edge).points) includePoint(point, `edge ${edge.id || index}`); + const label = labelRectFor(edge, index); + if (label) includeRect(label, `edge ${edge.id || index} label mask`); + } + for (const phase of asArray(workflow.phases)) { + if (!Number.isInteger(phase.fromCol) || !Number.isInteger(phase.toCol) + || phase.fromCol < 0 || phase.toCol >= layout.colXs.length || phase.fromCol > phase.toCol) continue; + const span = phaseSpan(phase); + includeRect({ x: span.x, y: 27, width: span.width, height: 16 }, `phase ${phase.id}`); + } + for (const group of asArray(workflow.groups)) { + if (!laneIndex.has(group.lane) || !Number.isInteger(group.fromCol) || !Number.isInteger(group.toCol) + || group.fromCol < 0 || group.toCol >= layout.colXs.length || group.fromCol > group.toCol) continue; + const span = groupSpan(group); + includeRect({ + x: span.x, + y: laneTop(group.lane) + layout.laneTitleH + GROUP_FRAME_TOP_INSET, + width: span.width, + height: workflow.schema_version === 2 + ? laneHeight(group.lane) - layout.laneTitleH + - GROUP_FRAME_TOP_INSET - GROUP_FRAME_BOTTOM_INSET + : layout.laneH - layout.laneTitleH - 16, + }, `group ${group.id}`); + if (workflow.schema_version === 2) { + const frameY = laneTop(group.lane) + layout.laneTitleH + GROUP_FRAME_TOP_INSET; + const labelBaseline = frameY + GROUP_LABEL_BASELINE_OFFSET; + includeRect({ + x: span.x + 10, + y: labelBaseline - GROUP_LABEL_MASK_ASCENT, + width: textUnits(group.label) * 5.6, + height: GROUP_LABEL_MASK_H, + }, `group ${group.id} label`); + } + } + if (workflowLegendEntries.length) { + for (const rect of workflowLegendRects()) includeRect(rect, `legend ${rect.kind}`); + } + return { + left, + top, + right, + bottom, + contributors: [...new Set([ + ...Object.values(owners), + ...asArray(layout.widthContributors), + ...asArray(layout.heightContributors), + ])], + }; +} + +function finalizeReadableViewBox() { + if (workflow.schema_version !== 2) { + requiredViewBox = [...viewBox]; + return; + } + const bounds = measuredContentBounds(); + requiredViewBox = [ + Math.max(minimumCanvasWidth, Math.ceil(bounds.right + 16)), + Math.max(autoHeight, Math.ceil(bounds.bottom + 18)), + ]; + const outsideOrigin = bounds.left < 0 || bounds.top < 0; + if (outsideOrigin) { + const hasAbsolutePins = hasAbsoluteWorkflowPins(workflow); + const message = `Workflow geometry extends above or left of the viewBox origin (${Math.round(bounds.left)}, ${Math.round(bounds.top)}).`; + throwDiagnosticError(message, [{ + code: hasAbsolutePins ? 'workflow/explicit-pin-conflict' : 'workflow/solver-budget-exhausted', + severity: 'error', + message, + subject: { diagramType: 'workflow', path: '/meta/viewBox' }, + evidence: { + actualViewBox: [...viewBox], + requiredViewBox: [...requiredViewBox], + contentBounds: [bounds.left, bounds.top, bounds.right, bounds.bottom], + contributors: bounds.contributors, + }, + supportedFixes: [], + }]); + } + if (!workflow.meta?.viewBox) { + viewBox = [...requiredViewBox]; + return; + } + const tooNarrow = viewBox[0] < requiredViewBox[0]; + const tooShort = viewBox[1] < requiredViewBox[1]; + if (!tooNarrow && !tooShort) return; + const message = `Workflow viewBox ${viewBox[0]}×${viewBox[1]} cannot contain the readable-v2 layout; minimum ${requiredViewBox[0]}×${requiredViewBox[1]}.`; + const supportedFixes = []; + if (acceptsFix((document) => { + document.meta.viewBox = [...requiredViewBox]; + })) { + supportedFixes.push(`set meta.viewBox to at least [${requiredViewBox[0]}, ${requiredViewBox[1]}]`); + } + if (acceptsFix((document) => { + delete document.meta.viewBox; + })) { + supportedFixes.push('omit meta.viewBox so the compiler can use its measured intrinsic canvas'); + } + throwDiagnosticError(message, [{ + code: 'workflow/viewbox-capacity', + severity: 'error', + message, + subject: { diagramType: 'workflow', path: '/meta/viewBox' }, + evidence: { + actualViewBox: [...viewBox], + requiredViewBox: [...requiredViewBox], + contentBounds: [bounds.left, bounds.top, bounds.right, bounds.bottom], + contributors: bounds.contributors, + }, + supportedFixes, + }]); +} + +function renderLane(lane, index) { + const y = laneTop(lane.id); + const height = laneHeight(index); + const exception = lane.variant === 'exception' + ? `\n ` + : ''; + const labelClass = lane.variant === 'exception' ? 't-security' : 't-dim'; + const prefix = lane.variant === 'exception' ? 'EX' : String(index + 1).padStart(2, '0'); + return ` ${exception} + ${prefix} / ${esc(lane.label)}`; +} + +function renderPhase(phase) { + const span = phaseSpan(phase); + const accent = variantAccent(phase.variant); + const [lineClass] = arrowClassMap[phase.variant || 'default'] || arrowClassMap.default; + return ` + + ${esc(phase.label)}`; +} + +function renderGroup(group, index) { + const span = groupSpan(group); + const y = laneTop(group.lane) + layout.laneTitleH + GROUP_FRAME_TOP_INSET; + const height = workflow.schema_version === 2 + ? laneHeight(group.lane) - layout.laneTitleH + - GROUP_FRAME_TOP_INSET - GROUP_FRAME_BOTTOM_INSET + : layout.laneH - layout.laneTitleH - 16; + const cls = group.variant === 'security' ? 'c-security-group' : 'c-lane'; + const textClass = variantAccent(group.variant); + const labelY = workflow.schema_version === 2 ? y + GROUP_LABEL_BASELINE_OFFSET : y + 14; + return ` + ${esc(group.label)}`; +} + +function renderNode(node) { + const fill = componentFill[node.type] || 'c-external'; + const accent = componentText[node.type] || 't-muted'; + const hasSub = node.sublabel != null && node.sublabel !== ''; + const labelFontSize = fittedNodeFontSize(node.label, brandLabelFitWidth(node, node.width), nodeTextFit.labelPreferred, nodeTextFit.labelMinimum); + const sublabelFontSize = fittedNodeFontSize(node.sublabel, node.width, nodeTextFit.sublabelPreferred, nodeTextFit.sublabelMinimum); + const tagFontSize = fittedNodeFontSize(node.tag, node.width, nodeTextFit.tagPreferred, nodeTextFit.tagMinimum); + const textRows = [{ text: node.label, font: labelFontSize, y: 21 }]; + if (hasSub) textRows.push({ text: node.sublabel, font: sublabelFontSize, y: 38 }); + if (node.tag) textRows.push({ text: node.tag, font: tagFontSize, y: node.height - 12 }); + const labelLayout = nodeLabelLayout({ width: node.width, height: node.height, rows: textRows, + brand: Boolean(brandMarkFor(node)), source: Boolean(sourceEvidence?.nodes?.[node.id]?.length) }); + const sub = hasSub + ? `\n ${esc(node.sublabel)}` + : ''; + const tag = node.tag + ? `\n ${esc(node.tag)}` + : ''; + const brand = renderBrandMark(node, { x: node.x + node.width - 22, y: node.y + 6 }); + const passport = { kind: node.type, sublabel: node.sublabel, tag: node.tag, context: nodeContext(node), ...brandMetadataFor(node) }; + return ` + ${focusNodeTitle(node.label, passport)} + + + ${renderSemanticSigil(node.type, { icon: node.icon, x: node.x + 6, y: node.y + labelLayout.sigilY, size: labelLayout.sigilSize })}${brand ? `\n ${brand}` : ''} + ${esc(node.label)}${sub}${tag} + `; +} + +function renderEdgePath(edge, index) { + const [cls, marker] = arrowClassMap[edge.variant || 'default'] || arrowClassMap.default; + const routed = pathFor(edge); + const strokeWidth = edge.width || (edge.variant === 'emphasis' ? 1.8 : 1.4); + const routing = independentAutomaticRoute(edge) ? ' data-composition-routing="workflow-v2-auto"' : ''; + const role = workflow.schema_version === 2 ? ` data-edge-role="${esc(edge.role || '')}"` : ''; + return ` `; +} + +function renderEdgeLabel(edge, index) { + if (!edge.label) return ''; + const routed = pathFor(edge); + const [lx, ly] = workflowEdgeLabelPoint(edge, routed.points); + const labelW = workflowLabelWidth(edge.label); + return ` + + ${esc(edge.label)} + `; +} + +function renderLegend() { + const obstacles = workflow.schema_version === 2 + ? relationshipLegendObstacles(workflow.edges, { + pointsFor: (edge) => pathFor(edge).points, + labelRectFor, + }) + : []; + return renderResolvedLegend({ + entries: workflowLegendEntries, + locale: workflow.meta.locale, + layout: workflowLegendLayout(obstacles), + renderSwatch: (entry) => ``, + }); +} + +function renderSvg() { + const readerFit = workflow.schema_version === 2 + && !workflow.meta?.viewBox + && hasVerticalStack(workflow) + && asArray(layout.laneHeights).some((height) => height > 104) + ? ' data-reader-fit="intrinsic-height"' + : ''; + const contract = workflow.schema_version === 2 ? ' data-layout-contract="readable-v2"' : ''; + return ` +${svgAccessibleText(workflow.meta, 'workflow')} +${renderDefinitions()} + + + + + +${workflow.lanes.map(renderLane).join('\n\n')} + + +${asArray(workflow.phases).map(renderPhase).join('\n')} + + +${asArray(workflow.groups).map(renderGroup).join('\n')} + + +${workflow.edges.map(renderEdgePath).join('\n')} + + +${[...nodes.values()].map(renderNode).join('\n\n')} + + +${workflow.edges.map(renderEdgeLabel).join('\n')} + + +${renderLegend()} + `; +} + + + try { + validateReadableInputsBeforeRouting(); + validateReadablePinnedGeometry(); + validateWorkflow(); + finalizeReadableViewBox(); + const svg = renderSvg(); + const receipt = { + contract: layout.contract, + viewBox: [...viewBox], + requiredViewBox: [...requiredViewBox], + columns: [...layout.colXs], + nodes: [...nodes.values()].map((node) => ({ + id: node.id, + lane: node.lane, + col: node.col, + x: node.x, + y: node.y, + width: node.width, + height: node.height, + })), + edges: workflow.edges.map((edge) => ({ + id: edge.id ?? null, + from: edge.from, + to: edge.to, + points: pathFor(edge).points.map((point) => [...point]), + })), + labels: workflow.edges.flatMap((edge) => { + if (!edge.label || !nodes.has(edge.from) || !nodes.has(edge.to)) return []; + const [x, y] = workflowEdgeLabelPoint(edge, pathFor(edge).points); + return [{ edge: edge.id ?? null, label: edge.label, x, y, width: workflowLabelWidth(edge.label), height: 14 }]; + }), + diagnostics: workflowDiagnostics, + }; + return { ok: true, svg, receipt }; + } catch (error) { + if (!Array.isArray(error?.archifyDiagnostics)) throw error; + const diagnostics = error.archifyDiagnostics.map((diagnostic) => ({ ...diagnostic })); + return compilerFailure(layout.contract, diagnostics, error.message); + } +} + +function feedbackFailure(request) { + const message = `Workflow edge "${request.edge || `${request.from}->${request.to}`}" exhausted bounded readable-v2 layout feedback without a feasible automatic route.`; + const diagnostics = [{ + code: 'workflow/solver-budget-exhausted', + severity: 'error', + message, + subject: { + diagramType: 'workflow', + edge: request.edge, + from: request.from, + to: request.to, + }, + evidence: { + attemptedCandidateFamilies: request.attemptedCandidateFamilies, + candidateCount: request.candidateCount, + }, + supportedFixes: [], + }]; + return compilerFailure('readable-v2', diagnostics, message); +} + +function compileWorkflowWithFeedback({ workflow, qualityProfile, sourceEvidence, discoverFixes = true } = {}) { + let layoutFeedback = {}; + for (let attempt = 0; attempt <= MAX_READABLE_LAYOUT_FEEDBACK_ROUNDS; attempt += 1) { + try { + return compileWorkflowInternal({ + workflow, + qualityProfile, + sourceEvidence, + discoverFixes, + layoutFeedback, + }); + } catch (error) { + if (!(error instanceof WorkflowLayoutFeedback)) throw error; + const request = error.request; + let nextFeedback = null; + if (request.kind === 'rank-gap-minimum' + && Number.isInteger(request.fromCol) + && Number.isInteger(request.toCol) + && Number.isFinite(request.minimum)) { + const key = `${request.fromCol}:${request.toCol}`; + const current = layoutFeedback.rankGapMinimums?.[key] ?? -Infinity; + if (request.minimum > current + 0.0001) { + nextFeedback = { + ...layoutFeedback, + rankGapMinimums: { + ...(layoutFeedback.rankGapMinimums || {}), + [key]: request.minimum, + }, + rankGapContributors: { + ...(layoutFeedback.rankGapContributors || {}), + [key]: [ + `rank ${request.fromCol}→${request.toCol} route clearance`, + `edge ${request.edge || `${request.from}->${request.to}`} route`, + ], + }, + }; + } + } else if (request.kind === 'lane-gap-minimum' + && Number.isFinite(request.minimum) + && request.minimum > (layoutFeedback.laneGapMin ?? -Infinity) + 0.0001) { + nextFeedback = { + ...layoutFeedback, + laneGapMin: request.minimum, + laneGapContributors: [ + `edge ${request.edge || `${request.from}->${request.to}`} lane-gap route clearance`, + ], + }; + } + if (!nextFeedback || attempt === MAX_READABLE_LAYOUT_FEEDBACK_ROUNDS) { + return feedbackFailure(request); + } + layoutFeedback = nextFeedback; + } + } + throw new Error('unreachable readable-v2 layout feedback state'); +} + +export function compileWorkflow({ workflow, qualityProfile, sourceEvidence } = {}) { + return compileWorkflowWithFeedback({ workflow, qualityProfile, sourceEvidence }); +} diff --git a/vendor/archify/renderers/workflow/workflow-migration-geometry.mjs b/vendor/archify/renderers/workflow/workflow-migration-geometry.mjs new file mode 100644 index 0000000..0053387 --- /dev/null +++ b/vendor/archify/renderers/workflow/workflow-migration-geometry.mjs @@ -0,0 +1,144 @@ +const TARGET_SCHEMA_VERSION = 2; + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +/** + * Return the authored workflow as a schema-v2 document without its capacity + * override. The compiler can use this projection to discover the intrinsic v2 + * rank plan before deciding whether an explicit viewBox needs to grow. + */ +export function intrinsicWorkflow(workflow) { + const intrinsic = clone(workflow); + intrinsic.schema_version = TARGET_SCHEMA_VERSION; + intrinsic.meta = { ...intrinsic.meta }; + delete intrinsic.meta.viewBox; + return intrinsic; +} + +/** + * Return a schema-v2 planning projection that removes authored route geometry + * which may only become valid after its legacy X coordinates are remapped. + * Rank-affecting automatic and straight relationships remain in the projection. + */ +export function planningWorkflow(workflow) { + const planned = intrinsicWorkflow(workflow); + planned.edges = planned.edges.flatMap((edge) => { + const hasRoutedGeometry = Array.isArray(edge.via) + || (edge.route && !['auto', 'straight'].includes(edge.route)) + || edge.channelX !== undefined + || edge.channelY !== undefined; + if (hasRoutedGeometry) return []; + + const automatic = {}; + for (const property of ['id', 'from', 'to', 'variant', 'role', 'width']) { + if (edge[property] !== undefined) automatic[property] = edge[property]; + } + if (edge.route === 'straight') automatic.route = 'straight'; + if (edge.labelAt === undefined && edge.label !== undefined) automatic.label = edge.label; + return [automatic]; + }); + + if (Array.isArray(planned.mainPath)) { + const projectedPairs = new Set(planned.edges.map((edge) => `${edge.from}\u0000${edge.to}`)); + const projectionBreaksMainPath = planned.mainPath.some((from, index) => ( + index < planned.mainPath.length - 1 + && !projectedPairs.has(`${from}\u0000${planned.mainPath[index + 1]}`) + )); + if (projectionBreaksMainPath) delete planned.mainPath; + } + + return planned; +} + +function mappedNumber(value) { + return Number(value.toFixed(6)); +} + +/** + * Build a deterministic piecewise-linear mapping between corresponding legacy + * and readable rank centers. Coordinates outside the rank span are extrapolated + * using the nearest segment so explicitly authored outside corridors retain + * their relative offset. + */ +export function createHorizontalRankMapper(oldColumns, newColumns) { + if ( + !Array.isArray(oldColumns) + || !Array.isArray(newColumns) + || oldColumns.length !== newColumns.length + || oldColumns.length < 2 + || !oldColumns.every(Number.isFinite) + || !newColumns.every(Number.isFinite) + ) { + throw new TypeError('Horizontal rank mapping requires matching finite column arrays.'); + } + for (let index = 1; index < oldColumns.length; index += 1) { + if (oldColumns[index] <= oldColumns[index - 1] || newColumns[index] <= newColumns[index - 1]) { + throw new TypeError('Horizontal rank mapping requires strictly increasing columns.'); + } + } + + return (x) => { + if (!Number.isFinite(x)) throw new TypeError('Horizontal rank mapping requires a finite x coordinate.'); + let segment = oldColumns.length - 2; + if (x <= oldColumns[0]) { + segment = 0; + } else { + for (let index = 0; index < oldColumns.length - 1; index += 1) { + if (x <= oldColumns[index + 1]) { + segment = index; + break; + } + } + } + const oldSpan = oldColumns[segment + 1] - oldColumns[segment]; + const newSpan = newColumns[segment + 1] - newColumns[segment]; + const ratio = (x - oldColumns[segment]) / oldSpan; + return mappedNumber(newColumns[segment] + ratio * newSpan); + }; +} + +/** + * Apply one horizontal coordinate mapping to every schema-v1 absolute X pin. + * The caller owns the supplied workflow; this function reports an audit trail + * for each changed coordinate in stable document order. + */ +export function mapExplicitCoordinates(workflow, mapX) { + const changedCoordinates = []; + const record = (path, owner, property) => { + const from = owner[property]; + const to = mapX(from); + owner[property] = to; + if (to !== from) changedCoordinates.push({ path, from, to }); + }; + + for (const [edgeIndex, edge] of workflow.edges.entries()) { + if (Array.isArray(edge.via)) { + for (const [pointIndex, point] of edge.via.entries()) { + if (Array.isArray(point) && Number.isFinite(point[0])) { + record(`/edges/${edgeIndex}/via/${pointIndex}/0`, point, 0); + } + } + } + if (Array.isArray(edge.labelAt) && Number.isFinite(edge.labelAt[0])) { + record(`/edges/${edgeIndex}/labelAt/0`, edge.labelAt, 0); + } + if (Number.isFinite(edge.channelX)) { + record(`/edges/${edgeIndex}/channelX`, edge, 'channelX'); + } + } + return changedCoordinates; +} + +/** + * Construct an independently owned schema-v2 candidate with all authored + * absolute X pins mapped to the readable rank plan. + */ +export function createMappedWorkflowCandidate(workflow, oldColumns, newColumns) { + const document = clone(workflow); + document.schema_version = TARGET_SCHEMA_VERSION; + const mapX = createHorizontalRankMapper(oldColumns, newColumns); + const changedCoordinates = mapExplicitCoordinates(document, mapX); + return { document, changedCoordinates }; +} diff --git a/vendor/archify/schemas/README.md b/vendor/archify/schemas/README.md new file mode 100644 index 0000000..f1a2763 --- /dev/null +++ b/vendor/archify/schemas/README.md @@ -0,0 +1,256 @@ +# Archify JSON IR Schemas + +Each typed renderer consumes a JSON intermediate representation (IR) validated +against one of the schemas in this folder before any layout work happens. + +## Files + +| Schema | Governs | Structural arrays | +|--------|---------|-------------------| +| `workflow.schema.json` | `diagram_type: "workflow"` | `lanes`, `phases`, `groups`, `mainPath`, `nodes`, `edges` | +| `sequence.schema.json` | `diagram_type: "sequence"` | `participants`, `segments`, `messages`, `activations` | +| `dataflow.schema.json` | `diagram_type: "dataflow"` | `stages`, `nodes`, `flows` | +| `lifecycle.schema.json` | `diagram_type: "lifecycle"` | `lanes`, `states`, `transitions` | +| `architecture.schema.json` | `diagram_type: "architecture"` | `components`, `boundaries`, `connections` | +| `common.schema.json` | shared `$defs` only (no top-level document) | — | + +Every diagram schema requires `schema_version`, `diagram_type`, `meta` (with +`title` and a durable portable `output`), and its structural arrays — except `segments`, `activations`, and +`cards`, which are optional — and sets `additionalProperties: false` at every +level, so unknown fields are rejected rather than silently ignored. + +Every `meta` object also accepts `animation: "trace"` for opt-in SVG/CSS motion +in generated HTML. Omit it, or set `"none"`, for the default static output. +It also accepts `locale`, any well-formed language tag (schema pattern +`^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$`). The field selects the fixed Viewer UI, +renderer-owned default legend and accessibility copy, document-title suffix, +and `` value; it does not translate authored strings. `en` and +`zh-CN` are built-in catalogs; any other tag needs a matching `translations` +object (see below) or the renderer falls back to English and discloses it. +Omitting `locale` preserves legacy behavior and resolves to English. +Malformed locale tags fail schema validation instead of being guessed or +silently rewritten. + +`meta.translations` supplies the Viewer message catalog for a `locale` with no +built-in catalog, as data: an object mapping canonical message keys (see +`catalogKeys()` in `renderers/shared/i18n.mjs`) to translated strings whose +`{placeholder}` tokens match the English source. A key that is missing, +unrecognized, or has mismatched placeholders falls back to English rather than +failing the render; `validate`/`render`/`deliver` report the resulting +coverage to stderr. Spanish uses the reusable `examples/locales/es.json` +catalog; dev inputs that previously used only `locale: "es"` now need this +object in `meta.translations` to retain Spanish UI. +`visual_preset` accepts `classic` (the stable default), `signal-flow` (luminous +motion-forward presentation), `blueprint` (high-contrast engineering review), +or `editorial` (warm publication-style design review and documentation). +Presets change only viewer styling; they do not alter semantic IDs or geometry. +Sequence `meta` additionally accepts `column_fit`. The default `fixed` keeps +the historical 108px column gap and 86px participant boxes, so an authored +diagram renders at the same coordinates no matter how wide its viewBox is. +`spread` derives the gap and box width from the viewBox instead, which turns a +wide canvas into column distance and label room rather than empty space on the +right. Lane order, IDs, and message semantics are unchanged either way. + +`meta.views` is retired. The schemas still accept the old guided-view shape so +existing files keep validating, but renderers ignore it; do not author it. + +### Legend presentation contract + +Every `meta` object accepts the same optional legend shape without changing +the schema version already selected for that renderer: + +```json +"legend": { + "mode": "auto", + "entries": { + "security": { "label": "restricted data", "visible": true } + } +} +``` + +`mode` is `auto` (the default), `all`, or `hidden`. `auto` includes only kinds +present in typed IR; `all` includes the renderer's full stable catalog; +`hidden` removes the complete legend and takes precedence over entry overrides. +Architecture documents that omit an explicit `viewBox` size that automatic +viewBox from the same measured resolved legend footprint used for final SVG +layout. Across all renderers, legacy documents that omit `meta.legend` use a +compatibility-safe implicit `auto`: if the resolved legend cannot fit an +explicit authored viewBox without overlap, Archify omits the complete legend +instead of turning a previously valid schema-v1 document into a hard failure. +Once an author adds `meta.legend` (including explicit `mode: "auto"`), the +layout is intentional and unfit labels or bands fail with a path-prefixed +diagnostic. An entry may set a non-empty, bounded `label`, boolean `visible`, +or both. +`visible: false` removes a resolved entry and `visible: true` forces a supported +but unused kind into the visual legend. Unknown kinds and properties fail +strict validation. + +Supported keys are renderer-owned: + +| Renderer | `meta.legend.entries` keys | +|---|---| +| Architecture | `frontend`, `backend`, `database`, `cloud`, `security`, `messagebus`, `external` | +| Workflow | `frontend`, `backend`, `security`, `messagebus`, `database`, `cloud`, `external` | +| Sequence | `emphasis`, `return`, `security`, `dashed`, `default` | +| Dataflow | `emphasis`, `security`, `dashed`, `database`, `default` | +| Lifecycle | `start`, `active`, `waiting`, `decision`, `success`, `failure`, `neutral`, `external` | + +Labels are presentation only: they do not rename the stable kind, change +nodes/relationships, or create Semantic Lens edge facts. Sequence message and +Dataflow flow-variant entries are visual keys. Component/state entries backed +by exact compiled node facts receive the interactive Semantic Legend bridge; +this includes Dataflow `database` when a real `nodes[].type: "database"` fact +exists. + +Every relationship collection (`connections`, `edges`, `messages`, `flows`, and +`transitions`) accepts an optional author-controlled `id` using the shared ID +pattern. The renderer keeps its source-order runtime key separately, while the +authored ID enables a stable `#relation=` viewer link that survives array +reordering. ID-less documents remain valid and their relationship pins stay +local to the current page. + +Every semantic node collection (`components`, `nodes`, `participants`, and +`states`) also accepts one optional `brand`: either a canonical string returned +by `archify brands --json`, or a digest-pinned `{ "url", "sha256" }` object +returned by `archify brands capture --json`. Known IDs and known-brand +domains use the bundled vector catalogue. Unknown URLs must be captured in that +explicit command before authoring; render and validate never perform an +unpinned network capture. Unsafe, unavailable, changed, or unsupported content +fails closed with a brand diagnostic. Omitted `brand` preserves the prior +output. + +## schema_version policy + +Workflow supports schema versions 1 and 2. Version 1 remains the fixed-layout +compatibility contract; version 2 opts into the readable workflow compiler and +can be produced explicitly with `archify migrate workflow ... --to-schema 2`. +Lifecycle also supports versions 1 and 2 with the same fields. Version 1 keeps +the fixed three-band layout; version 2 renders one row per lane on a shared +column grid (see the [lifecycle renderer](../renderers/lifecycle/README.md)). +Lifecycle has no migration command: to adopt v2, set `schema_version: 2`, +re-place lower-lane `col` values on the shared grid (v1 column `N` sat under +main column `N + 2`), and drop coordinates authored for the v1 canvas +(`viewBox`, `via`, `channelX`, `channelY`, `labelAt`). +The other three diagram schemas keep `schema_version` pinned to `1`; they have +no schema-version migration command. For any of the five diagram types, repair +a legacy missing or nonportable `meta.output` in the source and run `validate`. +For a workflow v1-to-v2 migration specifically, `migrate workflow` also accepts +`--output reports/diagram.html` to put that portable value in the separate v2 +destination without changing the legacy source. + +Workflow also accepts optional `semanticChecks`. `allowedRoots` and +`allowedTerminals` close the set of intentional graph sources and sinks; +`requiredEdges` requires exact authored relationships; and `requiredPaths` +requires directed reachability while allowing intermediate nodes. The compiler +evaluates these facts before layout and returns typed `workflow/*` diagnostics. +The field is additive and geometry-neutral: omitting it preserves existing +workflow behavior and including a satisfied contract does not change SVG or +layout-receipt bytes. + +A file that validates today must keep validating and rendering within its +declared version throughout the 2.x release line. The explicitly reviewed +portable-output hardening is the one exception: older v1 documents that omit +`meta.output` must add a portable POSIX-relative `.html` path (for example, +`reports/diagram.html`); ordinary explicit CLI output arguments do not replace +this durable authored value. The workflow-only v1-to-v2 migration command may +instead receive that value explicitly as `--output reports/diagram.html`; it +writes the value only to its separate validated destination. Additive viewer, +accessibility, and presentation improvements may enhance generated HTML, but +they must not reinterpret authored IR or turn a previously valid profile-less +v1 file into a new hard layout failure. Breaking IR changes require a new +version; additive, backwards-compatible fields do not. + +## Shared definitions (common.schema.json) + +The five diagram schemas reference `common.schema.json#/$defs/...`: + +- `id` — element identifiers, pattern `^[a-zA-Z][a-zA-Z0-9_-]*$` +- `point` — an `[x, y]` pair of numbers (used by `via` and `labelAt`) +- `componentType` — `frontend`, `backend`, `database`, `cloud`, `security`, + `messagebus`, `external` +- `locale` — a well-formed renderer locale tag (`en` and `zh-CN` are built in; + any other tag needs a matching `translations` object) +- `translations` — canonical message key → translated string, for a `locale` + with no built-in catalog +- `portableOutputPath` — the portable POSIX-relative `.html` path used by + `meta.output`; see the two output-path boundaries in the + [delivery contract](../references/delivery-contract.md#output-path-contracts) +- `brandMark` — one optional built-in brand ID or explicit HTTP(S) site URL +- `variant` — `default`, `emphasis`, `security`, `dashed` (sequence messages + extend this list locally with `return`) +- `legendMode` and `legendEntry` — the shared strict mode and label/visibility + override shapes used by each renderer-owned key map +- `guidedViews` — the retired `meta.views` shape, still accepted and ignored +- `cards` — the summary-card blocks rendered below the SVG + +Lifecycle state `type` is mode-specific (`start`/`active`/`waiting`/...) and +stays in `lifecycle.schema.json`. + +The JSON Schema definition is the portable contract's structurally expressible +preflight. The shipped generated validator wrapper and CLI additionally enforce +byte-based component limits and the complete runtime path contract; consumers +that need the same cross-platform acceptance boundary should use that wrapper. + +## Runtime validation + +At development time, `scripts/generate-validators.mjs` compiles all five +schemas with ajv's draft 2020-12 standalone generator using `strict: true` and +`allErrors: true`. The generated `renderers/shared/generated-validators.mjs` +is committed and shipped with the skill, so runtime validation has no npm or +network dependency. `renderers/shared/validator.mjs` applies the matching +standalone validator before the renderer's own layout checks. +The shared loader then checks cross-collection facts that JSON Schema cannot +express cleanly here: duplicate view IDs, duplicate focus IDs, focus IDs that do +not exist in the diagram's semantic collection, and duplicate authored +relationship IDs within the mode's relationship collection. + +All five modes support opt-in, revision-pinned repository evidence. +`meta.repository` names the repository URL and full commit SHA, with optional +`provider` (`github` or `gitee`) and `link_mode` (`web` or `local-only`; see the +authoring contract); a node may carry one to three `sources` with repo-relative POSIX paths, optional line +ranges, and optional labels. Sources are authored on the mode's own node +collection — Architecture `components`, Workflow and Data Flow `nodes`, +Sequence `participants`, Lifecycle `states` — and the verified payload is keyed +by node id. Shape is schema-checked, then the renderer requires +`--repo-root`: the local Git origin must match, and Git must prove the commit, +blobs, and requested lines. Verified evidence is embedded outside the canonical +SVG for the Semantic Passport and Node Finder; ordinary documents and visual +exports carry no repository evidence. + +## Visual quality and engineering truth + +`meta.quality_profile` and `meta.engineering_profile` answer different +questions. `quality_profile` is available in all five modes and controls how +strictly Archify judges composition. `engineering_profile` is an optional +Architecture-only semantic contract; omitting it preserves the ordinary v1 +behavior. + +The first engineering profile is `deployment-ownership`. Enable it only when +the user wants a fail-closed deployment review and the source facts are known. +It requires every non-external component to name an owner in `tag` and belong +to exactly one `region`; the document must contain both `region` and +`security-group` boundaries; every `database` must be inside a +`security-group`; each security group must contain members from one shared +region; and every connection whose region or security-group membership changes +must name the real crossing mechanism in `label`. + +The profile validates only authored IR. It does not discover infrastructure, +infer owners, or prove that a diagram matches a live environment. If a fact is +unknown, leave the profile unset or obtain the fact instead of inventing it. + +`npm test` runs the generator in check mode and fails when the committed +validators drift from their schemas. + +## Error format + +Schema violations exit non-zero. Each ajv error is reported on its own line as +the instance path — annotated with the nearest enclosing element's `id` or +`label` — followed by the message and parameters: + +```text +workflow schema validation failed: + /nodes/3 (id/label: "router") must NOT have additional properties {"additionalProperty":"colour"} +``` + +Schemas catch shape errors (types, enums, ranges, unknown fields); geometry +problems such as overlaps and label collisions are the renderers' job. diff --git a/vendor/archify/schemas/architecture.schema.json b/vendor/archify/schemas/architecture.schema.json new file mode 100644 index 0000000..5fe3b8e --- /dev/null +++ b/vendor/archify/schemas/architecture.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/tt-a1i/archify/schemas/architecture.schema.json", + "title": "Archify Architecture Diagram", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "diagram_type", "meta", "components"], + "properties": { + "schema_version": { "const": 1 }, + "diagram_type": { "const": "architecture" }, + "meta": { + "type": "object", + "additionalProperties": false, + "required": ["title", "output"], + "properties": { + "title": { "type": "string", "minLength": 1 }, + "locale": { "$ref": "common.schema.json#/$defs/locale" }, + "translations": { "$ref": "common.schema.json#/$defs/translations" }, + "subtitle": { "type": "string" }, + "output": { "$ref": "common.schema.json#/$defs/portableOutputPath" }, + "animation": { "$ref": "common.schema.json#/$defs/animation" }, + "visual_preset": { "$ref": "common.schema.json#/$defs/visualPreset" }, + "quality_profile": { "$ref": "common.schema.json#/$defs/qualityProfile" }, + "engineering_profile": { "enum": ["deployment-ownership"] }, + "repository": { "$ref": "common.schema.json#/$defs/repository" }, + "views": { "$ref": "common.schema.json#/$defs/guidedViews" }, + "legend": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { "$ref": "common.schema.json#/$defs/legendMode" }, + "entries": { + "type": "object", + "additionalProperties": false, + "properties": { + "frontend": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "backend": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "database": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "cloud": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "security": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "messagebus": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "external": { "$ref": "common.schema.json#/$defs/legendEntry" } + } + } + } + }, + "viewBox": { + "type": "array", + "prefixItems": [ + { "type": "number", "minimum": 320 }, + { "type": "number", "minimum": 240 } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "layout": { + "type": "object", + "additionalProperties": false, + "required": ["mode"], + "properties": { + "mode": { "enum": ["grid"] }, + "origin": { "$ref": "common.schema.json#/$defs/point" }, + "cols": { "type": "integer", "minimum": 1, "maximum": 12 }, + "gapX": { "type": "number", "minimum": 0 }, + "gapY": { "type": "number", "minimum": 0 }, + "cellW": { "type": "number", "minimum": 40 }, + "cellH": { "type": "number", "minimum": 24 } + } + }, + "components": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "label"], + "properties": { + "id": { "$ref": "common.schema.json#/$defs/id" }, + "type": { "$ref": "common.schema.json#/$defs/componentType" }, + "label": { "type": "string", "minLength": 1 }, + "sublabel": { "type": "string" }, + "tag": { "type": "string" }, + "icon": { "$ref": "common.schema.json#/$defs/nodeIcon" }, + "brand": { "$ref": "common.schema.json#/$defs/brandMark" }, + "sources": { "$ref": "common.schema.json#/$defs/sourceReferences" }, + "row": { "type": "integer", "minimum": 0 }, + "col": { "type": "integer", "minimum": 0 }, + "pos": { "$ref": "common.schema.json#/$defs/point" }, + "size": { + "type": "array", + "prefixItems": [ + { "type": "number", "exclusiveMinimum": 0 }, + { "type": "number", "exclusiveMinimum": 0 } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + } + } + } + }, + "boundaries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "label", "wraps"], + "properties": { + "kind": { "enum": ["region", "security-group"] }, + "label": { "type": "string", "minLength": 1 }, + "wraps": { + "type": "array", + "minItems": 1, + "items": { "$ref": "common.schema.json#/$defs/id" } + }, + "pad": { "type": "number", "minimum": 0 } + } + } + }, + "connections": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["from", "to"], + "properties": { + "id": { "$ref": "common.schema.json#/$defs/id" }, + "from": { "$ref": "common.schema.json#/$defs/id" }, + "to": { "$ref": "common.schema.json#/$defs/id" }, + "label": { "type": "string" }, + "variant": { "$ref": "common.schema.json#/$defs/variant" }, + "fromSide": { "$ref": "common.schema.json#/$defs/side" }, + "toSide": { "$ref": "common.schema.json#/$defs/side" }, + "route": { "enum": ["auto", "straight", "orthogonal-h", "orthogonal-v"] }, + "via": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/point" } + }, + "labelAt": { "$ref": "common.schema.json#/$defs/point" }, + "labelDx": { "type": "number" }, + "labelDy": { "type": "number" }, + "labelSegment": { "type": "integer", "minimum": 0 }, + "width": { "$ref": "common.schema.json#/$defs/relationshipWidth" } + } + } + }, + "cards": { "$ref": "common.schema.json#/$defs/cards" } + } +} diff --git a/vendor/archify/schemas/common.schema.json b/vendor/archify/schemas/common.schema.json new file mode 100644 index 0000000..2fe0c75 --- /dev/null +++ b/vendor/archify/schemas/common.schema.json @@ -0,0 +1,181 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/tt-a1i/archify/schemas/common.schema.json", + "title": "Archify Shared Definitions", + "$defs": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$" + }, + "locale": { + "type": "string", + "pattern": "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$", + "maxLength": 35 + }, + "translations": { + "type": "object", + "propertyNames": { "pattern": "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$" }, + "additionalProperties": { "type": "string", "minLength": 1, "maxLength": 400 }, + "maxProperties": 600 + }, + "animation": { + "enum": ["trace", "none"] + }, + "visualPreset": { + "enum": ["classic", "signal-flow", "blueprint", "editorial"] + }, + "qualityProfile": { + "enum": ["standard", "showcase"] + }, + "portableOutputPath": { + "description": "A portable POSIX-relative HTML output path. It must be safe to interpret identically on every supported host filesystem.", + "type": "string", + "minLength": 1, + "pattern": "^[^/]+(?:/[^/]+)*$", + "allOf": [ + { "pattern": "(?:^|/)[^/]+[.][Hh][Tt][Mm][Ll]$" }, + { "not": { "pattern": "[\\\\<>:\"|?*\\u0000-\\u001F\\u007F-\\u009F]" } }, + { "not": { "pattern": "[\\uD800-\\uDFFF]" } }, + { "not": { "pattern": "(^|/)[.]{1,2}(/|$)" } }, + { "not": { "pattern": "[. ](/|$)" } }, + { "not": { "pattern": "(^|/)[^/]{256}" } }, + { + "not": { + "pattern": "(^|/)(?:[Cc][Oo][Nn]|[Pp][Rr][Nn]|[Aa][Uu][Xx]|[Nn][Uu][Ll]|[Cc][Oo][Nn][Ii][Nn][$]|[Cc][Oo][Nn][Oo][Uu][Tt][$]|[Cc][Oo][Mm][1-9¹²³]|[Ll][Pp][Tt][1-9¹²³])(?:[.]|/|$)" + } + }, + { + "not": { + "pattern": "(^|/)[^/]*~[1-9][0-9]*(?:[.]|/|$)" + } + } + ] + }, + "side": { + "enum": ["left", "right", "top", "bottom"] + }, + "relationshipWidth": { + "type": "number", + "minimum": 0.5 + }, + "point": { + "type": "array", + "prefixItems": [ + { "type": "number" }, + { "type": "number" } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + }, + "componentType": { + "enum": ["frontend", "backend", "database", "cloud", "security", "messagebus", "external"] + }, + "nodeIcon": { + "description": "Decorative corner icon. Omit the icon field for the type default; none hides it. Does not change type, color, or legend grouping.", + "enum": ["calendar", "clock", "person", "briefcase", "flag", "moon", "frontend", "backend", "database", "cloud", "security", "messagebus", "external", "start", "active", "waiting", "success", "failure", "neutral", "none"] + }, + "brandMark": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "anyOf": [ + { "maxLength": 80, "pattern": "^[^\\r\\n]+$" }, + { "pattern": "^https?://" } + ] + }, + { + "type": "object", + "additionalProperties": false, + "required": ["url", "sha256"], + "properties": { + "url": { "type": "string", "minLength": 8, "maxLength": 2048, "pattern": "^https?://" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } + } + ] + }, + "variant": { + "enum": ["default", "emphasis", "security", "dashed"] + }, + "legendMode": { + "enum": ["auto", "all", "hidden"] + }, + "legendEntry": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "label": { "type": "string", "minLength": 1, "maxLength": 80 }, + "visible": { "type": "boolean" } + } + }, + "guidedViews": { + "type": "array", + "maxItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label", "focus"], + "properties": { + "id": { "$ref": "#/$defs/id" }, + "label": { "type": "string", "minLength": 1, "maxLength": 48 }, + "focus": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/id" } + }, + "note": { "type": "string", "maxLength": 140 } + } + } + }, + "repository": { + "type": "object", + "additionalProperties": false, + "required": ["url", "revision"], + "properties": { + "url": { + "type": "string", + "minLength": 1 + }, + "provider": { "enum": ["github", "gitee"] }, + "link_mode": { "enum": ["web", "local-only"] }, + "revision": { "type": "string", "pattern": "^[a-fA-F0-9]{40}$" } + } + }, + "sourceReferences": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { "type": "string", "minLength": 1, "maxLength": 240 }, + "line": { "type": "integer", "minimum": 1 }, + "end_line": { "type": "integer", "minimum": 1 }, + "label": { "type": "string", "minLength": 1, "maxLength": 48 } + } + } + }, + "cards": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["dot", "title", "items"], + "properties": { + "dot": { "enum": ["cyan", "emerald", "violet", "amber", "rose", "orange", "slate"] }, + "title": { "type": "string", "minLength": 1 }, + "items": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + } +} diff --git a/vendor/archify/schemas/dataflow.schema.json b/vendor/archify/schemas/dataflow.schema.json new file mode 100644 index 0000000..4d8eecf --- /dev/null +++ b/vendor/archify/schemas/dataflow.schema.json @@ -0,0 +1,254 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/tt-a1i/archify/schemas/dataflow.schema.json", + "title": "Archify Data Flow Diagram", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "diagram_type", + "meta", + "stages", + "nodes", + "flows" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "diagram_type": { + "const": "dataflow" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "output" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "locale": { + "$ref": "common.schema.json#/$defs/locale" + }, + "translations": { + "$ref": "common.schema.json#/$defs/translations" + }, + "subtitle": { + "type": "string" + }, + "output": { + "$ref": "common.schema.json#/$defs/portableOutputPath" + }, + "animation": { + "$ref": "common.schema.json#/$defs/animation" + }, + "visual_preset": { + "$ref": "common.schema.json#/$defs/visualPreset" + }, + "quality_profile": { + "$ref": "common.schema.json#/$defs/qualityProfile" + }, + "repository": { + "$ref": "common.schema.json#/$defs/repository" + }, + "views": { + "$ref": "common.schema.json#/$defs/guidedViews" + }, + "legend": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { "$ref": "common.schema.json#/$defs/legendMode" }, + "entries": { + "type": "object", + "additionalProperties": false, + "properties": { + "default": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "emphasis": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "security": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "dashed": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "database": { "$ref": "common.schema.json#/$defs/legendEntry" } + } + } + } + }, + "viewBox": { + "type": "array", + "prefixItems": [ + { + "type": "number", + "minimum": 360 + }, + { + "type": "number", + "minimum": 360 + } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "stages": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1 + } + } + } + }, + "nodes": { + "type": "array", + "minItems": 2, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "label", + "stage", + "row" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "type": { + "$ref": "common.schema.json#/$defs/componentType" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "sublabel": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "icon": { "$ref": "common.schema.json#/$defs/nodeIcon" }, + "brand": { + "$ref": "common.schema.json#/$defs/brandMark" + }, + "sources": { + "$ref": "common.schema.json#/$defs/sourceReferences" + }, + "stage": { + "type": "integer", + "minimum": 0 + }, + "row": { + "type": "integer", + "minimum": 0 + }, + "width": { + "type": "number", + "minimum": 48 + }, + "height": { + "type": "number", + "minimum": 36 + }, + "yOffset": { + "type": "number" + } + } + } + }, + "flows": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to", + "label" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "from": { + "$ref": "common.schema.json#/$defs/id" + }, + "to": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "classification": { + "type": "string" + }, + "variant": { + "$ref": "common.schema.json#/$defs/variant" + }, + "route": { + "enum": [ + "auto", + "straight", + "vertical-channel", + "bottom-channel", + "top-channel" + ] + }, + "fromSide": { + "$ref": "common.schema.json#/$defs/side" + }, + "toSide": { + "$ref": "common.schema.json#/$defs/side" + }, + "channelX": { + "type": "number" + }, + "channelY": { + "type": "number" + }, + "labelAt": { + "$ref": "common.schema.json#/$defs/point" + }, + "labelDx": { + "type": "number" + }, + "labelDy": { + "type": "number" + }, + "labelSegment": { + "type": "integer", + "minimum": 0 + }, + "via": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/point" + } + }, + "width": { + "$ref": "common.schema.json#/$defs/relationshipWidth" + } + } + } + }, + "cards": { + "$ref": "common.schema.json#/$defs/cards" + } + } +} diff --git a/vendor/archify/schemas/lifecycle.schema.json b/vendor/archify/schemas/lifecycle.schema.json new file mode 100644 index 0000000..83e4d2f --- /dev/null +++ b/vendor/archify/schemas/lifecycle.schema.json @@ -0,0 +1,280 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/tt-a1i/archify/schemas/lifecycle.schema.json", + "title": "Archify Lifecycle Diagram", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "diagram_type", + "meta", + "lanes", + "states", + "transitions" + ], + "properties": { + "schema_version": { + "enum": [ + 1, + 2 + ] + }, + "diagram_type": { + "const": "lifecycle" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "output" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "locale": { + "$ref": "common.schema.json#/$defs/locale" + }, + "translations": { + "$ref": "common.schema.json#/$defs/translations" + }, + "subtitle": { + "type": "string" + }, + "output": { + "$ref": "common.schema.json#/$defs/portableOutputPath" + }, + "animation": { + "$ref": "common.schema.json#/$defs/animation" + }, + "visual_preset": { + "$ref": "common.schema.json#/$defs/visualPreset" + }, + "quality_profile": { + "$ref": "common.schema.json#/$defs/qualityProfile" + }, + "repository": { + "$ref": "common.schema.json#/$defs/repository" + }, + "views": { + "$ref": "common.schema.json#/$defs/guidedViews" + }, + "legend": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { "$ref": "common.schema.json#/$defs/legendMode" }, + "entries": { + "type": "object", + "additionalProperties": false, + "properties": { + "start": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "active": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "waiting": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "decision": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "success": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "failure": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "neutral": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "external": { "$ref": "common.schema.json#/$defs/legendEntry" } + } + } + } + }, + "viewBox": { + "type": "array", + "prefixItems": [ + { + "type": "number", + "minimum": 420 + }, + { + "type": "number", + "minimum": 300 + } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "lanes": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string", + "minLength": 1 + } + } + } + }, + "states": { + "type": "array", + "minItems": 2, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "label", + "lane", + "col" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "type": { + "enum": [ + "start", + "active", + "waiting", + "decision", + "success", + "failure", + "neutral", + "external" + ] + }, + "label": { + "type": "string", + "minLength": 1 + }, + "sublabel": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "icon": { "$ref": "common.schema.json#/$defs/nodeIcon" }, + "brand": { + "$ref": "common.schema.json#/$defs/brandMark" + }, + "sources": { + "$ref": "common.schema.json#/$defs/sourceReferences" + }, + "step": { + "type": "string" + }, + "lane": { + "$ref": "common.schema.json#/$defs/id" + }, + "col": { + "type": "integer", + "minimum": 0, + "maximum": 4 + }, + "width": { + "type": "number", + "minimum": 48 + }, + "height": { + "type": "number", + "minimum": 36 + }, + "yOffset": { + "type": "number" + } + } + } + }, + "transitions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "from": { + "$ref": "common.schema.json#/$defs/id" + }, + "to": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string" + }, + "note": { + "type": "string" + }, + "variant": { + "$ref": "common.schema.json#/$defs/variant" + }, + "route": { + "enum": [ + "auto", + "straight", + "drop", + "bottom-channel", + "top-channel", + "right-channel", + "left-channel" + ] + }, + "fromSide": { + "$ref": "common.schema.json#/$defs/side" + }, + "toSide": { + "$ref": "common.schema.json#/$defs/side" + }, + "channelX": { + "type": "number" + }, + "channelY": { + "type": "number" + }, + "cornerRadius": { + "type": "number", + "minimum": 0 + }, + "labelAt": { + "$ref": "common.schema.json#/$defs/point" + }, + "labelDx": { + "type": "number" + }, + "labelDy": { + "type": "number" + }, + "labelSegment": { + "type": "integer", + "minimum": 0 + }, + "via": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/point" + } + }, + "width": { + "$ref": "common.schema.json#/$defs/relationshipWidth" + } + } + } + }, + "cards": { + "$ref": "common.schema.json#/$defs/cards" + } + } +} diff --git a/vendor/archify/schemas/sequence.schema.json b/vendor/archify/schemas/sequence.schema.json new file mode 100644 index 0000000..1755297 --- /dev/null +++ b/vendor/archify/schemas/sequence.schema.json @@ -0,0 +1,234 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/tt-a1i/archify/schemas/sequence.schema.json", + "title": "Archify Sequence Diagram", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "diagram_type", + "meta", + "participants", + "messages" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "diagram_type": { + "const": "sequence" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "output" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "locale": { + "$ref": "common.schema.json#/$defs/locale" + }, + "translations": { + "$ref": "common.schema.json#/$defs/translations" + }, + "subtitle": { + "type": "string" + }, + "output": { + "$ref": "common.schema.json#/$defs/portableOutputPath" + }, + "animation": { + "$ref": "common.schema.json#/$defs/animation" + }, + "visual_preset": { + "$ref": "common.schema.json#/$defs/visualPreset" + }, + "quality_profile": { + "$ref": "common.schema.json#/$defs/qualityProfile" + }, + "column_fit": { + "description": "Horizontal participant layout. Omit this field or use fixed for the stable 86px boxes and 108px gap. Use spread when a wide viewBox would leave unused horizontal space or meaningful participant labels do not fit the fixed boxes; spread derives wider boxes and gaps from the viewBox without changing participant order or message semantics.", + "enum": ["fixed", "spread"] + }, + "repository": { + "$ref": "common.schema.json#/$defs/repository" + }, + "views": { + "$ref": "common.schema.json#/$defs/guidedViews" + }, + "legend": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { "$ref": "common.schema.json#/$defs/legendMode" }, + "entries": { + "type": "object", + "additionalProperties": false, + "properties": { + "default": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "emphasis": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "security": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "dashed": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "return": { "$ref": "common.schema.json#/$defs/legendEntry" } + } + } + } + }, + "viewBox": { + "type": "array", + "prefixItems": [ + { + "type": "number", + "minimum": 480 + }, + { + "type": "number", + "minimum": 480 + } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "participants": { + "type": "array", + "minItems": 2, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "label" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "type": { + "$ref": "common.schema.json#/$defs/componentType" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "sublabel": { + "type": "string" + }, + "icon": { "$ref": "common.schema.json#/$defs/nodeIcon" }, + "brand": { + "$ref": "common.schema.json#/$defs/brandMark" + }, + "sources": { + "$ref": "common.schema.json#/$defs/sourceReferences" + } + } + } + }, + "segments": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to", + "label" + ], + "properties": { + "from": { + "type": "number" + }, + "to": { + "type": "number" + }, + "label": { + "type": "string", + "minLength": 1 + } + } + } + }, + "messages": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to", + "y", + "label" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "from": { + "$ref": "common.schema.json#/$defs/id" + }, + "to": { + "$ref": "common.schema.json#/$defs/id" + }, + "y": { + "type": "number", + "minimum": 160 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "variant": { + "enum": [ + "default", + "emphasis", + "security", + "dashed", + "return" + ] + }, + "note": { + "type": "string" + } + } + } + }, + "activations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "participant", + "from", + "to" + ], + "properties": { + "participant": { + "$ref": "common.schema.json#/$defs/id" + }, + "from": { + "type": "number" + }, + "to": { + "type": "number" + }, + "type": { + "$ref": "common.schema.json#/$defs/componentType" + } + } + } + }, + "cards": { + "$ref": "common.schema.json#/$defs/cards" + } + } +} diff --git a/vendor/archify/schemas/workflow.schema.json b/vendor/archify/schemas/workflow.schema.json new file mode 100644 index 0000000..dad1457 --- /dev/null +++ b/vendor/archify/schemas/workflow.schema.json @@ -0,0 +1,439 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/tt-a1i/archify/schemas/workflow.schema.json", + "title": "Archify Workflow Diagram", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "diagram_type", + "meta", + "lanes", + "nodes", + "edges" + ], + "properties": { + "schema_version": { + "enum": [ + 1, + 2 + ] + }, + "diagram_type": { + "const": "workflow" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "output" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "locale": { + "$ref": "common.schema.json#/$defs/locale" + }, + "translations": { + "$ref": "common.schema.json#/$defs/translations" + }, + "subtitle": { + "type": "string" + }, + "output": { + "$ref": "common.schema.json#/$defs/portableOutputPath" + }, + "animation": { + "enum": [ + "trace", + "none" + ] + }, + "visual_preset": { + "enum": [ + "classic", + "signal-flow", + "blueprint", + "editorial" + ] + }, + "quality_profile": { + "enum": [ + "standard", + "showcase" + ] + }, + "repository": { + "$ref": "common.schema.json#/$defs/repository" + }, + "views": { + "$ref": "common.schema.json#/$defs/guidedViews" + }, + "legend": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { "$ref": "common.schema.json#/$defs/legendMode" }, + "entries": { + "type": "object", + "additionalProperties": false, + "properties": { + "frontend": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "backend": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "database": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "cloud": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "security": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "messagebus": { "$ref": "common.schema.json#/$defs/legendEntry" }, + "external": { "$ref": "common.schema.json#/$defs/legendEntry" } + } + } + } + }, + "viewBox": { + "type": "array", + "prefixItems": [ + { + "type": "number", + "minimum": 700 + }, + { + "type": "number", + "minimum": 240 + } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "lanes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "variant": { + "enum": [ + "normal", + "exception" + ] + } + } + } + }, + "phases": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "fromCol", + "toCol" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "fromCol": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "toCol": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "variant": { + "enum": [ + "default", + "emphasis", + "security", + "dashed" + ] + } + } + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "lane", + "fromCol", + "toCol" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "lane": { + "$ref": "common.schema.json#/$defs/id" + }, + "fromCol": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "toCol": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "variant": { + "enum": [ + "default", + "emphasis", + "security", + "dashed" + ] + } + } + } + }, + "mainPath": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "semanticChecks": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "allowedRoots": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "allowedTerminals": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "requiredEdges": { + "type": "array", + "items": { + "$ref": "#/$defs/semanticRelation" + } + }, + "requiredPaths": { + "type": "array", + "items": { + "$ref": "#/$defs/semanticRelation" + } + } + } + }, + "nodes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "lane", + "col", + "type", + "label" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "lane": { + "$ref": "common.schema.json#/$defs/id" + }, + "col": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "type": { + "$ref": "common.schema.json#/$defs/componentType" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "sublabel": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "icon": { "$ref": "common.schema.json#/$defs/nodeIcon" }, + "brand": { + "$ref": "common.schema.json#/$defs/brandMark" + }, + "sources": { + "$ref": "common.schema.json#/$defs/sourceReferences" + }, + "width": { + "type": "number", + "minimum": 32 + }, + "height": { + "type": "number", + "minimum": 32 + }, + "yOffset": { + "type": "number" + } + } + } + }, + "edges": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to" + ], + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "from": { + "$ref": "common.schema.json#/$defs/id" + }, + "to": { + "$ref": "common.schema.json#/$defs/id" + }, + "label": { + "type": "string" + }, + "variant": { + "$ref": "common.schema.json#/$defs/variant" + }, + "role": { + "enum": [ + "main", + "branch", + "async", + "return", + "error" + ] + }, + "fromSide": { + "$ref": "#/$defs/side" + }, + "toSide": { + "$ref": "#/$defs/side" + }, + "route": { + "enum": [ + "auto", + "straight", + "drop", + "outside-right", + "return-left", + "bottom-channel", + "up-channel" + ] + }, + "via": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/point" + } + }, + "labelAt": { + "$ref": "common.schema.json#/$defs/point" + }, + "labelDx": { + "type": "number" + }, + "labelDy": { + "type": "number" + }, + "labelSegment": { + "type": "integer", + "minimum": 0 + }, + "channelX": { + "type": "number" + }, + "channelY": { + "type": "number" + }, + "bias": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "width": { + "type": "number", + "minimum": 0.5 + } + } + } + }, + "cards": { + "$ref": "common.schema.json#/$defs/cards" + } + }, + "$defs": { + "semanticRelation": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "$ref": "common.schema.json#/$defs/id" + }, + "to": { + "$ref": "common.schema.json#/$defs/id" + } + } + }, + "side": { + "enum": [ + "left", + "right", + "top", + "bottom" + ] + } + } +} diff --git a/vendor/archify/scripts/check-render-output.mjs b/vendor/archify/scripts/check-render-output.mjs new file mode 100644 index 0000000..ae6e741 --- /dev/null +++ b/vendor/archify/scripts/check-render-output.mjs @@ -0,0 +1,1388 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import { createHash } from 'node:crypto'; +import path from 'node:path'; +import { collectAmbiguousCorridors, collectArrowheadCollisions, collectBorderRuns, collectLabelCanvasOverflow, collectLabelRouteClearance, collectRouteRhythmIssues, describeLabelCanvasOverflow, formatRect, forwardCollinearAnalysisSegments, minimumLabelRouteClearance, routeBudgetMetrics } from '../renderers/shared/geometry.mjs'; +import { + DESKTOP_READABILITY_VIEWPORT, + DESKTOP_READER_DIAGRAM_WIDTH, + DECLARED_WIDE_READER_CONTRACT, + declaredWideReadabilityBudget, + MIN_PROJECTED_NODE_TEXT_PX, + describeFixedWidthOverflow, + predictedFixedWidthOverflow, + projectedNodeTextPx, +} from '../renderers/shared/desktop-readability.mjs'; + +const input = process.argv[2]; + +if (!input || input === '-h' || input === '--help') { + console.error('Usage: node scripts/check-render-output.mjs '); + process.exit(input ? 0 : 2); +} + +const htmlPath = path.resolve(input); +let html; +let artifact; +try { + const bytes = fs.readFileSync(htmlPath); + html = bytes.toString('utf8'); + artifact = { sha256: createHash('sha256').update(bytes).digest('hex'), bytes: bytes.byteLength }; +} catch (err) { + console.error(JSON.stringify({ + ok: false, + file: htmlPath, + checks: [{ name: 'file_readable', ok: false, details: [err.message] }], + }, null, 2)); + process.exit(1); +} + +const checks = []; +let composition = { + schemaVersion: 1, + profile: 'standard', + status: 'pass', + summary: { errors: 0, warnings: 0 }, + metrics: { + properCrossings: 0, + resolvedCrossovers: 0, + ambiguousCorridors: 0, + containerBorderRuns: 0, + labelRouteClearanceIssues: 0, + minLabelRouteClearance: null, + labelCanvasOverflowIssues: 0, + maxBends: 0, + routesOverSuggestedBends: 0, + maxStretch: null, + routesOverSuggestedStretch: 0, + minSegmentPx: null, + minInteriorSegmentPx: null, + shortSegmentCount: 0, + shortEndpointSegmentCount: 0, + shortInteriorSegmentCount: 0, + microSegmentCount: 0, + desktopReadabilityIssues: 0, + minProjectedNodeTextPx: null, + }, + suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 }, + issues: [], +}; + +const NON_FINITE_TOKEN = /\b(?:NaN|undefined|Infinity)\b/; +// Consume comments/CDATA as whole tokens, including any tag-like prose. +const SVG_TAG_TOKEN = /|$)||$)|<(\/?)([A-Za-z][\w:-]*)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>/g; +const AUTOMATIC_CROSSOVER_UNDERLAY_TAG = /]*\bdata-graph-role="automatic-crossover-underlay"[^>]*\/>/i; +const HTML_VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']); +const SVG_HTML_INTEGRATION_POINTS = new Set(['foreignobject', 'desc', 'title']); +const HTML_ATTRIBUTE = /([A-Za-z_:][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g; +const readerContract = readerContractFromHtml(html); +const NUMERIC_ATTRS = new Set([ + 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'dx', 'dy', 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', + 'fr', 'width', 'height', 'd', 'points', 'pathlength', 'transform', 'viewbox', 'offset', + 'opacity', 'fill-opacity', 'flood-opacity', 'stop-opacity', 'stroke-opacity', 'font-size', + 'font-size-adjust', 'font-weight', 'letter-spacing', 'word-spacing', 'kerning', 'stroke-width', + 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'textlength', 'startoffset', + 'rotate', 'markerwidth', 'markerheight', 'refx', 'refy', 'orient', 'patterntransform', + 'gradienttransform', 'filterres', 'stddeviation', 'basefrequency', 'numoctaves', 'seed', + 'surfacescale', 'diffuseconstant', 'specularconstant', 'specularexponent', + 'limitingconeangle', 'azimuth', 'elevation', 'pointsatx', 'pointsaty', 'pointsatz', + 'kernelmatrix', 'order', 'divisor', 'bias', 'targetx', 'targety', 'kernelunitlength', + 'scale', 'radius', 'k1', 'k2', 'k3', 'k4', 'tablevalues', 'slope', 'intercept', + 'amplitude', 'exponent', +]); +const ELEMENT_NUMERIC_ATTRS = new Map([ + ['fecolormatrix', new Set(['values'])], + ['fepointlight', new Set(['z'])], + ['fespotlight', new Set(['z'])], +]); + +function addCheck(name, ok, details = []) { + checks.push({ name, ok, details }); +} + +const svgMatches = [...html.matchAll(//gi)]; +addCheck('single_svg', svgMatches.length === 1, [`found ${svgMatches.length} block(s)`]); + +if (svgMatches.length === 1) { + const svg = svgMatches[0][0]; + const svgRoot = svg.match(/]*>/i)?.[0] || ''; + const svgAttrs = parseAttrs(svgRoot); + const qualityProfile = svgAttrs['data-quality-profile'] || 'standard'; + const workflowV2 = svgAttrs['data-layout-contract'] === 'readable-v2'; + const qualityGatesEnforced = svgAttrs['data-quality-gates'] !== 'advisory'; + const nonFiniteAttrs = collectNonFiniteAttrs(svg); + addCheck('finite_svg', nonFiniteAttrs.length === 0, nonFiniteAttrs); + const legendStart = svg.indexOf(''); + const beforeLegend = legendStart >= 0 ? svg.slice(0, legendStart) : svg; + const desktopReadability = collectDesktopReadability(svgAttrs, beforeLegend, readerContract); + const desktopReadabilityIssue = desktopReadability.issue; + // The browser gate measures this later; the geometry is already certain here. + const viewportHeightIssue = predictedFixedWidthOverflow({ + viewBoxWidth: viewBoxSize(svgAttrs)[0], + viewBoxHeight: viewBoxSize(svgAttrs)[1], + readerFit: svgAttrs['data-reader-fit'] || null, + diagramType: svgAttrs['data-diagram-type'] || null, + }); + const arrows = collectArrows(beforeLegend, workflowV2); + const diagonal = arrows.flatMap((arrow) => diagonalStraightSegments(arrow).map((segment) => ({ arrow, ...segment }))); + addCheck( + 'orthogonal_arrows', + diagonal.length === 0, + diagonal.map(({ arrow, segmentIndex }) => `${arrow.kind} ${arrow.index} segment ${segmentIndex + 1}: expected an orthogonal segment or an explicitly authored direct straight route; ${arrow.raw}`), + ); + const measuredRelationshipCrossings = collectRelationshipCrossings(arrows, workflowV2); + const resolvedCrossovers = measuredRelationshipCrossings.filter((hit) => ( + hit.left.crossoverHalo && hit.right.crossoverHalo + )); + const relationshipCrossings = measuredRelationshipCrossings.filter((hit) => ( + !hit.left.crossoverHalo || !hit.right.crossoverHalo + )); + const compositionFrames = collectCompositionFrames(beforeLegend); + const containerBorderRuns = collectBorderRuns({ + routedRelations: arrows + .filter((arrow) => arrow.from && arrow.to && arrow.borderSegments.length) + .map((arrow) => ({ + relation: arrow, + relationIndex: arrow.index, + segments: arrow.borderSegments, + })), + frames: compositionFrames, + }); + const routedRelationships = arrows + .filter((arrow) => arrow.from && arrow.to && arrow.routePoints.length) + .map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })); + const nodeRects = svgAttrs.transform ? [] : collectUntransformedNodeRects(beforeLegend); + const routeMetrics = routeBudgetMetrics({ routedRelations: routedRelationships }); + // Architecture marks only one of its two Reader fits with its type. + const crowdedSides = svgAttrs['data-diagram-type'] === 'architecture' || svgAttrs['data-reader-primary-text'] === '14' + ? crowdedNodeSides(arrows, nodeRects) : []; + const routeRhythmIssues = collectRouteRhythmIssues({ routedRelations: routedRelationships }); + const ambiguousCorridors = collectAmbiguousCorridors({ + routedRelations: routedRelationships, + includeSharedEndpoints: (left, right) => workflowV2 || (left.independentPorts && right.independentPorts), + allowShortWorkflowTrunks: workflowV2, + includeSharedEndpointCounterflow: (left, right) => left.automaticWorkflowRoute && right.automaticWorkflowRoute, + }); + const arrowheadCollisions = collectArrowheadCollisions({ + routedRelations: routedRelationships.filter((entry) => workflowV2 || entry.relation.independentPorts), + allowShortWorkflowTrunks: workflowV2, + }); + const relationshipLabels = collectRelationshipLabelMasks(beforeLegend, arrows); + const leadingSpace = collectArchitectureLeadingSpace({ + svgAttrs, fragment: beforeLegend, nodeRects, frames: compositionFrames, + arrows, labels: relationshipLabels, + }); + const sequenceColumnSpace = collectSequenceColumnSpace({ svgAttrs, fragment: beforeLegend, nodeRects, arrows }); + const labelClearanceThreshold = qualityProfile === 'showcase' ? 4 : 2; + const labelRouteMeasurements = collectLabelRouteClearance({ + labels: relationshipLabels, + routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })), + threshold: Number.MAX_VALUE, + }); + const labelRouteClearance = collectLabelRouteClearance({ + labels: relationshipLabels, + routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })), + threshold: labelClearanceThreshold, + }); + // The renderers bound their own label rects, but `check` also re-measures an + // artifact it did not produce; see collectLabelCanvasOverflow in + // shared/geometry.mjs. + const labelCanvasOverflow = collectLabelCanvasOverflow({ + labels: relationshipLabels, + viewBox: viewBoxRect(svgAttrs), + }); + const crossingIsError = qualityProfile === 'showcase'; + const corridorIsError = qualityProfile === 'showcase'; + const rhythmIsError = qualityProfile === 'showcase'; + const labelClearanceIsError = qualityProfile === 'showcase'; + const labelContainmentIsError = qualityProfile === 'showcase'; + const desktopReadabilityIsError = qualityProfile === 'showcase'; + // Certain geometry, but new: surface it as evidence first (CONTRIBUTING.md#product-and-compatibility-contracts). + const viewportHeightIsError = false; + const compositionErrors = (qualityGatesEnforced ? containerBorderRuns.length : 0) + + (crossingIsError ? relationshipCrossings.length : 0) + + (corridorIsError ? ambiguousCorridors.length + arrowheadCollisions.length : 0) + + (labelClearanceIsError ? labelRouteClearance.length : 0) + + (labelContainmentIsError ? labelCanvasOverflow.length : 0) + + (rhythmIsError ? routeRhythmIssues.length : 0) + + (desktopReadabilityIsError && desktopReadabilityIssue ? 1 : 0) + + (viewportHeightIsError && viewportHeightIssue ? 1 : 0); + const compositionWarnings = (qualityGatesEnforced ? 0 : containerBorderRuns.length) + + (crossingIsError ? 0 : relationshipCrossings.length) + + (corridorIsError ? 0 : ambiguousCorridors.length + arrowheadCollisions.length) + + (labelClearanceIsError ? 0 : labelRouteClearance.length) + + (labelContainmentIsError ? 0 : labelCanvasOverflow.length) + + (rhythmIsError ? 0 : routeRhythmIssues.length) + + (desktopReadabilityIsError || !desktopReadabilityIssue ? 0 : 1) + + (viewportHeightIsError || !viewportHeightIssue ? 0 : 1); + composition = { + schemaVersion: 1, + profile: qualityProfile, + status: compositionErrors ? 'fail' : 'pass', + summary: { + errors: compositionErrors, + warnings: compositionWarnings, + }, + metrics: { + properCrossings: relationshipCrossings.length, + resolvedCrossovers: resolvedCrossovers.length, + ambiguousCorridors: ambiguousCorridors.length, + arrowheadCollisions: arrowheadCollisions.length, + containerBorderRuns: containerBorderRuns.length, + labelRouteClearanceIssues: labelRouteClearance.length, + labelCanvasOverflowIssues: labelCanvasOverflow.length, + minLabelRouteClearance: minimumLabelRouteClearance(labelRouteMeasurements), + desktopReadabilityIssues: desktopReadabilityIssue ? 1 : 0, + viewportHeightIssues: viewportHeightIssue ? 1 : 0, + minProjectedNodeTextPx: desktopReadability.evidence.minimumProjectedTextPx, + ...roundedRouteMetrics(routeMetrics), + }, + suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 }, + // Perceptual review needs the affected relationships, not just a total. + // These are review evidence, not new pass/fail thresholds: a short, clear + // crossover can be preferable to a long crossing-free detour. + routeReview: { + crossings: resolvedCrossovers.map((hit) => { + const shared = [hit.left.from, hit.left.to].find((id) => id && (id === hit.right.from || id === hit.right.to)); + return { + left: relationshipRecord(hit.left), + right: relationshipRecord(hit.right), + point: hit.point, + ...(shared ? { sharedNode: shared } : {}), + }; + }), + ...(crowdedSides.length ? { crowdedSides } : {}), + detours: routedRelationships.flatMap((entry) => { + const metrics = routeBudgetMetrics({ routedRelations: [entry] }); + const blockers = directCorridorBlockers(entry.relation, nodeRects); + return metrics.routesOverSuggestedBends || metrics.routesOverSuggestedStretch ? [{ + relationship: relationshipRecord(entry.relation), + bends: metrics.maxBends, + stretch: metrics.maxStretch == null ? null : Math.round(metrics.maxStretch * 1000) / 1000, + ...(blockers.length ? { directCorridorBlockers: blockers } : {}), + }] : []; + }), + }, + leadingSpace, + ...(sequenceColumnSpace ? { sequenceColumnSpace } : {}), + desktopReadability: desktopReadability.evidence, + issues: [ + ...containerBorderRuns.map((hit) => ({ + severity: qualityGatesEnforced ? 'error' : 'warning', + code: 'composition/container-border-run', + relationship: relationshipRecord(hit.relation), + frame: frameRecord(hit.frame), + side: hit.side, + segmentIndex: hit.segmentIndex, + overlapLength: Math.round(hit.overlapLength * 10) / 10, + from: hit.overlapStart.map((value) => Math.round(value * 10) / 10), + to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10), + })), + ...labelRouteClearance.map((hit) => ({ + severity: labelClearanceIsError ? 'error' : 'warning', + code: 'composition/label-route-clearance', + label: hit.label?.label || hit.labelRelation?.label || '', + labelRelationship: relationshipRecord(hit.labelRelation), + otherRelationship: relationshipRecord(hit.otherRelation), + segmentIndex: hit.segmentIndex, + labelRect: roundedRect(hit.rect), + clearance: Math.round(hit.clearance * 10) / 10, + intersectionLength: Math.round((hit.intersectionLength || 0) * 10) / 10, + threshold: hit.threshold, + from: hit.start.map((value) => Math.round(value * 10) / 10), + to: hit.end.map((value) => Math.round(value * 10) / 10), + })), + ...labelCanvasOverflow.map((hit) => { + const label = hit.label?.label || hit.relation?.label || ''; + return { + severity: labelContainmentIsError ? 'error' : 'warning', + code: 'composition/label-canvas-containment', + label, + relationship: relationshipRecord(hit.relation), + labelRect: roundedRect(hit.rect), + viewBox: hit.viewBox, + viewBoxOrigin: hit.viewBoxOrigin, + overflowPx: hit.overflowPx, + detail: `[composition/label-canvas-containment] ${qualityProfile} label "${label}" on ${relationshipName(hit.relation)} extends past the ${describeLabelCanvasOverflow(hit)} (label rect ${formatRect(hit.rect)}; viewBox ${hit.viewBox[0]}x${hit.viewBox[1]}${hit.viewBoxOrigin.some(Boolean) ? ` at ${hit.viewBoxOrigin[0]},${hit.viewBoxOrigin[1]}` : ''}) — use renderer-supported label controls (shorten the label or reorder participants for sequence; otherwise labelAt, labelDx, labelDy, or labelSegment), or enlarge meta.viewBox.`, + }; + }), + ...relationshipCrossings.map((hit) => ({ + severity: crossingIsError ? 'error' : 'warning', + code: 'composition/proper-crossing', + relationship: relationshipRecord(hit.left), + otherRelationship: relationshipRecord(hit.right), + point: hit.point.map((value) => Math.round(value * 10) / 10), + })), + ...ambiguousCorridors.map((hit) => ({ + severity: corridorIsError ? 'error' : 'warning', + code: 'composition/ambiguous-corridor', + relationship: relationshipRecord(hit.left.relation), + otherRelationship: relationshipRecord(hit.right.relation), + segmentIndex: hit.leftSegment, + otherSegmentIndex: hit.rightSegment, + overlapLength: Math.round(hit.overlapLength * 10) / 10, + from: hit.overlapStart.map((value) => Math.round(value * 10) / 10), + to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10), + })), + ...arrowheadCollisions.map((hit) => ({ + severity: corridorIsError ? 'error' : 'warning', + code: 'composition/arrowhead-collision', + relationship: relationshipRecord(hit.left.relation), + otherRelationship: relationshipRecord(hit.right.relation), + distancePx: hit.distance, + minimumPx: hit.minimum, + endpoints: [hit.left.tip, hit.right.tip], + })), + ...routeRhythmIssues.map((hit) => ({ + severity: rhythmIsError ? 'error' : 'warning', + code: hit.code, + relationship: relationshipRecord(hit.relation), + segmentIndex: hit.segmentIndex, + position: hit.position, + length: Math.round(hit.length * 10) / 10, + from: hit.start.map((value) => Math.round(value * 10) / 10), + to: hit.end.map((value) => Math.round(value * 10) / 10), + })), + ...(desktopReadabilityIssue ? [{ + severity: desktopReadabilityIsError ? 'error' : 'warning', + code: 'composition/desktop-readability', + ...(desktopReadabilityIssue.nodeId ? { nodeId: desktopReadabilityIssue.nodeId } : {}), + owner: desktopReadabilityIssue.owner, + viewportWidth: DESKTOP_READABILITY_VIEWPORT.width, + viewportHeight: DESKTOP_READABILITY_VIEWPORT.height, + availableDiagramWidth: desktopReadability.evidence.availableDiagramWidth, + budgetBasis: desktopReadability.evidence.budgetBasis, + readerContract: desktopReadability.evidence.readerContract, + requestedTargetPx: desktopReadability.evidence.requestedTargetPx, + requestedTargetMet: desktopReadability.evidence.requestedTargetMet, + budgetLimit: desktopReadability.evidence.limit, + viewBoxWidth: desktopReadabilityIssue.viewBoxWidth, + scale: desktopReadabilityIssue.scale, + text: desktopReadabilityIssue.text, + detail: desktopReadabilityIssue.detail, + sourceFontPx: desktopReadabilityIssue.sourceFontPx, + projectedFontPx: desktopReadabilityIssue.projectedFontPx, + minimumProjectedFontPx: MIN_PROJECTED_NODE_TEXT_PX, + }] : []), + ...(viewportHeightIssue ? [{ + severity: viewportHeightIsError ? 'error' : 'warning', + code: 'composition/viewport-height', + readerFit: svgAttrs['data-reader-fit'] || null, + viewBoxWidth: viewBoxSize(svgAttrs)[0], + viewBoxHeight: viewBoxSize(svgAttrs)[1], + ...viewportHeightIssue, + detail: `[composition/viewport-height] ${describeFixedWidthOverflow({ ...viewportHeightIssue, viewBoxWidth: viewBoxSize(svgAttrs)[0], viewBoxHeight: viewBoxSize(svgAttrs)[1] })}`, + }] : []), + ], + }; + addCheck( + 'label_route_clearance', + !labelClearanceIsError || labelRouteClearance.length === 0, + labelRouteClearance.map((hit) => ( + `[composition/label-route-clearance] ${qualityProfile} label "${hit.label?.label || hit.labelRelation?.label || ''}" on ${relationshipName(hit.labelRelation)} is ${Math.round(hit.clearance * 10) / 10}px from ${relationshipName(hit.otherRelation)} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]${hit.intersectionLength > 0 ? ` with ${Math.round(hit.intersectionLength * 10) / 10}px hidden by the mask` : ''} (minimum ${hit.threshold}px) — use renderer-supported label controls (message y for sequence; otherwise labelAt, labelDx, labelDy, or labelSegment), or adjust the other relationship route/via/channel.` + )), + ); + addCheck( + 'relationship_crossings', + !crossingIsError || relationshipCrossings.length === 0, + relationshipCrossings.map((hit) => ( + `[composition/proper-crossing] ${qualityProfile} ${relationshipName(hit.left)} crosses ${relationshipName(hit.right)} at [${formatPoint(hit.point)}]` + )), + ); + addCheck( + 'relationship_corridors', + !corridorIsError || ambiguousCorridors.length + arrowheadCollisions.length === 0, + [...ambiguousCorridors.map((hit) => ( + `[composition/ambiguous-corridor] ${qualityProfile} ${relationshipName(hit.left.relation)} shares a ${Math.round(hit.overlapLength * 10) / 10}px corridor with ${relationshipName(hit.right.relation)} at [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]` + )), ...arrowheadCollisions.map((hit) => ( + `[composition/arrowhead-collision] ${qualityProfile} ${relationshipName(hit.left.relation)} and ${relationshipName(hit.right.relation)} have incoming arrowheads ${hit.distance}px apart (minimum ${hit.minimum}px) — enlarge or reposition the destination, or choose separate toSide ports.` + ))], + ); + addCheck( + 'container_border_runs', + !qualityGatesEnforced || containerBorderRuns.length === 0, + containerBorderRuns.map((hit) => ( + `[composition/container-border-run] ${relationshipName(hit.relation)} follows ${frameName(hit.frame)} ${hit.side} border for ${Math.round(hit.overlapLength * 10) / 10}px on segment ${hit.segmentIndex} [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]` + )), + ); + addCheck( + 'route_rhythm', + !rhythmIsError || routeRhythmIssues.length === 0, + routeRhythmIssues.map((hit) => ( + `[${hit.code}] ${qualityProfile} ${relationshipName(hit.relation)} has a ${Math.round(hit.length * 10) / 10}px ${hit.position} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]` + )), + ); + + if (legendStart >= 0) { + const legendFragment = svg.slice(legendStart); + const legendBoxes = collectLegendBoxes(legendFragment); + const collisions = collectLegendCollisions(arrows, legendBoxes); + addCheck( + 'legend_clearance', + collisions.length === 0, + collisions.map((hit) => `${hit.arrow.kind} ${hit.arrow.index} crosses legend ${hit.box.label}`), + ); + } else { + addCheck('legend_clearance', true, ['no legend marker found']); + } +} + +const ok = checks.every((check) => check.ok) && composition.status !== 'fail'; +console.log(JSON.stringify({ ok, file: htmlPath, artifact, checks, composition }, null, 2)); +// Let pending stdout writes drain: large receipts are asynchronous when piped. +process.exitCode = ok ? 0 : 1; + +function collectArrows(fragment, useActualPoints = false) { + const arrows = []; + let index = 0; + let previousTag = null; + let previousTagEnd = 0; + + for (const tag of fragment.matchAll(/<(path|line)\b[^>]*>/gi)) { + const tagStart = tag.index; + const raw = tag[0]; + const gap = previousTag ? fragment.slice(previousTagEnd, tagStart) : ''; + const precedingUnderlay = previousTag + && /^\s*$/.test(gap) + && previousTag.name === 'path' + && previousTag.underlayAttrs; + previousTag = { + name: tag[1].toLowerCase(), + underlayAttrs: tag[1].toLowerCase() === 'path' && AUTOMATIC_CROSSOVER_UNDERLAY_TAG.test(raw) + ? parseAttrs(raw) + : null, + }; + previousTagEnd = tagStart + raw.length; + if (!/\bclass="[^"]*\ba-(?:default|emphasis|security|dashed)\b/.test(raw)) continue; + if (!/\bmarker-end=/.test(raw)) continue; + const attrs = parseAttrs(raw); + const routeStrokeWidth = numberAttr(attrs, 'stroke-width'); + const underlayStrokeWidth = precedingUnderlay ? numberAttr(precedingUnderlay, 'stroke-width') : NaN; + const verifiedCrossoverHalo = attrs['data-composition-crossover'] === 'halo' + && precedingUnderlay?.d === attrs.d + && precedingUnderlay?.fill === 'none' + && precedingUnderlay?.stroke === 'var(--mask)' + && precedingUnderlay?.['pointer-events'] === 'none' + && Number.isFinite(routeStrokeWidth) + && Number.isFinite(underlayStrokeWidth) + && underlayStrokeWidth >= routeStrokeWidth + 3; + const segments = tag[1].toLowerCase() === 'line' + ? lineSegments(attrs) + : pathSegments(attrs.d || ''); + const borderSegments = tag[1].toLowerCase() === 'line' + ? segments + : straightPathSegments(attrs.d || ''); + arrows.push({ + kind: tag[1].toLowerCase(), + index: index += 1, + raw, + // Trust route intent only for a semantic edge with one visible direct + // segment. A stale marker on bent/curved geometry cannot waive the gate. + authoredStraight: attrs['data-composition-route'] === 'straight' + && Boolean(attrs['data-edge-from'] && attrs['data-edge-to']) + && segments.length === 1 && borderSegments.length === 1 + && (tag[1].toLowerCase() === 'line' || /^\s*M\s+[-+\d.eE]+\s+[-+\d.eE]+\s+L\s+[-+\d.eE]+\s+[-+\d.eE]+\s*$/.test(attrs.d || '')), + crossoverHalo: verifiedCrossoverHalo, + independentPorts: verifiedCrossoverHalo && attrs['data-composition-independent'] === 'true', + // Compatibility with first-round exports lacking a root layout contract. + // Readable-v2's root contract supersedes this narrower automatic-pair rule. + // This marker never certifies a crossover halo or waives a quality rule. + automaticWorkflowRoute: attrs['data-composition-routing'] === 'workflow-v2-auto', + width: routeStrokeWidth, + variant: raw.match(/\ba-(default|emphasis|security|dashed)\b/)?.[1] || 'default', + role: attrs['data-edge-role'], + segments, + borderSegments, + routePoints: (!useActualPoints && parseRoutePoints(attrs['data-composition-points'])) || ( + borderSegments.length ? [borderSegments[0].start, ...borderSegments.map((segment) => segment.end)] : [] + ), + from: attrs['data-edge-from'] || attrs['data-composition-edge-from'], + to: attrs['data-edge-to'] || attrs['data-composition-edge-to'], + id: attrs['data-edge-id'] || attrs['data-composition-edge-id'], + key: attrs['data-edge-key'], + label: attrs['data-edge-label'], + offset: tag.index, + }); + } + + return arrows; +} + +function collectRelationshipLabelMasks(fragment, arrows) { + const labels = []; + for (const match of fragment.matchAll(/]*\bdata-edge-(?:key|id|from)="[^"]*"[^>]*>[\s\S]*?<\/g>/gi)) { + const group = match[0]; + const groupAttrs = parseAttrs(group.match(/]*>/i)?.[0] || ''); + const rectTag = [...group.matchAll(/]*>/gi)] + .map((item) => item[0]) + .find((tag) => /\bclass="[^"]*\bc-mask\b/.test(tag)); + if (!rectTag) continue; + const attrs = parseAttrs(rectTag); + const rect = { + x: numberAttr(attrs, 'x'), + y: numberAttr(attrs, 'y'), + width: numberAttr(attrs, 'width'), + height: numberAttr(attrs, 'height'), + }; + if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) continue; + const groupStart = match.index; + const groupEnd = groupStart + group.length; + const containedOwner = arrows.find((arrow) => ( + arrow.offset > groupStart + && arrow.offset < groupEnd + && arrow.from === groupAttrs['data-edge-from'] + && arrow.to === groupAttrs['data-edge-to'] + && (!groupAttrs['data-edge-id'] || !arrow.id || arrow.id === groupAttrs['data-edge-id']) + )); + const owner = arrows.find((arrow) => ( + groupAttrs['data-edge-key'] !== undefined && arrow.key === groupAttrs['data-edge-key'] + )) || containedOwner || arrows.find((arrow) => ( + arrow.id === groupAttrs['data-edge-id'] + && arrow.from === groupAttrs['data-edge-from'] + && arrow.to === groupAttrs['data-edge-to'] + )); + if (!owner) continue; + if (owner.key === undefined && groupAttrs['data-edge-key'] !== undefined) owner.key = groupAttrs['data-edge-key']; + if (!owner.id && groupAttrs['data-edge-id']) owner.id = groupAttrs['data-edge-id']; + if (!owner.label && groupAttrs['data-edge-label']) owner.label = groupAttrs['data-edge-label']; + labels.push({ + relation: owner, + relationIndex: owner.index, + label: groupAttrs['data-edge-label'] || '', + rect, + }); + } + return labels; +} + +function roundedRect(rect) { + return Object.fromEntries(Object.entries(rect).map(([key, value]) => [key, Math.round(value * 10) / 10])); +} + +function roundedRouteMetrics(metrics) { + return { + ...metrics, + maxStretch: metrics.maxStretch == null ? null : Math.round(metrics.maxStretch * 1000) / 1000, + minSegmentPx: metrics.minSegmentPx == null ? null : Math.round(metrics.minSegmentPx * 10) / 10, + minInteriorSegmentPx: metrics.minInteriorSegmentPx == null ? null : Math.round(metrics.minInteriorSegmentPx * 10) / 10, + }; +} + +function parseRoutePoints(value) { + if (!value) return null; + const points = value.split(';').map((pair) => pair.split(',').map(Number)); + return points.length >= 2 && points.every(isPoint) ? points : null; +} + +function collectRelationshipCrossings(arrows, includeSharedEndpoints = false) { + const relationships = arrows.filter((arrow) => arrow.from && arrow.to && arrow.segments.length).map(arrow => ({ + ...arrow, + // Readable-v2 routePoints come from the visible path, never its metadata. + // A straight-through via is not a visual endpoint; preserve real bends. + segments: includeSharedEndpoints ? forwardCollinearAnalysisSegments(arrow.routePoints) : arrow.segments, + })); + const crossings = []; + for (let leftIndex = 0; leftIndex < relationships.length; leftIndex += 1) { + const left = relationships[leftIndex]; + for (let rightIndex = leftIndex + 1; rightIndex < relationships.length; rightIndex += 1) { + const right = relationships[rightIndex]; + // A shared semantic endpoint does not make an interior X a junction + // when both paths opt into automatic workflow or independent-port checks. + if ([left.from, left.to].some((id) => id === right.from || id === right.to) + && !includeSharedEndpoints + && !(left.independentPorts && right.independentPorts) + && !(left.automaticWorkflowRoute && right.automaticWorkflowRoute)) continue; + let point = null; + for (const leftSegment of left.segments) { + for (const rightSegment of right.segments) { + point = properSegmentIntersection(leftSegment.start, leftSegment.end, rightSegment.start, rightSegment.end); + if (point) break; + } + if (point) break; + } + if (point) crossings.push({ left, right, point }); + } + } + return crossings; +} + +function relationshipName(arrow) { + return arrow.id + ? `relationship id "${arrow.id}" ("${arrow.from}" -> "${arrow.to}")` + : `relationship "${arrow.from}" -> "${arrow.to}"`; +} + +// Review evidence only. Mask rectangles are the visible node bounds emitted by +// our renderers. Skip transformed ancestry rather than mixing coordinate spaces. +function collectUntransformedNodeRects(fragment) { + const groups = []; + const nodes = new Map(); + for (const token of fragment.matchAll(SVG_TAG_TOKEN)) { + if (!token[2]) continue; + const name = token[2].toLowerCase(); + if (name === 'g') { + if (token[1]) groups.pop(); + else if (!/\/\s*>$/.test(token[0])) groups.push(parseAttrs(token[0])); + continue; + } + if (name !== 'rect' || token[1] || groups.some((group) => group.transform)) continue; + const attrs = parseAttrs(token[0]); + const owner = [...groups].reverse().find((group) => group['data-node-id']); + if (!owner || attrs.transform || !String(attrs.class || '').split(/\s+/).includes('c-mask')) continue; + const box = ['x', 'y', 'width', 'height'].map((key) => numberAttr(attrs, key)); + if (!box.every(Number.isFinite) || box[2] <= 0 || box[3] <= 0) continue; + const id = owner['data-node-id']; + // Ambiguous ownership is not reliable evidence for moving a node. + nodes.set(id, nodes.has(id) ? null : { id, label: owner['data-node-label'] || id, box }); + } + return [...nodes.values()].filter(Boolean); +} + +function directCorridorBlockers(relation, nodes) { + const from = nodes.find((node) => node.id === relation.from); + const to = nodes.find((node) => node.id === relation.to); + if (!from || !to || from === to) return []; + const center = ({ box: [x, y, w, h] }) => [x + w / 2, y + h / 2]; + const a = center(from); + const b = center(to); + const horizontal = Math.abs(a[1] - b[1]) < 0.01; + const vertical = Math.abs(a[0] - b[0]) < 0.01; + if (horizontal === vertical) return []; + const axis = horizontal ? 0 : 1; + const cross = 1 - axis; + const [first, last] = a[axis] < b[axis] ? [from, to] : [to, from]; + const low = first.box[axis] + first.box[axis + 2]; + const high = last.box[axis]; + if (high <= low) return []; + return nodes.filter((node) => node !== from && node !== to + && node.box[axis] < high && node.box[axis] + node.box[axis + 2] > low + && node.box[cross] < a[cross] && node.box[cross] + node.box[cross + 2] > a[cross]); +} + +// Automatic ports need a 16px corner gutter and 14px between neighbours, so a +// side facing more counterparts than that fits pushes routes onto other sides. +function crowdedNodeSides(arrows, nodes) { + const byId = new Map(nodes.map((node) => [node.id, node])); + const demand = new Map(); + for (const arrow of arrows) { + const from = byId.get(arrow.from); + const to = byId.get(arrow.to); + if (!from || !to || from === to) continue; + for (const [node, other] of [[from, to], [to, from]]) { + const [x, y, w, h] = node.box; + const dx = other.box[0] + other.box[2] / 2 - (x + w / 2); + const dy = other.box[1] + other.box[3] / 2 - (y + h / 2); + const side = dx !== 0 && Math.abs(dx) >= Math.abs(dy) ? (dx < 0 ? 'left' : 'right') : (dy > 0 ? 'bottom' : 'top'); + const key = `${node.id}\u0000${side}`; + const entry = demand.get(key) || { node, side, relationships: 0 }; + entry.relationships += 1; + demand.set(key, entry); + } + } + return [...demand.values()].flatMap(({ node, side, relationships }) => { + const sidePx = side === 'left' || side === 'right' ? node.box[3] : node.box[2]; + const neededPx = 32 + 14 * (relationships - 1); + return relationships > 1 && sidePx < neededPx + ? [{ node: node.id, label: node.label, side, relationships, sidePx, neededPx }] : []; + }); +} + +function relationshipRecord(arrow) { + const stableIndex = Number(arrow.key); + return { + id: arrow.id, + from: arrow.from, + to: arrow.to, + label: arrow.label || '', + collectionIndex: Number.isInteger(stableIndex) && stableIndex >= 0 ? stableIndex : arrow.index - 1, + artifactIndex: arrow.index, + }; +} + +function collectCompositionFrames(fragment) { + const frames = []; + for (const match of fragment.matchAll(/<(rect|path|line)\b[^>]*>/gi)) { + const attrs = parseAttrs(match[0]); + const kind = attrs['data-composition-frame-kind']; + if (!kind) continue; + const identity = attrs['data-composition-frame-id'] || frames.length; + if (match[1].toLowerCase() === 'rect') { + const frame = { + kind, + id: identity, + x: numberAttr(attrs, 'x'), + y: numberAttr(attrs, 'y'), + width: numberAttr(attrs, 'width'), + height: numberAttr(attrs, 'height'), + radius: numberAttr(attrs, 'rx') || 0, + }; + if ([frame.x, frame.y, frame.width, frame.height].every(Number.isFinite)) frames.push(frame); + continue; + } + const segments = match[1].toLowerCase() === 'line' + ? lineSegments(attrs) + : pathSegments(attrs.d || ''); + for (const [segmentIndex, segment] of segments.entries()) { + frames.push({ + kind, + id: segments.length > 1 ? `${identity}:${segmentIndex}` : identity, + shape: 'line', + start: segment.start, + end: segment.end, + }); + } + } + return frames; +} + +// Evidence for author review only. Automatic Architecture is identifiable by +// its intrinsic Reader contract; authored canvases must retain their geometry. +function collectArchitectureLeadingSpace({ svgAttrs, fragment, nodeRects, frames, arrows, labels }) { + const evidence = { measured: false, reviewSuggested: false }; + if (svgAttrs['data-reader-fit'] !== 'intrinsic-height' + || svgAttrs['data-reader-primary-text'] !== '14' + || svgAttrs.transform + || [...fragment.matchAll(/]*\btransform\s*=[^>]*>/gi)] + .some((match) => !/\bdata-semantic-sigil=/.test(match[0])) + || /<(?:path|line|rect|text)\b[^>]*\btransform\s*=/i.test(fragment)) return evidence; + const [originX, originY, width, height] = viewBoxRect(svgAttrs); + if (![originX, originY, width, height].every(Number.isFinite) || width <= 0 || height <= 0) return evidence; + const nodeCount = [...fragment.matchAll(/]*\bdata-node-id=/gi)].length; + if (!nodeRects.length || nodeRects.length !== nodeCount) return evidence; + const semanticArrows = arrows.filter((arrow) => arrow.from && arrow.to); + if (semanticArrows.some((arrow) => !arrow.routePoints.length)) return evidence; + + const occupied = nodeRects.map((node) => node.box[1]); + for (const frame of frames) { + const top = frame.shape === 'line' + ? Math.min(frame.start[1], frame.end[1]) : frame.y; + if (!Number.isFinite(top)) return evidence; + occupied.push(top); + } + // Boundary titles can protrude above their structural frame. + for (const match of fragment.matchAll(/]*\bdata-graph-role="structural-frame-label"[^>]*>[\s\S]*?]*\bdata-graph-role="structural-frame-label-mask"[^>]*>/gi)) { + const rect = parseAttrs(match[0].match(/]*>/i)?.[0] || ''); + const y = numberAttr(rect, 'y'); + if (!Number.isFinite(y)) return evidence; + occupied.push(y); + } + for (const arrow of semanticArrows) { + for (const point of arrow.routePoints) occupied.push(point[1]); + } + for (const label of labels) occupied.push(label.rect.y); + if (!occupied.every(Number.isFinite)) return evidence; + const occupiedTop = Math.min(...occupied); + const gap = Math.max(0, occupiedTop - originY); + const heights = nodeRects.map((node) => node.box[3]).sort((a, b) => a - b); + const typicalNodeHeight = heights[Math.floor(heights.length / 2)]; + const ratio = gap / height; + return { + measured: true, + emptyTopPx: Math.round(gap * 10) / 10, + emptyTopRatio: Math.round(ratio * 1000) / 1000, + occupiedTop: Math.round(occupiedTop * 10) / 10, + viewBoxTop: originY, + canvasHeight: height, + typicalNodeHeight, + reviewSuggested: gap > 2 * typicalNodeHeight && ratio > 0.2, + }; +} + +// Advisory only: fixed columns are a compatibility contract. Measure semantic +// content, including long labels/notes, rather than treating every wide canvas +// as wasted space. Auto-sized segment frames do not add a participant column. +function collectSequenceColumnSpace({ svgAttrs, fragment, nodeRects, arrows }) { + const columnFit = svgAttrs['data-sequence-column-fit']; + if (!['fixed', 'spread'].includes(columnFit)) return null; + const evidence = { measured: false, reviewSuggested: false, columnFit }; + if (svgAttrs.transform || /$/.test(token[0])) brandGroups.push(inBrand); + } + const [originX, , width, height] = viewBoxRect(svgAttrs); + if (![originX, width, height].every(Number.isFinite) || width <= 0 || height <= 0) return evidence; + const nodeCount = [...fragment.matchAll(/]*\bdata-node-id=/gi)].length; + if (!nodeRects.length || nodeCount !== nodeRects.length) return evidence; + const semanticArrows = arrows.filter((arrow) => arrow.from && arrow.to); + if (semanticArrows.some((arrow) => !arrow.routePoints.length)) return evidence; + const rightEdges = nodeRects.map((node) => node.box[0] + node.box[2]); + for (const arrow of semanticArrows) { + for (const point of arrow.routePoints) rightEdges.push(point[0]); + } + // Label plates, activations and segment titles also reserve horizontal room. + for (const match of fragment.matchAll(/]*>/gi)) { + const attrs = parseAttrs(match[0]); + if (!String(attrs.class || '').split(/\s+/).includes('c-mask')) continue; + rightEdges.push(numberAttr(attrs, 'x') + numberAttr(attrs, 'width')); + } + for (const match of fragment.matchAll(/]*)>([\s\S]*?)<\/text>/gi)) { + const box = textBox(parseAttrs(match[1]), stripTags(match[2]).trim()); + if (!box) return evidence; + rightEdges.push(box.x2); + } + if (!rightEdges.every(Number.isFinite)) return evidence; + const occupiedRight = Math.max(...rightEdges); + const gap = Math.max(0, originX + width - occupiedRight); + const ratio = gap / width; + const widths = nodeRects.map((node) => node.box[2]).sort((a, b) => a - b); + const typicalParticipantWidth = widths[Math.floor(widths.length / 2)]; + return { + measured: true, + columnFit, + participantCount: nodeCount, + occupiedRight: Math.round(occupiedRight * 10) / 10, + viewBoxLeft: originX, + canvasWidth: width, + emptyRightPx: Math.round(gap * 10) / 10, + emptyRightRatio: Math.round(ratio * 1000) / 1000, + typicalParticipantWidth, + // Avoid stretching a small conversation merely to fill its canvas. These + // conservative review thresholds never contribute errors or warnings. + reviewSuggested: columnFit === 'fixed' && nodeCount >= 4 + && gap > 2 * typicalParticipantWidth && ratio > 0.25, + }; +} + +function frameName(frame) { + return `${frame.kind || 'frame'} "${frame.id}"`; +} + +function frameRecord(frame) { + return { kind: frame.kind, id: frame.id }; +} + +function formatPoint(point) { + return point.map((value) => Math.round(value * 10) / 10).join(', '); +} + +function lineSegments(attrs) { + const start = [numberAttr(attrs, 'x1'), numberAttr(attrs, 'y1')]; + const end = [numberAttr(attrs, 'x2'), numberAttr(attrs, 'y2')]; + if (!isPoint(start) || !isPoint(end)) return []; + return [{ start, end }]; +} + +function pathSegments(d) { + const points = pointsFromPath(d); + const segments = []; + for (let i = 1; i < points.length; i += 1) { + segments.push({ start: points[i - 1], end: points[i] }); + } + return segments; +} + +// Border runs use exact visible primitives. Non-collinear Q curves are never +// flattened into chords here: a tangent or sampled near-horizontal curve is +// not a structural border run. A fully collinear Q remains a straight visible +// primitive and is included. +function straightPathSegments(d) { + const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || []; + const segments = []; + let i = 0; + let command = ''; + let current = [0, 0]; + let start = null; + while (i < tokens.length) { + if (isCommand(tokens[i])) command = tokens[i++]; + if (!command) break; + const absolute = command === command.toUpperCase(); + switch (command.toUpperCase()) { + case 'M': + case 'L': { + let first = true; + while (i + 1 < tokens.length && !isCommand(tokens[i])) { + const point = [Number.parseFloat(tokens[i++]), Number.parseFloat(tokens[i++])]; + if (!point.every(Number.isFinite)) break; + const next = absolute ? point : [current[0] + point[0], current[1] + point[1]]; + if (command.toUpperCase() === 'L' || !first) segments.push({ start: current, end: next }); + current = next; + if (!start) start = current; + first = false; + } + break; + } + case 'H': { + while (i < tokens.length && !isCommand(tokens[i])) { + const value = Number.parseFloat(tokens[i++]); + if (!Number.isFinite(value)) break; + const next = [absolute ? value : current[0] + value, current[1]]; + segments.push({ start: current, end: next }); + current = next; + } + break; + } + case 'V': { + while (i < tokens.length && !isCommand(tokens[i])) { + const value = Number.parseFloat(tokens[i++]); + if (!Number.isFinite(value)) break; + const next = [current[0], absolute ? value : current[1] + value]; + segments.push({ start: current, end: next }); + current = next; + } + break; + } + case 'Q': { + while (i + 3 < tokens.length && !isCommand(tokens[i])) { + const values = [0, 0, 0, 0].map(() => Number.parseFloat(tokens[i++])); + if (!values.every(Number.isFinite)) break; + const control = absolute ? values.slice(0, 2) : [current[0] + values[0], current[1] + values[1]]; + const end = absolute ? values.slice(2, 4) : [current[0] + values[2], current[1] + values[3]]; + if (Math.abs(crossProduct(current, control, end)) <= 1e-9) segments.push({ start: current, end }); + current = end; + } + break; + } + case 'Z': + if (start) segments.push({ start: current, end: start }); + current = start || current; + command = ''; + break; + default: + return []; + } + } + return segments.filter(({ start: a, end: b }) => isPoint(a) && isPoint(b)); +} + +function diagonalStraightSegments(arrow) { + if (arrow.authoredStraight) return []; + return arrow.borderSegments.flatMap(({ start, end }, segmentIndex) => ( + Math.abs(start[0] - end[0]) > 0.01 && Math.abs(start[1] - end[1]) > 0.01 + ? [{ segmentIndex, start, end }] + : [] + )); +} + +function collectLegendBoxes(fragment) { + const boxes = []; + + for (const match of fragment.matchAll(/]*>/gi)) { + const attrs = parseAttrs(match[0]); + const x = numberAttr(attrs, 'x'); + const y = numberAttr(attrs, 'y'); + const width = numberAttr(attrs, 'width'); + const height = numberAttr(attrs, 'height'); + if ([x, y, width, height].every(Number.isFinite)) { + boxes.push({ x1: x, y1: y, x2: x + width, y2: y + height, label: `rect@${x},${y}` }); + } + } + + for (const match of fragment.matchAll(/]*)>([\s\S]*?)<\/text>/gi)) { + const attrs = parseAttrs(match[1]); + const box = textBox(attrs, stripTags(match[2]).trim()); + if (box) boxes.push(box); + } + + return boxes; +} + +function collectLegendCollisions(arrows, boxes) { + const collisions = []; + for (const arrow of arrows) { + for (const segment of arrow.segments) { + for (const box of boxes) { + if (segmentIntersectsBox(segment, padBox(box, 2))) { + collisions.push({ arrow, box }); + } + } + } + } + return collisions; +} + +function textBox(attrs, text) { + const x = numberAttr(attrs, 'x'); + const y = numberAttr(attrs, 'y'); + const fontSize = Number.parseFloat(attrs['font-size'] || '10'); + if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(fontSize)) return null; + const width = estimatedTextWidth(text, fontSize); + const anchor = attrs['text-anchor'] || 'start'; + let x1 = x; + if (anchor === 'middle') x1 = x - width / 2; + if (anchor === 'end') x1 = x - width; + return { + x1, + y1: y - fontSize, + x2: x1 + width, + y2: y + fontSize * 0.25, + label: text || `text@${x},${y}`, + }; +} + +// A foreign artifact may author a legal non-zero viewBox origin, so containment +// needs all four numbers; sizing checks read the trailing pair. +function viewBoxRect(svgAttrs) { + const viewBox = String(svgAttrs.viewBox || '').trim().split(/[\s,]+/).map(Number); + return viewBox.length === 4 ? viewBox : [Number.NaN, Number.NaN, Number.NaN, Number.NaN]; +} + +function viewBoxSize(svgAttrs) { + return viewBoxRect(svgAttrs).slice(2); +} + +function hasAttribute(attrs, name) { + return new RegExp('(?:^|\\s)' + name + '(?:\\s*=|\\s|$)', 'i').test(attrs); +} + +function readerContractFromHtml(source) { + const markers = [...source.matchAll(/]*>/gi)] + .map((match) => parseAttrs(match[0])) + .filter((attrs) => attrs.name === 'archify-reader-contract'); + return markers.length === 1 && markers[0].content === DECLARED_WIDE_READER_CONTRACT + ? DECLARED_WIDE_READER_CONTRACT + : null; +} + +function collectDesktopReadability(svgAttrs, fragment, contract) { + const [viewBoxWidth, viewBoxHeight] = viewBoxSize(svgAttrs); + const requestedMinimumTextPx = Number.parseFloat(svgAttrs['data-reader-min-text'] || ''); + const entries = []; + let invalidSemanticText = false; + const groups = []; + // Keep the complete ancestry, rather than a node-only stack: an edge can + // share its context group with its label or nest that group (Sequence). + for (const match of fragment.matchAll(/|$)||$)|]*)>([\s\S]*?)<\/text>|]*>|<\/g\s*>/gi)) { + if (match[0].startsWith('$/.test(match[0])) groups.push(parseAttrs(match[0])); + continue; + } + const attrs = parseAttrs(match[1]); + if (attrs['data-detail'] === 'fine' || groups.some((group) => group['data-detail'] === 'fine')) continue; + const primary = hasAttribute(match[1], 'data-node-label'); + const boundary = hasAttribute(match[1], 'data-boundary-label'); + const context = attrs['data-detail'] === 'context' + || groups.some((group) => group['data-detail'] === 'context'); + const nodeOwner = [...groups].reverse().find((group) => group['data-node-id']); + const edgeOwner = [...groups].reverse().find((group) => group['data-edge-from'] && group['data-edge-to']); + const owner = primary + ? { kind: 'node', id: nodeOwner?.['data-node-id'] || null } + : boundary ? { kind: 'boundary', id: null } + : context && edgeOwner ? { + kind: 'edge', id: edgeOwner['data-edge-id'] || null, + from: edgeOwner['data-edge-from'], to: edgeOwner['data-edge-to'], + } + : context && nodeOwner ? { kind: 'node', id: nodeOwner['data-node-id'] } + : null; + // A context text with neither semantic owner is legend/fine/loose copy. + if (!owner) continue; + const fontSize = Number.parseFloat(attrs['font-size'] || ''); + if (!Number.isFinite(fontSize)) { + invalidSemanticText = true; + continue; + } + if (fontSize <= 0) invalidSemanticText = true; + entries.push({ + ...(owner.kind === 'node' && owner.id ? { nodeId: owner.id } : {}), + owner, + text: stripTags(match[2]).trim(), + detail: primary ? 'primary' : boundary ? 'boundary' : owner.kind === 'edge' ? 'edge' : 'context', + sourceFontPx: fontSize, + }); + } + const minimumSourceTextPx = entries.length ? Math.min(...entries.map((entry) => entry.sourceFontPx)) : Number.NaN; + const eligible = contract === DECLARED_WIDE_READER_CONTRACT + && svgAttrs['data-reader-fit'] === 'intrinsic-height' + && Number.isFinite(requestedMinimumTextPx) && requestedMinimumTextPx > 0 + && !invalidSemanticText && entries.length > 0; + const declared = eligible ? declaredWideReadabilityBudget({ + viewBoxWidth, + viewBoxHeight, + minimumSourceTextPx, + requestedMinimumTextPx, + }) : null; + const availableDiagramWidth = declared?.guaranteedSvgWidth ?? DESKTOP_READER_DIAGRAM_WIDTH; + const budgetBasis = declared ? 'recognized-declared-wide' : 'legacy-930'; + const scale = Number.isFinite(viewBoxWidth) && viewBoxWidth > 0 + ? Math.min(1, availableDiagramWidth / viewBoxWidth) : Number.NaN; + const projected = entries.map((entry) => ({ + ...entry, + viewBoxWidth, + scale, + projectedFontPx: projectedNodeTextPx(entry.sourceFontPx, viewBoxWidth, availableDiagramWidth), + })); + const worst = projected.reduce((current, entry) => ( + !current || entry.projectedFontPx < current.projectedFontPx ? entry : current + ), null); + const projectedMinimumTextPx = Number.isFinite(worst?.projectedFontPx) ? worst.projectedFontPx : null; + const hardFloorMet = Number.isFinite(worst?.projectedFontPx) + ? worst.projectedFontPx >= MIN_PROJECTED_NODE_TEXT_PX : null; + const requestedTargetMet = worst && Number.isFinite(requestedMinimumTextPx) + ? worst.projectedFontPx >= requestedMinimumTextPx : null; + const evidence = { + budgetBasis, + readerContract: contract, + availableDiagramWidth, + actualBudgetPx: availableDiagramWidth, + ...(declared ? { + actualReaderWidth: declared.actualReaderWidth, + desiredReaderWidth: declared.desiredReaderWidth, + viewportCap: declared.viewportCap, + limit: declared.limit, + } : { limit: 'legacy' }), + requestedTargetPx: Number.isFinite(requestedMinimumTextPx) ? requestedMinimumTextPx : null, + requestedTargetMet, + hardFloorPx: MIN_PROJECTED_NODE_TEXT_PX, + hardFloorMet, + minimumOwner: worst?.owner || null, + minimumSourceTextPx: worst?.sourceFontPx ?? null, + minimumProjectedTextPx: projectedMinimumTextPx, + semanticTextCount: entries.length, + }; + return { evidence, issue: worst && hardFloorMet === false ? worst : null }; +} + +function estimatedTextWidth(text, fontSize) { + let units = 0; + for (const char of text) units += char.charCodeAt(0) > 255 ? 1.8 : 0.62; + return Math.max(fontSize, units * fontSize); +} + +function pointsFromPath(d) { + const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || []; + const points = []; + let i = 0; + let command = ''; + let current = [0, 0]; + let start = null; + + while (i < tokens.length) { + if (isCommand(tokens[i])) command = tokens[i++]; + if (!command) break; + + const absolute = command === command.toUpperCase(); + switch (command.toUpperCase()) { + case 'M': + case 'L': { + while (i + 1 < tokens.length && !isCommand(tokens[i])) { + const x = Number.parseFloat(tokens[i++]); + const y = Number.parseFloat(tokens[i++]); + if (!Number.isFinite(x) || !Number.isFinite(y)) break; + current = absolute ? [x, y] : [current[0] + x, current[1] + y]; + points.push(current); + if (!start) start = current; + } + break; + } + case 'H': { + while (i < tokens.length && !isCommand(tokens[i])) { + const x = Number.parseFloat(tokens[i++]); + if (!Number.isFinite(x)) break; + current = absolute ? [x, current[1]] : [current[0] + x, current[1]]; + points.push(current); + } + break; + } + case 'V': { + while (i < tokens.length && !isCommand(tokens[i])) { + const y = Number.parseFloat(tokens[i++]); + if (!Number.isFinite(y)) break; + current = absolute ? [current[0], y] : [current[0], current[1] + y]; + points.push(current); + } + break; + } + case 'Q': { + while (i + 3 < tokens.length && !isCommand(tokens[i])) { + const controlX = Number.parseFloat(tokens[i++]); + const controlY = Number.parseFloat(tokens[i++]); + const endX = Number.parseFloat(tokens[i++]); + const endY = Number.parseFloat(tokens[i++]); + if (![controlX, controlY, endX, endY].every(Number.isFinite)) break; + const control = absolute + ? [controlX, controlY] + : [current[0] + controlX, current[1] + controlY]; + const end = absolute + ? [endX, endY] + : [current[0] + endX, current[1] + endY]; + const startPoint = current; + for (let step = 1; step <= 8; step += 1) { + const amount = step / 8; + const remaining = 1 - amount; + points.push([ + remaining * remaining * startPoint[0] + 2 * remaining * amount * control[0] + amount * amount * end[0], + remaining * remaining * startPoint[1] + 2 * remaining * amount * control[1] + amount * amount * end[1], + ]); + } + current = end; + } + break; + } + case 'Z': { + if (start) points.push(start); + break; + } + default: + return []; + } + } + + return points.filter(isPoint); +} + +function properSegmentIntersection(a, b, c, d) { + const abC = crossProduct(a, b, c); + const abD = crossProduct(a, b, d); + const cdA = crossProduct(c, d, a); + const cdB = crossProduct(c, d, b); + const epsilon = 1e-9; + const opposite = (left, right) => (left > epsilon && right < -epsilon) || (left < -epsilon && right > epsilon); + if (!opposite(abC, abD) || !opposite(cdA, cdB)) return null; + const denominator = (a[0] - b[0]) * (c[1] - d[1]) - (a[1] - b[1]) * (c[0] - d[0]); + if (Math.abs(denominator) < epsilon) return null; + const ab = a[0] * b[1] - a[1] * b[0]; + const cd = c[0] * d[1] - c[1] * d[0]; + return [ + (ab * (c[0] - d[0]) - (a[0] - b[0]) * cd) / denominator, + (ab * (c[1] - d[1]) - (a[1] - b[1]) * cd) / denominator, + ]; +} + +function crossProduct(a, b, c) { + return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); +} + +function segmentIntersectsBox(segment, box) { + const { start, end } = segment; + if (pointInsideBox(start, box) || pointInsideBox(end, box)) return true; + const edges = [ + [[box.x1, box.y1], [box.x2, box.y1]], + [[box.x2, box.y1], [box.x2, box.y2]], + [[box.x2, box.y2], [box.x1, box.y2]], + [[box.x1, box.y2], [box.x1, box.y1]], + ]; + return edges.some(([a, b]) => segmentsIntersect(start, end, a, b)); +} + +function segmentsIntersect(a, b, c, d) { + const o1 = orientation(a, b, c); + const o2 = orientation(a, b, d); + const o3 = orientation(c, d, a); + const o4 = orientation(c, d, b); + + if (o1 !== o2 && o3 !== o4) return true; + if (o1 === 0 && onSegment(a, c, b)) return true; + if (o2 === 0 && onSegment(a, d, b)) return true; + if (o3 === 0 && onSegment(c, a, d)) return true; + if (o4 === 0 && onSegment(c, b, d)) return true; + return false; +} + +function orientation(a, b, c) { + const value = (b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]); + if (Math.abs(value) < 1e-9) return 0; + return value > 0 ? 1 : 2; +} + +function onSegment(a, b, c) { + return b[0] <= Math.max(a[0], c[0]) + 1e-9 + && b[0] + 1e-9 >= Math.min(a[0], c[0]) + && b[1] <= Math.max(a[1], c[1]) + 1e-9 + && b[1] + 1e-9 >= Math.min(a[1], c[1]); +} + +function pointInsideBox(point, box) { + return point[0] >= box.x1 && point[0] <= box.x2 && point[1] >= box.y1 && point[1] <= box.y2; +} + +function padBox(box, padding) { + return { + ...box, + x1: box.x1 - padding, + y1: box.y1 - padding, + x2: box.x2 + padding, + y2: box.y2 + padding, + }; +} + +// Only geometry/numeric attributes are scanned: authored prose such as node +// tags, text, aria-labels, or data-* attributes may legitimately +// mention "NaN" or "Infinity" without any coordinate being non-finite. +function collectNonFiniteAttrs(svg) { + const details = []; + const stack = []; + for (const match of svg.matchAll(SVG_TAG_TOKEN)) { + if (!match[2]) continue; + const element = match[2].toLowerCase(); + if (match[1]) { + const index = stack.map(entry => entry.element).lastIndexOf(element); + if (index >= 0) stack.length = index; + continue; + } + const parent = stack[stack.length - 1]; + const inSvg = element === 'svg' + || Boolean(parent?.inSvg && !SVG_HTML_INTEGRATION_POINTS.has(parent.element)); + if (inSvg) { + for (const [name, value] of attrEntries(match[0])) { + if (!isNumericAttr(element, name) || !NON_FINITE_TOKEN.test(decodeNumericReferences(value))) continue; + details.push(`${match[2]} ${name}="${value}"`); + } + } + // HTML void elements do not open a context; SVG self-closing tags do not + // either. A nested inside foreignObject restores SVG checking. + if (!(inSvg ? /\/\s*>$/.test(match[0]) : HTML_VOID_ELEMENTS.has(element))) { + stack.push({ element, inSvg }); + } + } + return details; +} + +function decodeNumericReferences(value) { + // Numeric references can encode every letter of NaN/Infinity/undefined. + // Decode once, locally: other checks and diagnostic evidence keep raw values. + return value.replace(/&#(?:x([0-9a-f]+)|([0-9]+));?/gi, (_, hex, decimal) => { + const point = Number.parseInt(hex || decimal, hex ? 16 : 10); + return point > 0 && point <= 0x10ffff && !(point >= 0xd800 && point <= 0xdfff) + ? String.fromCodePoint(point) : '\uFFFD'; + }); +} + +function isNumericAttr(elementName, attrName) { + const normalizedAttr = attrName.toLowerCase(); + return NUMERIC_ATTRS.has(normalizedAttr) + || ELEMENT_NUMERIC_ATTRS.get(elementName.toLowerCase())?.has(normalizedAttr); +} + +function attrEntries(tag) { + return [...tag.matchAll(HTML_ATTRIBUTE)].map((match) => [ + match[1], + match[2] ?? match[3] ?? match[4], + ]); +} + +function parseAttrs(tag) { + return Object.fromEntries(attrEntries(tag)); +} + +function numberAttr(attrs, name) { + return Number.parseFloat(attrs[name]); +} + +function isCommand(token) { + return /^[A-Za-z]$/.test(token); +} + +function isPoint(point) { + return Array.isArray(point) && point.length === 2 && point.every(Number.isFinite); +} + +function stripTags(value) { + return value.replace(/<[^>]*>/g, ''); +} diff --git a/vendor/archify/scripts/check-update.mjs b/vendor/archify/scripts/check-update.mjs new file mode 100644 index 0000000..369eeac --- /dev/null +++ b/vendor/archify/scripts/check-update.mjs @@ -0,0 +1,1710 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto'; +import { constants as fsConstants } from 'node:fs'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { fileURLToPath } from 'node:url'; + +import { + DEFAULT_MANIFEST_URL, + SKILL_ID, + UpdateContractError, + compareSemver, + isStableCoreVersion, + parseSemver, + validateLocalRelease, + validateReleaseNotesUrl, + validateStableUpdateManifest, +} from './update-contract.mjs'; +import { sameEntry } from '../renderers/shared/path-semantics.mjs'; + +const OPERATION_STATE_FILE = 'state.json'; +const OPERATION_OWNER_FILE = 'owner.json'; +const ACTIVE_CLAIM_DIRECTORY = 'active-claim'; +const OPERATION_NAME = /^(reserved|pending|committed|fenced|cancelled)-(\d{1,20})$/; +const MAX_OPERATION_GENERATION = (10n ** 20n) - 1n; +const CHECK_TTL_MS = 24 * 60 * 60 * 1_000; +const MAX_CACHE_HORIZON_MS = Math.ceil(CHECK_TTL_MS * 1.2); +const FIRST_FAILURE_DELAY_MS = 6 * 60 * 60 * 1_000; +const LATER_FAILURE_DELAY_MS = 24 * 60 * 60 * 1_000; +const DEFAULT_TIMEOUT_MS = 1_000; +const MAX_RESPONSE_BYTES = 32 * 1_024; +const MAX_LOCAL_RELEASE_BYTES = 4 * 1_024; +const MAX_CACHE_STATE_BYTES = 64 * 1_024; +const MAX_CLAIM_OWNER_BYTES = 1_024; +const LONGEST_ISO_TIMESTAMP = '+275760-09-13T00:00:00.000Z'; +const LOCK_STALE_MS = 30_000; +const PREFERENCE_LOCK_WAIT_MS = 1_200; +const LOCK_RETRY_DELAY_MS = 20; +const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/; +const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); +const defaultReleasePath = path.resolve(scriptDirectory, '..', 'skill-release.json'); + +class FileIdentityChangedError extends Error {} +class ManifestWithdrawnError extends Error {} + +function silent(reason) { + return { status: 'silent', reason }; +} + +function updatesDisabled() { + return process.env.ARCHIFY_UPDATE_CHECK_DISABLED === '1'; +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function hasOnlyKeys(value, allowed) { + return Object.keys(value).every((key) => allowed.has(key)); +} + +function validateManifest(value) { + const manifest = validateStableUpdateManifest(value); + return { + version: manifest.version, + targetDigest: `sha256:${manifest.artifact.sha256}`, + severity: manifest.severity, + releaseNotes: manifest.releaseNotes, + }; +} + +function validateCachedCandidate(value) { + if (!isPlainObject(value) + || !hasOnlyKeys(value, new Set([ + 'version', 'targetDigest', 'severity', 'releaseNotes', 'checkedAt', + ]))) return null; + try { + if (!isStableCoreVersion(value.version)) return null; + if (!DIGEST_PATTERN.test(value.targetDigest) + || !['normal', 'security'].includes(value.severity)) return null; + validateReleaseNotesUrl(value.releaseNotes, value.version); + if (typeof value.checkedAt !== 'string' || !Number.isFinite(Date.parse(value.checkedAt))) return null; + return { ...value }; + } catch { + return null; + } +} + +function emptyState(installedVersion = null) { + return { + schemaVersion: 2, + skillId: SKILL_ID, + installedVersion, + check: { + nextCheckAt: null, + consecutiveFailures: 0, + }, + preferences: [], + }; +} + +function validPreference(value) { + return isPlainObject(value) + && hasOnlyKeys(value, new Set(['targetDigest', 'version', 'mode', 'until'])) + && DIGEST_PATTERN.test(value.targetDigest) + && isStableCoreVersion(value.version) + && ['ignore', 'snooze'].includes(value.mode) + && (value.mode === 'ignore' ? value.until === null + : typeof value.until === 'string' && Number.isFinite(Date.parse(value.until))); +} + +function encodeBoundedState(state) { + const encoded = Buffer.from(`${JSON.stringify(state)}\n`, 'utf8'); + return encoded.length <= MAX_CACHE_STATE_BYTES ? encoded : null; +} + +function stateWithFailureCheck(state, nextCheckAt, consecutiveFailures, withdrawCandidate = false) { + const failedState = { + ...state, + check: { nextCheckAt, consecutiveFailures }, + }; + if (withdrawCandidate) delete failedState.candidate; + return failedState; +} + +// A committed state must leave room for its own failure backoff. +function encodeRecoverableState(state) { + const encoded = encodeBoundedState(state); + if (!encoded || !encodeBoundedState(stateWithFailureCheck(state, LONGEST_ISO_TIMESTAMP, 2))) return null; + return encoded; +} + +function versionCacheDirectory(cacheDirectory, installedVersion) { + const partition = crypto.createHash('sha256').update(installedVersion).digest('hex').slice(0, 24); + return path.join(cacheDirectory, `version-${partition}`); +} + +function normalizeState(value, installedVersion) { + const effectiveInstalledVersion = installedVersion ?? value?.installedVersion ?? null; + if (!isPlainObject(value) || value.schemaVersion !== 2 || value.skillId !== SKILL_ID + || (installedVersion !== null && value.installedVersion !== installedVersion) + || (value.installedVersion !== null && typeof value.installedVersion !== 'string') + || !isPlainObject(value.check) + || !(value.check.nextCheckAt === null || typeof value.check.nextCheckAt === 'string') + || !Number.isSafeInteger(value.check.consecutiveFailures) + || value.check.consecutiveFailures < 0 + || !Array.isArray(value.preferences) || value.preferences.length > 32 + || value.preferences.some((preference) => !validPreference(preference))) return null; + if (effectiveInstalledVersion !== null) { + try { + parseSemver(effectiveInstalledVersion); + } catch { + return null; + } + } + + const state = emptyState(effectiveInstalledVersion); + state.check.nextCheckAt = value.check.nextCheckAt; + state.check.consecutiveFailures = value.check.consecutiveFailures; + state.preferences = value.preferences.map((preference) => ({ ...preference })); + const hasCandidate = Object.hasOwn(value, 'candidate'); + const candidate = validateCachedCandidate(value.candidate); + if (hasCandidate && !candidate) return null; + if (candidate) state.candidate = candidate; + return encodeRecoverableState(state) ? state : null; +} + +function isSameFile(left, right) { + if (left.dev !== right.dev || left.ino !== right.ino) return false; + if (left.ino !== 0n) return true; + if (left.birthtimeNs === 0n || right.birthtimeNs === 0n) return false; + return left.birthtimeNs === right.birthtimeNs + && left.mode === right.mode; +} + +function assertBoundedRegularFile(metadata, maxBytes) { + if (!metadata.isFile() + || metadata.isSymbolicLink() + || metadata.size > BigInt(maxBytes)) { + throw new Error('unsafe JSON file'); + } +} + +async function readJsonFile(target, maxBytes, expectedMetadata = null) { + const pathMetadata = await fs.lstat(target, { bigint: true }); + if (expectedMetadata && !isSameFile(expectedMetadata, pathMetadata)) { + throw new FileIdentityChangedError('JSON file identity changed'); + } + assertBoundedRegularFile(pathMetadata, maxBytes); + const flags = fsConstants.O_RDONLY + | (fsConstants.O_NOFOLLOW ?? 0) + | (fsConstants.O_NONBLOCK ?? 0); + const handle = await fs.open(target, flags); + try { + const handleMetadata = await handle.stat({ bigint: true }); + const currentPathMetadata = await fs.lstat(target, { bigint: true }); + assertBoundedRegularFile(handleMetadata, maxBytes); + assertBoundedRegularFile(currentPathMetadata, maxBytes); + if (!isSameFile(pathMetadata, handleMetadata) + || !isSameFile(currentPathMetadata, handleMetadata)) { + throw new FileIdentityChangedError('JSON file changed while opening'); + } + + const chunks = []; + let size = 0; + while (size <= maxBytes) { + const chunk = Buffer.allocUnsafe(Math.min(8 * 1_024, (maxBytes + 1) - size)); + const { bytesRead } = await handle.read(chunk, 0, chunk.length, size); + if (bytesRead === 0) break; + chunks.push(chunk.subarray(0, bytesRead)); + size += bytesRead; + } + if (size > maxBytes) throw new Error('JSON file is too large'); + return JSON.parse(Buffer.concat(chunks, size).toString('utf8')); + } finally { + await handle.close(); + } +} + +function isWithinDirectory(directory, target) { + const relative = path.relative(directory, target); + return relative === '' + // path-contract-allow: lexical-capability -- No-follow snapshots bind this mutation boundary. + || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`)); +} + +async function physicalDirectorySuffix(directory, target) { + const absoluteTarget = path.resolve(target); + const parsed = path.parse(absoluteTarget); + const segments = absoluteTarget.slice(parsed.root.length).split(path.sep).filter(Boolean); + let prefix = parsed.root; + for (let index = 0; index < segments.length; index += 1) { + prefix = path.join(prefix, segments[index]); + const relation = sameEntry(directory, prefix); + if (relation.status === 'match') { + const metadata = await fs.lstat(prefix, { bigint: true }); + if (metadata.isSymbolicLink()) { + throw new Error(`trusted cache prefix is a symbolic link or junction: ${prefix}`); + } + return segments.slice(index + 1); + } + if (relation.status === 'unknown' && relation.reason.code === 'entry-missing') break; + } + return null; +} + +async function canonicalizeTrustedDirectoryPrefix(target) { + const absoluteTarget = path.resolve(target); + const trustedDirectories = [...new Set([os.homedir(), os.tmpdir()].map((directory) => ( + path.resolve(directory) + )))].sort((left, right) => right.length - left.length); + for (const trustedDirectory of trustedDirectories) { + const suffix = await physicalDirectorySuffix(trustedDirectory, absoluteTarget); + if (suffix === null) continue; + try { + const canonicalDirectory = await fs.realpath(trustedDirectory); + return path.resolve(canonicalDirectory, ...suffix); + } catch { + // Fall back to validating the absolute path from its filesystem root. + } + } + return target; +} + +function assertSafeDirectory(metadata) { + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new Error('unsafe cache directory'); + } +} + +class CachePathChangedError extends Error {} +class CacheOperationRaceError extends Error {} +class OperationFencedError extends Error {} + +const preparedCacheDirectories = new Map(); + +function invalidateCacheToken(token, message, cause) { + token.trusted = false; + if (cause instanceof CachePathChangedError) return cause; + const error = new CachePathChangedError(message); + if (cause) error.cause = cause; + return error; +} + +async function verifyDirectorySnapshots(token, snapshots, description) { + if (!token.trusted) throw new CachePathChangedError('cache directory is no longer trusted'); + try { + for (const snapshot of snapshots) { + const metadata = await fs.lstat(snapshot.directory, { bigint: true }); + assertSafeDirectory(metadata); + if (!isSameFile(snapshot.metadata, metadata)) { + throw new Error(`${description} changed`); + } + } + } catch (error) { + throw invalidateCacheToken(token, `${description} changed`, error); + } +} + +async function verifyCacheToken(token) { + await verifyDirectorySnapshots(token, token.ancestorSnapshots, 'cache directory'); +} + +async function guardedCacheRead(token, read) { + await verifyCacheToken(token); + let result; + try { + result = await read(); + } catch (error) { + await verifyCacheToken(token); + throw error; + } + await verifyCacheToken(token); + return result; +} + +function cacheTokenFor(cacheDirectory) { + const token = preparedCacheDirectories.get(path.resolve(cacheDirectory)); + if (!token || !token.trusted) { + throw new CachePathChangedError('cache directory has not been prepared safely'); + } + return token; +} + +function resolveCacheTarget(token, target) { + const resolved = path.resolve(target); + const relative = path.relative(token.directory, resolved); + // This is a lexical capability check, not a physical-containment query. The + // async lstat snapshots below bind the prepared directory and every mutation + // parent across awaits; following a link here would weaken that no-link rule. + if (relative === '' || !isWithinDirectory(token.directory, resolved)) { + throw invalidateCacheToken(token, 'cache mutation escaped its prepared directory'); + } + return resolved; +} + +async function captureMutationParentSnapshots(token, targets) { + await verifyCacheToken(token); + const snapshots = new Map(); + try { + for (const target of targets) { + const parent = path.dirname(resolveCacheTarget(token, target)); + const relative = path.relative(token.directory, parent); + let current = token.directory; + for (const component of relative.split(path.sep).filter(Boolean)) { + current = path.join(current, component); + if (snapshots.has(current)) continue; + const metadata = await fs.lstat(current, { bigint: true }); + assertSafeDirectory(metadata); + snapshots.set(current, { directory: current, metadata }); + } + } + } catch (error) { + throw invalidateCacheToken(token, 'cache mutation parent changed', error); + } + const captured = [...snapshots.values()]; + await verifyMutationParentSnapshots(token, captured); + await verifyCacheToken(token); + return captured; +} + +async function verifyMutationParentSnapshots(token, snapshots) { + try { + for (const snapshot of snapshots) { + const metadata = await fs.lstat(snapshot.directory, { bigint: true }); + assertSafeDirectory(metadata); + if (!isSameFile(snapshot.metadata, metadata)) { + throw new Error('cache mutation parent changed'); + } + } + } catch (error) { + const raced = new CacheOperationRaceError('cache mutation parent changed'); + raced.cause = error; + throw raced; + } +} + +async function verifyMutationContext(token, parentSnapshots) { + await verifyCacheToken(token); + await verifyMutationParentSnapshots(token, parentSnapshots); + await verifyCacheToken(token); +} + +function assertSafeCacheEntry(metadata) { + if (metadata.isSymbolicLink() || (!metadata.isDirectory() && !metadata.isFile())) { + throw new Error('unsafe cache entry'); + } +} + +function assertSafeRegularFile(metadata) { + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error('unsafe cache file'); + } +} + +function assertRenameableCacheEntry(metadata) { + if (!metadata.isDirectory() && !metadata.isFile() && !metadata.isSymbolicLink()) { + throw new Error('unsafe cache rename entry'); + } +} + +async function stableCacheEntry( + token, + target, + validate, + expectedMetadata = null, + missingIsOperationRace = false, +) { + const resolved = resolveCacheTarget(token, target); + try { + const first = await fs.lstat(resolved, { bigint: true }); + validate(first); + if (expectedMetadata && !isSameFile(expectedMetadata, first)) { + throw new Error('cache entry identity changed'); + } + await verifyCacheToken(token); + const second = await fs.lstat(resolved, { bigint: true }); + validate(second); + if (!isSameFile(first, second) + || (expectedMetadata && !isSameFile(expectedMetadata, second))) { + throw new Error('cache entry identity changed'); + } + return second; + } catch (error) { + if (missingIsOperationRace && error?.code === 'ENOENT') { + await verifyCacheToken(token); + throw error; + } + throw invalidateCacheToken(token, 'cache mutation produced an untrusted object', error); + } +} + +async function guardedCacheEntryRead( + token, + target, + validate, + read, + expectedMetadata = null, +) { + const metadata = await stableCacheEntry( + token, + target, + validate, + expectedMetadata, + true, + ); + let result; + try { + result = await read(metadata); + } catch (error) { + await stableCacheEntry(token, target, validate, metadata, true); + throw error; + } + await stableCacheEntry(token, target, validate, metadata, true); + return result; +} + +async function stablePreparedDirectory(token, target) { + try { + const first = await fs.lstat(target, { bigint: true }); + assertSafeDirectory(first); + await verifyCacheToken(token); + const second = await fs.lstat(target, { bigint: true }); + assertSafeDirectory(second); + if (!isSameFile(first, second)) throw new Error('prepared directory identity changed'); + return second; + } catch (error) { + throw invalidateCacheToken(token, 'prepared directory could not be verified', error); + } +} + +async function assertCacheTargetAbsent(token, target) { + const resolved = resolveCacheTarget(token, target); + try { + await fs.lstat(resolved, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT') return; + throw invalidateCacheToken(token, 'cache mutation target could not be verified', error); + } + throw invalidateCacheToken(token, 'cache mutation left an unexpected object'); +} + +async function guardedCacheMutation(token, parentSnapshots, { + mutate, + verifyBefore = null, + verifyAfter, +}) { + await verifyMutationContext(token, parentSnapshots); + if (verifyBefore) await verifyBefore(); + let result; + try { + result = await mutate(); + } catch (error) { + await verifyMutationContext(token, parentSnapshots); + throw error; + } + try { + await verifyMutationContext(token, parentSnapshots); + const verified = await verifyAfter(result); + await verifyMutationContext(token, parentSnapshots); + return verified ?? result; + } catch (error) { + if (error instanceof CacheOperationRaceError) throw error; + throw invalidateCacheToken(token, 'cache mutation could not be verified', error); + } +} + +async function cacheMkdirWithToken(token, target, options, parentSnapshots = null) { + const resolved = path.resolve(target); + const parents = parentSnapshots ?? await captureMutationParentSnapshots(token, [resolved]); + return guardedCacheMutation(token, parents, { + mutate: () => fs.mkdir(resolved, options), + verifyAfter: () => (parentSnapshots === null + ? stableCacheEntry(token, resolved, assertSafeDirectory) + : stablePreparedDirectory(token, resolved)), + }); +} + +async function cacheMkdir(cacheDirectory, target, options) { + return cacheMkdirWithToken(cacheTokenFor(cacheDirectory), target, options); +} + +async function cacheWriteFile(cacheDirectory, target, data, options) { + if (options?.flag !== 'wx') { + throw new Error('cache files must be created exclusively'); + } + const token = cacheTokenFor(cacheDirectory); + const resolved = resolveCacheTarget(token, target); + const parents = await captureMutationParentSnapshots(token, [resolved]); + return guardedCacheMutation(token, parents, { + mutate: async () => { + const flags = fsConstants.O_WRONLY + | fsConstants.O_CREAT + | fsConstants.O_EXCL + | (fsConstants.O_NOFOLLOW ?? 0); + const handle = await fs.open(resolved, flags, options.mode); + try { + const openedMetadata = await handle.stat({ bigint: true }); + if (!openedMetadata.isFile() || openedMetadata.isSymbolicLink()) { + throw new Error('cache write did not open a regular file'); + } + await handle.writeFile(data, options.encoding ? { encoding: options.encoding } : undefined); + const writtenMetadata = await handle.stat({ bigint: true }); + const expectedBytes = Buffer.isBuffer(data) + ? data.length + : Buffer.byteLength(data, options.encoding || 'utf8'); + if (!isSameFile(openedMetadata, writtenMetadata) + || writtenMetadata.size !== BigInt(expectedBytes)) { + throw new Error('cache write did not preserve the opened file identity and size'); + } + return writtenMetadata; + } finally { + await handle.close(); + } + }, + verifyAfter: (writtenMetadata) => stableCacheEntry(token, resolved, (metadata) => { + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error('cache write did not create a regular file'); + } + }, writtenMetadata), + }); +} + +async function cacheRename( + cacheDirectory, + source, + destination, + validate, + expectedSourceMetadata = null, +) { + if (typeof validate !== 'function') { + throw new TypeError('cache rename requires an explicit entry validator'); + } + const token = cacheTokenFor(cacheDirectory); + const resolvedSource = resolveCacheTarget(token, source); + const resolvedDestination = resolveCacheTarget(token, destination); + const parents = await captureMutationParentSnapshots( + token, + [resolvedSource, resolvedDestination], + ); + const sourceMetadata = await stableCacheEntry( + token, + resolvedSource, + validate, + expectedSourceMetadata, + true, + ); + return guardedCacheMutation(token, parents, { + verifyBefore: () => stableCacheEntry( + token, + resolvedSource, + validate, + sourceMetadata, + true, + ), + mutate: () => fs.rename(resolvedSource, resolvedDestination), + verifyAfter: async () => { + await assertCacheTargetAbsent(token, resolvedSource); + return stableCacheEntry( + token, + resolvedDestination, + validate, + sourceMetadata, + ); + }, + }); +} + +async function cacheRm(cacheDirectory, target, options) { + if (options?.recursive) { + throw new Error('recursive cache removal is not allowed'); + } + const token = cacheTokenFor(cacheDirectory); + const resolved = resolveCacheTarget(token, target); + const parents = await captureMutationParentSnapshots(token, [resolved]); + let initialMetadata; + try { + initialMetadata = await fs.lstat(resolved, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT' && options?.force) { + await verifyMutationContext(token, parents); + return; + } + throw error; + } + try { + assertSafeCacheEntry(initialMetadata); + } catch (error) { + throw invalidateCacheToken(token, 'cache removal target is unsafe', error); + } + const targetMetadata = await stableCacheEntry( + token, + resolved, + assertSafeCacheEntry, + initialMetadata, + ); + return guardedCacheMutation(token, parents, { + verifyBefore: () => stableCacheEntry( + token, + resolved, + assertSafeCacheEntry, + targetMetadata, + ), + mutate: () => fs.rm(resolved, options), + verifyAfter: () => assertCacheTargetAbsent(token, resolved), + }); +} + +async function prepareCacheDirectory(cacheDirectory) { + const absoluteTarget = path.resolve(cacheDirectory); + const target = await canonicalizeTrustedDirectoryPrefix(absoluteTarget); + const parsed = path.parse(target); + const components = target.slice(parsed.root.length).split(path.sep).filter(Boolean); + let current = parsed.root; + const token = { + directory: target, + ancestorSnapshots: [], + trusted: true, + }; + + for (const component of components) { + current = path.join(current, component); + let metadata; + try { + metadata = await fs.lstat(current, { bigint: true }); + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + try { + await cacheMkdirWithToken(token, current, { mode: 0o700 }, []); + } catch (mkdirError) { + if (mkdirError?.code !== 'EEXIST') throw mkdirError; + } + metadata = await fs.lstat(current, { bigint: true }); + } + assertSafeDirectory(metadata); + token.ancestorSnapshots.push({ directory: current, metadata }); + } + + await verifyCacheToken(token); + preparedCacheDirectories.set(target, token); + return target; +} + +function operationName(kind, generation) { + return `${kind}-${generation.toString().padStart(20, '0')}`; +} + +function parseOperationName(name) { + const match = OPERATION_NAME.exec(name); + if (!match) return null; + return { kind: match[1], generation: BigInt(match[2]) }; +} + +async function listOperations(cacheDirectory, token = cacheTokenFor(cacheDirectory)) { + return guardedCacheRead(token, async () => { + try { + const entries = await fs.readdir(cacheDirectory, { withFileTypes: true }); + return entries.flatMap((entry) => { + const parsed = parseOperationName(entry.name); + if (!parsed) return []; + if (!entry.isDirectory() || entry.isSymbolicLink()) { + throw invalidateCacheToken( + token, + `cache operation ${entry.name} is not a safe directory`, + ); + } + return [{ ...parsed, name: entry.name, directory: path.join(cacheDirectory, entry.name) }]; + }); + } catch (error) { + if (error?.code === 'ENOENT') return []; + throw error; + } + }); +} + +async function readState(cacheDirectory, installedVersion = null) { + const token = cacheTokenFor(cacheDirectory); + return guardedCacheRead(token, async () => { + const operations = (await listOperations(cacheDirectory, token)) + .filter((operation) => operation.kind === 'committed') + .sort((left, right) => (left.generation > right.generation ? -1 : 1)); + for (const operation of operations) { + try { + const state = await guardedCacheEntryRead( + token, + operation.directory, + assertSafeDirectory, + async () => normalizeState( + await readJsonFile( + path.join(operation.directory, OPERATION_STATE_FILE), + MAX_CACHE_STATE_BYTES, + ), + installedVersion, + ), + ); + if (state) return state; + } catch (error) { + if (error instanceof FileIdentityChangedError + || error instanceof CachePathChangedError + || error instanceof CacheOperationRaceError) throw error; + // An incomplete or corrupt generation is ignored in favor of the previous commit. + } + } + return emptyState(installedVersion); + }); +} + +async function operationIsActive( + cacheDirectory, + operation, + token = cacheTokenFor(cacheDirectory), +) { + try { + return await guardedCacheEntryRead( + token, + operation.directory, + assertSafeDirectory, + async (directoryMetadata) => { + try { + const ownerMetadata = await fs.lstat( + path.join(operation.directory, OPERATION_OWNER_FILE), + { bigint: true }, + ); + if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink()) return false; + try { + const owner = await readJsonFile( + path.join(operation.directory, OPERATION_OWNER_FILE), + MAX_CLAIM_OWNER_BYTES, + ownerMetadata, + ); + if (Number.isSafeInteger(owner?.pid) && owner.pid > 0 && processIsGone(owner.pid)) { + return false; + } + } catch (error) { + if (error instanceof FileIdentityChangedError) throw error; + // A malformed owner still gets the hard lease before recovery. + } + return metadataIsWithinLease(ownerMetadata); + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + return metadataIsWithinLease(directoryMetadata); + } + }, + ); + } catch (error) { + if (error?.code === 'ENOENT') return false; + throw error; + } +} + +function processIsGone(pid) { + try { + process.kill(pid, 0); + return false; + } catch (error) { + return error?.code === 'ESRCH'; + } +} + +function validateClaimOwner(value) { + if (!isPlainObject(value) + || !hasOnlyKeys(value, new Set(['pid', 'token', 'generation'])) + || !Number.isSafeInteger(value.pid) + || value.pid <= 0 + || typeof value.token !== 'string' + || !/^[a-f0-9]{32}$/.test(value.token) + || typeof value.generation !== 'string' + || !/^[1-9]\d*$/.test(value.generation)) return null; + return { ...value, generation: BigInt(value.generation) }; +} + +function metadataIsWithinLease(metadata) { + if (typeof metadata.mtimeNs === 'bigint') { + const ageNs = (BigInt(Date.now()) * 1_000_000n) - metadata.mtimeNs; + const absoluteAgeNs = ageNs < 0n ? -ageNs : ageNs; + return absoluteAgeNs <= BigInt(LOCK_STALE_MS) * 1_000_000n; + } + return Math.abs(Date.now() - metadata.mtimeMs) <= LOCK_STALE_MS; +} + +function corruptClaimStatus(directory, directoryMetadata, ownerMetadata = directoryMetadata) { + if (metadataIsWithinLease(ownerMetadata)) { + return { state: 'busy', directory, metadata: directoryMetadata, owner: null }; + } + const fingerprint = crypto.createHash('sha256') + .update(JSON.stringify({ + directory: { + dev: directoryMetadata.dev.toString(), + ino: directoryMetadata.ino.toString(), + mode: directoryMetadata.mode.toString(), + birthtimeNs: directoryMetadata.birthtimeNs.toString(), + }, + })) + .digest('hex') + .slice(0, 24); + return { + state: 'releasable', + directory, + metadata: directoryMetadata, + key: `corrupt-${fingerprint}`, + owner: null, + }; +} + +async function inspectActiveClaim(cacheDirectory) { + const token = cacheTokenFor(cacheDirectory); + return guardedCacheRead(token, async () => { + const directory = path.join(cacheDirectory, ACTIVE_CLAIM_DIRECTORY); + let directoryMetadata; + try { + directoryMetadata = await fs.lstat(directory, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT') return { state: 'absent', directory }; + throw error; + } + if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()) { + return corruptClaimStatus(directory, directoryMetadata); + } + return guardedCacheEntryRead( + token, + directory, + assertSafeDirectory, + async (stableDirectoryMetadata) => { + const ownerPath = path.join(directory, OPERATION_OWNER_FILE); + let ownerMetadata = stableDirectoryMetadata; + let owner = null; + try { + ownerMetadata = await fs.lstat(ownerPath, { bigint: true }); + if (!ownerMetadata.isFile() + || ownerMetadata.isSymbolicLink() + || ownerMetadata.size > BigInt(MAX_CLAIM_OWNER_BYTES)) { + return corruptClaimStatus(directory, stableDirectoryMetadata, ownerMetadata); + } + const ownerSource = await readJsonFile( + ownerPath, + MAX_CLAIM_OWNER_BYTES, + ownerMetadata, + ); + owner = validateClaimOwner(ownerSource); + } catch (error) { + if (error instanceof FileIdentityChangedError) throw error; + // A malformed claim is recoverable only after the same hard lease as a crashed owner. + } + + if (owner) { + const operations = await listOperations(cacheDirectory, token); + if (operations.some((operation) => operation.generation === owner.generation + && ['committed', 'fenced', 'cancelled'].includes(operation.kind))) { + return { + state: 'releasable', + directory, + metadata: stableDirectoryMetadata, + key: owner.generation.toString(), + owner, + }; + } + const pending = operations.find((operation) => operation.kind === 'pending' + && operation.generation === owner.generation); + if (pending) { + if (await operationIsActive(cacheDirectory, pending, token)) { + return { + state: 'busy', + directory, + metadata: stableDirectoryMetadata, + owner, + }; + } + return { + state: 'releasable', + directory, + metadata: stableDirectoryMetadata, + key: owner.generation.toString(), + owner, + }; + } + } + + return corruptClaimStatus(directory, stableDirectoryMetadata, ownerMetadata); + }, + directoryMetadata, + ); + }); +} + +async function retireActiveClaim(cacheDirectory, claim) { + const retirement = path.join(cacheDirectory, `retired-claim-${claim.key}`); + await cacheMkdir(cacheDirectory, retirement, { mode: 0o700 }).catch((error) => { + if (error?.code !== 'EEXIST') throw error; + }); + try { + const retirementMetadata = await fs.lstat(retirement); + if (!retirementMetadata.isDirectory() || retirementMetadata.isSymbolicLink()) return false; + } catch (error) { + if (error instanceof CacheOperationRaceError) return false; + if (error?.code === 'ENOENT') return false; + throw error; + } + try { + const metadata = await fs.lstat(claim.directory); + if (metadata.isDirectory() && !metadata.isSymbolicLink()) { + await cacheWriteFile( + cacheDirectory, + path.join(claim.directory, '.retirement-guard'), + `${claim.key}\n`, + { encoding: 'utf8', flag: 'wx', mode: 0o600 }, + ).catch((error) => { + if (error?.code !== 'EEXIST') throw error; + }); + } + } catch (error) { + if (error instanceof CacheOperationRaceError) return false; + if (error?.code === 'ENOENT') return false; + throw error; + } + try { + await cacheRename( + cacheDirectory, + claim.directory, + path.join(retirement, 'active'), + assertRenameableCacheEntry, + claim.metadata, + ); + return true; + } catch (error) { + if (error instanceof CacheOperationRaceError) return false; + if (['ENOENT', 'EEXIST', 'ENOTEMPTY', 'EPERM'].includes(error?.code)) return false; + throw error; + } +} + +async function prepareOperationClaim(cacheDirectory, operation) { + const directory = path.join(cacheDirectory, operationName('claim', operation.generation)); + await cacheMkdir(cacheDirectory, directory, { mode: 0o700 }); + await cacheWriteFile( + cacheDirectory, + path.join(directory, OPERATION_OWNER_FILE), + `${JSON.stringify({ + pid: process.pid, + token: operation.token, + generation: operation.generation.toString(), + })}\n`, + { encoding: 'utf8', flag: 'wx', mode: 0o600 }, + ); + operation.preparedClaimDirectory = directory; +} + +async function discardPreparedClaim(cacheDirectory, operation) { + if (!operation.preparedClaimDirectory) return; + const discarded = path.join( + cacheDirectory, + `discarded-claim-${operation.generation.toString().padStart(20, '0')}-${operation.token}`, + ); + try { + await cacheRename( + cacheDirectory, + operation.preparedClaimDirectory, + discarded, + assertSafeDirectory, + ); + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + } + delete operation.preparedClaimDirectory; +} + +async function promoteOperationClaim(cacheDirectory, operation) { + await prepareOperationClaim(cacheDirectory, operation); + const activeDirectory = path.join(cacheDirectory, ACTIVE_CLAIM_DIRECTORY); + for (let attempt = 0; attempt < 8; attempt += 1) { + if (await operationIsSuperseded(cacheDirectory, operation)) { + await discardPreparedClaim(cacheDirectory, operation); + return false; + } + const active = await inspectActiveClaim(cacheDirectory); + if (active.state === 'busy') { + await discardPreparedClaim(cacheDirectory, operation); + return false; + } + if (active.state === 'releasable') { + if (active.owner?.generation > operation.generation) { + await discardPreparedClaim(cacheDirectory, operation); + return false; + } + await retireActiveClaim(cacheDirectory, active); + continue; + } + try { + await cacheRename( + cacheDirectory, + operation.preparedClaimDirectory, + activeDirectory, + assertSafeDirectory, + ); + delete operation.preparedClaimDirectory; + if (!await operationIsActive(cacheDirectory, operation)) return false; + return true; + } catch (promotionError) { + const raced = await inspectActiveClaim(cacheDirectory); + if (raced.state === 'absent') { + if (['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(promotionError?.code)) continue; + await discardPreparedClaim(cacheDirectory, operation); + throw promotionError; + } + continue; + } + } + await discardPreparedClaim(cacheDirectory, operation); + return false; +} + +async function operationOwnsActiveClaim(cacheDirectory, operation) { + const active = await inspectActiveClaim(cacheDirectory); + return active.state === 'busy' + && active.owner?.generation === operation.generation + && active.owner.token === operation.token; +} + +async function hasActivePendingOperation(cacheDirectory) { + const token = cacheTokenFor(cacheDirectory); + return guardedCacheRead(token, async () => { + const pending = (await listOperations(cacheDirectory, token)) + .filter((operation) => operation.kind === 'pending'); + return (await Promise.all(pending.map( + (operation) => operationIsActive(cacheDirectory, operation, token), + ))).some(Boolean); + }); +} + +async function reserveOperation(cacheDirectory) { + cacheDirectory = await prepareCacheDirectory(cacheDirectory); + const operations = await listOperations(cacheDirectory); + const occupied = new Set(operations.map((operation) => operation.generation.toString())); + let generation = 1n; + while (true) { + while (occupied.has(generation.toString())) generation += 1n; + if (generation > MAX_OPERATION_GENERATION) throw new Error('operation generation space exhausted'); + const reservation = path.join(cacheDirectory, operationName('reserved', generation)); + try { + await cacheMkdir(cacheDirectory, reservation, { mode: 0o700 }); + } catch (error) { + if (error?.code !== 'EEXIST') throw error; + occupied.add(generation.toString()); + generation += 1n; + continue; + } + const directory = path.join(cacheDirectory, operationName('pending', generation)); + try { + await cacheMkdir(cacheDirectory, directory, { mode: 0o700 }); + const operation = { + generation, + token: crypto.randomBytes(16).toString('hex'), + directory, + }; + try { + await cacheWriteFile( + cacheDirectory, + path.join(directory, OPERATION_OWNER_FILE), + `${JSON.stringify({ pid: process.pid, token: operation.token })}\n`, + { encoding: 'utf8', flag: 'wx', mode: 0o600 }, + ); + } catch (error) { + if (await operationWasFenced(cacheDirectory, operation)) { + const fencedError = new OperationFencedError('operation was fenced while reserving'); + fencedError.cause = error; + throw fencedError; + } + await cacheRename( + cacheDirectory, + directory, + path.join(cacheDirectory, operationName('cancelled', generation)), + assertSafeDirectory, + ).catch(() => {}); + throw error; + } + return operation; + } catch (error) { + if (error?.code !== 'EEXIST') throw error; + occupied.add(generation.toString()); + generation += 1n; + } + } +} + +async function operationIsSuperseded(cacheDirectory, operation) { + return (await listOperations(cacheDirectory)) + .some((candidate) => candidate.generation > operation.generation); +} + +async function operationWasFenced(cacheDirectory, operation) { + const token = cacheTokenFor(cacheDirectory); + return guardedCacheRead(token, async () => { + try { + await fs.lstat(operation.directory, { bigint: true }); + return false; + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + } + const fencedDirectory = path.join( + cacheDirectory, + operationName('fenced', operation.generation), + ); + try { + await stableCacheEntry( + token, + fencedDirectory, + assertSafeDirectory, + null, + true, + ); + return true; + } catch (error) { + if (error?.code === 'ENOENT') return false; + throw error; + } + }); +} + +async function transitionOperation(cacheDirectory, operation, kind) { + const destination = path.join(cacheDirectory, operationName(kind, operation.generation)); + await cacheRename(cacheDirectory, operation.directory, destination, assertSafeDirectory); +} + +async function cancelOperation(cacheDirectory, operation) { + try { + await transitionOperation(cacheDirectory, operation, 'cancelled'); + } catch (error) { + // A higher generation may already have fenced this unique pending directory. + if (error?.code !== 'ENOENT') throw error; + } +} + +async function acquireOperation(cacheDirectory) { + cacheDirectory = await prepareCacheDirectory(cacheDirectory); + if ((await inspectActiveClaim(cacheDirectory)).state === 'busy') return null; + if (await hasActivePendingOperation(cacheDirectory)) return null; + let operation; + try { + operation = await reserveOperation(cacheDirectory); + } catch (error) { + if (error instanceof OperationFencedError) return null; + throw error; + } + if (!await operationIsSuperseded(cacheDirectory, operation) + && await promoteOperationClaim(cacheDirectory, operation)) return operation; + await cancelOperation(cacheDirectory, operation); + return null; +} + +async function fenceLowerOperations(cacheDirectory, operation) { + const lower = (await listOperations(cacheDirectory)) + .filter((candidate) => candidate.kind === 'pending' + && candidate.generation < operation.generation) + .sort((left, right) => (left.generation < right.generation ? -1 : 1)); + for (const candidate of lower) { + try { + await cacheRename( + cacheDirectory, + candidate.directory, + path.join(cacheDirectory, operationName('fenced', candidate.generation)), + assertSafeDirectory, + ); + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + } + } +} + +async function commitOperation(cacheDirectory, operation, installedVersion, mutate) { + if (!await operationOwnsActiveClaim(cacheDirectory, operation)) { + await cancelOperation(cacheDirectory, operation); + return { fenced: true, result: silent('check-in-progress') }; + } + await fenceLowerOperations(cacheDirectory, operation); + if (!await operationOwnsActiveClaim(cacheDirectory, operation)) { + await cancelOperation(cacheDirectory, operation); + return { fenced: true, result: silent('check-in-progress') }; + } + const state = await readState(cacheDirectory, installedVersion); + const decision = mutate(state); + if (!decision.state) { + await cancelOperation(cacheDirectory, operation); + return { fenced: false, result: decision.result }; + } + let committedState = decision.state; + let committedResult = decision.result; + let encodedState = encodeRecoverableState(committedState); + if (!encodedState && decision.capacityFallback) { + committedState = decision.capacityFallback.state; + committedResult = decision.capacityFallback.result; + encodedState = encodeRecoverableState(committedState); + } + if (!encodedState) { + await cancelOperation(cacheDirectory, operation); + return { fenced: false, result: silent('cache-unavailable') }; + } + const temporary = path.join( + operation.directory, + `.state.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`, + ); + try { + await cacheWriteFile(cacheDirectory, temporary, encodedState, { + flag: 'wx', + mode: 0o600, + }); + await cacheRename( + cacheDirectory, + temporary, + path.join(operation.directory, OPERATION_STATE_FILE), + assertSafeRegularFile, + ); + await transitionOperation(cacheDirectory, operation, 'committed'); + return { fenced: false, result: committedResult }; + } catch (error) { + if (await operationWasFenced(cacheDirectory, operation)) { + return { fenced: true, result: silent('check-in-progress') }; + } + await cacheRm(cacheDirectory, temporary, { force: true }).catch(() => {}); + throw error; + } +} + +function nextSuccessfulCheck(nowMs, random) { + const boundedRandom = Math.min(1, Math.max(0, Number(random()) || 0)); + const multiplier = 0.8 + (boundedRandom * 0.4); + return new Date(nowMs + Math.round(CHECK_TTL_MS * multiplier)).toISOString(); +} + +function nextFailedCheck(nowMs, failures) { + const delay = failures <= 1 ? FIRST_FAILURE_DELAY_MS : LATER_FAILURE_DELAY_MS; + return new Date(nowMs + delay).toISOString(); +} + +function stateAfterFailedCheck(state, nowMs, withdrawCandidate = false) { + const failures = Math.min(state.check.consecutiveFailures + 1, 2); + return stateWithFailureCheck( + state, + nextFailedCheck(nowMs, failures), + failures, + withdrawCandidate, + ); +} + +function eventKeyForDigest(targetDigest) { + return `${SKILL_ID}@${targetDigest}`; +} + +function digestForEventKey(eventKey) { + const prefix = `${SKILL_ID}@`; + if (typeof eventKey !== 'string' || !eventKey.startsWith(prefix)) return null; + const digest = eventKey.slice(prefix.length); + return DIGEST_PATTERN.test(digest) ? digest : null; +} + +function notification(localRelease, candidate, source) { + return { + status: 'update_available', + eventKey: eventKeyForDigest(candidate.targetDigest), + installedVersion: localRelease.version, + latestVersion: candidate.version, + targetDigest: candidate.targetDigest, + severity: candidate.severity, + summary: `Archify ${candidate.version} is available; see the official release notes for details.`, + releaseNotes: candidate.releaseNotes, + source, + checkedAt: candidate.checkedAt, + noticeRequired: true, + }; +} + +function resultForCandidate(localRelease, state, { source = 'cache', nowMs = Date.now() } = {}) { + const { candidate } = state; + if (!candidate) return silent('cache-valid'); + if (compareSemver(candidate.version, localRelease.version) <= 0) { + return { + status: 'current', installedVersion: localRelease.version, + availableVersion: candidate.version, checkedAt: candidate.checkedAt, + source, noticeRequired: false, + }; + } + const result = notification(localRelease, candidate, source); + const preference = state.preferences?.find((entry) => entry.targetDigest === candidate.targetDigest + && entry.version === candidate.version); + if (preference?.mode === 'ignore') return { ...result, noticeRequired: false, reason: 'ignored' }; + if (preference?.mode === 'snooze' && Date.parse(preference.until) > nowMs) { + return { ...result, noticeRequired: false, reason: 'snoozed', suppressedUntil: preference.until }; + } + return result; +} + +async function readBoundedBody(response) { + const contentLength = Number(response.headers.get('content-length')); + if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) { + await cancelResponseBody(response); + throw new UpdateContractError('manifest response is too large'); + } + if (!response.body || typeof response.body.getReader !== 'function') { + throw new UpdateContractError('manifest response has no bounded stream'); + } + const reader = response.body.getReader(); + const chunks = []; + let size = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > MAX_RESPONSE_BYTES) { + await reader.cancel().catch(() => {}); + throw new UpdateContractError('manifest response is too large'); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const joined = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + joined.set(chunk, offset); + offset += chunk.byteLength; + } + try { + return new TextDecoder('utf-8', { fatal: true }).decode(joined); + } catch { + throw new UpdateContractError('manifest is not valid UTF-8'); + } +} + +async function cancelResponseBody(response) { + try { + if (response.body && typeof response.body.cancel === 'function') { + await response.body.cancel(); + } + } catch { + // Network cleanup is best-effort; the original response classification wins. + } +} + +async function fetchCandidate({ fetchImpl, manifestUrl, timeoutMs }) { + if (manifestUrl !== DEFAULT_MANIFEST_URL) throw new UpdateContractError('unexpected manifest URL'); + const controller = new AbortController(); + let timer; + const timeout = new Promise((_resolve, reject) => { + timer = setTimeout(() => { + controller.abort(); + reject(new Error('update check timed out')); + }, timeoutMs); + }); + const request = (async () => { + const headers = { accept: 'application/json' }; + const response = await fetchImpl(manifestUrl, { + method: 'GET', + headers, + redirect: 'error', + signal: controller.signal, + }); + if (response.status !== 200) { + await cancelResponseBody(response); + if (response.status === 404 || response.status === 410) { + throw new ManifestWithdrawnError('manifest was withdrawn'); + } + throw new Error(`manifest returned HTTP ${response.status}`); + } + const mediaType = response.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase(); + if (mediaType !== 'application/json') { + await cancelResponseBody(response); + throw new UpdateContractError('manifest is not JSON'); + } + const source = await readBoundedBody(response); + let decoded; + try { + decoded = JSON.parse(source); + } catch { + throw new UpdateContractError('manifest is not valid bounded JSON'); + } + return { candidate: validateManifest(decoded) }; + })(); + try { + return await Promise.race([request, timeout]); + } finally { + clearTimeout(timer); + } +} + +function defaultCacheDirectory() { + const homeDirectory = os.homedir(); + if (process.platform === 'win32') { + const localData = process.env.LOCALAPPDATA; + return path.join(localData && path.isAbsolute(localData) ? localData : path.join(homeDirectory, 'AppData', 'Local'), 'archify-skill'); + } + if (process.platform === 'darwin') return path.join(homeDirectory, 'Library', 'Caches', 'archify-skill'); + const xdgCache = process.env.XDG_CACHE_HOME; + return path.join(xdgCache && path.isAbsolute(xdgCache) ? xdgCache : path.join(homeDirectory, '.cache'), 'archify-skill'); +} + +function freshStateResult(localRelease, state, nowMs) { + const nextCheckAt = Date.parse(state.check.nextCheckAt || ''); + if (Number.isFinite(nextCheckAt) && nextCheckAt > nowMs + && nextCheckAt <= nowMs + MAX_CACHE_HORIZON_MS) { + return resultForCandidate(localRelease, state, { nowMs }); + } + return null; +} + +async function resultAfterLosingOperation(cacheDirectory, operation, localRelease, nowMs) { + await cancelOperation(cacheDirectory, operation); + const cached = await readState(cacheDirectory, localRelease.version); + return cached.candidate ? resultForCandidate(localRelease, cached, { nowMs }) : silent('check-in-progress'); +} + +async function waitUntilRetry(deadline, monotonicNow) { + let remaining; + try { + remaining = deadline - Number(monotonicNow()); + } catch { + return false; + } + if (!Number.isFinite(remaining) || remaining <= 0) return false; + await new Promise((resolve) => setTimeout( + resolve, + Math.min(LOCK_RETRY_DELAY_MS, remaining), + )); + return true; +} + +export async function checkForUpdate({ + releasePath = defaultReleasePath, + cacheDirectory = defaultCacheDirectory(), + fetchImpl = globalThis.fetch, + now = Date.now, + random = Math.random, + timeoutMs = DEFAULT_TIMEOUT_MS, +} = {}) { + if (updatesDisabled()) return silent('disabled'); + if (typeof fetchImpl !== 'function') return silent('runtime-unavailable'); + let localRelease; + try { + localRelease = validateLocalRelease(await readJsonFile(releasePath, MAX_LOCAL_RELEASE_BYTES)); + } catch { + return silent('invalid-local-release'); + } + + const nowMs = Number(now()); + if (!Number.isFinite(nowMs)) return silent('invalid-clock'); + let stateDirectory = versionCacheDirectory(cacheDirectory, localRelease.version); + let state; + try { + stateDirectory = await prepareCacheDirectory(stateDirectory); + state = await readState(stateDirectory, localRelease.version); + } catch { + return silent('cache-unavailable'); + } + const cachedResult = freshStateResult(localRelease, state, nowMs); + if (cachedResult) return cachedResult; + + let operation; + try { + operation = await acquireOperation(stateDirectory); + } catch { + return silent('cache-unavailable'); + } + if (!operation) { + try { + const cached = await readState(stateDirectory, localRelease.version); + return cached.candidate ? resultForCandidate(localRelease, cached, { nowMs }) : silent('check-in-progress'); + } catch { + return silent('cache-unavailable'); + } + } + + try { + state = await readState(stateDirectory, localRelease.version); + const racedResult = freshStateResult(localRelease, state, nowMs); + if (racedResult) { + await cancelOperation(stateDirectory, operation); + return racedResult; + } + + if (!await operationOwnsActiveClaim(stateDirectory, operation)) { + return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs); + } + + await verifyCacheToken(cacheTokenFor(stateDirectory)); + if (!await operationOwnsActiveClaim(stateDirectory, operation)) { + return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs); + } + let fetched; + try { + fetched = await fetchCandidate({ + fetchImpl, + manifestUrl: localRelease.updateManifestUrl, + timeoutMs, + }); + } catch (error) { + const withdrawn = error instanceof ManifestWithdrawnError; + const reason = withdrawn ? 'withdrawn' + : error instanceof UpdateContractError ? 'invalid-manifest' : 'check-failed'; + // Offline or invalid checks keep repeating the last validated candidate. + const failedResult = (known) => (!withdrawn && known.candidate + ? resultForCandidate(localRelease, known, { nowMs }) : silent(reason)); + try { + const committed = await commitOperation( + stateDirectory, + operation, + localRelease.version, + (current) => ({ + state: stateAfterFailedCheck(current, nowMs, withdrawn), + result: failedResult(current), + }), + ); + if (committed.fenced) { + return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs); + } + return committed.result; + } catch { + await cancelOperation(stateDirectory, operation).catch(() => {}); + return failedResult(state); + } + } + + const candidate = { ...fetched.candidate, checkedAt: new Date(nowMs).toISOString() }; + try { + const committed = await commitOperation( + stateDirectory, + operation, + localRelease.version, + (current) => { + const next = { + ...current, + candidate, + check: { + nextCheckAt: nextSuccessfulCheck(nowMs, random), + consecutiveFailures: 0, + }, + }; + const result = resultForCandidate(localRelease, next, { source: 'network', nowMs }); + return { + state: next, + result, + capacityFallback: { state: stateAfterFailedCheck(current, nowMs, true), result }, + }; + }, + ); + if (committed.fenced) { + return await resultAfterLosingOperation(stateDirectory, operation, localRelease, nowMs); + } + return committed.result; + } catch { + await cancelOperation(stateDirectory, operation).catch(() => {}); + return resultForCandidate(localRelease, { ...state, candidate }, { source: 'network', nowMs }); + } + } catch { + await cancelOperation(stateDirectory, operation).catch(() => {}); + return silent('cache-unavailable'); + } +} + +export async function setUpdatePreference({ + releasePath = defaultReleasePath, + cacheDirectory = defaultCacheDirectory(), + eventKey, + mode, + days = 7, + now = Date.now, + monotonicNow = () => performance.now(), +} = {}) { + const targetDigest = digestForEventKey(eventKey); + if (!targetDigest || !['snooze', 'ignore'].includes(mode) + || (mode === 'snooze' && (!Number.isInteger(days) || days < 1 || days > 365))) { + return silent('invalid-preference'); + } + let localRelease; + try { + localRelease = validateLocalRelease(await readJsonFile(releasePath, MAX_LOCAL_RELEASE_BYTES)); + } catch { + return silent('invalid-local-release'); + } + let stateDirectory = versionCacheDirectory(cacheDirectory, localRelease.version); + try { + stateDirectory = await prepareCacheDirectory(stateDirectory); + } catch { + return silent('cache-unavailable'); + } + const nowMs = Number(now()); + if (!Number.isFinite(nowMs)) return silent('invalid-clock'); + const deadline = Number(monotonicNow()) + PREFERENCE_LOCK_WAIT_MS; + while (Number(monotonicNow()) < deadline) { + let operation; + try { + operation = await acquireOperation(stateDirectory); + } catch { + return silent('cache-unavailable'); + } + if (!operation) { + if (!await waitUntilRetry(deadline, monotonicNow)) break; + continue; + } + try { + const committed = await commitOperation( + stateDirectory, operation, localRelease.version, (state) => { + if (!state.candidate || state.candidate.targetDigest !== targetDigest + || compareSemver(state.candidate.version, localRelease.version) <= 0) { + return { state: null, result: silent('invalid-preference') }; + } + const preference = { + targetDigest, + version: state.candidate.version, + mode, + until: mode === 'snooze' ? new Date(nowMs + days * 24 * 60 * 60 * 1_000).toISOString() : null, + }; + const preferences = (state.preferences || []) + .filter((entry) => entry.targetDigest !== targetDigest) + .slice(-30); + preferences.push(preference); + return { + state: { ...state, preferences }, + result: { status: mode === 'snooze' ? 'snoozed' : 'ignored', eventKey, + ...(preference.until ? { suppressedUntil: preference.until } : {}) }, + }; + }, + ); + if (!committed.fenced) return committed.result; + } catch { + await cancelOperation(stateDirectory, operation).catch(() => {}); + return silent('cache-unavailable'); + } + } + return silent('check-in-progress'); +} + +async function runCli() { + if (updatesDisabled()) return silent('disabled'); + const argumentsList = process.argv.slice(2); + if (argumentsList.length === 0) return checkForUpdate(); + if (argumentsList.length === 2 && argumentsList[0] === '--ack') { + return silent('legacy-ack-no-op'); + } + if (argumentsList.length === 2 && argumentsList[0] === '--snooze') { + return setUpdatePreference({ eventKey: argumentsList[1], mode: 'snooze' }); + } + if (argumentsList.length === 2 && argumentsList[0] === '--ignore') { + return setUpdatePreference({ eventKey: argumentsList[1], mode: 'ignore' }); + } + return silent('invalid-arguments'); +} + +export function isMainModule({ + argvPath = process.argv[1], + modulePath = fileURLToPath(import.meta.url), +} = {}) { + if (!argvPath) return false; + try { + return sameEntry(argvPath, modulePath).status === 'match'; + } catch { + return false; + } +} + +if (isMainModule()) { + let result; + try { + result = await runCli(); + } catch { + result = silent('check-failed'); + } + process.stdout.write(`${JSON.stringify(result)}\n`); +} diff --git a/vendor/archify/scripts/delivery-update-child.mjs b/vendor/archify/scripts/delivery-update-child.mjs new file mode 100644 index 0000000..dfe6ad8 --- /dev/null +++ b/vendor/archify/scripts/delivery-update-child.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +import { checkForUpdate } from './check-update.mjs'; + +// The parent can be busy in synchronous renderer work. Enforce its original +// deadline here, in a separate event loop, so cache work cannot continue late. +if (!/^\d{1,30}$/.test(process.argv[2] || '')) process.exit(1); +const deadlineAt = BigInt(process.argv[2]); +const remainingMs = Number(deadlineAt - process.hrtime.bigint()) / 1_000_000; +if (remainingMs <= 0) process.exit(1); +const deadlineTimer = setTimeout(() => process.kill(process.pid, 'SIGKILL'), remainingMs); +deadlineTimer.unref(); + +const result = await checkForUpdate({ + ...(process.env.ARCHIFY_UPDATE_RELEASE_PATH + ? { releasePath: process.env.ARCHIFY_UPDATE_RELEASE_PATH } : {}), + ...(process.env.ARCHIFY_UPDATE_CACHE_DIRECTORY + ? { cacheDirectory: process.env.ARCHIFY_UPDATE_CACHE_DIRECTORY } : {}), + // Leave time to record a failed fetch, so a slow network backs off instead + // of being killed before its result reaches the cache. + timeoutMs: Math.max(1, Math.floor(remainingMs - 150)), +}); +process.stdout.write(`${JSON.stringify(result)}\n`); diff --git a/vendor/archify/scripts/generate-brand-marks.mjs b/vendor/archify/scripts/generate-brand-marks.mjs new file mode 100644 index 0000000..73490ba --- /dev/null +++ b/vendor/archify/scripts/generate-brand-marks.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as simpleIcons from 'simple-icons'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(here, '..'); +const catalogPath = path.join(root, 'brand-marks', 'catalog.json'); +const outputPath = path.join(root, 'renderers', 'shared', 'generated-brand-marks.mjs'); +const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8')); +const simpleIconsVersion = JSON.parse(fs.readFileSync( + path.join(root, 'node_modules', 'simple-icons', 'package.json'), + 'utf8', +)).version; +const simpleBySlug = new Map(Object.values(simpleIcons) + .filter((icon) => icon && typeof icon === 'object' && icon.slug && icon.path) + .map((icon) => [icon.slug, icon])); + +function normalizedList(value) { + return [...new Set((Array.isArray(value) ? value : []) + .map((item) => String(item).trim()) + .filter(Boolean))]; +} + +function lookupForms(value) { + const raw = String(value ?? '').trim().toLocaleLowerCase('en-US'); + if (!raw) return []; + return [...new Set([ + raw, + raw.replace(/[\s_]+/g, '-'), + raw.replace(/[\s_.-]+/g, ''), + ])]; +} + +function fail(message) { + console.error(`brand catalog: ${message}`); + process.exit(1); +} + +if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.marks) || catalog.marks.length === 0) { + fail('catalog.json must contain a non-empty schemaVersion 1 marks array'); +} + +const ids = new Set(); +const lookupKeys = new Map(); +const domains = new Map(); +const generated = catalog.marks.map((entry, index) => { + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.id || '')) fail(`marks[${index}] has an invalid id`); + if (ids.has(entry.id)) fail(`duplicate id ${entry.id}`); + ids.add(entry.id); + + const aliases = normalizedList(entry.aliases); + const entryDomains = normalizedList(entry.domains).map((domain) => domain.toLowerCase()); + for (const key of [entry.id, ...aliases]) { + for (const form of lookupForms(key)) { + if (lookupKeys.has(form) && lookupKeys.get(form) !== entry.id) { + fail(`lookup key ${JSON.stringify(key)} is shared by ${lookupKeys.get(form)} and ${entry.id}`); + } + lookupKeys.set(form, entry.id); + } + } + for (const domain of entryDomains) { + if (domains.has(domain) && domains.get(domain) !== entry.id) { + fail(`domain ${domain} is shared by ${domains.get(domain)} and ${entry.id}`); + } + domains.set(domain, entry.id); + } + + let mark; + if (entry.simpleIcon) { + const icon = simpleBySlug.get(entry.simpleIcon); + if (!icon) fail(`${entry.id} references missing Simple Icons slug ${entry.simpleIcon}`); + mark = { + id: entry.id, + title: entry.title || icon.title, + category: entry.category, + aliases, + domains: entryDomains, + viewBox: 24, + hex: icon.hex, + path: icon.path, + provenance: { + provider: 'Simple Icons', + providerVersion: simpleIconsVersion, + source: icon.source, + ...(icon.guidelines ? { guidelines: icon.guidelines } : {}), + ...(icon.license ? { license: icon.license } : {}), + }, + }; + } else if (entry.custom) { + const custom = entry.custom; + if (!entry.title || !custom.path || !custom.source || !/^[0-9A-F]{6}$/i.test(custom.hex || '')) { + fail(`${entry.id} custom mark requires title, path, source, and six-digit hex`); + } + mark = { + id: entry.id, + title: entry.title, + category: entry.category, + aliases, + domains: entryDomains, + viewBox: custom.viewBox || 24, + hex: custom.hex.toUpperCase(), + path: custom.path, + provenance: { + provider: 'Official brand asset', + source: custom.source, + ...(custom.guidelines ? { guidelines: custom.guidelines } : {}), + }, + }; + } else { + fail(`${entry.id} must provide simpleIcon or custom`); + } + if (!mark.category || !mark.title) fail(`${entry.id} is missing category or title`); + for (const form of lookupForms(mark.title)) { + if (lookupKeys.has(form) && lookupKeys.get(form) !== entry.id) { + fail(`title ${JSON.stringify(mark.title)} is shared by ${lookupKeys.get(form)} and ${entry.id}`); + } + lookupKeys.set(form, entry.id); + } + return mark; +}).sort((left, right) => left.id.localeCompare(right.id)); + +const banner = `// Generated by scripts/generate-brand-marks.mjs from brand-marks/catalog.json.\n// Simple Icons ${simpleIconsVersion}. Do not edit by hand.\n`; +const source = `${banner}export const BRAND_MARKS = Object.freeze(${JSON.stringify(generated, null, 2)});\n`; + +if (process.argv.includes('--check')) { + const current = fs.existsSync(outputPath) + ? fs.readFileSync(outputPath, 'utf8').replace(/\r\n?/g, '\n') + : ''; + if (current !== source) { + console.error('generated brand marks are stale — run npm run generate:brand-marks'); + process.exit(1); + } +} else { + const temporary = `${outputPath}.${process.pid}.tmp`; + fs.writeFileSync(temporary, source); + fs.renameSync(temporary, outputPath); + console.log(`generated ${path.relative(root, outputPath)} (${generated.length} marks)`); +} diff --git a/vendor/archify/scripts/generate-validators.mjs b/vendor/archify/scripts/generate-validators.mjs new file mode 100644 index 0000000..5d1672a --- /dev/null +++ b/vendor/archify/scripts/generate-validators.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import Ajv2020 from 'ajv/dist/2020.js'; +import standaloneCode from 'ajv/dist/standalone/index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '..'); +const schemasDir = path.join(root, 'schemas'); +const output = path.join(root, 'renderers/shared/generated-validators.mjs'); +const diagramTypes = ['workflow', 'sequence', 'dataflow', 'lifecycle', 'architecture']; + +const ajv = new Ajv2020({ + allErrors: true, + strict: true, + code: { source: true, esm: true }, +}); +ajv.addSchema(JSON.parse(fs.readFileSync(path.join(schemasDir, 'common.schema.json'), 'utf8'))); + +const schemaIds = {}; +for (const type of diagramTypes) { + const schema = JSON.parse(fs.readFileSync(path.join(schemasDir, `${type}.schema.json`), 'utf8')); + ajv.addSchema(schema); + schemaIds[type] = schema.$id; +} + +const banner = '// Generated by scripts/generate-validators.mjs. Do not edit by hand.\n'; +const ajvUcs2Import = 'require("ajv/dist/runtime/ucs2length").default'; +const inlineUcs2Length = `function ucs2length(str) { + const len = str.length; + let length = 0; + let pos = 0; + while (pos < len) { + length += 1; + const value = str.charCodeAt(pos++); + if (value >= 0xd800 && value <= 0xdbff && pos < len + && (str.charCodeAt(pos) & 0xfc00) === 0xdc00) pos += 1; + } + return length; +}`; +let validatorCode = standaloneCode(ajv, schemaIds); +if (!validatorCode.includes(ajvUcs2Import)) { + throw new Error('AJV standalone output no longer contains the expected ucs2length helper'); +} +validatorCode = validatorCode.replaceAll(ajvUcs2Import, inlineUcs2Length); +if (validatorCode.includes('require(')) { + throw new Error('AJV standalone output contains an unexpected runtime dependency'); +} +for (const type of diagramTypes) { + const exportPattern = new RegExp(`export const ${type} = (validate\\d+);`); + const match = validatorCode.match(exportPattern); + if (!match) throw new Error(`AJV standalone output no longer exports the ${type} validator as expected`); + validatorCode = validatorCode.replace(exportPattern, `const ${type}Schema = ${match[1]};`); +} + +const portableOutputWrappers = diagramTypes.map((type) => `export function ${type}(data, context = undefined) { + if (!${type}Schema(data, context)) { + ${type}.errors = ${type}Schema.errors; + return false; + } + const output = data?.meta?.output; + if (typeof output === 'string') { + try { + validatePortablePath(output, { profile: 'output' }); + } catch (error) { + ${type}.errors = [{ + instancePath: \`${'${context?.instancePath || \'\'}'}/meta/output\`, + schemaPath: 'common.schema.json#/$defs/portableOutputPath', + keyword: 'portablePath', + params: { reason: error?.reason || 'invalid' }, + message: 'must satisfy the portable output path contract', + }]; + return false; + } + } + ${type}.errors = null; + return true; +} +${type}.evaluated = ${type}Schema.evaluated;`).join('\n'); + +const generated = `${banner}import { validatePortablePath } from './portable-path.mjs';\n${validatorCode}\n${portableOutputWrappers}\n`; + +if (process.argv.includes('--check')) { + const current = fs.existsSync(output) + ? fs.readFileSync(output, 'utf8').replace(/\r\n?/g, '\n') + : ''; + if (current !== generated) { + console.error('generated validators are stale — run npm run generate:validators'); + process.exit(1); + } +} else { + const temporary = `${output}.${process.pid}.tmp`; + fs.writeFileSync(temporary, generated); + fs.renameSync(temporary, output); + console.log(`generated ${path.relative(root, output)}`); +} diff --git a/vendor/archify/scripts/render-examples.mjs b/vendor/archify/scripts/render-examples.mjs new file mode 100644 index 0000000..9889a2f --- /dev/null +++ b/vendor/archify/scripts/render-examples.mjs @@ -0,0 +1,26 @@ +// Re-render every bundled example from its JSON IR. Installed skills keep HTML +// beside the JSON examples; the development script passes the golden directory. + +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const skillRoot = path.resolve(__dirname, '..'); +const outputRoot = path.resolve(process.argv[2] || path.join(skillRoot, 'examples')); + +const TARGETS = [ + ['workflow', 'agent-tool-call.workflow.json', 'workflow-agent-tool-call-rendered.html'], + ['sequence', 'cache-miss-request.sequence.json', 'sequence-cache-miss-request.html'], + ['dataflow', 'product-analytics.dataflow.json', 'dataflow-product-analytics.html'], + ['lifecycle', 'agent-run.lifecycle.json', 'lifecycle-agent-run.html'], + ['architecture', 'web-app.architecture.json', 'web-app-rendered.html'], +]; + +for (const [mode, input, output] of TARGETS) { + execFileSync(process.execPath, [ + path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), + path.join(skillRoot, 'examples', input), + path.join(outputRoot, output), + ], { stdio: 'inherit' }); +} diff --git a/vendor/archify/scripts/update-contract.mjs b/vendor/archify/scripts/update-contract.mjs new file mode 100644 index 0000000..527ae8a --- /dev/null +++ b/vendor/archify/scripts/update-contract.mjs @@ -0,0 +1,182 @@ +export const SKILL_ID = 'archify'; +export const EXPECTED_REPOSITORY = 'https://github.com/tt-a1i/archify'; +export const DEFAULT_MANIFEST_URL = 'https://tt-a1i.github.io/archify/skill-updates/archify/stable.json'; + +const CONTROL_OR_BIDI = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u; +const HEX_40 = /^[a-f0-9]{40}$/; +const HEX_64 = /^[a-f0-9]{64}$/; +const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; +const UTC_SECONDS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; + +export class UpdateContractError extends Error { + constructor(message) { + super(message); + this.name = 'UpdateContractError'; + } +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function hasExactKeys(value, expected) { + return isPlainObject(value) + && Object.keys(value).sort().join('\0') === [...expected].sort().join('\0'); +} + +export function parseSemver(value) { + if (typeof value !== 'string' || value.length > 128) { + throw new UpdateContractError(`invalid SemVer: ${JSON.stringify(value)}`); + } + const match = SEMVER.exec(value); + if (!match) throw new UpdateContractError(`invalid SemVer: ${JSON.stringify(value)}`); + const prerelease = match[4]?.split('.') ?? null; + if (prerelease?.some((identifier) => /^\d+$/.test(identifier) + && identifier.length > 1 && identifier.startsWith('0'))) { + throw new UpdateContractError(`invalid SemVer: ${JSON.stringify(value)}`); + } + return { + core: match.slice(1, 4), + prerelease, + build: match[5]?.split('.') ?? null, + }; +} + +function compareNumericIdentifiers(left, right) { + if (left.length !== right.length) return left.length < right.length ? -1 : 1; + if (left === right) return 0; + return left < right ? -1 : 1; +} + +function comparePrerelease(left, right) { + if (left === null && right === null) return 0; + if (left === null) return 1; + if (right === null) return -1; + const length = Math.max(left.length, right.length); + for (let index = 0; index < length; index += 1) { + if (left[index] === undefined) return -1; + if (right[index] === undefined) return 1; + if (left[index] === right[index]) continue; + const leftNumeric = /^\d+$/.test(left[index]); + const rightNumeric = /^\d+$/.test(right[index]); + if (leftNumeric && rightNumeric) return compareNumericIdentifiers(left[index], right[index]); + if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1; + return left[index] < right[index] ? -1 : 1; + } + return 0; +} + +export function compareSemver(leftValue, rightValue) { + const left = parseSemver(leftValue); + const right = parseSemver(rightValue); + for (let index = 0; index < left.core.length; index += 1) { + const comparison = compareNumericIdentifiers(left.core[index], right.core[index]); + if (comparison !== 0) return comparison; + } + return comparePrerelease(left.prerelease, right.prerelease); +} + +export function releaseChannelForVersion(value) { + return parseSemver(value).prerelease ? 'development' : 'stable'; +} + +export function isStableCoreVersion(value) { + try { + const parsed = parseSemver(value); + return parsed.prerelease === null && parsed.build === null; + } catch { + return false; + } +} + +export function validateCanonicalUtcTimestamp(value) { + if (typeof value !== 'string' || !UTC_SECONDS.test(value)) { + throw new UpdateContractError('publication time must use YYYY-MM-DDTHH:mm:ssZ'); + } + const timestamp = Date.parse(value); + if (!Number.isFinite(timestamp) + || new Date(timestamp).toISOString().replace('.000Z', 'Z') !== value) { + throw new UpdateContractError('publication time is not a real UTC calendar instant'); + } + return value; +} + +export function validateLocalRelease(value) { + if (!hasExactKeys(value, [ + 'schemaVersion', 'skillId', 'channel', 'version', 'source', 'updateManifestUrl', + ]) + || value.schemaVersion !== 1 + || value.skillId !== SKILL_ID + || !hasExactKeys(value.source, ['repository']) + || value.source.repository !== EXPECTED_REPOSITORY + || value.updateManifestUrl !== DEFAULT_MANIFEST_URL) { + throw new UpdateContractError('invalid local release identity'); + } + const expectedChannel = releaseChannelForVersion(value.version); + if (value.channel !== expectedChannel) { + throw new UpdateContractError('local release channel does not match its version'); + } + return { + schemaVersion: value.schemaVersion, + skillId: value.skillId, + channel: value.channel, + version: value.version, + source: { repository: value.source.repository }, + updateManifestUrl: value.updateManifestUrl, + }; +} + +export function validateReleaseNotesUrl(value, version) { + if (!isStableCoreVersion(version)) { + throw new UpdateContractError('release notes require a stable core version'); + } + const expected = `https://github.com/tt-a1i/archify/releases/tag/v${version}`; + if (value !== expected) { + throw new UpdateContractError('release notes URL is outside the exact trusted release path'); + } + return value; +} + +export function validateStableUpdateManifest(value) { + if (!hasExactKeys(value, [ + 'schemaVersion', 'skillId', 'channel', 'version', 'publishedAt', 'source', + 'artifact', 'summary', 'releaseNotes', 'severity', + ]) + || value.schemaVersion !== 1 + || value.skillId !== SKILL_ID + || value.channel !== 'stable' + || !isStableCoreVersion(value.version) + || !hasExactKeys(value.source, ['repository', 'ref', 'treeSha']) + || value.source.repository !== EXPECTED_REPOSITORY + || value.source.ref !== `v${value.version}` + || !HEX_40.test(value.source.treeSha) + || !hasExactKeys(value.artifact, ['sha256']) + || !HEX_64.test(value.artifact.sha256)) { + throw new UpdateContractError('invalid immutable stable release identity'); + } + validateCanonicalUtcTimestamp(value.publishedAt); + if (typeof value.summary !== 'string' || value.summary.length < 1 || value.summary.length > 160 + || CONTROL_OR_BIDI.test(value.summary)) { + throw new UpdateContractError('invalid release summary'); + } + validateReleaseNotesUrl(value.releaseNotes, value.version); + if (!['normal', 'security'].includes(value.severity)) { + throw new UpdateContractError('invalid update severity'); + } + return { + schemaVersion: value.schemaVersion, + skillId: value.skillId, + channel: value.channel, + version: value.version, + publishedAt: value.publishedAt, + source: { + repository: value.source.repository, + ref: value.source.ref, + treeSha: value.source.treeSha, + }, + artifact: { sha256: value.artifact.sha256 }, + summary: value.summary, + releaseNotes: value.releaseNotes, + severity: value.severity, + }; +} diff --git a/vendor/archify/skill-release.json b/vendor/archify/skill-release.json new file mode 100644 index 0000000..49739ec --- /dev/null +++ b/vendor/archify/skill-release.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "skillId": "archify", + "channel": "stable", + "version": "3.0.1", + "source": { + "repository": "https://github.com/tt-a1i/archify" + }, + "updateManifestUrl": "https://tt-a1i.github.io/archify/skill-updates/archify/stable.json" +} From ff7e19b55824907e7a7df73d40e529b95100b5ce Mon Sep 17 00:00:00 2001 From: Jarvis Jr Hermes Date: Fri, 2 Oct 2026 10:35:47 +0000 Subject: [PATCH 2/2] test: cover diagram cleanup collision and receipt boundaries --- scripts/portal.test.mjs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/portal.test.mjs b/scripts/portal.test.mjs index 7247ae8..cf4cbe7 100644 --- a/scripts/portal.test.mjs +++ b/scripts/portal.test.mjs @@ -1,8 +1,36 @@ import {test} from 'node:test'; import {createHash} from 'node:crypto'; import assert from 'node:assert/strict'; -import {readFileSync, existsSync} from 'node:fs'; +import {readFileSync, existsSync, mkdtempSync, mkdirSync, writeFileSync, symlinkSync, unlinkSync, rmSync, readdirSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join, resolve} from 'node:path'; +import {spawnSync} from 'node:child_process'; const read = p => readFileSync(p, 'utf8'); +test('real build removes stale diagrams, keeps receipts private and rejects collisions', () => { + const root = mkdtempSync(join(tmpdir(), 'eldorado-diagrams-')); + try { + mkdirSync(join(root,'diagrams')); + symlinkSync(resolve('vendor'),join(root,'vendor'),'dir'); + const spec = JSON.parse(read('diagrams/organization-structure.architecture.json')); + const writeSpec = name => { + spec.meta.output = `static/diagrams/${name}/index.html`; + writeFileSync(join(root,`diagrams/${name}.architecture.json`),JSON.stringify(spec)); + }; + const build = () => spawnSync(process.execPath,[resolve('scripts/build-diagrams.mjs')],{cwd:root,encoding:'utf8'}); + writeSpec('old'); + let result = build(); assert.equal(result.status,0,result.stderr); + assert(existsSync(join(root,'static/diagrams/old/index.html'))); + assert(!readdirSync(join(root,'static/diagrams'),{recursive:true}).some(p=>p.endsWith('.delivery.json'))); + unlinkSync(join(root,'diagrams/old.architecture.json')); + writeSpec('new'); + result=build(); assert.equal(result.status,0,result.stderr); + assert(!existsSync(join(root,'static/diagrams/old'))); + assert(!existsSync(join(root,'static/diagrams/old.json'))); + writeFileSync(join(root,'diagrams/new.workflow.json'),JSON.stringify(spec)); + result=build(); assert.notEqual(result.status,0); + assert(result.stderr.includes('Duplicate diagram output')); + } finally {rmSync(root,{recursive:true,force:true});} +}); test('vendored source matches the reviewed upstream snapshot', () => { const checksums = JSON.parse(read('vendor/archify-checksums.json')); for (const [path, hash] of Object.entries(checksums)) {