From d56e784e978272f7a1ae4e513742b739a4e660db Mon Sep 17 00:00:00 2001 From: Oleg Lukasonok Date: Wed, 30 Sep 2026 12:40:55 +0300 Subject: [PATCH] Archify Diagram Viewer 0.1.0 VS Code extension that previews Archify diagrams from their JSON sources (live, as you type) and opens rendered Archify HTML in a viewer tab. Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime. Adds validation diagnostics, JSON schema help, source-link navigation, export saving, render-to-file and open-in-browser commands. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 6 + .vscode/launch.json | 13 + .vscode/tasks.json | 11 + .vscodeignore | 12 + CHANGELOG.md | 12 + LICENSE | 28 + README.md | 65 + esbuild.mjs | 48 + media/preview.svg | 1 + package-lock.json | 4710 ++++++ package.json | 315 + schemas/architecture.schema.json | 325 + schemas/common.schema.json | 354 + schemas/dataflow.schema.json | 267 + schemas/lifecycle.schema.json | 299 + schemas/sequence.schema.json | 250 + schemas/workflow.schema.json | 456 + scripts/sync-archify.mjs | 72 + src/detect.ts | 77 + src/diagnostics.ts | 132 + src/extension.ts | 172 + src/htmlViewer.ts | 116 + src/jsonPointer.ts | 39 + src/preview.ts | 232 + src/renderer.ts | 195 + src/runtime.ts | 77 + src/webviewHtml.ts | 231 + src/webviewMessages.ts | 100 + test/detect.test.ts | 49 + test/fixtures/agent-run.lifecycle.json | 55 + test/fixtures/agent-tool-call.workflow.json | 74 + .../fixtures/cache-miss-request.sequence.json | 77 + test/fixtures/event-stream.dataflow.json | 52 + test/fixtures/mco-runtime.architecture.json | 293 + .../production-deployment.architecture.json | 51 + test/integration/run.mjs | 37 + test/integration/suite.ts | 121 + test/renderer.test.ts | 76 + test/webviewHtml.test.ts | 37 + tsconfig.json | 13 + vendor/archify.json | 5 + vendor/archify/LICENSE | 22 + 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 ++ 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 + 119 files changed, 66708 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 .vscodeignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 esbuild.mjs create mode 100644 media/preview.svg create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 schemas/architecture.schema.json create mode 100644 schemas/common.schema.json create mode 100644 schemas/dataflow.schema.json create mode 100644 schemas/lifecycle.schema.json create mode 100644 schemas/sequence.schema.json create mode 100644 schemas/workflow.schema.json create mode 100644 scripts/sync-archify.mjs create mode 100644 src/detect.ts create mode 100644 src/diagnostics.ts create mode 100644 src/extension.ts create mode 100644 src/htmlViewer.ts create mode 100644 src/jsonPointer.ts create mode 100644 src/preview.ts create mode 100644 src/renderer.ts create mode 100644 src/runtime.ts create mode 100644 src/webviewHtml.ts create mode 100644 src/webviewMessages.ts create mode 100644 test/detect.test.ts create mode 100644 test/fixtures/agent-run.lifecycle.json create mode 100644 test/fixtures/agent-tool-call.workflow.json create mode 100644 test/fixtures/cache-miss-request.sequence.json create mode 100644 test/fixtures/event-stream.dataflow.json create mode 100644 test/fixtures/mco-runtime.architecture.json create mode 100644 test/fixtures/production-deployment.architecture.json create mode 100644 test/integration/run.mjs create mode 100644 test/integration/suite.ts create mode 100644 test/renderer.test.ts create mode 100644 test/webviewHtml.test.ts create mode 100644 tsconfig.json create mode 100644 vendor/archify.json create mode 100644 vendor/archify/LICENSE 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/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/.gitignore b/.gitignore new file mode 100644 index 0000000..9a7d130 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +out/ +*.vsix +.DS_Store +.vscode-test/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a77f38e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/test/fixtures"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "npm: build" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d7c0981 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": "$esbuild" + } + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..6d35214 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,12 @@ +src/** +test/** +out/** +scripts/** +node_modules/** +.vscode/** +.gitignore +esbuild.mjs +tsconfig.json +**/*.map +**/*.ts +.vscode-test/** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0507547 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.1.0 — 2026-09-30 + +- Live preview for Archify JSON sources (architecture, workflow, sequence, dataflow, lifecycle), re-rendered as you type. +- Read-only Archify viewer for rendered `.html` files, reloaded when the file changes. +- Problems panel diagnostics from `archify validate`, mapped to the JSON location. +- JSON schema completion and hover for `*.architecture.json`, `*.workflow.json`, `*.sequence.json`, `*.dataflow.json` and `*.lifecycle.json`. +- Source-evidence links open the referenced file and lines in the editor. +- Viewer exports (PNG, SVG, share card, …) save through a VS Code save dialog. +- Render to HTML file and open in browser commands. +- Bundles the Archify 3.0.1 renderer; runs on VS Code's own Node runtime. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5346ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +MIT License + +Copyright (c) 2026 VSCode Extentions (gitea.lego-cloud.eu/vscode-extensions) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +vendor/archify/ contains the Archify renderer, Copyright (c) 2026 tt-a1i +(Archify), MIT License. See vendor/archify/LICENSE and +vendor/archify/THIRD_PARTY_NOTICES.md. schemas/ is derived from the same +project. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d26585 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Archify Diagram Viewer + +View [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. Open a diagram's JSON source and see the interactive diagram next to it, updated as you type. Open rendered Archify HTML files in a viewer tab instead of a browser. + +This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed. + +## Features + +- **Live preview**: for `*.architecture.json`, `*.workflow.json`, `*.sequence.json`, `*.dataflow.json` and `*.lifecycle.json` files, or any JSON with a `diagram_type`. Use the preview button in the editor title, Cmd/Ctrl+K V (to the side) or Cmd/Ctrl+Shift+V. The preview keeps the focus and route you selected (`#focus=…`, `#route=…`) when it re-renders. +- **HTML viewer**: right-click a rendered `.html` file and choose **Open in Archify Viewer**, or use *Reopen Editor With… → Archify Diagram Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk. +- **Problems**: `archify validate` runs on the source and reports schema, layout and evidence problems at the matching JSON location. +- **Schema help**: completion and hover documentation come from Archify's own JSON schemas. +- **Source links**: in source-backed diagrams, clicking a source reference opens that file and line range in the editor. If the file is not in the workspace, the web link opens instead. +- **Export**: the diagram's own Export menu (PNG, SVG, share card, …) saves through a VS Code save dialog. +- **Commands**: *Archify: Render to HTML File…*, *Archify: Open in Browser*, *Archify: Refresh Preview*, *Archify: Show Source*. + +## Source evidence + +A diagram that declares `meta.repository` and `sources` is verified against a local Git checkout whose `origin` and pinned commit match. The extension uses the Git top-level directory of the diagram file, or `archify.repoRoot` when set. + +When verification fails, the default (`archify.sourceEvidence: "fallback"`) still shows the diagram without source links, explains why in a notice, and reports the reason as a warning. Set it to `"strict"` to treat this as an error, as the Archify CLI does. *Render to HTML File…* always verifies strictly and asks before rendering without source links. + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `archify.theme` | `vscode` | `vscode` follows the color theme; `dark` or `light` force one; `diagram` keeps the diagram's own choice. | +| `archify.preview.liveUpdate` | `true` | Re-render while typing; when off, re-render on save. | +| `archify.preview.debounceMs` | `400` | Delay before re-rendering after an edit. | +| `archify.validation.enabled` | `true` | Report validation results in Problems. | +| `archify.quality` | `""` | Force the `standard` or `showcase` quality profile. | +| `archify.sourceEvidence` | `fallback` | `fallback` or `strict` handling of unverifiable source evidence. | +| `archify.repoRoot` | `""` | Checkout used to verify source evidence. | +| `archify.nodePath` | `""` | Node.js 18+ executable to use instead of VS Code's runtime. | + +## Security + +Diagrams run in a webview with a strict content security policy. Only the page's own inline scripts run (via a per-render nonce), there is no network access for scripts or fetches, and links open through VS Code. + +## Development + +```bash +npm install +npm run build # bundle to dist/ +npm test # unit tests + renderer tests (Node) +npm run test:integration # runs the suite inside VS Code +npm run package # build archify-vscode-ext-.vsix +``` + +Press F5 to launch an Extension Development Host with the test fixtures. + +### Updating the bundled renderer + +`vendor/archify/` and `schemas/` are generated from an Archify checkout: + +```bash +node scripts/sync-archify.mjs /path/to/archify # default: ../../ws-storm/archify +npm test +``` + +`vendor/archify.json` records the upstream version and commit. + +## License + +MIT. See [LICENSE](LICENSE). The bundled renderer is MIT-licensed by tt-a1i; see `vendor/archify/LICENSE` and `vendor/archify/THIRD_PARTY_NOTICES.md`. diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100644 index 0000000..9ce0143 --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,48 @@ +import * as esbuild from 'esbuild'; +import fs from 'node:fs'; +import path from 'node:path'; + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); +const tests = process.argv.includes('--tests'); + +const common = { + bundle: true, + format: 'cjs', + platform: 'node', + target: 'node20', + sourcemap: !production, + minify: production, + logLevel: 'info', + // jsonc-parser's UMD entry uses dynamic requires esbuild cannot follow. + mainFields: ['module', 'main'], +}; + +if (tests) { + // Unit tests cover the modules that do not import 'vscode'; the + // integration suite runs inside VS Code, which provides it. + const entryPoints = fs.readdirSync('test') + .filter((file) => file.endsWith('.test.ts')) + .map((file) => path.join('test', file)); + await esbuild.build({ ...common, entryPoints, outdir: 'out/test', sourcemap: true }); + await esbuild.build({ + ...common, + entryPoints: ['test/integration/suite.ts'], + outfile: 'out/test/integration/suite.js', + external: ['vscode'], + sourcemap: true, + }); +} else { + const ctx = await esbuild.context({ + ...common, + entryPoints: ['src/extension.ts'], + outfile: 'dist/extension.js', + external: ['vscode'], + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} diff --git a/media/preview.svg b/media/preview.svg new file mode 100644 index 0000000..0b2b1dc --- /dev/null +++ b/media/preview.svg @@ -0,0 +1 @@ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7cc27d7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4710 @@ +{ + "name": "archify-vscode-ext", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "archify-vscode-ext", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.0.0", + "@types/vscode": "~1.100.0", + "@vscode/test-electron": "^3.1.0", + "@vscode/vsce": "^3.0.0", + "esbuild": "^0.25.0", + "jsonc-parser": "^3.3.1", + "typescript": "^5.8.0" + }, + "engines": { + "vscode": "^1.100.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.1.tgz", + "integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz", + "integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^6.0.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.23.0.tgz", + "integrity": "sha512-IRCwkRCK47hBXK+7bu67UZWY7HS0Jx1dZgi4C1HVbjlBum8Jy8fb2l87xFr6HxdQJWy951zRUHtwwDITHFAtaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", + "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", + "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.8.0", + "@textlint/resolver": "15.8.0", + "@textlint/types": "15.8.0", + "debug": "^4.4.3", + "js-yaml": "^4.3.0", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=20.18.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", + "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", + "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", + "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/test-electron": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", + "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", + "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", + "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.8.tgz", + "integrity": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad5153d --- /dev/null +++ b/package.json @@ -0,0 +1,315 @@ +{ + "name": "archify-vscode-ext", + "displayName": "Archify Diagram Viewer", + "description": "Preview Archify diagrams (architecture, workflow, sequence, dataflow, lifecycle) from their JSON sources and open rendered Archify HTML inside VS Code.", + "version": "0.1.0", + "publisher": "vscode-extensions", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext.git" + }, + "engines": { + "vscode": "^1.100.0" + }, + "categories": [ + "Visualization", + "Programming Languages" + ], + "keywords": [ + "archify", + "diagram", + "architecture", + "sequence", + "workflow" + ], + "main": "./dist/extension.js", + "activationEvents": [ + "onLanguage:json", + "onLanguage:jsonc", + "onLanguage:html", + "workspaceContains:**/*.{architecture,workflow,sequence,dataflow,lifecycle}.json", + "onWebviewPanel:archify.preview" + ], + "contributes": { + "commands": [ + { + "command": "archify.showPreview", + "title": "Open Preview", + "category": "Archify", + "icon": "$(open-preview)" + }, + { + "command": "archify.showPreviewToSide", + "title": "Open Preview to the Side", + "category": "Archify", + "icon": "$(open-preview)" + }, + { + "command": "archify.openHtmlViewer", + "title": "Open in Archify Viewer", + "category": "Archify", + "icon": "$(open-preview)" + }, + { + "command": "archify.renderToHtml", + "title": "Render to HTML File\u2026", + "category": "Archify", + "icon": "$(file-code)" + }, + { + "command": "archify.openInBrowser", + "title": "Open in Browser", + "category": "Archify", + "icon": "$(globe)" + }, + { + "command": "archify.refreshPreview", + "title": "Refresh Preview", + "category": "Archify", + "icon": "$(refresh)" + }, + { + "command": "archify.showSource", + "title": "Show Source", + "category": "Archify", + "icon": "$(go-to-file)" + } + ], + "menus": { + "commandPalette": [ + { + "command": "archify.showPreview", + "when": "archify.isDiagramSource || archify.isArchifyHtml" + }, + { + "command": "archify.showPreviewToSide", + "when": "archify.isDiagramSource" + }, + { + "command": "archify.openHtmlViewer", + "when": "resourceExtname == .html" + }, + { + "command": "archify.renderToHtml", + "when": "archify.isDiagramSource || archifyPreviewFocus" + }, + { + "command": "archify.openInBrowser", + "when": "archify.isDiagramSource || archify.isArchifyHtml || archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer'" + }, + { + "command": "archify.refreshPreview", + "when": "archifyPreviewFocus" + }, + { + "command": "archify.showSource", + "when": "archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer'" + } + ], + "editor/title": [ + { + "command": "archify.showPreviewToSide", + "when": "archify.isDiagramSource && !notebookEditorFocused", + "group": "navigation" + }, + { + "command": "archify.openHtmlViewer", + "when": "archify.isArchifyHtml && activeCustomEditorId != 'archify.htmlViewer'", + "group": "navigation" + }, + { + "command": "archify.refreshPreview", + "when": "archifyPreviewFocus", + "group": "navigation" + }, + { + "command": "archify.showSource", + "when": "archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer'", + "group": "navigation" + }, + { + "command": "archify.openInBrowser", + "when": "archifyPreviewFocus || activeCustomEditorId == 'archify.htmlViewer'", + "group": "1_archify" + }, + { + "command": "archify.renderToHtml", + "when": "archify.isDiagramSource || archifyPreviewFocus", + "group": "1_archify" + } + ], + "explorer/context": [ + { + "command": "archify.showPreview", + "when": "resourceFilename =~ /\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json$/", + "group": "navigation@20" + }, + { + "command": "archify.renderToHtml", + "when": "resourceFilename =~ /\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json$/", + "group": "navigation@21" + }, + { + "command": "archify.openHtmlViewer", + "when": "resourceExtname == .html", + "group": "navigation@20" + } + ], + "editor/title/context": [ + { + "command": "archify.showPreview", + "when": "resourceFilename =~ /\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json$/", + "group": "1_open" + } + ] + }, + "keybindings": [ + { + "command": "archify.showPreviewToSide", + "key": "ctrl+k v", + "mac": "cmd+k v", + "when": "editorTextFocus && archify.isDiagramSource" + }, + { + "command": "archify.showPreview", + "key": "ctrl+shift+v", + "mac": "cmd+shift+v", + "when": "editorTextFocus && archify.isDiagramSource" + } + ], + "customEditors": [ + { + "viewType": "archify.htmlViewer", + "displayName": "Archify Diagram Viewer", + "selector": [ + { + "filenamePattern": "*.html" + } + ], + "priority": "option" + } + ], + "jsonValidation": [ + { + "fileMatch": "*.architecture.json", + "url": "./schemas/architecture.schema.json" + }, + { + "fileMatch": "*.workflow.json", + "url": "./schemas/workflow.schema.json" + }, + { + "fileMatch": "*.sequence.json", + "url": "./schemas/sequence.schema.json" + }, + { + "fileMatch": "*.dataflow.json", + "url": "./schemas/dataflow.schema.json" + }, + { + "fileMatch": "*.lifecycle.json", + "url": "./schemas/lifecycle.schema.json" + } + ], + "configuration": { + "title": "Archify", + "properties": { + "archify.theme": { + "type": "string", + "enum": [ + "vscode", + "dark", + "light", + "diagram" + ], + "enumDescriptions": [ + "Follow the VS Code color theme.", + "Always use the dark diagram theme.", + "Always use the light diagram theme.", + "Keep whatever the diagram or its toolbar chooses." + ], + "default": "vscode", + "description": "Theme used when showing Archify diagrams." + }, + "archify.preview.liveUpdate": { + "type": "boolean", + "default": true, + "description": "Re-render the preview while you type. When off, the preview updates on save." + }, + "archify.preview.debounceMs": { + "type": "number", + "default": 400, + "minimum": 50, + "description": "Delay in milliseconds before re-rendering after an edit." + }, + "archify.validation.enabled": { + "type": "boolean", + "default": true, + "description": "Report Archify validation results (schema, layout and source evidence) in the Problems panel." + }, + "archify.quality": { + "type": "string", + "enum": [ + "", + "standard", + "showcase" + ], + "enumDescriptions": [ + "Use the quality profile declared in the diagram.", + "Force the standard quality gate.", + "Force the showcase quality gate." + ], + "default": "", + "description": "Quality profile passed to the Archify renderer." + }, + "archify.sourceEvidence": { + "type": "string", + "enum": [ + "fallback", + "strict" + ], + "enumDescriptions": [ + "If source evidence cannot be verified, still show the diagram without source links and explain why.", + "Treat unverifiable source evidence as a render error, exactly like the Archify CLI." + ], + "default": "fallback", + "description": "How to handle diagrams whose repository source evidence cannot be verified locally." + }, + "archify.repoRoot": { + "type": "string", + "default": "", + "description": "Git checkout used to verify source evidence. Empty means the Git top-level directory of the diagram file." + }, + "archify.nodePath": { + "type": "string", + "default": "", + "description": "Node.js executable (18 or newer) used to run the renderer. Empty means the runtime bundled with VS Code." + } + } + } + }, + "scripts": { + "sync:archify": "node scripts/sync-archify.mjs", + "build": "node esbuild.mjs", + "watch": "node esbuild.mjs --watch", + "typecheck": "tsc --noEmit", + "test": "node esbuild.mjs --tests && node --test \"out/test/*.test.js\"", + "vscode:prepublish": "node esbuild.mjs --production", + "package": "vsce package --no-dependencies --baseContentUrl https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext/src/branch/main --baseImagesUrl https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext/raw/branch/main", + "test:integration": "node esbuild.mjs && node esbuild.mjs --tests && node test/integration/run.mjs" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/vscode": "~1.100.0", + "@vscode/test-electron": "^3.1.0", + "@vscode/vsce": "^3.0.0", + "esbuild": "^0.25.0", + "jsonc-parser": "^3.3.1", + "typescript": "^5.8.0" + }, + "bugs": { + "url": "https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext/issues" + }, + "homepage": "https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext#readme" +} diff --git a/schemas/architecture.schema.json b/schemas/architecture.schema.json new file mode 100644 index 0000000..63946f0 --- /dev/null +++ b/schemas/architecture.schema.json @@ -0,0 +1,325 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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/schemas/common.schema.json b/schemas/common.schema.json new file mode 100644 index 0000000..23fd4ea --- /dev/null +++ b/schemas/common.schema.json @@ -0,0 +1,354 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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/schemas/dataflow.schema.json b/schemas/dataflow.schema.json new file mode 100644 index 0000000..f323115 --- /dev/null +++ b/schemas/dataflow.schema.json @@ -0,0 +1,267 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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/schemas/lifecycle.schema.json b/schemas/lifecycle.schema.json new file mode 100644 index 0000000..d3fa012 --- /dev/null +++ b/schemas/lifecycle.schema.json @@ -0,0 +1,299 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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/schemas/sequence.schema.json b/schemas/sequence.schema.json new file mode 100644 index 0000000..457d1c2 --- /dev/null +++ b/schemas/sequence.schema.json @@ -0,0 +1,250 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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/schemas/workflow.schema.json b/schemas/workflow.schema.json new file mode 100644 index 0000000..7c33e97 --- /dev/null +++ b/schemas/workflow.schema.json @@ -0,0 +1,456 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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/scripts/sync-archify.mjs b/scripts/sync-archify.mjs new file mode 100644 index 0000000..024b24e --- /dev/null +++ b/scripts/sync-archify.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +// Copies the Archify renderer runtime into vendor/archify and writes +// VS Code-friendly copies of its JSON schemas into schemas/. +// +// Usage: node scripts/sync-archify.mjs [path-to-archify-checkout] +// Default source: ../../ws-storm/archify (or $ARCHIFY_SRC). + +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const source = path.resolve( + process.argv[2] || process.env.ARCHIFY_SRC || path.join(root, '..', '..', 'ws-storm', 'archify'), +); +const skill = path.join(source, 'archify'); + +// Runtime pieces the CLI reads at render/validate time. test/ and examples/ +// are development-only and large, so they are left out. +const RUNTIME = [ + 'LICENSE', + 'THIRD_PARTY_NOTICES.md', + 'package.json', + 'skill-release.json', + 'assets', + 'bin', + 'brand-marks', + 'delta', + 'migrations', + 'recipes', + 'references', + 'renderers', + 'schemas', + 'scripts', +]; + +if (!fs.existsSync(path.join(skill, 'bin', 'archify.mjs'))) { + console.error(`Archify checkout not found at ${source}`); + process.exit(1); +} + +const vendor = path.join(root, 'vendor', 'archify'); +fs.rmSync(vendor, { recursive: true, force: true }); +fs.mkdirSync(vendor, { recursive: true }); +for (const entry of RUNTIME) { + fs.cpSync(path.join(skill, entry), path.join(vendor, entry), { recursive: true }); +} + +// The upstream schemas carry absolute GitHub $ids, which would make VS Code +// resolve "common.schema.json" refs over the network. Dropping $id lets refs +// resolve next to the bundled file instead. +const schemaOut = path.join(root, 'schemas'); +fs.rmSync(schemaOut, { recursive: true, force: true }); +fs.mkdirSync(schemaOut, { recursive: true }); +for (const file of fs.readdirSync(path.join(skill, 'schemas'))) { + if (!file.endsWith('.schema.json')) continue; + const schema = JSON.parse(fs.readFileSync(path.join(skill, 'schemas', file), 'utf8')); + delete schema.$id; + fs.writeFileSync(path.join(schemaOut, file), `${JSON.stringify(schema, null, 2)}\n`); +} + +let revision = 'unknown'; +try { + revision = execFileSync('git', ['-C', source, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); +} catch {} +const { version } = JSON.parse(fs.readFileSync(path.join(skill, 'package.json'), 'utf8')); +fs.writeFileSync( + path.join(root, 'vendor', 'archify.json'), + `${JSON.stringify({ upstream: 'https://github.com/tt-a1i/archify', version, revision }, null, 2)}\n`, +); +console.log(`Synced archify ${version} (${revision.slice(0, 12)}) from ${source}`); diff --git a/src/detect.ts b/src/detect.ts new file mode 100644 index 0000000..95ce4db --- /dev/null +++ b/src/detect.ts @@ -0,0 +1,77 @@ +import { parse, ParseError } from 'jsonc-parser'; + +export const DIAGRAM_TYPES = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'] as const; +export type DiagramType = (typeof DIAGRAM_TYPES)[number]; + +/** Node collection that may carry `sources` evidence, per diagram type. */ +export const EVIDENCE_COLLECTIONS: Record = { + architecture: 'components', + workflow: 'nodes', + sequence: 'participants', + dataflow: 'nodes', + lifecycle: 'states', +}; + +const FILENAME_RE = /\.(architecture|workflow|sequence|dataflow|lifecycle)\.json$/i; +const MAX_SNIFF_BYTES = 8 * 1024 * 1024; + +export function isDiagramType(value: unknown): value is DiagramType { + return typeof value === 'string' && (DIAGRAM_TYPES as readonly string[]).includes(value); +} + +export function typeFromFilename(fileName: string): DiagramType | undefined { + const match = FILENAME_RE.exec(fileName); + return match ? (match[1].toLowerCase() as DiagramType) : undefined; +} + +export interface DiagramSource { + type: DiagramType; + diagram: Record; +} + +/** + * Parses JSON text and returns the diagram when it declares an Archify + * `diagram_type`. Falls back to the file name for documents that do not + * parse yet, so a half-typed source still counts as a diagram. + */ +export function readDiagramSource(text: string, fileName = ''): DiagramSource | undefined { + if (text.length > MAX_SNIFF_BYTES) return undefined; + const errors: ParseError[] = []; + const value = parse(text, errors, { allowTrailingComma: true }); + if (value && typeof value === 'object' && !Array.isArray(value) && isDiagramType(value.diagram_type)) { + return { type: value.diagram_type, diagram: value }; + } + const byName = typeFromFilename(fileName); + if (byName) { + const diagram = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; + return { type: byName, diagram }; + } + return undefined; +} + +/** True when the HTML was produced by the Archify renderer. */ +export function isArchifyHtml(text: string): boolean { + const head = text.slice(0, 4096); + return /): boolean { + const meta = diagram.meta as Record | undefined; + if (meta && meta.repository) return true; + const nodes = diagram[EVIDENCE_COLLECTIONS[type]]; + return Array.isArray(nodes) && nodes.some((node) => Array.isArray(node?.sources) && node.sources.length > 0); +} + +/** Returns a copy of the diagram with repository evidence removed. */ +export function stripSourceEvidence(type: DiagramType, diagram: Record): Record { + const copy = structuredClone(diagram); + const meta = copy.meta as Record | undefined; + if (meta) delete meta.repository; + const nodes = copy[EVIDENCE_COLLECTIONS[type]]; + if (Array.isArray(nodes)) { + for (const node of nodes) { + if (node && typeof node === 'object') delete node.sources; + } + } + return copy; +} diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 0000000..159b68d --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,132 @@ +import { parse, ParseError } from 'jsonc-parser'; +import * as vscode from 'vscode'; +import { readDiagramSource } from './detect'; +import { rangeForPointer } from './jsonPointer'; +import { ArchifyDiagnostic, isEvidenceError, validate } from './renderer'; +import { evidenceMode, log, runtimeFor } from './runtime'; + +const JSON_LANGUAGES = new Set(['json', 'jsonc']); + +function severity(diagnostic: ArchifyDiagnostic, downgradeEvidence: boolean): vscode.DiagnosticSeverity { + if (downgradeEvidence && isEvidenceError(`${diagnostic.code ?? ''} ${diagnostic.message}`)) { + return vscode.DiagnosticSeverity.Warning; + } + switch (diagnostic.severity) { + case 'warning': + return vscode.DiagnosticSeverity.Warning; + case 'info': + return vscode.DiagnosticSeverity.Information; + default: + return vscode.DiagnosticSeverity.Error; + } +} + +/** Runs `archify validate` on diagram sources and reports into Problems. */ +export class DiagnosticsController implements vscode.Disposable { + private readonly collection = vscode.languages.createDiagnosticCollection('archify'); + private readonly timers = new Map(); + private readonly sequence = new Map(); + private readonly disposables: vscode.Disposable[]; + + constructor(private readonly context: vscode.ExtensionContext) { + this.disposables = [ + this.collection, + vscode.workspace.onDidOpenTextDocument((doc) => this.schedule(doc, 0)), + vscode.workspace.onDidChangeTextDocument((event) => { + if (event.contentChanges.length) this.schedule(event.document, 700); + }), + vscode.workspace.onDidSaveTextDocument((doc) => this.schedule(doc, 0)), + vscode.workspace.onDidCloseTextDocument((doc) => this.clear(doc.uri)), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('archify')) this.revalidateAll(); + }), + ]; + this.revalidateAll(); + } + + private enabled(uri: vscode.Uri): boolean { + return vscode.workspace.getConfiguration('archify', uri).get('validation.enabled', true); + } + + revalidateAll(): void { + for (const doc of vscode.workspace.textDocuments) this.schedule(doc, 0); + } + + private clear(uri: vscode.Uri): void { + const key = uri.toString(); + clearTimeout(this.timers.get(key)); + this.timers.delete(key); + this.sequence.set(key, (this.sequence.get(key) ?? 0) + 1); + this.collection.delete(uri); + } + + private schedule(doc: vscode.TextDocument, delay: number): void { + if (!JSON_LANGUAGES.has(doc.languageId)) return; + const key = doc.uri.toString(); + clearTimeout(this.timers.get(key)); + this.timers.set(key, setTimeout(() => void this.validate(doc), delay)); + } + + private async validate(doc: vscode.TextDocument): Promise { + const key = doc.uri.toString(); + this.timers.delete(key); + const seq = (this.sequence.get(key) ?? 0) + 1; + this.sequence.set(key, seq); + + const text = doc.getText(); + const source = readDiagramSource(text, doc.fileName); + if (!source || !this.enabled(doc.uri) || doc.isClosed) { + this.collection.delete(doc.uri); + return; + } + // Leave syntax errors to the JSON language service. + const syntax: ParseError[] = []; + parse(text, syntax, { allowTrailingComma: false }); + if (syntax.length) { + this.collection.delete(doc.uri); + return; + } + + try { + const runtime = await runtimeFor(this.context, doc.uri); + const result = await validate(runtime, source.type, text); + if (seq !== this.sequence.get(key) || doc.isClosed) return; + + if (result.crash) { + const diagnostic = new vscode.Diagnostic( + new vscode.Range(0, 0, 0, 0), + `Archify validation did not complete: ${result.crash.split('\n')[0]}`, + vscode.DiagnosticSeverity.Error, + ); + diagnostic.source = 'archify'; + this.collection.set(doc.uri, [diagnostic]); + return; + } + + const downgradeEvidence = evidenceMode(doc.uri) === 'fallback'; + this.collection.set( + doc.uri, + result.diagnostics.map((item) => { + const offsets = rangeForPointer(text, item.subject?.path); + const range = new vscode.Range(doc.positionAt(offsets.start), doc.positionAt(offsets.end)); + const fix = item.supportedFixes?.[0]; + const diagnostic = new vscode.Diagnostic( + range, + fix ? `${item.message}\nFix: ${fix}` : item.message, + severity(item, downgradeEvidence), + ); + diagnostic.source = 'archify'; + if (item.code) diagnostic.code = item.code; + return diagnostic; + }), + ); + } catch (error) { + log().error(`Validation failed for ${doc.uri.fsPath}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + dispose(): void { + for (const timer of this.timers.values()) clearTimeout(timer); + for (const disposable of this.disposables.splice(0)) disposable.dispose(); + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..580e5f4 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,172 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import * as vscode from 'vscode'; +import { isArchifyHtml, readDiagramSource } from './detect'; +import { DiagnosticsController } from './diagnostics'; +import { HTML_VIEW_TYPE, HtmlViewerProvider } from './htmlViewer'; +import { PreviewManager } from './preview'; +import { isEvidenceError, renderDiagram, renderToFile } from './renderer'; +import { clearGitRootCache, evidenceMode, log, readText, runtimeFor } from './runtime'; + +const JSON_LANGUAGES = new Set(['json', 'jsonc']); + +function isHtmlUri(uri: vscode.Uri): boolean { + return /\.html?$/i.test(uri.path); +} + +export function activate(context: vscode.ExtensionContext): void { + const previews = new PreviewManager(context); + const htmlViewer = new HtmlViewerProvider(); + context.subscriptions.push(previews, htmlViewer, new DiagnosticsController(context), log()); + + /** The file a command applies to: explicit argument, focused preview or viewer, then the text editor. */ + function targetUri(arg?: unknown): vscode.Uri | undefined { + if (arg instanceof vscode.Uri) return arg; + return previews.activePreview?.uri ?? htmlViewer.activeUri ?? vscode.window.activeTextEditor?.document.uri; + } + + function sourceUri(arg?: unknown): vscode.Uri | undefined { + const uri = targetUri(arg); + if (!uri) vscode.window.showWarningMessage('Archify: open a diagram JSON source first.'); + return uri; + } + + // Context keys for editor title buttons and keybindings. + let contextTimer: NodeJS.Timeout | undefined; + const updateContext = () => { + const doc = vscode.window.activeTextEditor?.document; + const diagram = Boolean(doc && JSON_LANGUAGES.has(doc.languageId) && readDiagramSource(doc.getText(), doc.fileName)); + const html = Boolean(doc && doc.languageId === 'html' && isArchifyHtml(doc.getText())); + void vscode.commands.executeCommand('setContext', 'archify.isDiagramSource', diagram); + void vscode.commands.executeCommand('setContext', 'archify.isArchifyHtml', html); + }; + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(updateContext), + vscode.workspace.onDidChangeTextDocument((event) => { + if (event.document !== vscode.window.activeTextEditor?.document) return; + clearTimeout(contextTimer); + contextTimer = setTimeout(updateContext, 300); + }), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('archify.repoRoot')) clearGitRootCache(); + }), + { dispose: () => clearTimeout(contextTimer) }, + ); + updateContext(); + + const openHtmlViewer = (uri: vscode.Uri, viewColumn?: vscode.ViewColumn) => + vscode.commands.executeCommand('vscode.openWith', uri, HTML_VIEW_TYPE, viewColumn); + + context.subscriptions.push( + vscode.commands.registerCommand('archify.showPreview', (arg?: unknown) => { + const uri = sourceUri(arg); + if (!uri) return; + if (isHtmlUri(uri)) return openHtmlViewer(uri); + previews.show(uri, vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.Active); + }), + + vscode.commands.registerCommand('archify.showPreviewToSide', (arg?: unknown) => { + const uri = sourceUri(arg); + if (!uri) return; + if (isHtmlUri(uri)) return openHtmlViewer(uri, vscode.ViewColumn.Beside); + previews.show(uri, vscode.ViewColumn.Beside, true); + }), + + vscode.commands.registerCommand('archify.openHtmlViewer', (arg?: unknown) => { + const uri = sourceUri(arg); + if (uri) return openHtmlViewer(uri); + }), + + vscode.commands.registerCommand('archify.refreshPreview', () => previews.activePreview?.update(true)), + + vscode.commands.registerCommand('archify.showSource', async () => { + const preview = previews.activePreview; + if (preview) { + const column = preview.panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One; + await vscode.window.showTextDocument(preview.uri, { viewColumn: column }); + return; + } + const html = htmlViewer.activeUri; + if (html) await vscode.commands.executeCommand('vscode.openWith', html, 'default'); + }), + + vscode.commands.registerCommand('archify.renderToHtml', async (arg?: unknown) => { + const uri = sourceUri(arg); + if (!uri) return; + const text = await readText(uri); + const source = readDiagramSource(text, uri.path); + if (!source) { + vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} is not an Archify diagram source.`); + return; + } + const meta = (source.diagram.meta ?? {}) as { output?: unknown }; + const authored = typeof meta.output === 'string' && !/[\\/]/.test(meta.output) ? meta.output : undefined; + const fileName = authored ?? `${path.basename(uri.path).replace(/\.json$/i, '')}.html`; + const defaultUri = uri.scheme === 'file' ? vscode.Uri.joinPath(uri, '..', fileName) : undefined; + const target = await vscode.window.showSaveDialog({ + defaultUri, + filters: { HTML: ['html'] }, + saveLabel: 'Render', + }); + if (!target) return; + + const runtime = await runtimeFor(context, uri); + let result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: 'Archify: rendering…' }, + () => renderToFile(runtime, { ...source, text }, target.fsPath), + ); + if (!result.ok && isEvidenceError(result.error)) { + const choice = await vscode.window.showWarningMessage( + 'Archify could not verify the diagram\'s source evidence against a local checkout.', + { modal: true, detail: result.error }, + 'Render Without Source Links', + ); + if (choice) result = await renderToFile(runtime, { ...source, text }, target.fsPath, true); + else return; + } + if (!result.ok) { + log().error(result.error ?? 'render failed'); + vscode.window.showErrorMessage('Archify: render failed.', { modal: true, detail: result.error }); + return; + } + const choice = await vscode.window.showInformationMessage( + `Rendered ${path.basename(target.fsPath)}`, + 'Open in Viewer', + 'Open in Browser', + ); + if (choice === 'Open in Viewer') await openHtmlViewer(target); + if (choice === 'Open in Browser') await vscode.env.openExternal(target); + }), + + vscode.commands.registerCommand('archify.openInBrowser', async (arg?: unknown) => { + const uri = sourceUri(arg); + if (!uri) return; + if (isHtmlUri(uri)) { + await vscode.env.openExternal(uri); + return; + } + const text = await readText(uri); + const source = readDiagramSource(text, uri.path); + if (!source) { + vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} is not an Archify diagram source.`); + return; + } + const runtime = await runtimeFor(context, uri); + const result = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: 'Archify: rendering…' }, + () => renderDiagram(runtime, { ...source, text }, evidenceMode(uri)), + ); + if (!result.ok || !result.html) { + vscode.window.showErrorMessage('Archify: render failed.', { modal: true, detail: result.error }); + return; + } + const dir = path.join(context.globalStorageUri.fsPath, 'browser'); + await fs.mkdir(dir, { recursive: true }); + const file = path.join(dir, `${path.basename(uri.path).replace(/\.json$/i, '')}.html`); + await fs.writeFile(file, result.html, 'utf8'); + await vscode.env.openExternal(vscode.Uri.file(file)); + }), + ); +} + +export function deactivate(): void {} diff --git a/src/htmlViewer.ts b/src/htmlViewer.ts new file mode 100644 index 0000000..117f87c --- /dev/null +++ b/src/htmlViewer.ts @@ -0,0 +1,116 @@ +import path from 'node:path'; +import * as vscode from 'vscode'; +import { isArchifyHtml } from './detect'; +import { viewerTheme } from './runtime'; +import { handleWebviewMessage, WebviewMessage } from './webviewMessages'; +import { makeNonce, messagePage, prepareDiagramHtml } from './webviewHtml'; + +export const HTML_VIEW_TYPE = 'archify.htmlViewer'; + +class HtmlDocument implements vscode.CustomDocument { + constructor(readonly uri: vscode.Uri) {} + dispose(): void {} +} + +/** Read-only custom editor that shows rendered Archify HTML files. */ +export class HtmlViewerProvider implements vscode.CustomReadonlyEditorProvider, vscode.Disposable { + private readonly viewers = new Map Promise>(); + private active: { uri: vscode.Uri; panel: vscode.WebviewPanel } | undefined; + private readonly disposables: vscode.Disposable[]; + + constructor() { + this.disposables = [ + vscode.window.registerCustomEditorProvider(HTML_VIEW_TYPE, this, { + webviewOptions: { retainContextWhenHidden: true, enableFindWidget: true }, + supportsMultipleEditorsPerDocument: true, + }), + vscode.window.onDidChangeActiveColorTheme(() => this.reloadAll()), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('archify.theme')) this.reloadAll(); + }), + ]; + } + + /** File shown in the focused Archify HTML viewer, if any. */ + get activeUri(): vscode.Uri | undefined { + return this.active?.uri; + } + + dispose(): void { + for (const disposable of this.disposables.splice(0)) disposable.dispose(); + } + + openCustomDocument(uri: vscode.Uri): HtmlDocument { + return new HtmlDocument(uri); + } + + async resolveCustomEditor(document: HtmlDocument, panel: vscode.WebviewPanel): Promise { + const uri = document.uri; + let hash: string | undefined; + panel.webview.options = { enableScripts: true, localResourceRoots: [] }; + + const reload = async () => { + let text: string; + try { + text = new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(uri)); + } catch (error) { + panel.webview.html = messagePage( + { cspSource: panel.webview.cspSource, nonce: makeNonce() }, + 'Cannot open file', + uri.fsPath, + error instanceof Error ? error.message : String(error), + ); + return; + } + panel.webview.html = prepareDiagramHtml(text, { + cspSource: panel.webview.cspSource, + nonce: makeNonce(), + theme: viewerTheme(), + hash, + notice: isArchifyHtml(text) + ? undefined + : { kind: 'info', text: 'This file was not generated by Archify. It is shown in a sandbox without network access.' }, + }); + }; + + this.viewers.set(panel, reload); + + const disposables: vscode.Disposable[] = [ + panel.webview.onDidReceiveMessage((message: WebviewMessage) => { + if (message.type === 'hash') { + hash = message.hash; + return; + } + void handleWebviewMessage(message, uri, panel); + }), + panel.onDidChangeViewState(() => { + if (panel.active) this.active = { uri, panel }; + else if (this.active?.panel === panel) this.active = undefined; + }), + ]; + if (uri.scheme === 'file') { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(vscode.Uri.file(path.dirname(uri.fsPath)), path.basename(uri.fsPath)), + ); + let timer: NodeJS.Timeout | undefined; + const onChange = () => { + clearTimeout(timer); + timer = setTimeout(() => void reload(), 300); + }; + disposables.push(watcher, watcher.onDidChange(onChange), watcher.onDidCreate(onChange), { + dispose: () => clearTimeout(timer), + }); + } + panel.onDidDispose(() => { + this.viewers.delete(panel); + if (this.active?.panel === panel) this.active = undefined; + for (const disposable of disposables) disposable.dispose(); + }); + if (panel.active) this.active = { uri, panel }; + await reload(); + } + + reloadAll(): void { + for (const reload of this.viewers.values()) void reload(); + } +} diff --git a/src/jsonPointer.ts b/src/jsonPointer.ts new file mode 100644 index 0000000..0dfb6a0 --- /dev/null +++ b/src/jsonPointer.ts @@ -0,0 +1,39 @@ +import { findNodeAtLocation, Node, parseTree } from 'jsonc-parser'; + +export interface OffsetRange { + start: number; + end: number; +} + +/** Splits an RFC 6901 JSON pointer into path segments. */ +export function pointerSegments(pointer: string): (string | number)[] { + if (!pointer || pointer === '/') return []; + return pointer + .replace(/^\//, '') + .split('/') + .map((raw) => raw.replace(/~1/g, '/').replace(/~0/g, '~')) + .map((segment) => (/^(0|[1-9]\d*)$/.test(segment) ? Number(segment) : segment)); +} + +/** + * Maps a JSON pointer to a text range. Objects and arrays resolve to their + * property key (or opening bracket) so a "missing property" error does not + * underline a whole block. Unresolvable paths fall back to the deepest + * ancestor that exists. + */ +export function rangeForPointer(text: string, pointer: string | undefined): OffsetRange { + const root = parseTree(text, [], { allowTrailingComma: true }); + if (!root) return { start: 0, end: 0 }; + const segments = pointerSegments(pointer ?? ''); + let node: Node | undefined; + for (let length = segments.length; length >= 0 && !node; length -= 1) { + node = findNodeAtLocation(root, segments.slice(0, length)); + } + if (!node) return { start: 0, end: 0 }; + if (node.type === 'object' || node.type === 'array') { + const key = node.parent?.type === 'property' ? node.parent.children?.[0] : undefined; + if (key) return { start: key.offset, end: key.offset + key.length }; + return { start: node.offset, end: node.offset + 1 }; + } + return { start: node.offset, end: node.offset + node.length }; +} diff --git a/src/preview.ts b/src/preview.ts new file mode 100644 index 0000000..b4ed062 --- /dev/null +++ b/src/preview.ts @@ -0,0 +1,232 @@ +import path from 'node:path'; +import * as vscode from 'vscode'; +import { readDiagramSource } from './detect'; +import { renderDiagram } from './renderer'; +import { evidenceMode, log, readText, runtimeFor, viewerTheme } from './runtime'; +import { handleWebviewMessage, WebviewMessage } from './webviewMessages'; +import { makeNonce, messagePage, prepareDiagramHtml } from './webviewHtml'; + +export const PREVIEW_VIEW_TYPE = 'archify.preview'; + +interface PreviewState { + uri: string; + hash?: string; +} + +class DiagramPreview implements vscode.Disposable { + private seq = 0; + private hash: string | undefined; + private lastHtml: string | undefined; + private rendered = false; + private timer: NodeJS.Timeout | undefined; + private readonly disposables: vscode.Disposable[] = []; + private readonly onDisposeEmitter = new vscode.EventEmitter(); + readonly onDidDispose = this.onDisposeEmitter.event; + + constructor( + private readonly context: vscode.ExtensionContext, + readonly panel: vscode.WebviewPanel, + readonly uri: vscode.Uri, + state?: PreviewState, + ) { + this.hash = state?.hash; + panel.webview.options = { enableScripts: true, localResourceRoots: [] }; + panel.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'preview.svg'); + panel.title = `Preview ${path.basename(uri.path)}`; + + this.disposables.push( + panel.onDidDispose(() => this.dispose()), + panel.webview.onDidReceiveMessage((message: WebviewMessage) => { + if (message.type === 'hash') { + this.hash = message.hash; + return; + } + void handleWebviewMessage(message, this.uri, this.panel); + }), + ); + + if (uri.scheme === 'file') { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(vscode.Uri.file(path.dirname(uri.fsPath)), path.basename(uri.fsPath)), + ); + const onDisk = () => { + // Open documents are tracked through text change events instead. + if (!vscode.workspace.textDocuments.some((doc) => doc.uri.toString() === uri.toString())) this.scheduleUpdate(); + }; + this.disposables.push(watcher, watcher.onDidChange(onDisk), watcher.onDidCreate(onDisk)); + } + + this.showMessage('Rendering…', `Rendering ${path.basename(uri.path)} with Archify.`); + void this.update(); + } + + private state(): PreviewState { + return { uri: this.uri.toString(), hash: this.hash }; + } + + private showMessage(title: string, body: string, detail?: string): void { + this.lastHtml = undefined; + this.panel.webview.html = messagePage( + { cspSource: this.panel.webview.cspSource, nonce: makeNonce(), state: this.state() }, + title, + body, + detail, + ); + } + + scheduleUpdate(): void { + const delay = vscode.workspace.getConfiguration('archify', this.uri).get('preview.debounceMs', 400); + clearTimeout(this.timer); + this.timer = setTimeout(() => void this.update(), delay); + } + + async update(force = false): Promise { + clearTimeout(this.timer); + const seq = ++this.seq; + let text: string; + try { + text = await readText(this.uri); + } catch (error) { + this.showMessage('Cannot read diagram', this.uri.fsPath, error instanceof Error ? error.message : String(error)); + return; + } + const source = readDiagramSource(text, this.uri.path); + if (!source) { + this.showMessage('Not an Archify diagram', 'The file has no "diagram_type" of architecture, workflow, sequence, dataflow or lifecycle.'); + return; + } + + let result; + try { + const runtime = await runtimeFor(this.context, this.uri); + result = await renderDiagram(runtime, { ...source, text }, evidenceMode(this.uri)); + } catch (error) { + result = { ok: false, error: error instanceof Error ? error.message : String(error) }; + } + if (seq !== this.seq) return; // a newer render superseded this one + + if (!result.ok || !result.html) { + log().warn(`Render failed for ${this.uri.fsPath}: ${result.error}`); + if (this.rendered && this.lastHtml) { + void this.panel.webview.postMessage({ + type: 'notice', + notice: { kind: 'error', text: `Render failed; showing the last successful render.\n${firstLines(result.error)}` }, + }); + } else { + this.showMessage('Archify could not render this diagram', 'Fix the problems below (also listed in the Problems panel) and save.', result.error); + } + return; + } + + const html = prepareDiagramHtml(result.html, { + cspSource: this.panel.webview.cspSource, + nonce: makeNonce(), + theme: viewerTheme(), + hash: this.hash, + state: this.state(), + notice: result.evidenceNotice ? { kind: 'warning', text: result.evidenceNotice } : undefined, + }); + // Nonces differ per render, so compare the renderer output instead. + const fingerprint = `${viewerTheme()}\u0000${result.evidenceNotice ?? ''}\u0000${result.html}`; + if (!force && this.rendered && fingerprint === this.lastHtml) { + void this.panel.webview.postMessage({ type: 'notice', notice: undefined }); + return; + } + this.lastHtml = fingerprint; + this.rendered = true; + this.panel.webview.html = html; + } + + dispose(): void { + clearTimeout(this.timer); + this.onDisposeEmitter.fire(); + this.onDisposeEmitter.dispose(); + for (const disposable of this.disposables.splice(0)) disposable.dispose(); + } +} + +function firstLines(text: string | undefined, count = 6): string { + return (text ?? '').split('\n').slice(0, count).join('\n'); +} + +export class PreviewManager implements vscode.Disposable, vscode.WebviewPanelSerializer { + private readonly previews = new Map(); + private active: DiagramPreview | undefined; + private readonly disposables: vscode.Disposable[] = []; + + constructor(private readonly context: vscode.ExtensionContext) { + this.disposables.push( + vscode.window.registerWebviewPanelSerializer(PREVIEW_VIEW_TYPE, this), + vscode.workspace.onDidChangeTextDocument((event) => { + const preview = this.previews.get(event.document.uri.toString()); + if (preview && event.contentChanges.length && this.liveUpdate(preview.uri)) preview.scheduleUpdate(); + }), + vscode.workspace.onDidSaveTextDocument((doc) => void this.previews.get(doc.uri.toString())?.update()), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('archify')) this.refreshAll(); + }), + vscode.window.onDidChangeActiveColorTheme(() => this.refreshAll()), + ); + } + + private liveUpdate(uri: vscode.Uri): boolean { + return vscode.workspace.getConfiguration('archify', uri).get('preview.liveUpdate', true); + } + + get activePreview(): DiagramPreview | undefined { + return this.active; + } + + show(uri: vscode.Uri, viewColumn: vscode.ViewColumn, preserveFocus = false): void { + const existing = this.previews.get(uri.toString()); + if (existing) { + existing.panel.reveal(viewColumn, preserveFocus); + return; + } + const panel = vscode.window.createWebviewPanel( + PREVIEW_VIEW_TYPE, + `Preview ${path.basename(uri.path)}`, + { viewColumn, preserveFocus }, + { enableScripts: true, retainContextWhenHidden: true, enableFindWidget: true, localResourceRoots: [] }, + ); + this.track(new DiagramPreview(this.context, panel, uri)); + } + + async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: PreviewState | undefined): Promise { + if (!state?.uri) { + panel.dispose(); + return; + } + this.track(new DiagramPreview(this.context, panel, vscode.Uri.parse(state.uri), state)); + } + + private track(preview: DiagramPreview): void { + const key = preview.uri.toString(); + this.previews.get(key)?.panel.dispose(); + this.previews.set(key, preview); + const onViewState = () => { + if (preview.panel.active) this.active = preview; + else if (this.active === preview) this.active = undefined; + void vscode.commands.executeCommand('setContext', 'archifyPreviewFocus', Boolean(this.active)); + }; + preview.panel.onDidChangeViewState(onViewState); + onViewState(); + preview.onDidDispose(() => { + if (this.previews.get(key) === preview) this.previews.delete(key); + if (this.active === preview) { + this.active = undefined; + void vscode.commands.executeCommand('setContext', 'archifyPreviewFocus', false); + } + }); + } + + refreshAll(force = false): void { + for (const preview of this.previews.values()) void preview.update(force); + } + + dispose(): void { + for (const preview of this.previews.values()) preview.panel.dispose(); + this.previews.clear(); + for (const disposable of this.disposables.splice(0)) disposable.dispose(); + } +} diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 0000000..64d703f --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,195 @@ +import { spawn } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { DiagramSource, DiagramType, hasSourceEvidence, stripSourceEvidence } from './detect'; + +export interface RuntimeOptions { + /** Absolute path to vendor/archify/bin/archify.mjs. */ + cliPath: string; + /** Node (or Electron) executable. */ + nodePath: string; + /** Set ELECTRON_RUN_AS_NODE so VS Code's Electron binary behaves as Node. */ + runAsNode: boolean; + /** Scratch directory for input/output files. */ + workDir: string; + quality?: string; + repoRoot?: string; + timeoutMs?: number; + log?: (line: string) => void; +} + +export interface ArchifyDiagnostic { + code?: string; + severity?: 'error' | 'warning' | 'info'; + message: string; + subject?: { path?: string; identity?: string; [key: string]: unknown }; + supportedFixes?: string[]; +} + +export interface ValidateResult { + ok: boolean; + diagnostics: ArchifyDiagnostic[]; + /** Set when the CLI did not produce parseable JSON. */ + crash?: string; +} + +export interface RenderResult { + ok: boolean; + html?: string; + error?: string; + /** Set when the diagram rendered without its source evidence. */ + evidenceNotice?: string; +} + +interface RunResult { + code: number | null; + stdout: string; + stderr: string; +} + +const EVIDENCE_ERROR_RE = /repository-evidence\/|source evidence|--repo-root|Evidence (root|repository|revision)/i; + +function run(options: RuntimeOptions, args: string[], env: Record = {}): Promise { + return new Promise((resolve, reject) => { + options.log?.(`archify ${args.join(' ')}`); + const child = spawn(options.nodePath, [options.cliPath, ...args], { + cwd: options.workDir, + env: { + ...process.env, + ...(options.runAsNode ? { ELECTRON_RUN_AS_NODE: '1' } : {}), + ...env, + }, + windowsHide: true, + }); + let stdout = ''; + let stderr = ''; + child.stdout.setEncoding('utf8').on('data', (chunk: string) => (stdout += chunk)); + child.stderr.setEncoding('utf8').on('data', (chunk: string) => (stderr += chunk)); + const timer = setTimeout(() => child.kill(), options.timeoutMs ?? 60_000); + child.on('error', (error) => { + clearTimeout(timer); + reject(error); + }); + child.on('close', (code) => { + clearTimeout(timer); + if (stderr.trim()) options.log?.(stderr.trim()); + resolve({ code, stdout, stderr }); + }); + }); +} + +function commonArgs(options: RuntimeOptions, withRepoRoot: boolean): string[] { + const args: string[] = []; + if (options.quality) args.push('--quality', options.quality); + if (withRepoRoot && options.repoRoot) args.push('--repo-root', options.repoRoot); + return args; +} + +async function withScratch(options: RuntimeOptions, body: (dir: string) => Promise): Promise { + await fs.mkdir(options.workDir, { recursive: true }); + const dir = await fs.mkdtemp(path.join(options.workDir, 'run-')); + try { + return await body(dir); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } +} + +export async function validate(options: RuntimeOptions, type: DiagramType, text: string): Promise { + return withScratch(options, async (dir) => { + const input = path.join(dir, `diagram.${type}.json`); + await fs.writeFile(input, text, 'utf8'); + const result = await run(options, ['validate', type, input, '--json', ...commonArgs(options, true)]); + try { + const report = JSON.parse(result.stdout); + const diagnostics: ArchifyDiagnostic[] = Array.isArray(report.diagnostics) ? report.diagnostics : []; + if (!report.ok && diagnostics.length === 0 && report.error) { + diagnostics.push({ severity: 'error', message: String(report.error) }); + } + return { ok: Boolean(report.ok), diagnostics }; + } catch { + const crash = (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim(); + return { ok: false, diagnostics: [], crash }; + } + }); +} + +async function renderOnce( + options: RuntimeOptions, + type: DiagramType, + text: string, + withRepoRoot: boolean, +): Promise { + return withScratch(options, async (dir) => { + const input = path.join(dir, `diagram.${type}.json`); + const output = path.join(dir, 'diagram.html'); + await fs.writeFile(input, text, 'utf8'); + const result = await run(options, ['render', type, input, output, ...commonArgs(options, withRepoRoot)]); + if (result.code === 0) { + try { + return { ok: true, html: await fs.readFile(output, 'utf8') }; + } catch { + // fall through to the error below + } + } + const error = (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim(); + return { ok: false, error }; + }); +} + +/** + * Renders a diagram to HTML. With `evidence: 'fallback'`, a diagram whose + * source evidence cannot be verified is rendered again without it, and the + * reason is returned as `evidenceNotice`. + */ +export async function renderDiagram( + options: RuntimeOptions, + source: DiagramSource & { text: string }, + evidence: 'fallback' | 'strict', +): Promise { + const withEvidence = hasSourceEvidence(source.type, source.diagram); + if (withEvidence && evidence === 'fallback' && !options.repoRoot) { + const stripped = JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2); + const result = await renderOnce(options, source.type, stripped, false); + return { + ...result, + evidenceNotice: 'Source links are hidden: the diagram is not inside a Git checkout, so its source evidence cannot be verified. Set "archify.repoRoot" to the matching checkout.', + }; + } + const first = await renderOnce(options, source.type, source.text, withEvidence); + if (first.ok || !withEvidence || evidence !== 'fallback' || !isEvidenceError(first.error)) { + return first; + } + const stripped = JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2); + const second = await renderOnce(options, source.type, stripped, false); + if (!second.ok) return first; + const reason = (first.error ?? '').split('\n').find((line) => line.trim()) ?? 'verification failed'; + return { ...second, evidenceNotice: `Source links are hidden: ${reason.replace(/^\[[^\]]+\]\s*/, '')}` }; +} + +/** + * Renders to a file the user chose. Source evidence is verified strictly + * unless `stripEvidence` is set, in which case it is removed first. + */ +export async function renderToFile( + options: RuntimeOptions, + source: DiagramSource & { text: string }, + outputPath: string, + stripEvidence = false, +): Promise { + const text = stripEvidence + ? JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2) + : source.text; + const withEvidence = !stripEvidence && hasSourceEvidence(source.type, source.diagram); + return withScratch(options, async (dir) => { + const input = path.join(dir, `diagram.${source.type}.json`); + await fs.writeFile(input, text, 'utf8'); + const result = await run(options, ['render', source.type, input, outputPath, ...commonArgs(options, withEvidence)]); + if (result.code === 0) return { ok: true }; + return { ok: false, error: (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim() }; + }); +} + +export function isEvidenceError(error: string | undefined): boolean { + return EVIDENCE_ERROR_RE.test(error ?? ''); +} diff --git a/src/runtime.ts b/src/runtime.ts new file mode 100644 index 0000000..b5286ac --- /dev/null +++ b/src/runtime.ts @@ -0,0 +1,77 @@ +import { execFile } from 'node:child_process'; +import path from 'node:path'; +import * as vscode from 'vscode'; +import { RuntimeOptions } from './renderer'; +import { ViewerTheme } from './webviewHtml'; + +let output: vscode.LogOutputChannel | undefined; + +export function log(): vscode.LogOutputChannel { + output ??= vscode.window.createOutputChannel('Archify', { log: true }); + return output; +} + +const gitRoots = new Map>(); + +function gitTopLevel(dir: string): Promise { + let pending = gitRoots.get(dir); + if (!pending) { + pending = new Promise((resolve) => { + execFile('git', ['rev-parse', '--show-toplevel'], { cwd: dir, timeout: 10_000 }, (error, stdout) => { + resolve(error ? undefined : path.resolve(stdout.trim())); + }); + }); + gitRoots.set(dir, pending); + } + return pending; +} + +export function clearGitRootCache(): void { + gitRoots.clear(); +} + +/** The checkout used to verify source evidence for a file, if any. */ +export async function repoRootFor(uri: vscode.Uri | undefined): Promise { + const configured = vscode.workspace.getConfiguration('archify', uri).get('repoRoot', '').trim(); + if (configured) { + if (path.isAbsolute(configured)) return configured; + const folder = (uri && vscode.workspace.getWorkspaceFolder(uri)) ?? vscode.workspace.workspaceFolders?.[0]; + return folder ? path.resolve(folder.uri.fsPath, configured) : path.resolve(configured); + } + if (!uri || uri.scheme !== 'file') return undefined; + return gitTopLevel(path.dirname(uri.fsPath)); +} + +export async function runtimeFor(context: vscode.ExtensionContext, uri: vscode.Uri | undefined): Promise { + const config = vscode.workspace.getConfiguration('archify', uri); + const nodePath = config.get('nodePath', '').trim(); + const quality = config.get('quality', ''); + return { + cliPath: context.asAbsolutePath(path.join('vendor', 'archify', 'bin', 'archify.mjs')), + nodePath: nodePath || process.execPath, + runAsNode: !nodePath, + workDir: path.join(context.globalStorageUri.fsPath, 'renders'), + quality: quality || undefined, + repoRoot: await repoRootFor(uri), + log: (line) => log().debug(line), + }; +} + +export function evidenceMode(uri: vscode.Uri | undefined): 'fallback' | 'strict' { + return vscode.workspace.getConfiguration('archify', uri).get<'fallback' | 'strict'>('sourceEvidence', 'fallback'); +} + +export function viewerTheme(): ViewerTheme { + const setting = vscode.workspace.getConfiguration('archify').get('theme', 'vscode'); + if (setting === 'dark' || setting === 'light') return setting; + if (setting === 'diagram') return undefined; + const kind = vscode.window.activeColorTheme.kind; + return kind === vscode.ColorThemeKind.Light || kind === vscode.ColorThemeKind.HighContrastLight ? 'light' : 'dark'; +} + +/** Current text of a document: the open editor buffer, else the file on disk. */ +export async function readText(uri: vscode.Uri): Promise { + const open = vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === uri.toString()); + if (open) return open.getText(); + return new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(uri)); +} diff --git a/src/webviewHtml.ts b/src/webviewHtml.ts new file mode 100644 index 0000000..4e70428 --- /dev/null +++ b/src/webviewHtml.ts @@ -0,0 +1,231 @@ +export type ViewerTheme = 'dark' | 'light' | undefined; + +export interface PageOptions { + /** webview.cspSource */ + cspSource: string; + nonce: string; + /** Forced diagram theme; undefined keeps the diagram's own choice. */ + theme: ViewerTheme; + /** Location hash to restore (Archify keeps focus/route state there). */ + hash?: string; + /** Opaque state handed to vscode.setState for panel restore. */ + state?: unknown; + /** Banner shown on top of the diagram. */ + notice?: { text: string; kind: 'info' | 'warning' | 'error' }; +} + +export function makeNonce(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let nonce = ''; + for (let i = 0; i < 32; i += 1) nonce += chars.charAt(Math.floor(Math.random() * chars.length)); + return nonce; +} + +export function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** JSON that is safe to embed inside an inline \n` + + `\n`; + + let page = html + // Drop any CSP the file brings along; ours is the one that applies. + .replace(/]*>/gi, '') + // Allow the page's own inline scripts to run under the nonce policy. + .replace(/]*\bnonce=)/gi, ` + + + +

${escapeHtml(title)}

+

${escapeHtml(body)}

+${detail ? `
${escapeHtml(detail)}
` : ''} + +`; +} diff --git a/src/webviewMessages.ts b/src/webviewMessages.ts new file mode 100644 index 0000000..d81aaa9 --- /dev/null +++ b/src/webviewMessages.ts @@ -0,0 +1,100 @@ +import path from 'node:path'; +import * as vscode from 'vscode'; +import { log, repoRootFor } from './runtime'; + +export type WebviewMessage = + | { type: 'hash'; hash: string } + | { type: 'save'; filename: string; base64: string } + | { type: 'openExternal'; href: string } + | { type: 'openRelative'; href: string } + | { type: 'openSource'; path: string; line?: number; endLine?: number; href?: string } + | { type: 'error'; message: string }; + +/** Column for opening files next to a webview without covering it. */ +function besideColumn(panel: vscode.WebviewPanel): vscode.ViewColumn { + return panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One; +} + +async function exists(uri: vscode.Uri): Promise { + try { + await vscode.workspace.fs.stat(uri); + return true; + } catch { + return false; + } +} + +/** + * Opens a diagram's source evidence in the editor. The path is relative to + * the repository root; the diagram's checkout and the workspace folders are + * tried in turn before falling back to the web link. + */ +async function openSource( + message: Extract, + context: vscode.Uri, + panel: vscode.WebviewPanel, +): Promise { + const relative = message.path.replace(/^\/+/, ''); + const roots = new Set(); + const repoRoot = await repoRootFor(context); + if (repoRoot) roots.add(repoRoot); + for (const folder of vscode.workspace.workspaceFolders ?? []) roots.add(folder.uri.fsPath); + + for (const root of roots) { + const candidate = vscode.Uri.file(path.join(root, relative)); + if (relative && (await exists(candidate))) { + const start = Math.max((message.line ?? 1) - 1, 0); + const end = Math.max((message.endLine ?? message.line ?? 1) - 1, start); + const selection = new vscode.Range(start, 0, end, Number.MAX_SAFE_INTEGER); + await vscode.window.showTextDocument(candidate, { viewColumn: besideColumn(panel), selection }); + return; + } + } + if (message.href && /^https?:/i.test(message.href)) { + await vscode.env.openExternal(vscode.Uri.parse(message.href)); + return; + } + vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`); +} + +export async function handleWebviewMessage( + message: WebviewMessage, + context: vscode.Uri, + panel: vscode.WebviewPanel, +): Promise { + try { + switch (message.type) { + case 'openExternal': + await vscode.env.openExternal(vscode.Uri.parse(message.href)); + break; + case 'openRelative': { + const [file] = message.href.split('#'); + if (!file) break; + const target = vscode.Uri.joinPath(context, '..', decodeURIComponent(file)); + await vscode.commands.executeCommand('vscode.open', target, besideColumn(panel)); + break; + } + case 'openSource': + await openSource(message, context, panel); + break; + case 'save': { + const name = path.basename(message.filename || 'archify-export'); + const defaultUri = vscode.Uri.joinPath(context, '..', name); + const target = await vscode.window.showSaveDialog({ defaultUri, saveLabel: 'Export' }); + if (!target) break; + await vscode.workspace.fs.writeFile(target, Buffer.from(message.base64, 'base64')); + const choice = await vscode.window.showInformationMessage(`Exported ${path.basename(target.fsPath)}`, 'Reveal'); + if (choice === 'Reveal') await vscode.commands.executeCommand('revealFileInOS', target); + break; + } + case 'error': + vscode.window.showErrorMessage(`Archify: ${message.message}`); + break; + case 'hash': + break; + } + } catch (error) { + log().error(error instanceof Error ? error : String(error)); + vscode.window.showErrorMessage(`Archify: ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/test/detect.test.ts b/test/detect.test.ts new file mode 100644 index 0000000..97f8127 --- /dev/null +++ b/test/detect.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { hasSourceEvidence, isArchifyHtml, readDiagramSource, stripSourceEvidence, typeFromFilename } from '../src/detect'; +import { pointerSegments, rangeForPointer } from '../src/jsonPointer'; + +test('reads diagram_type from content', () => { + const source = readDiagramSource('{"schema_version":1,"diagram_type":"sequence","meta":{}}', 'plain.json'); + assert.equal(source?.type, 'sequence'); +}); + +test('falls back to the file name for unparseable sources', () => { + assert.equal(readDiagramSource('{"diagram_type": ', 'x.workflow.json')?.type, 'workflow'); + assert.equal(readDiagramSource('{"name":"pkg"}', 'package.json'), undefined); + assert.equal(typeFromFilename('/a/b/Web.Architecture.json'), 'architecture'); +}); + +test('recognizes Archify HTML by its generator meta', () => { + assert.ok(isArchifyHtml('')); + assert.ok(!isArchifyHtml('')); +}); + +test('strips source evidence without touching the original', () => { + const diagram = { + meta: { title: 't', repository: { url: 'https://github.com/a/b', revision: 'x' } }, + components: [{ id: 'a', sources: [{ path: 'src/a.ts' }] }, { id: 'b' }], + }; + assert.ok(hasSourceEvidence('architecture', diagram)); + const stripped = stripSourceEvidence('architecture', diagram); + assert.ok(!hasSourceEvidence('architecture', stripped)); + assert.ok(diagram.meta.repository, 'original keeps its repository'); + assert.deepEqual((stripped.components as unknown[])[1], { id: 'b' }); +}); + +test('json pointer segments decode escapes and indexes', () => { + assert.deepEqual(pointerSegments('/components/0/a~1b~0c'), ['components', 0, 'a/b~c']); + assert.deepEqual(pointerSegments(''), []); +}); + +test('json pointer ranges point at keys for containers and values for leaves', () => { + const text = '{\n "meta": { "title": "x" },\n "components": [ { "id": "a" } ]\n}'; + const meta = rangeForPointer(text, '/meta'); + assert.equal(text.slice(meta.start, meta.end), '"meta"'); + const title = rangeForPointer(text, '/meta/title'); + assert.equal(text.slice(title.start, title.end), '"x"'); + const item = rangeForPointer(text, '/components/0'); + assert.equal(text.slice(item.start, item.end), '{'); + const missing = rangeForPointer(text, '/components/0/label'); + assert.equal(text.slice(missing.start, missing.end), '{', 'falls back to the nearest existing ancestor'); +}); diff --git a/test/fixtures/agent-run.lifecycle.json b/test/fixtures/agent-run.lifecycle.json new file mode 100644 index 0000000..abf7360 --- /dev/null +++ b/test/fixtures/agent-run.lifecycle.json @@ -0,0 +1,55 @@ +{ + "schema_version": 1, + "diagram_type": "lifecycle", + "meta": { + "title": "Agent Run Lifecycle", + "output": "examples/lifecycle-agent-run.html", + "viewBox": [1030, 630], + "animation": "trace", + "quality_profile": "showcase" + }, + "lanes": [ + { "id": "main", "label": "Lifecycle phases" }, + { "id": "waiting", "label": "Interruptions" }, + { "id": "exceptions", "label": "Recovery loop" }, + { "id": "terminal", "label": "Terminal exits" } + ], + "states": [ + { "id": "queued", "type": "start", "label": "Queued", "sublabel": "request accepted", "lane": "main", "col": 0, "step": "01", "tag": "entry" }, + { "id": "planning", "type": "active", "label": "Planning", "sublabel": "build task graph", "lane": "main", "col": 1, "step": "02", "tag": "model" }, + { "id": "executing", "type": "active", "label": "Executing", "sublabel": "tool calls", "lane": "main", "col": 2, "step": "03", "tag": "work" }, + { "id": "reviewing", "type": "decision", "label": "Reviewing", "sublabel": "quality gate", "lane": "main", "col": 3, "step": "04", "tag": "check" }, + { "id": "completed", "type": "success", "label": "Completed", "sublabel": "final response", "lane": "main", "col": 4, "step": "05", "tag": "done" }, + { "id": "approval", "type": "waiting", "label": "Needs Approval", "sublabel": "human gate", "lane": "waiting", "col": 0, "tag": "pause" }, + { "id": "blocked", "type": "waiting", "label": "Blocked", "sublabel": "missing input", "lane": "waiting", "col": 1, "tag": "wait" }, + { "id": "failed", "type": "failure", "label": "Failed", "sublabel": "recoverable error", "lane": "exceptions", "col": 0, "yOffset": 78, "tag": "retryable" }, + { "id": "cancelled", "type": "failure", "label": "Cancelled", "sublabel": "user stopped", "lane": "terminal", "col": 0, "tag": "terminal" }, + { "id": "expired", "type": "failure", "label": "Expired", "sublabel": "timeout", "lane": "terminal", "col": 1, "tag": "terminal" } + ], + "transitions": [ + { "id": "approval-needed", "from": "executing", "to": "approval", "variant": "security", "fromSide": "bottom", "toSide": "top", "route": "straight" }, + { "id": "review-blocked", "from": "reviewing", "to": "blocked", "variant": "default", "route": "drop" }, + { "id": "execution-failed", "from": "executing", "to": "failed", "variant": "security", "fromSide": "left", "toSide": "left", "via": [[320, 157], [320, 385]] }, + { "id": "failed-retry", "from": "failed", "to": "executing", "variant": "emphasis", "fromSide": "left", "toSide": "top", "via": [[20, 385], [20, 80], [402, 80]] }, + { "id": "block-expired", "from": "blocked", "to": "expired", "variant": "security", "fromSide": "bottom", "toSide": "top", "route": "straight" }, + { "id": "approval-cancelled", "from": "approval", "to": "cancelled", "variant": "security", "fromSide": "bottom", "toSide": "top", "via": [[480, 336], [480, 432], [402, 432]] } + ], + "cards": [ + { + "dot": "emerald", + "title": "Main Path + Waits", + "items": [ + "The run has five ordered phases from queue to completion", + "Approval and missing input pause the run without ending it" + ] + }, + { + "dot": "rose", + "title": "Recovery + Terminal Exits", + "items": [ + "Failed loops back while retry budget remains", + "Cancelled and Expired are terminal exits with no return path" + ] + } + ] +} diff --git a/test/fixtures/agent-tool-call.workflow.json b/test/fixtures/agent-tool-call.workflow.json new file mode 100644 index 0000000..a462549 --- /dev/null +++ b/test/fixtures/agent-tool-call.workflow.json @@ -0,0 +1,74 @@ +{ + "schema_version": 2, + "diagram_type": "workflow", + "meta": { + "title": "Agent Tool Call Workflow", + "animation": "trace", + "visual_preset": "signal-flow", + "quality_profile": "showcase", + "output": "examples/workflow-agent-tool-call-rendered.html" + }, + "lanes": [ + { "id": "ui", "label": "User Interface" }, + { "id": "agent", "label": "Agent Runtime" }, + { "id": "policy", "label": "Policy & Recovery", "variant": "exception" }, + { "id": "tools", "label": "Tool Execution & Evidence" } + ], + "phases": [ + { "id": "intake", "label": "Intake", "fromCol": 0, "toCol": 1 }, + { "id": "reasoning", "label": "Plan + route", "fromCol": 2, "toCol": 3, "variant": "emphasis" }, + { "id": "execution", "label": "Execute + report", "fromCol": 4, "toCol": 5, "variant": "dashed" } + ], + "groups": [ + { "id": "agent_loop", "label": "Planning loop", "lane": "agent", "fromCol": 2, "toCol": 3, "variant": "emphasis" }, + { "id": "exception_path", "label": "Human or policy stop", "lane": "policy", "fromCol": 3, "toCol": 5, "variant": "security" }, + { "id": "evidence_path", "label": "Evidence path", "lane": "tools", "fromCol": 1, "toCol": 2, "variant": "dashed" }, + { "id": "tool_work", "label": "Tool work", "lane": "tools", "fromCol": 4, "toCol": 5, "variant": "dashed" } + ], + "mainPath": ["user", "chat", "planner", "router", "approval", "tool", "external", "final"], + "nodes": [ + { "id": "user", "lane": "ui", "col": 0, "type": "external", "label": "User", "sublabel": "asks for work", "width": 132 }, + { "id": "chat", "lane": "ui", "col": 1, "type": "frontend", "label": "Chat Surface", "sublabel": "thread + files", "width": 132 }, + { "id": "final", "lane": "ui", "col": 5, "type": "backend", "label": "Final Reply", "sublabel": "answer + changes", "width": 132 }, + { "id": "planner", "lane": "agent", "col": 2, "type": "backend", "label": "Agent Planner", "sublabel": "plan next step", "tag": "context aware", "width": 132 }, + { "id": "router", "lane": "agent", "col": 3, "type": "backend", "label": "Tool Router", "sublabel": "choose capability", "width": 132 }, + { "id": "approval", "lane": "policy", "col": 3, "type": "security", "label": "Approval Gate", "sublabel": "scope + consent", "tag": "block risky ops", "width": 132 }, + { "id": "blocked", "lane": "policy", "col": 4, "type": "security", "label": "Blocked", "sublabel": "wait or reject", "width": 132 }, + { "id": "retry", "lane": "policy", "col": 5, "type": "messagebus", "label": "Retry Path", "sublabel": "revise request", "width": 132 }, + { "id": "tool", "lane": "tools", "col": 4, "type": "messagebus", "label": "Tool Call", "sublabel": "shell / browser / MCP", "tag": "structured result", "width": 132 }, + { "id": "external", "lane": "tools", "col": 5, "type": "cloud", "label": "External API", "sublabel": "network service", "width": 132 }, + { "id": "store", "lane": "tools", "col": 1, "type": "database", "label": "Context Store", "sublabel": "repo + memory", "width": 132 }, + { "id": "trace", "lane": "tools", "col": 2, "type": "database", "label": "Trace Log", "sublabel": "events + output", "width": 132 } + ], + "edges": [ + { "id": "request-chat", "from": "user", "to": "chat", "variant": "default" }, + { "id": "plan-request", "from": "chat", "to": "planner", "label": "plan", "variant": "emphasis" }, + { "id": "planner-route", "from": "planner", "to": "router", "variant": "default" }, + { "id": "approval-check", "from": "router", "to": "approval", "label": "needs approval?", "variant": "security" }, + { "id": "approved-tool", "from": "approval", "to": "tool", "variant": "emphasis" }, + { "id": "approval-denied", "from": "approval", "to": "blocked", "label": "denied", "variant": "security", "role": "error" }, + { "id": "retry-request", "from": "blocked", "to": "retry", "variant": "dashed", "role": "branch" }, + { "id": "tool-external-call", "from": "tool", "to": "external", "variant": "default" }, + { "id": "external-reply", "from": "external", "to": "final", "variant": "emphasis", "role": "return", "fromSide": "right", "toSide": "right", "route": "outside-right", "width": 1.2 }, + { "id": "record-result", "from": "external", "to": "trace", "label": "record result", "variant": "dashed", "fromSide": "bottom", "toSide": "bottom", "route": "bottom-channel", "labelSegment": 1 }, + { "id": "write-trace-memory", "from": "store", "to": "trace", "label": "trace + memory", "variant": "dashed" } + ], + "cards": [ + { + "dot": "cyan", + "title": "Compiler Contract", + "items": [ + "Lanes and columns determine node placement", + "Labels reserve clearance; routes stay orthogonal" + ] + }, + { + "dot": "rose", + "title": "Runtime Semantics", + "items": [ + "Approval gates risky work before tool execution", + "Evidence returns through isolated trace and memory" + ] + } + ] +} diff --git a/test/fixtures/cache-miss-request.sequence.json b/test/fixtures/cache-miss-request.sequence.json new file mode 100644 index 0000000..4ef2fab --- /dev/null +++ b/test/fixtures/cache-miss-request.sequence.json @@ -0,0 +1,77 @@ +{ + "schema_version": 1, + "diagram_type": "sequence", + "meta": { + "title": "Cache Miss Request Sequence", + "output": "examples/sequence-cache-miss-request.html", + "viewBox": [1080, 580], + "column_fit": "spread", + "animation": "trace", + "quality_profile": "showcase" + }, + "participants": [ + { "id": "user", "type": "external", "label": "User", "sublabel": "browser session" }, + { "id": "web", "type": "frontend", "label": "Web App", "sublabel": "React UI" }, + { "id": "api", "type": "backend", "label": "API", "sublabel": "request handler" }, + { "id": "auth", "type": "security", "label": "Auth", "sublabel": "JWT verify" }, + { "id": "redis", "type": "database", "label": "Redis", "sublabel": "cache" }, + { "id": "db", "type": "database", "label": "Postgres", "sublabel": "source of truth" }, + { "id": "trace", "type": "messagebus", "label": "Trace", "sublabel": "async event" } + ], + "segments": [ + { "from": 150, "to": 250, "label": "Request" }, + { "from": 260, "to": 370, "label": "Fallback" }, + { "from": 380, "to": 480, "label": "Response + trace" } + ], + "messages": [ + { "id": "open-page", "from": "user", "to": "web", "y": 160, "label": "open page", "variant": "default" }, + { "id": "dashboard-request", "from": "web", "to": "api", "y": 185, "label": "GET /dashboard", "variant": "emphasis" }, + { "id": "verify-jwt", "from": "api", "to": "auth", "y": 210, "label": "verify JWT", "variant": "security" }, + { "id": "auth-claims", "from": "auth", "to": "api", "y": 238, "label": "claims ok", "variant": "return" }, + { "id": "cache-read", "from": "api", "to": "redis", "y": 270, "label": "read cache", "variant": "default" }, + { "id": "cache-miss", "from": "redis", "to": "api", "y": 298, "label": "miss", "variant": "return" }, + { "id": "profile-query", "from": "api", "to": "db", "y": 330, "label": "query profile + metrics", "variant": "emphasis" }, + { "id": "profile-rows", "from": "db", "to": "api", "y": 358, "label": "rows", "variant": "return" }, + { "id": "cache-write", "from": "api", "to": "redis", "y": 390, "label": "set cache", "variant": "dashed" }, + { "id": "trace-emit", "from": "api", "to": "trace", "y": 418, "label": "emit trace", "variant": "dashed" }, + { "id": "dashboard-response", "from": "api", "to": "web", "y": 443, "label": "200 JSON", "variant": "return" }, + { "id": "page-render", "from": "web", "to": "user", "y": 468, "label": "render", "variant": "return" } + ], + "activations": [ + { "participant": "web", "from": 180, "to": 474, "type": "frontend" }, + { "participant": "api", "from": 185, "to": 450, "type": "backend" }, + { "participant": "auth", "from": 205, "to": 244, "type": "security" }, + { "participant": "redis", "from": 265, "to": 304, "type": "database" }, + { "participant": "db", "from": 325, "to": 364, "type": "database" }, + { "participant": "trace", "from": 413, "to": 449, "type": "messagebus" } + ], + "cards": [ + { + "dot": "emerald", + "title": "Happy Path", + "items": [ + "The main request is Web App -> API -> data source -> response", + "Return messages are quieter than forward calls", + "Activation bars make ownership duration visible" + ] + }, + { + "dot": "rose", + "title": "Policy + Fallback", + "items": [ + "JWT verification is colored as a security interaction", + "Cache miss is visible without overpowering the main path", + "Database access only appears after cache fallback" + ] + }, + { + "dot": "orange", + "title": "Async Trace", + "items": [ + "Trace emission is dashed and secondary", + "It does not block the response path", + "The diagram separates user-facing latency from observability" + ] + } + ] +} diff --git a/test/fixtures/event-stream.dataflow.json b/test/fixtures/event-stream.dataflow.json new file mode 100644 index 0000000..3f653da --- /dev/null +++ b/test/fixtures/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/test/fixtures/mco-runtime.architecture.json b/test/fixtures/mco-runtime.architecture.json new file mode 100644 index 0000000..20d9f95 --- /dev/null +++ b/test/fixtures/mco-runtime.architecture.json @@ -0,0 +1,293 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "MCO Runtime Architecture", + "subtitle": "mco-org/mco · explicit provider policy · parallel invocation · durable raw answers", + "output": "docs/cases/mco-runtime.architecture.html", + "animation": "trace", + "visual_preset": "signal-flow", + "quality_profile": "showcase", + "repository": { + "url": "https://github.com/mco-org/mco", + "revision": "9f1a1cf1afdc04d7b5406782b40dfec76d9bc798" + } + }, + "components": [ + { + "id": "callers", + "type": "external", + "label": "Callers", + "sublabel": "human · coding agent", + "pos": [5, 250], + "size": [130, 60] + }, + { + "id": "entry", + "type": "frontend", + "label": "Entry Surfaces", + "sublabel": "npm shim · Python CLI · MCP", + "pos": [175, 250], + "size": [160, 60], + "sources": [ + { "path": "bin/mco.js", "line": 1, "label": "npm entry" }, + { "path": "runtime/mcp_server.py", "line": 108, "end_line": 153, "label": "MCP entry" } + ] + }, + { + "id": "router", + "type": "backend", + "label": "Command Router", + "sublabel": "run · review · doctor · session", + "pos": [360, 250], + "size": [180, 60], + "sources": [ + { "path": "runtime/cli.py", "line": 1945, "label": "CLI main" } + ] + }, + { + "id": "config", + "type": "database", + "label": "Config + Selection", + "sublabel": "files · explicit provider team", + "pos": [565, 110], + "size": [175, 60], + "tag": "no silent team", + "sources": [ + { "path": "runtime/config.py", "line": 24, "label": "Review config" } + ] + }, + { + "id": "policy", + "type": "security", + "label": "Execution Policy", + "sublabel": "read_only · write · yolo", + "pos": [565, 250], + "size": [175, 60], + "tag": "explicit boundary", + "sources": [ + { "path": "runtime/policy.py", "line": 34, "label": "Provider policy" } + ] + }, + { + "id": "workflow", + "type": "backend", + "label": "Invocation Workflow", + "sublabel": "parallel · chain · debate · synthesize", + "pos": [765, 250], + "size": [220, 60], + "sources": [ + { "path": "runtime/invocation_runtime.py", "line": 696, "label": "Workflow entry" }, + { "path": "runtime/invocation_runtime.py", "line": 156, "label": "Single invocation" } + ] + }, + { + "id": "adapters", + "type": "messagebus", + "label": "Provider Adapters", + "sublabel": "shim / ACP · detect · run · poll · cancel", + "pos": [1010, 250], + "size": [230, 60], + "sources": [ + { "path": "runtime/adapters/__init__.py", "line": 57, "label": "Adapter registry" }, + { "path": "runtime/contracts.py", "line": 77, "label": "Provider contract" } + ] + }, + { + "id": "provider_clis", + "type": "external", + "label": "Provider CLIs", + "sublabel": "Claude · Codex · Pi · 7 more", + "pos": [1035, 0], + "size": [180, 60] + }, + { + "id": "answer_transport", + "type": "messagebus", + "label": "Answer Transport", + "sublabel": "plain · JSON · JSONL · ACP events", + "pos": [765, 420], + "size": [190, 60], + "sources": [ + { "path": "runtime/answer_transport.py", "line": 79, "label": "Transport decoders" } + ] + }, + { + "id": "artifacts", + "type": "cloud", + "label": "Invocation Artifacts", + "sublabel": "answers · result.md · run.json", + "pos": [1010, 420], + "size": [170, 60], + "tag": "raw evidence", + "sources": [ + { "path": "runtime/invocation_artifacts.py", "line": 8, "label": "Artifact writer" } + ] + }, + { + "id": "consumers", + "type": "external", + "label": "Output Consumers", + "sublabel": "terminal · calling agent · CI", + "pos": [1005, 520], + "size": [180, 60] + }, + { + "id": "session_daemon", + "type": "backend", + "label": "Session Daemon", + "sublabel": "detached worker · Unix socket", + "pos": [365, 420], + "size": [170, 60], + "sources": [ + { "path": "runtime/session/daemon.py", "line": 50, "label": "Daemon context" } + ] + }, + { + "id": "session_state", + "type": "database", + "label": "Session State", + "sublabel": "state.json · history.jsonl", + "pos": [565, 420], + "size": [170, 60], + "tag": ".mco/sessions", + "sources": [ + { "path": "runtime/session/state.py", "line": 25, "label": "Session state" } + ] + } + ], + "boundaries": [ + { + "kind": "region", + "label": "mco local package / Python runtime", + "wraps": ["entry", "router", "config", "policy", "workflow", "adapters", "answer_transport", "artifacts", "session_daemon", "session_state"] + }, + { + "kind": "security-group", + "label": "explicit execution-policy boundary", + "wraps": ["policy", "workflow", "adapters"] + } + ], + "connections": [ + { + "id": "caller-entry", + "from": "callers", + "to": "entry", + "variant": "emphasis" + }, + { + "id": "entry-route", + "from": "entry", + "to": "router", + "variant": "emphasis" + }, + { + "id": "route-policy", + "from": "router", + "to": "policy", + "variant": "security" + }, + { + "id": "config-policy", + "from": "config", + "to": "policy", + "variant": "security", + "fromSide": "bottom", + "toSide": "top" + }, + { + "id": "policy-workflow", + "from": "policy", + "to": "workflow", + "variant": "emphasis" + }, + { + "id": "workflow-adapters", + "from": "workflow", + "to": "adapters", + "variant": "emphasis" + }, + { + "id": "adapters-providers", + "from": "adapters", + "to": "provider_clis", + "label": "native CLI / ACP", + "variant": "emphasis", + "fromSide": "top", + "toSide": "bottom" + }, + { + "id": "adapters-answer", + "from": "adapters", + "to": "answer_transport", + "label": "decode events", + "labelAt": [995, 365], + "fromSide": "bottom", + "toSide": "top" + }, + { + "id": "answer-artifacts", + "from": "answer_transport", + "to": "artifacts", + "variant": "emphasis" + }, + { + "id": "artifacts-consumers", + "from": "artifacts", + "to": "consumers", + "label": "raw answers", + "labelAt": [1095, 510], + "variant": "emphasis", + "fromSide": "bottom", + "toSide": "top" + }, + { + "id": "router-session", + "from": "router", + "to": "session_daemon", + "label": "start · send · resume", + "labelAt": [450, 365], + "variant": "dashed", + "fromSide": "bottom", + "toSide": "top", + "route": "straight" + }, + { + "id": "session-state", + "from": "session_daemon", + "to": "session_state", + "label": "persist", + "labelAt": [550, 406], + "variant": "dashed" + } + ], + "cards": [ + { + "dot": "cyan", + "title": "Source-backed dispatch", + "items": [ + "runtime/cli.py requires an explicit provider or named agent team", + "runtime/policy.py validates models, context, permissions, and risk", + "runtime/invocation_runtime.py owns parallelism, timeouts, and cancellation" + ] + }, + { + "dot": "emerald", + "title": "Raw-answer contract", + "items": [ + "Adapters keep each provider CLI behind one shared runtime contract", + "answer_transport.py decodes output without inventing findings or consensus", + "Artifacts persist answers and run metadata for terminal, agents, or CI" + ] + }, + { + "dot": "violet", + "title": "Real repository proof", + "items": [ + "Repository: github.com/mco-org/mco", + "Source snapshot: main @ 9f1a1cf", + "Evidence read: README, CLI, policy, adapters, invocation runtime, sessions" + ] + } + ] +} diff --git a/test/fixtures/production-deployment.architecture.json b/test/fixtures/production-deployment.architecture.json new file mode 100644 index 0000000..bbcf9bd --- /dev/null +++ b/test/fixtures/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/test/integration/run.mjs b/test/integration/run.mjs new file mode 100644 index 0000000..87830ad --- /dev/null +++ b/test/integration/run.mjs @@ -0,0 +1,37 @@ +// Runs the integration suite inside a real VS Code instance with a throwaway +// profile. Uses the locally installed VS Code when VSCODE_PATH is set or the +// default macOS location exists; otherwise downloads one. +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { runTests } from '@vscode/test-electron'; + +// When launched from a VS Code terminal this is inherited and would make the +// test instance start as plain Node. +delete process.env.ELECTRON_RUN_AS_NODE; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const macApp = '/Applications/Visual Studio Code.app/Contents/MacOS/Code'; +const vscodeExecutablePath = process.env.VSCODE_PATH || (fs.existsSync(macApp) ? macApp : undefined); +const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-ext-it-')); +const workspace = path.join(scratch, 'workspace'); +fs.cpSync(path.join(root, 'test', 'fixtures'), workspace, { recursive: true }); + +try { + await runTests({ + vscodeExecutablePath, + extensionDevelopmentPath: root, + extensionTestsPath: path.join(root, 'out', 'test', 'integration', 'suite.js'), + launchArgs: [ + workspace, + '--disable-extensions', + '--disable-workspace-trust', + '--skip-welcome', + '--skip-release-notes', + `--user-data-dir=${path.join(scratch, 'user-data')}`, + ], + }); +} finally { + fs.rmSync(scratch, { recursive: true, force: true }); +} diff --git a/test/integration/suite.ts b/test/integration/suite.ts new file mode 100644 index 0000000..6dbec80 --- /dev/null +++ b/test/integration/suite.ts @@ -0,0 +1,121 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import * as vscode from 'vscode'; + +const EXTENSION_ID = 'vscode-extensions.archify-vscode-ext'; + +function workspaceFile(name: string): vscode.Uri { + const folder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(folder, 'a workspace folder is open'); + return vscode.Uri.joinPath(folder.uri, name); +} + +async function waitFor(what: string, probe: () => T | undefined | false, timeoutMs = 20_000): Promise { + const start = Date.now(); + for (;;) { + const value = probe(); + if (value) return value; + if (Date.now() - start > timeoutMs) throw new Error(`Timed out waiting for ${what}`); + await new Promise((resolve) => setTimeout(resolve, 200)); + } +} + +function webviewTabs(viewTypeSuffix: string): vscode.Tab[] { + return vscode.window.tabGroups.all + .flatMap((group) => group.tabs) + .filter((tab) => + (tab.input instanceof vscode.TabInputWebview && tab.input.viewType.endsWith(viewTypeSuffix)) || + (tab.input instanceof vscode.TabInputCustom && tab.input.viewType === viewTypeSuffix)); +} + +async function check(name: string, body: () => Promise): Promise { + try { + await body(); + console.log(` ✔ ${name}`); + } catch (error) { + console.log(` ✖ ${name}`); + throw error; + } +} + +export async function run(): Promise { + console.log('Archify extension integration suite'); + + await check('activates on a diagram source and sets the context key', async () => { + const doc = await vscode.workspace.openTextDocument(workspaceFile('production-deployment.architecture.json')); + await vscode.window.showTextDocument(doc); + const extension = vscode.extensions.getExtension(EXTENSION_ID); + assert.ok(extension, 'extension is installed'); + await waitFor('activation', () => extension.isActive); + }); + + await check('opens a live preview beside the source', async () => { + await vscode.commands.executeCommand('archify.showPreviewToSide'); + const tabs = await waitFor('preview tab', () => { + const found = webviewTabs('archify.preview'); + return found.length > 0 && found; + }); + assert.equal(tabs.length, 1); + assert.match(tabs[0].label, /Preview production-deployment\.architecture\.json/); + }); + + await check('reports validation problems in the Problems panel with JSON ranges', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'json', + content: JSON.stringify({ schema_version: 1, diagram_type: 'architecture', meta: { title: 'x' }, components: [{ id: 'a' }] }, null, 2), + }); + await vscode.window.showTextDocument(doc); + const diagnostics = await waitFor('archify diagnostics', () => { + const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify'); + return found.length > 0 && found; + }); + const component = diagnostics.find((d) => /components\/0/.test(d.message)); + assert.ok(component, 'a diagnostic for /components/0'); + assert.equal(doc.getText(component.range), '{', 'range points at the component object'); + assert.equal(component.severity, vscode.DiagnosticSeverity.Error); + }); + + await check('clears problems once the document is fixed', async () => { + const good = await vscode.workspace.openTextDocument(workspaceFile('event-stream.dataflow.json')); + const doc = await vscode.workspace.openTextDocument({ language: 'json', content: good.getText() }); + await vscode.window.showTextDocument(doc); + await new Promise((resolve) => setTimeout(resolve, 3000)); + const errors = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.severity === vscode.DiagnosticSeverity.Error); + assert.deepEqual(errors.map((d) => d.message), []); + }); + + await check('renders to an HTML file and opens it in the Archify viewer', async () => { + const source = workspaceFile('cache-miss-request.sequence.json'); + const output = vscode.Uri.file(path.join(path.dirname(source.fsPath), 'cache-miss-request.sequence.html')); + // Stub the save dialog and the follow-up notification. + const window = vscode.window as unknown as Record; + const originalSave = window.showSaveDialog; + const originalInfo = window.showInformationMessage; + window.showSaveDialog = async () => output; + window.showInformationMessage = async () => 'Open in Viewer'; + try { + await vscode.commands.executeCommand('archify.renderToHtml', source); + } finally { + window.showSaveDialog = originalSave; + window.showInformationMessage = originalInfo; + } + const html = new TextDecoder().decode(await vscode.workspace.fs.readFile(output)); + assert.match(html, / + + +`; + +test('adds one CSP and nonces every inline script', () => { + const html = prepareDiagramHtml(page, { cspSource: 'vscode-resource:', nonce: 'N0NCE', theme: 'light' }); + const csps = html.match(/Content-Security-Policy/g) ?? []; + assert.equal(csps.length, 1); + assert.match(html, /script-src ('|')nonce-N0NCE/); + const scripts = html.match(/]*>/g) ?? []; + assert.equal(scripts.length, 4, 'bridge plus the three page scripts'); + for (const tag of scripts) assert.match(tag, /nonce="N0NCE"/); +}); + +test('bridge runs before the page scripts and pins the theme', () => { + const html = prepareDiagramHtml(page, { cspSource: 'x', nonce: 'n', theme: 'dark', hash: '#focus=api' }); + const bridge = html.indexOf('acquireVsCodeApi'); + assert.ok(bridge > 0 && bridge < html.indexOf('var a = 1')); + assert.match(html, /"theme":"dark"/); + assert.match(html, /"hash":"#focus=api"/); +}); + +test('embedded config cannot close the script element', () => { + const html = prepareDiagramHtml(page, { + cspSource: 'x', + nonce: 'n', + theme: undefined, + notice: { kind: 'warning', text: '' }, + }); + assert.ok(!html.includes(' + + + + + + + +
+ +
+
+
+

[PROJECT NAME] Architecture

+
+

[Subtitle description]

+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Users + Browser/Mobile + + + + + Auth Provider + OAuth 2.0 + + + + AWS Region: us-west-2 + + + + + CloudFront + CDN + + + + + S3 Buckets + • bucket-one + • bucket-two + • bucket-three + OAI Protected + + + + sg-name :port + + + + + Load Balancer + HTTPS :443 + + + + + API Server + FastAPI :8000 + + + + + Database + PostgreSQL + + + + + Frontend + React + TypeScript + Additional detail + More info + domain.example.com + + + + + + HTTPS + + + + + + + OAI + + + + + TLS + + + + JWT + PKCE + + + Legend + + + Frontend + + + Backend + + + Cloud Service + + + Database + + + Security + + + Auth Flow + + + Security Group + + +

+ + + + + + + + + +
+ + + +
+ + + + diff --git a/vendor/archify/bin/archify.mjs b/vendor/archify/bin/archify.mjs new file mode 100755 index 0000000..fa2066d --- /dev/null +++ b/vendor/archify/bin/archify.mjs @@ -0,0 +1,6936 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { createHash, randomUUID } from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const skillRoot = path.resolve(__dirname, '..'); + +const TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']); +const DELIVERY_SIDECAR_SUFFIXES = Object.freeze([ + '.delivery.json', + '.delivery-pending.json', + '.delivery-lock.json', +]); +const COMPARE_SIDECAR_SUFFIXES = Object.freeze(['.receipt.json']); +const DELIVERY_DIRECTORY_LOCK = '.archify-delivery-lock.json'; +let sidecarNamespaceComponentKeyRuntime; +let boundedSidecarStem; +let isBoundedSidecarStem; +let sidecarStemFromComponent; +let sidecarStemNeedsBounding; + +async function loadSidecarPathRuntime() { + ({ + boundedSidecarStem, + isBoundedSidecarStem, + sidecarStemFromComponent, + sidecarStemNeedsBounding, + } = await import('../renderers/shared/sidecar-path.mjs')); +} + +function deliverySidecarNamespace(artifactPath) { + let artifact = path.resolve(artifactPath); + try { + artifact = fs.realpathSync.native(artifact); + } catch (error) { + if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error; + } + if (typeof sidecarNamespaceComponentKeyRuntime !== 'function') { + throw new Error('The sidecar namespace runtime is unavailable.'); + } + const namespace = sidecarNamespaceComponentKeyRuntime( + path.dirname(artifact), + path.basename(artifact), + ); + if (namespace.status !== 'resolved') { + 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; + } + return { directory: namespace.directoryPath, ...sidecarStemFromComponent(namespace.componentKey) }; +} + +function deliverySidecarPath(artifactPath, suffix) { + const { directory, stem, options } = deliverySidecarNamespace(artifactPath); + return path.join( + directory, + `${boundedSidecarStem(stem, DELIVERY_SIDECAR_SUFFIXES, options)}${suffix}`, + ); +} + +function deliveryProvenancePath(artifactPath) { + return deliverySidecarPath(artifactPath, '.delivery.json'); +} + +function deliveryPendingPath(artifactPath) { + return deliverySidecarPath(artifactPath, '.delivery-pending.json'); +} + +function rawHeadDeliverySidecarPath(artifactPath, suffix) { + const { directory, stem, options } = deliverySidecarNamespace(artifactPath); + if (options?.force) return undefined; + if (sidecarStemNeedsBounding(stem, DELIVERY_SIDECAR_SUFFIXES) + || !isBoundedSidecarStem(stem)) return undefined; + return path.join(directory, `${stem}${suffix}`); +} + +function rawHeadDeliveryProvenancePath(artifactPath) { + return rawHeadDeliverySidecarPath(artifactPath, '.delivery.json'); +} + +function rawHeadDeliveryPendingPath(artifactPath) { + return rawHeadDeliverySidecarPath(artifactPath, '.delivery-pending.json'); +} + +function legacyExtensionDeliverySidecarPath(artifactPath, suffix) { + let artifact = path.resolve(artifactPath); + try { + artifact = fs.realpathSync.native(artifact); + } catch (error) { + if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error; + } + const component = path.basename(artifact); + if (!/\.html$/iu.test(component) || component.endsWith('.html')) return undefined; + const legacyComponent = `${component.slice(0, -'.html'.length)}${suffix}`; + if (legacyComponent.length > 255 || Buffer.byteLength(legacyComponent, 'utf8') > 255) { + return undefined; + } + return path.join(path.dirname(artifact), legacyComponent); +} + +function canonicalExistingDeliveryOutput(outputPath) { + const output = path.resolve(outputPath); + try { + return fs.realpathSync.native(output); + } catch (error) { + if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return output; + throw error; + } +} + +function legacyDeliveryLockPath(artifactPath) { + const artifact = path.resolve(artifactPath); + return `${artifact.replace(/\.html?$/i, '')}.delivery-lock.json`; +} + +function legacyDeliveryLockPaths(artifactPaths) { + const paths = []; + for (const artifactPath of artifactPaths) { + const lockPath = legacyDeliveryLockPath(artifactPath); + const name = path.basename(lockPath); + const canExist = process.platform === 'win32' + ? name.length <= 255 + : Buffer.byteLength(name, 'utf8') <= 255; + if (canExist && !paths.includes(lockPath)) paths.push(lockPath); + } + return paths; +} + +function deliveryLockPath(artifactPath) { + // A directory-wide mutex is deliberately conservative. Future 8.3, case, + // and Unicode aliases cannot be assigned a sound canonical filename before + // the artifact exists, so a per-artifact lock can split one delivery into + // multiple owners. Provenance and pending journals remain artifact-specific. + const artifact = path.resolve(artifactPath); + let directory; + try { + directory = path.dirname(fs.realpathSync.native(artifact)); + } catch (error) { + if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error; + const authoredDirectory = path.dirname(artifact); + try { + directory = fs.realpathSync.native(authoredDirectory); + } catch (directoryError) { + if (directoryError?.code !== 'ENOENT' && directoryError?.code !== 'ENOTDIR') throw directoryError; + directory = authoredDirectory; + } + } + return path.join(directory, DELIVERY_DIRECTORY_LOCK); +} + +function deliveryLockInvalid(lockPath) { + return Object.assign(new Error(`Unrecognized delivery lock at "${lockPath}"; inspect the existing entry before retrying.`), { + deliveryLockCode: 'delivery/lock-invalid', + deliveryLockPath: lockPath, + }); +} + +function regularDeliveryLockExists(lockPath) { + try { + if (!fs.lstatSync(lockPath).isFile()) throw deliveryLockInvalid(lockPath); + return true; + } catch (error) { + if (error.deliveryLockCode) throw error; + if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; + throw deliveryLockInvalid(lockPath); + } +} + +function rejectExistingDeliveryLock(lockPath, output, fileBindingRuntime) { + const { + captureRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + let lock; + let captured; + try { + if (typeof captureRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + captured = captureRegularFileBinding(lockPath, { + subject: 'delivery-lock', + expectedLinks: 1, + includeContent: true, + }); + if (captured.status !== 'captured') throw new Error('Not a stable regular lock file.'); + lock = JSON.parse(captured.content.buffer.toString('utf8')); + if (lock?.schemaVersion !== 1 || !Number.isSafeInteger(lock.pid) || lock.pid <= 0 + || typeof lock.receiptId !== 'string' + || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(lock.receiptId)) { + throw new Error('Unrecognized delivery lock.'); + } + } catch { + throw deliveryLockInvalid(lockPath); + } finally { + if (captured?.binding) releaseRegularFileBinding(captured.binding); + } + if (processIsRunning(lock.pid)) { + throw Object.assign(new Error(`Another delivery attempt owns "${output}" through lock "${lockPath}".`), { + deliveryLockCode: 'delivery/concurrent-attempt', + deliveryLockPath: lockPath, + }); + } + throw Object.assign(new Error(`A stale delivery lock remains at "${lockPath}"; recover it serially before retrying.`), { + deliveryLockCode: 'delivery/lock-stale', + deliveryLockPath: lockPath, + deliveryLockOwner: { pid: lock.pid, receiptId: lock.receiptId }, + }); +} + +function processIsRunning(pid) { + if (!Number.isInteger(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error.code !== 'ESRCH'; + } +} + +const deliveryOwnershipStates = new WeakMap(); + +function deliveryOwnershipFailure(state, operation, reason, evidence = {}) { + const error = new Error(`Delivery ownership was lost while attempting to ${operation}: ${reason}`); + error.deliveryOwnershipCode = 'delivery/ownership-lost'; + error.deliveryOwnershipDetails = { + operation, + lock: state?.lockPath, + expectedReceiptId: state?.receiptId, + reason, + ...evidence, + }; + if (state && state.phase !== 'released') state.phase = 'lost'; + return error; +} + +function deliveryOwnershipState(ownership, operation) { + const state = deliveryOwnershipStates.get(ownership); + if (!state) throw deliveryOwnershipFailure(undefined, operation, 'the ownership capability is invalid'); + if (state.phase === 'lost' || state.phase === 'released') { + throw deliveryOwnershipFailure(state, operation, `the capability is already ${state.phase}`); + } + return state; +} + +function sameFileIdentity(actual, expected) { + const hasIdentity = (entry) => entry?.ino !== undefined && entry.ino !== 0 && entry.ino !== 0n; + return hasIdentity(actual) + && hasIdentity(expected) + && actual.dev === expected.dev + && actual.ino === expected.ino; +} + +function assertOwnedDeliveryLock(state, ownedLock, operation, { verifyReceipt = true } = {}) { + const { + captureRegularFileBinding, + releaseRegularFileBinding, + } = state?.fileBindingRuntime || {}; + let captured; + try { + if (typeof captureRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + captured = captureRegularFileBinding(ownedLock.path, { + subject: 'owned-delivery-lock', + ...(ownedLock.content ? { + expectedSha256: ownedLock.content.sha256, + expectedBytes: ownedLock.content.bytes, + } : {}), + expectedMode: Number(ownedLock.identity.mode & 0o777n), + expectedLinks: 1, + includeContent: verifyReceipt, + }); + } catch (error) { + throw deliveryOwnershipFailure(state, operation, 'an owned lock entry can no longer be inspected', { + lock: ownedLock.path, + ...(error?.code ? { systemCode: error.code } : {}), + }); + } + if (captured.status !== 'captured' + || captured.identity.device !== ownedLock.identity.dev + || captured.identity.inode !== ownedLock.identity.ino) { + if (captured?.binding) releaseRegularFileBinding(captured.binding); + throw deliveryOwnershipFailure(state, operation, 'an owned lock path no longer names the entry created by this attempt', { + lock: ownedLock.path, + ...(captured?.reason ? { lockState: captured.reason } : {}), + }); + } + try { + if (!verifyReceipt) return; + let lock; + try { + lock = JSON.parse(captured.content.buffer.toString('utf8')); + } catch (error) { + throw deliveryOwnershipFailure(state, operation, 'an owned lock receipt can no longer be verified', { + lock: ownedLock.path, + ...(error?.code ? { systemCode: error.code } : {}), + }); + } + if (lock?.schemaVersion !== 1 || lock.receiptId !== state.receiptId || lock.pid !== state.pid) { + throw deliveryOwnershipFailure(state, operation, 'an owned lock receipt no longer matches this attempt', { + lock: ownedLock.path, + ...(typeof lock?.receiptId === 'string' ? { observedReceiptId: lock.receiptId } : {}), + }); + } + } catch (error) { + throw error; + } finally { + releaseRegularFileBinding(captured.binding); + } +} + +function assertDeliveryOwnership(ownership, operation, { allowInitializing = false, pending = 'ignore' } = {}) { + const state = deliveryOwnershipState(ownership, operation); + const verifyReceipt = !allowInitializing || !['acquiring', 'initializing'].includes(state.phase); + for (const ownedLock of state.ownedLocks) { + assertOwnedDeliveryLock(state, ownedLock, operation, { verifyReceipt }); + } + if (pending !== 'ignore') { + const pendingPath = state.pendingPath; + if (pending === 'absent') { + try { + fs.lstatSync(pendingPath); + } catch (error) { + if (error.code === 'ENOENT') return state; + throw deliveryOwnershipFailure(state, operation, 'the finalized delivery journal can no longer be inspected', { + journal: pendingPath, + ...(error?.code ? { systemCode: error.code } : {}), + }); + } + throw deliveryOwnershipFailure(state, operation, 'the finalized delivery journal was recreated', { journal: pendingPath }); + } + const { + captureRegularFileBinding, + releaseRegularFileBinding, + } = state.fileBindingRuntime || {}; + let captured; + try { + if (typeof captureRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + captured = captureRegularFileBinding(pendingPath, { + subject: 'owned-delivery-journal', + expectedSha256: state.pendingContent?.sha256, + expectedBytes: state.pendingContent?.bytes, + expectedMode: state.pendingIdentity?.mode, + expectedLinks: 1, + includeContent: pending === 'owned', + }); + } catch (error) { + throw deliveryOwnershipFailure(state, operation, 'the owned delivery journal can no longer be inspected', { + journal: pendingPath, + ...(error?.code ? { systemCode: error.code } : {}), + }); + } + if (captured.status !== 'captured' + || !state.pendingIdentity + || captured.identity.device !== state.pendingIdentity.dev + || captured.identity.inode !== state.pendingIdentity.ino) { + if (captured?.binding) releaseRegularFileBinding(captured.binding); + throw deliveryOwnershipFailure(state, operation, 'the delivery journal no longer names the entry created by this attempt', { + journal: pendingPath, + ...(captured?.reason ? { journalState: captured.reason } : {}), + }); + } + try { + if (pending === 'owned') { + let journal; + try { + journal = JSON.parse(captured.content.buffer.toString('utf8')); + } catch { + throw deliveryOwnershipFailure(state, operation, 'the delivery journal receipt is malformed', { + journal: pendingPath, + }); + } + if (journal?.receiptId !== state.receiptId) { + throw deliveryOwnershipFailure(state, operation, 'the delivery journal receipt belongs to another attempt', { + journal: pendingPath, + ...(typeof journal?.receiptId === 'string' ? { observedReceiptId: journal.receiptId } : {}), + }); + } + } + } finally { + releaseRegularFileBinding(captured.binding); + } + } + return state; +} + +function quarantineRemoveOwnedDeliveryEntry(state, { + filePath, + identity, + content, + subject, + operation, + evidenceKey, +}) { + const { + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + releaseRegularFileBinding, + } = state?.fileBindingRuntime || {}; + let captured; + try { + if (typeof captureRegularFileBinding !== 'function' + || typeof quarantineRemoveRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + captured = captureRegularFileBinding(filePath, { + subject, + ...(content ? { + expectedSha256: content.sha256, + expectedBytes: content.bytes, + } : {}), + expectedMode: typeof identity?.mode === 'bigint' + ? Number(identity.mode & 0o777n) + : identity?.mode, + expectedIdentity: { + device: identity?.dev, + inode: identity?.ino, + }, + expectedLinks: 1, + }); + } catch (error) { + throw deliveryOwnershipFailure(state, operation, `the owned ${subject} can no longer be captured for removal`, { + [evidenceKey]: filePath, + ...(error?.code ? { systemCode: error.code } : {}), + }); + } + if (captured.status !== 'captured') { + throw deliveryOwnershipFailure(state, operation, `the owned ${subject} changed before removal`, { + [evidenceKey]: filePath, + entryState: captured.reason, + }); + } + try { + const removed = quarantineRemoveRegularFileBinding(captured.binding, filePath, { + subject, + expectedLinks: 1, + }); + if (removed.status === 'removed') return; + const removalCode = removed.reason?.code; + if (removed.status === 'unknown' + && ['removal-quarantine-create-failed', `${subject}-quarantine-move-failed`] + .includes(removalCode)) { + const detail = removed.reason?.message ? `: ${removed.reason.message}` : ''; + const error = new Error(`The owned ${subject} could not be moved into its private removal quarantine (${removalCode})${detail}.`); + if (removed.reason?.systemCode) error.code = removed.reason.systemCode; + if (subject === 'delivery-lock') { + error.deliveryOwnershipCode = 'delivery/lock-release'; + error.deliveryOwnershipDetails = { + operation, + lock: filePath, + reason: error.message, + entryState: removed.reason, + }; + } else { + error.deliveryJournalRemovalState = removed; + } + throw error; + } + throw deliveryOwnershipFailure( + state, + operation, + removed.status === 'recovery-required' + ? `a replacement ${subject} could not be restored from quarantine` + : `the public ${subject} changed during removal and was preserved`, + { + [evidenceKey]: filePath, + entryState: removed.reason, + ...(removed.status === 'recovery-required' ? { + recoveryRequired: true, + recoveryDirectory: removed.recoveryDirectory, + recoveryFiles: [{ + label: `preserved replacement ${subject}`, + path: removed.recoveryFile, + target: filePath, + }], + } : {}), + }, + ); + } finally { + releaseRegularFileBinding(captured.binding); + } +} + +function releaseDeliveryOwnership(ownership, { allowInitializing = false } = {}) { + const current = deliveryOwnershipState(ownership, 'release the delivery lock'); + const pending = current.phase === 'finalized' + ? 'absent' + : current.pendingIdentity + ? 'identity' + : 'ignore'; + const state = assertDeliveryOwnership(ownership, 'release the delivery lock', { allowInitializing, pending }); + const verifyReceipt = !allowInitializing || !['acquiring', 'initializing'].includes(state.phase); + for (const ownedLock of state.ownedLocks) { + assertOwnedDeliveryLock(state, ownedLock, 'release the delivery lock', { verifyReceipt }); + quarantineRemoveOwnedDeliveryEntry(state, { + filePath: ownedLock.path, + identity: ownedLock.identity, + content: ownedLock.content, + subject: 'delivery-lock', + operation: 'release the delivery lock', + evidenceKey: 'lock', + }); + } + state.phase = 'released'; +} + +function acquireDeliveryLock( + output, + receiptId, + inputPath, + pathsAlias, + recordInitializationFailure, + legacyOutputPaths = [], + fileBindingRuntime, +) { + const stableOutput = canonicalExistingDeliveryOutput(output); + const lockPath = deliveryLockPath(stableOutput); + const comparableLockPath = (candidate) => { + const absolute = path.resolve(candidate); + try { + return path.join(fs.realpathSync.native(path.dirname(absolute)), path.basename(absolute)); + } catch (error) { + if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error; + return absolute; + } + }; + const lockPathsAlias = (left, right) => { + const comparableLeft = comparableLockPath(left); + const comparableRight = comparableLockPath(right); + if (!pathsAlias(path.dirname(comparableLeft), path.dirname(comparableRight))) return false; + return pathsAlias(comparableLeft, comparableRight); + }; + const legacyFencePathsAlias = (left, right) => { + const comparableLeft = comparableLockPath(left); + const comparableRight = comparableLockPath(right); + if (comparableLeft === comparableRight) return true; + try { + return lockPathsAlias(comparableLeft, comparableRight); + } catch { + // An indeterminate future-name relation is not evidence of aliasing. + // Keep both candidates; an EEXIST below is accepted only when dev/ino + // proves that it names a lock already created by this attempt. + return false; + } + }; + const legacyLockPaths = []; + for (const candidate of legacyDeliveryLockPaths([ + ...legacyOutputPaths.filter(Boolean), + output, + stableOutput, + ])) { + if (!legacyLockPaths.some((existing) => legacyFencePathsAlias(candidate, existing))) { + legacyLockPaths.push(candidate); + } + } + const pendingPath = deliveryPendingPath(stableOutput); + const provenancePath = deliveryProvenancePath(stableOutput); + const rawHeadPendingPath = rawHeadDeliveryPendingPath(stableOutput); + const existingLegacyLocks = legacyLockPaths.filter((candidate) => regularDeliveryLockExists(candidate)); + regularDeliveryLockExists(lockPath); + const conflictingLockPath = [lockPath, ...legacyLockPaths] + .find((candidate) => lockPathsAlias(candidate, inputPath)); + if (conflictingLockPath) { + throw Object.assign(new Error('Delivery lock path aliases the input specification; choose another output path.'), { + deliveryLockCode: 'delivery/lock-path-conflict', + deliveryLockPath: conflictingLockPath, + }); + } + if (existingLegacyLocks.length) { + rejectExistingDeliveryLock(existingLegacyLocks[0], output, fileBindingRuntime); + } + const openedLocks = []; + const ownership = Object.freeze({}); + deliveryOwnershipStates.set(ownership, { + output: stableOutput, + lockPath, + pendingPath, + provenancePath, + receiptId, + pid: process.pid, + ownedLocks: openedLocks, + fileBindingRuntime, + phase: 'acquiring', + }); + let collisionPath; + try { + const openOwnedLock = (ownedLockPath, { allowOwnedAlias = false } = {}) => { + let descriptor; + try { + descriptor = fs.openSync(ownedLockPath, 'wx', 0o600); + } catch (error) { + if (error.code === 'EEXIST' && allowOwnedAlias) { + try { + const existingEntry = fs.lstatSync(ownedLockPath, { bigint: true }); + if (existingEntry.isFile() + && openedLocks.some((ownedLock) => sameFileIdentity(existingEntry, ownedLock.identity))) { + return undefined; + } + } catch { + // Preserve the original EEXIST classification below. + } + } + if (error.code === 'EEXIST') collisionPath = ownedLockPath; + throw error; + } + let handleIdentity; + let handleIdentityError; + try { + handleIdentity = fs.fstatSync(descriptor, { bigint: true }); + } catch (error) { + // A transient first inspection failure must not strand the public + // lock. Bind ownership from the still-open handle before cleanup. + try { + handleIdentity = fs.fstatSync(descriptor, { bigint: true }); + } catch { + // Both handle metadata reads failed. Stamp the still-open inode with + // this attempt's unpredictable receipt before consulting the public + // path. A replacement claimant cannot be mistaken for our empty + // lock, while a successful content-bound capture gives cleanup the + // identity it needs after the original descriptor is closed. + const recoveryReceipt = `${JSON.stringify({ schemaVersion: 1, receiptId, pid: process.pid })}\n`; + const recoveryContent = artifactIdentity(Buffer.from(recoveryReceipt)); + let captured; + try { + fs.writeFileSync(descriptor, recoveryReceipt); + captured = fileBindingRuntime?.captureRegularFileBinding?.(ownedLockPath, { + subject: 'owned-delivery-lock', + expectedSha256: recoveryContent.sha256, + expectedBytes: recoveryContent.bytes, + expectedLinks: 1, + }); + } catch {} + if (captured?.status === 'captured') { + fileBindingRuntime.releaseRegularFileBinding(captured.binding); + openedLocks.push({ + path: ownedLockPath, + identity: { + dev: captured.identity.device, + ino: captured.identity.inode, + mode: BigInt(captured.mode), + }, + content: recoveryContent, + descriptor, + }); + } else { + try { + fs.closeSync(descriptor); + } catch (closeError) { + error.lockCleanupError = closeError.message; + } + } + throw error; + } + handleIdentityError = error; + } + const ownedLock = { + path: ownedLockPath, + identity: handleIdentity, + descriptor, + }; + openedLocks.push(ownedLock); + const pathIdentity = fs.lstatSync(ownedLockPath, { bigint: true }); + if (!handleIdentity.isFile() + || handleIdentity.ino === 0n + || !pathIdentity.isFile() + || pathIdentity.isSymbolicLink() + || pathIdentity.ino === 0n + || !sameFileIdentity(pathIdentity, handleIdentity)) { + throw new Error('Delivery lock path identity could not be verified safely.'); + } + if (handleIdentityError) throw handleIdentityError; + return ownedLock; + }; + + openOwnedLock(lockPath); + for (const legacyLockPath of legacyLockPaths) { + openOwnedLock(legacyLockPath, { allowOwnedAlias: true }); + } + const state = deliveryOwnershipStates.get(ownership); + if (rawHeadPendingPath && pathEntryExists(rawHeadPendingPath)) { + throw Object.assign(new Error(`A pre-namespace delivery journal remains at "${rawHeadPendingPath}"; recover it before retrying.`), { + deliveryLockCode: 'delivery/legacy-pending', + deliveryLockPath: rawHeadPendingPath, + }); + } + state.phase = 'initializing'; + const lockReceipt = `${JSON.stringify({ schemaVersion: 1, receiptId, pid: process.pid })}\n`; + const lockContent = artifactIdentity(Buffer.from(lockReceipt)); + for (const ownedLock of openedLocks) { + fs.writeFileSync(ownedLock.descriptor, lockReceipt); + ownedLock.content = lockContent; + } + for (const ownedLock of openedLocks) { + fs.closeSync(ownedLock.descriptor); + ownedLock.descriptor = undefined; + } + state.phase = 'owned'; + return ownership; + } catch (error) { + for (const ownedLock of openedLocks) { + if (ownedLock.descriptor === undefined) continue; + try { + fs.closeSync(ownedLock.descriptor); + ownedLock.descriptor = undefined; + } catch (closeError) { + error.lockCleanupError = closeError.message; + } + } + const state = deliveryOwnershipStates.get(ownership); + let preserveOwnership = false; + try { + if (state.phase === 'initializing') { + assertDeliveryOwnership(ownership, 'record a lock initialization failure', { allowInitializing: true }); + error.deliveryFailureRecord = recordInitializationFailure?.(error, ownership); + if (error.deliveryFailureRecord?.journalRecovery) { + error.deliveryJournalRecovery = error.deliveryFailureRecord.journalRecovery.deliveryJournalRecovery; + preserveOwnership = true; + } + if (error.deliveryFailureRecord?.provenanceRecovery) { + error.deliveryProvenanceRecovery = error.deliveryFailureRecord.provenanceRecovery.deliveryProvenanceRecovery; + preserveOwnership = true; + } + } + if (!preserveOwnership) { + releaseDeliveryOwnership(ownership, { allowInitializing: true }); + } + } catch (cleanupError) { + if (cleanupError.deliveryOwnershipCode) { + cleanupError.deliveryOwnershipDetails = { + ...(cleanupError.deliveryOwnershipDetails || {}), + initializationError: error.message, + ...(error?.code ? { initializationSystemCode: error.code } : {}), + }; + if (cleanupError.deliveryOwnershipCode === 'delivery/lock-release' && error.deliveryFailureRecord) { + cleanupError.deliveryFailureRecord = error.deliveryFailureRecord; + } + throw cleanupError; + } + error.lockCleanupError = cleanupError.message; + } + if (collisionPath) rejectExistingDeliveryLock(collisionPath, output, fileBindingRuntime); + throw error; + } +} + +function pathEntryExists(file) { + try { + fs.lstatSync(file); + return true; + } catch (error) { + if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; + throw error; + } +} + +function captureOwnedStagingFile(registry, filePath, fileBindingRuntime, { + subject = 'private-staging-entry', + content, + expectedLinks = 1, +} = {}) { + const { captureRegularFileBinding } = fileBindingRuntime || {}; + if (typeof captureRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + let captured = captureRegularFileBinding(filePath, { + subject, + ...(content ? { + expectedSha256: content.sha256, + expectedBytes: content.bytes, + } : {}), + expectedLinks, + }); + if (captured.status === 'unsupported' + && String(captured.reason?.code || '').endsWith('-hardlinked') + && content) { + const observedLinks = Number(captured.reason?.links); + if (Number.isSafeInteger(observedLinks) && observedLinks > expectedLinks) { + captured = captureRegularFileBinding(filePath, { + subject, + expectedSha256: content.sha256, + expectedBytes: content.bytes, + expectedLinks: observedLinks, + }); + expectedLinks = observedLinks; + } + } + if (captured.status !== 'captured') { + const error = new Error(`Could not bind the owned staging entry "${filePath}" (${captured.reason?.code || 'unknown state'}).`); + error.stagingCapture = captured; + throw error; + } + registry.push({ filePath, binding: captured.binding, subject, expectedLinks }); + return captured; +} + +function cleanupOwnedStagingDirectory(directory, identity, registry, fileBindingRuntime) { + const { + quarantineRemoveRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + if (typeof quarantineRemoveRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + const failures = []; + for (const entry of [...registry].reverse()) { + try { + const removed = quarantineRemoveRegularFileBinding(entry.binding, entry.filePath, { + subject: entry.subject, + expectedLinks: entry.expectedLinks, + }); + // A missing path means the transaction already moved or finalized this + // owned inode. Every other non-removal result is deliberately preserved: + // it may be a concurrent claimant rather than an entry we created. + if (removed.status !== 'removed' + && !['ENOENT', 'ENOTDIR'].includes(removed.reason?.systemCode)) { + failures.push(`${entry.filePath}: ${removed.reason?.code || removed.status}`); + } + } catch (error) { + failures.push(`${entry.filePath}: ${error.message}`); + } finally { + const released = releaseRegularFileBinding(entry.binding); + if (released.status !== 'released') { + failures.push(`${entry.filePath}: ${released.reason?.code || released.status}`); + } + } + } + registry.length = 0; + try { + if (!removeOwnedEmptyStagingDirectory(directory, identity, { throwOnFailure: true })) { + failures.push(`${directory}: staging directory was retained because its identity changed`); + } + } catch (error) { + failures.push(`${directory}: ${error.message}`); + } + if (failures.length) throw new Error(failures.join('; ')); +} + +function createOwnedEmptyStagingDirectory(prefix) { + // Node's Windows mkdtemp binding does not consistently promote an ordinary + // UNC prefix past MAX_PATH. Use the equivalent namespaced spelling for the + // syscall while preserving the same physical directory identity. + const directory = fs.mkdtempSync(path.toNamespacedPath(prefix)); + const metadata = fs.lstatSync(directory, { bigint: true }); + if (!metadata.isDirectory() || metadata.isSymbolicLink() || metadata.ino === 0n) { + throw new Error(`Private staging directory identity is unavailable: ${directory}`); + } + return { + directory, + identity: { device: metadata.dev, inode: metadata.ino }, + }; +} + +function createStagingRetirementQuarantine(parent) { + for (let attempt = 0; attempt < 8; attempt += 1) { + const directory = path.join(parent, `.archify-staging-remove-${randomUUID()}`); + try { + fs.mkdirSync(directory, { mode: 0o700 }); + return directory; + } catch (error) { + if (error?.code !== 'EEXIST') return undefined; + } + } + return undefined; +} + +const stagingRemovalSignal = new Int32Array(new SharedArrayBuffer(4)); +const stagingRemovalAttempts = 10; + +function removeEmptyStagingDirectoryWithRetry(directory) { + let failure; + for (let attempt = 0; attempt < stagingRemovalAttempts; attempt += 1) { + try { + fs.rmdirSync(directory); + return; + } catch (error) { + failure = error; + // SMB can acknowledge the last file removal before the directory view + // catches up. Retrying an empty-directory removal is claimant-safe: any + // real entry keeps returning ENOTEMPTY and is never removed recursively. + if (error?.code !== 'ENOTEMPTY' || attempt === stagingRemovalAttempts - 1) break; + Atomics.wait( + stagingRemovalSignal, + 0, + 0, + Math.min(5 * (2 ** attempt), 250), + ); + } + } + throw failure; +} + +function stagingDirectoryAppearsEmpty(directory) { + for (let attempt = 0; attempt < stagingRemovalAttempts; attempt += 1) { + try { + if (fs.readdirSync(directory).length === 0) return true; + } catch { + return false; + } + if (attempt === stagingRemovalAttempts - 1) break; + Atomics.wait( + stagingRemovalSignal, + 0, + 0, + Math.min(5 * (2 ** attempt), 250), + ); + } + return false; +} + +function removeOwnedEmptyStagingDirectory(directory, identity, { throwOnFailure = false } = {}) { + let current; + try { + current = fs.lstatSync(directory, { bigint: true }); + } catch (error) { + if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return false; + throw error; + } + if (!current.isDirectory() + || current.isSymbolicLink() + || current.ino === 0n + || current.dev !== identity.device + || current.ino !== identity.inode) return false; + if (!stagingDirectoryAppearsEmpty(directory)) return false; + const quarantine = createStagingRetirementQuarantine(path.dirname(directory)); + if (!quarantine) return false; + const movedPath = path.join(quarantine, path.basename(directory)); + try { + fs.renameSync(directory, movedPath); + } catch (error) { + if (throwOnFailure) throw error; + return false; + } + let moved; + try { + moved = fs.lstatSync(movedPath, { bigint: true }); + } catch { + return false; + } + if (!moved.isDirectory() + || moved.isSymbolicLink() + || moved.ino === 0n + || moved.dev !== identity.device + || moved.ino !== identity.inode) { + // A claimant won the public move boundary. Preserve it under the private + // quarantine name; directories have no portable no-clobber restore link. + return false; + } + try { + // The unpredictable 0700 quarantine closes the public replacement race. + // Never recurse: unexpected contents remain available for recovery. + removeEmptyStagingDirectoryWithRetry(movedPath); + removeEmptyStagingDirectoryWithRetry(quarantine); + return true; + } catch (error) { + if (throwOnFailure) throw error; + return false; + } +} + +function releaseOwnedStagingBindings(registry, fileBindingRuntime) { + const { releaseRegularFileBinding } = fileBindingRuntime || {}; + if (typeof releaseRegularFileBinding !== 'function') return; + for (const entry of registry) releaseRegularFileBinding(entry.binding); + registry.length = 0; +} + +function artifactIdentity(artifact) { + return { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength }; +} + +function beginDeliveryAttempt({ ownership, input, pathsAlias, fileBindingRuntime }) { + const state = assertDeliveryOwnership(ownership, 'begin the delivery journal', { allowInitializing: true }); + const { + output, pendingPath: journal, provenancePath, receiptId, + } = state; + for (const protectedPath of [input, output, provenancePath].filter(Boolean)) { + if (pathsAlias(journal, protectedPath)) throw new Error('Delivery journal aliases an input or output.'); + } + const published = writeDeliveryProvenance(journal, { + schemaVersion: 1, command: 'deliver', status: 'pending', receiptId, + input, output: path.resolve(output), + }, { + beforeReplace: () => assertDeliveryOwnership(ownership, 'create the delivery journal', { allowInitializing: true }), + fileBindingRuntime, + }); + state.pendingIdentity = { + dev: published.identity.device, + ino: published.identity.inode, + mode: published.identity.mode, + }; + state.pendingContent = published.content; + assertDeliveryOwnership(ownership, 'finish creating the delivery journal', { allowInitializing: true }); +} + +function writeDeliveryProvenance(file, value, { beforeReplace, fileBindingRuntime } = {}) { + const { + backupPublicRegularFileBinding, + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + if (typeof captureRegularFileBinding !== 'function' + || typeof backupPublicRegularFileBinding !== 'function' + || typeof quarantineRemoveRegularFileBinding !== 'function' + || typeof verifyRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + const error = new Error('The regular-file binding runtime is unavailable.'); + error.deliveryJournalState = { + status: 'unknown', + reason: { code: 'atomic-output-runtime-unavailable' }, + }; + throw error; + } + const staging = createOwnedEmptyStagingDirectory( + path.join(path.dirname(file), '.archify-provenance-'), + ); + const stagingDirectory = staging.directory; + const temporary = path.join(stagingDirectory, path.basename(file)); + const backup = path.join(stagingDirectory, '.previous-journal'); + const serialized = Buffer.from(`${JSON.stringify(value, null, 2)}\n`); + const expectedContent = artifactIdentity(serialized); + let candidateCapture; + let previousCapture; + let candidatePresent = false; + let backupPresent = false; + let backupVerified = false; + let published = false; + let retainStaging = false; + const bindingError = (result, action) => { + const error = new Error(`Could not ${action} delivery journal "${file}" safely (${result.reason?.code || 'unknown target state'}).`); + error.deliveryJournalState = result; + return error; + }; + try { + fs.writeFileSync(temporary, serialized, { flag: 'wx' }); + candidatePresent = true; + candidateCapture = captureRegularFileBinding(temporary, { + subject: 'delivery-journal-candidate', + expectedSha256: expectedContent.sha256, + expectedBytes: expectedContent.bytes, + expectedLinks: 1, + }); + if (candidateCapture.status !== 'captured') throw bindingError(candidateCapture, 'prepare the'); + + let existing = false; + try { + fs.lstatSync(file); + existing = true; + } catch (error) { + if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error; + } + if (existing) { + previousCapture = captureRegularFileBinding(file, { + subject: 'previous-delivery-journal', + expectedLinks: 1, + }); + if (previousCapture.status !== 'captured') { + throw bindingError(previousCapture, 'snapshot the existing'); + } + } + beforeReplace?.(); + if (previousCapture) { + const moved = backupPublicRegularFileBinding(previousCapture.binding, file, backup, { + subject: 'previous-delivery-journal', + }); + backupPresent = moved.backupCreated === true; + backupVerified = moved.backupVerified === true; + if (moved.status !== 'backed-up') throw bindingError(moved, 'back up the existing'); + } + + const ready = verifyRegularFileBinding(candidateCapture.binding, { + filePath: temporary, + expectedLinks: 1, + }); + if (ready.status !== 'match') throw bindingError(ready, 'verify the staged'); + try { + fs.linkSync(temporary, file); + } catch (error) { + if (error?.code === 'EEXIST') { + throw bindingError({ + status: 'different', + reason: { code: 'target-claimed-during-publish', phase: 'journal-publish' }, + }, 'publish the'); + } + throw error; + } + published = true; + for (const target of [temporary, file]) { + const linked = verifyRegularFileBinding(candidateCapture.binding, { + filePath: target, + expectedLinks: 2, + }); + if (linked.status !== 'match') throw bindingError(linked, 'verify the published'); + } + const retiredCandidate = quarantineRemoveRegularFileBinding( + candidateCapture.binding, + temporary, + { subject: 'delivery-journal-candidate', expectedLinks: 2 }, + ); + if (retiredCandidate.status !== 'removed') { + retainStaging = true; + throw bindingError(retiredCandidate, 'retire the staged'); + } + candidatePresent = false; + const finalized = verifyRegularFileBinding(candidateCapture.binding, { + filePath: file, + expectedLinks: 1, + }); + if (finalized.status !== 'match') throw bindingError(finalized, 'finalize the'); + if (backupPresent) { + const previous = verifyRegularFileBinding(previousCapture.binding, { + filePath: backup, + expectedLinks: 1, + }); + if (previous.status !== 'match') throw bindingError(previous, 'verify the previous'); + const retiredBackup = quarantineRemoveRegularFileBinding( + previousCapture.binding, + backup, + { subject: 'previous-delivery-journal', expectedLinks: 1 }, + ); + if (retiredBackup.status !== 'removed') { + retainStaging = true; + throw bindingError(retiredBackup, 'retire the previous'); + } + backupPresent = false; + } + return { + identity: candidateCapture.identity, + content: candidateCapture.content, + }; + } catch (cause) { + const rollbackErrors = []; + const publicRecoveryFiles = []; + if (cause.deliveryJournalState?.status === 'recovery-required' + && cause.deliveryJournalState.recoveryFile) { + publicRecoveryFiles.push({ + label: 'preserved journal replacement', + path: cause.deliveryJournalState.recoveryFile, + target: file, + }); + } + if (published) { + try { + const removed = quarantineRemoveRegularFileBinding(candidateCapture.binding, file, { + subject: 'published-delivery-journal', + expectedLinks: candidatePresent ? 2 : 1, + }); + if (removed.status === 'removed') { + published = false; + } else { + rollbackErrors.push(`the published journal could not be removed safely (${removed.reason?.code || 'unknown target state'})`); + if (removed.status === 'recovery-required') { + publicRecoveryFiles.push({ + label: 'preserved journal replacement', + path: removed.recoveryFile, + target: file, + }); + } + } + } catch (error) { + if (error?.code !== 'ENOENT') rollbackErrors.push(`published journal cleanup failed (${error.message})`); + } + } + if (candidatePresent) { + try { + const current = verifyRegularFileBinding(candidateCapture.binding, { + filePath: temporary, + expectedLinks: published ? 2 : 1, + }); + if (current.status === 'match') { + const retiredCandidate = quarantineRemoveRegularFileBinding( + candidateCapture.binding, + temporary, + { + subject: 'delivery-journal-candidate', + expectedLinks: published ? 2 : 1, + }, + ); + if (retiredCandidate.status !== 'removed') { + throw bindingError(retiredCandidate, 'retire the staged'); + } + candidatePresent = false; + } else { + rollbackErrors.push('the staged journal candidate was replaced before cleanup'); + retainStaging = true; + } + } catch (error) { + if (error?.code !== 'ENOENT') rollbackErrors.push(`staged journal cleanup failed (${error.message})`); + } + } + if (backupPresent) { + try { + if (!backupVerified) { + throw new Error('the backup entry was never verified as the previous journal'); + } + if (pathEntryExists(file)) throw new Error('a new journal claimant prevents restoring the previous journal'); + const previous = verifyRegularFileBinding(previousCapture.binding, { + filePath: backup, + expectedLinks: 1, + }); + if (previous.status !== 'match') { + backupVerified = false; + throw bindingError(previous, 'verify the previous'); + } + try { + fs.linkSync(backup, file); + } catch (error) { + if (error?.code === 'EEXIST') { + throw new Error('a new journal claimant prevents restoring the previous journal'); + } + throw error; + } + for (const target of [backup, file]) { + const restored = verifyRegularFileBinding(previousCapture.binding, { + filePath: target, + expectedLinks: 2, + }); + if (restored.status !== 'match') throw bindingError(restored, 'verify the restored'); + } + const retiredBackup = quarantineRemoveRegularFileBinding( + previousCapture.binding, + backup, + { subject: 'previous-delivery-journal', expectedLinks: 2 }, + ); + if (retiredBackup.status !== 'removed') { + throw bindingError(retiredBackup, 'retire the restored backup'); + } + backupPresent = false; + const restored = verifyRegularFileBinding(previousCapture.binding, { + filePath: file, + expectedLinks: 1, + }); + if (restored.status !== 'match') throw bindingError(restored, 'finalize the restored'); + } catch (error) { + rollbackErrors.push(`previous journal restore failed (${error.message})`); + retainStaging = backupPresent || candidatePresent; + } + } + const claimantConflict = cause.deliveryJournalState?.reason?.code === 'target-claimed-during-publish'; + if (rollbackErrors.length || claimantConflict) { + cause.deliveryJournalRecovery = { + recoveryRequired: true, + journal: file, + ...(claimantConflict ? { claimantConflict: true } : {}), + rollbackErrors, + ...(retainStaging ? { + recoveryDirectory: stagingDirectory, + } : publicRecoveryFiles.length ? { + recoveryDirectory: path.dirname(publicRecoveryFiles[0].path), + } : {}), + ...(backupPresent || (candidatePresent && retainStaging) || publicRecoveryFiles.length ? { + recoveryFiles: [ + ...(backupPresent ? [{ + label: backupVerified + ? 'previous delivery journal' + : 'unverified preserved journal backup entry', + path: backup, + target: file, + verifiedPrevious: backupVerified, + }] : []), + ...(candidatePresent && retainStaging ? [{ label: 'preserved staged journal entry', path: temporary }] : []), + ...publicRecoveryFiles, + ], + } : {}), + }; + } + throw cause; + } finally { + if (candidateCapture?.binding) releaseRegularFileBinding(candidateCapture.binding); + if (previousCapture?.binding) releaseRegularFileBinding(previousCapture.binding); + if (!retainStaging) removeOwnedEmptyStagingDirectory(stagingDirectory, staging.identity); + } +} + +function writeCapturedDeliveryProvenance(file, value, { + beforeCommit, + capture, + verifyAtomicOutput, + fileBindingRuntime, +} = {}) { + const { + backupPublicRegularFileBinding, + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + if (!capture + || typeof verifyAtomicOutput !== 'function' + || typeof backupPublicRegularFileBinding !== 'function' + || typeof captureRegularFileBinding !== 'function' + || typeof quarantineRemoveRegularFileBinding !== 'function' + || typeof verifyRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw deliveryTargetStateError(file, 'delivery provenance', { + status: 'unknown', + reason: { code: 'atomic-output-runtime-unavailable' }, + }); + } + const staging = createOwnedEmptyStagingDirectory( + path.join(path.dirname(file), '.archify-provenance-'), + ); + const stagingDirectory = staging.directory; + const candidate = path.join(stagingDirectory, path.basename(file)); + const backup = path.join(stagingDirectory, '.previous-provenance'); + let candidateIdentity; + let candidateBinding; + let previousBinding; + let candidatePresent = false; + let backupPresent = false; + let backupVerified = false; + let published = false; + let retainStaging = false; + try { + const serialized = Buffer.from(`${JSON.stringify(value, null, 2)}\n`); + const expectedContent = artifactIdentity(serialized); + fs.writeFileSync(candidate, serialized, { flag: 'wx' }); + candidatePresent = true; + if (capture.mode !== null) fs.chmodSync(candidate, capture.mode); + const candidateCapture = captureRegularFileBinding(candidate, { + subject: 'delivery-provenance-candidate', + expectedSha256: expectedContent.sha256, + expectedBytes: expectedContent.bytes, + ...(capture.mode === null ? {} : { expectedMode: capture.mode }), + expectedLinks: 1, + }); + if (candidateCapture.status !== 'captured') { + throw deliveryTargetStateError(file, 'delivery provenance', candidateCapture); + } + candidateBinding = candidateCapture.binding; + candidateIdentity = { + dev: candidateCapture.identity.device, + ino: candidateCapture.identity.inode, + }; + beforeCommit?.(); + const verification = verifyAtomicOutput(capture.snapshot); + if (verification.status !== 'match') { + throw deliveryTargetStateError(file, 'delivery provenance', verification); + } + if (capture.snapshot.target.kind === 'file') { + const expected = capture.snapshot.target; + const previous = captureRegularFileBinding(capture.commitPath, { + subject: 'previous-delivery-provenance', + expectedIdentity: { device: expected.device, inode: expected.inode }, + expectedMode: expected.mode, + expectedLinks: 1, + }); + if (previous.status !== 'captured') { + throw deliveryTargetStateError(file, 'delivery provenance', previous); + } + previousBinding = previous.binding; + const moved = backupPublicRegularFileBinding( + previousBinding, + capture.commitPath, + backup, + { subject: 'previous-delivery-provenance' }, + ); + backupPresent = moved.backupCreated === true; + backupVerified = moved.backupVerified === true; + if (moved.status !== 'backed-up') { + throw deliveryTargetStateError(file, 'delivery provenance', moved); + } + } + const currentCandidate = verifyRegularFileBinding(candidateBinding, { + filePath: candidate, + expectedLinks: 1, + }); + if (currentCandidate.status !== 'match') { + throw deliveryTargetStateError(file, 'delivery provenance', currentCandidate); + } + try { + fs.linkSync(candidate, capture.commitPath); + } catch (error) { + if (error?.code === 'EEXIST') { + throw deliveryTargetStateError(file, 'delivery provenance', { + status: 'different', + reason: { code: 'target-claimed-during-publish' }, + }); + } + throw error; + } + published = true; + for (const target of [candidate, capture.commitPath]) { + const linked = verifyRegularFileBinding(candidateBinding, { + filePath: target, + expectedLinks: 2, + }); + if (linked.status !== 'match') { + throw deliveryTargetStateError(file, 'delivery provenance', linked); + } + } + const retiredCandidate = quarantineRemoveRegularFileBinding( + candidateBinding, + candidate, + { subject: 'delivery-provenance-candidate', expectedLinks: 2 }, + ); + if (retiredCandidate.status !== 'removed') { + retainStaging = true; + throw deliveryTargetStateError(file, 'delivery provenance', retiredCandidate); + } + candidatePresent = false; + const finalized = verifyRegularFileBinding(candidateBinding, { + filePath: capture.commitPath, + expectedLinks: 1, + }); + if (finalized.status !== 'match') { + throw deliveryTargetStateError(file, 'delivery provenance', finalized); + } + if (backupPresent) { + const currentBackup = verifyRegularFileBinding(previousBinding, { + filePath: backup, + expectedLinks: 1, + }); + if (currentBackup.status !== 'match') { + backupVerified = false; + throw deliveryTargetStateError(file, 'delivery provenance', { + status: 'different', + reason: { code: 'backup-identity-changed-before-finalize' }, + }); + } + const retiredBackup = quarantineRemoveRegularFileBinding( + previousBinding, + backup, + { subject: 'previous-delivery-provenance', expectedLinks: 1 }, + ); + if (retiredBackup.status !== 'removed') { + retainStaging = true; + throw deliveryTargetStateError(file, 'delivery provenance', retiredBackup); + } + backupPresent = false; + } + } catch (cause) { + const rollbackErrors = []; + const publicRecoveryFiles = []; + if (cause.deliveryTargetState?.status === 'recovery-required' + && cause.deliveryTargetState.recoveryFile) { + publicRecoveryFiles.push({ + label: 'preserved provenance replacement', + path: cause.deliveryTargetState.recoveryFile, + target: capture.commitPath, + }); + } + if (published) { + try { + const removed = quarantineRemoveRegularFileBinding(candidateBinding, capture.commitPath, { + subject: 'published-delivery-provenance', + expectedLinks: candidatePresent ? 2 : 1, + }); + if (removed.status === 'removed') { + published = false; + } else { + rollbackErrors.push(`the published provenance could not be removed safely (${removed.reason?.code || 'unknown target state'})`); + if (removed.status === 'recovery-required') { + publicRecoveryFiles.push({ + label: 'preserved provenance replacement', + path: removed.recoveryFile, + target: capture.commitPath, + }); + } + } + } catch (error) { + if (error?.code !== 'ENOENT') rollbackErrors.push(`published provenance cleanup failed (${error.message})`); + } + } + if (candidatePresent) { + try { + const current = verifyRegularFileBinding(candidateBinding, { + filePath: candidate, + expectedLinks: published ? 2 : 1, + }); + if (current.status === 'match') { + const retiredCandidate = quarantineRemoveRegularFileBinding( + candidateBinding, + candidate, + { + subject: 'delivery-provenance-candidate', + expectedLinks: published ? 2 : 1, + }, + ); + if (retiredCandidate.status !== 'removed') { + throw deliveryTargetStateError( + file, + 'delivery provenance', + retiredCandidate, + ); + } + candidatePresent = false; + } else { + rollbackErrors.push('the staged provenance candidate was replaced before cleanup'); + retainStaging = true; + } + } catch (error) { + if (error?.code !== 'ENOENT') { + rollbackErrors.push(`staged provenance cleanup failed (${error.message})`); + retainStaging = true; + } + } + } + if (backupPresent) { + try { + if (!backupVerified) { + throw new Error('the backup entry was never verified as the previous provenance'); + } + if (pathEntryExists(capture.commitPath)) { + throw new Error('a new target claimant prevents restoring the previous provenance'); + } + const currentBackup = verifyRegularFileBinding(previousBinding, { + filePath: backup, + expectedLinks: 1, + }); + if (currentBackup.status !== 'match') { + backupVerified = false; + throw new Error('the previous provenance backup identity changed'); + } + fs.linkSync(backup, capture.commitPath); + for (const target of [backup, capture.commitPath]) { + const restored = verifyRegularFileBinding(previousBinding, { + filePath: target, + expectedLinks: 2, + }); + if (restored.status !== 'match') { + throw new Error('the restored provenance identity could not be verified'); + } + } + const retiredBackup = quarantineRemoveRegularFileBinding( + previousBinding, + backup, + { subject: 'previous-delivery-provenance', expectedLinks: 2 }, + ); + if (retiredBackup.status !== 'removed') { + throw deliveryTargetStateError(file, 'delivery provenance', retiredBackup); + } + backupPresent = false; + const restoredFinal = verifyRegularFileBinding(previousBinding, { + filePath: capture.commitPath, + expectedLinks: 1, + }); + if (restoredFinal.status !== 'match') { + throw new Error('the restored provenance link count could not be verified'); + } + } catch (error) { + rollbackErrors.push(`previous provenance restore failed (${error.message})`); + retainStaging = true; + } + } + const recoveryFiles = [ + ...(backupPresent ? [{ + label: backupVerified + ? 'preserved delivery provenance backup entry' + : 'unverified preserved delivery provenance backup entry', + path: backup, + target: capture.commitPath, + verifiedPrevious: backupVerified, + }] : []), + ...(candidatePresent && retainStaging ? [{ label: 'preserved staged provenance entry', path: candidate }] : []), + ...publicRecoveryFiles, + ]; + if (rollbackErrors.length) { + cause.deliveryProvenanceRecovery = { + recoveryRequired: true, + rollbackErrors, + ...(recoveryFiles.length ? { + recoveryDirectory: retainStaging + ? stagingDirectory + : path.dirname(recoveryFiles[0].path), + recoveryFiles, + } : {}), + }; + } + if (rollbackErrors.length && cause?.archifyDiagnostics?.[0]) { + const [issue] = cause.archifyDiagnostics; + issue.evidence = { + ...(issue.evidence || {}), + rollbackErrors, + ...(recoveryFiles.length ? { + recoveryDirectory: retainStaging + ? stagingDirectory + : path.dirname(recoveryFiles[0].path), + recoveryFile: recoveryFiles[0].path, + recoveryFiles, + } : {}), + }; + } + throw cause; + } finally { + if (candidateBinding) releaseRegularFileBinding(candidateBinding); + if (previousBinding) releaseRegularFileBinding(previousBinding); + if (!retainStaging) removeOwnedEmptyStagingDirectory(stagingDirectory, staging.identity); + } +} + +function recordDeliveryFailure(options) { + const { + output, stage, input, error, receiptId, pathsAlias, ownership: suppliedOwnership, + releaseOwnership: shouldReleaseSuppliedOwnership = false, legacyOutputPaths = [], + captureAtomicOutput, verifyAtomicOutput, fileBindingRuntime, + } = options; + if (!output || !/\.html?$/i.test(output)) return { ok: true, status: 'not-applicable' }; + let ownership = suppliedOwnership; + let acquiredHere = false; + if (!ownership) { + try { + ownership = acquireDeliveryLock( + output, + receiptId, + input, + pathsAlias, + (lockError, initializingOwnership) => recordDeliveryFailure({ + ...options, + error: `Could not start delivery for "${output}": ${lockError.message}`, + ownership: initializingOwnership, + releaseOwnership: false, + }), + legacyOutputPaths, + fileBindingRuntime, + ); + acquiredHere = true; + } catch (lockError) { + const reportedLockError = lockError?.code === 'ARCHIFY_SIDECAR_NAMESPACE_INDETERMINATE' + && lockError.sidecarNamespaceReason?.code === 'sidecar-directory-missing' + ? Object.assign( + new Error(`Delivery lock parent directory does not exist for "${output}".`), + { code: 'ENOENT', cause: lockError }, + ) + : lockError; + return { + ...(lockError.deliveryFailureRecord || { ok: false, status: 'unrecorded' }), + lockError: reportedLockError, + }; + } + } + const state = deliveryOwnershipStates.get(ownership); + let recorded; + if (!state?.deliveryTargets) { + try { + captureDeliveryTargetState({ + state, + requestedOutput: legacyOutputPaths[0] || output, + pathsAlias, + captureAtomicOutput, + verifyAtomicOutput, + }); + } catch (captureError) { + recorded = { + ok: false, + status: 'unrecorded', + diagnostic: captureError.archifyDiagnostics?.[0] || diagnostic({ + code: 'output/target-indeterminate', + message: captureError.message, + subject: { output }, + evidence: { ...(captureError?.code ? { systemCode: captureError.code } : {}) }, + supportedFixes: ['use ordinary absent or regular-file delivery targets, then retry'], + }), + }; + } + } + const stableOutput = state?.output || canonicalExistingDeliveryOutput(output); + // Keep independent evidence even when the artifact is unreadable or the + // provenance target is locked, aliases the input, or cannot be replaced. + let journalError; + if (!recorded && !state?.pendingIdentity) { + try { + beginDeliveryAttempt({ ownership, input, pathsAlias, fileBindingRuntime }); + } catch (cause) { + if (cause.deliveryOwnershipCode === 'delivery/ownership-lost') { + recorded = { ok: false, status: 'unrecorded', ownershipError: cause }; + } else if (cause.deliveryJournalRecovery?.recoveryRequired) { + recorded = { + ok: false, + status: 'unrecorded', + journalRecovery: cause, + diagnostic: deliveryJournalRecoveryDiagnostic(stableOutput, cause), + }; + } + journalError = cause; + } + } + if (!recorded) { + let artifact; + let artifactBinding; + const { + captureRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + try { + if (typeof captureRegularFileBinding === 'function') { + const artifactCapture = captureRegularFileBinding(stableOutput, { + subject: 'failed-delivery-artifact', + expectedLinks: 1, + includeContent: true, + }); + if (artifactCapture.status === 'captured') { + artifact = artifactCapture.content.buffer; + artifactBinding = artifactCapture.binding; + } else if (['ENOENT', 'ENOTDIR'].includes(artifactCapture.reason?.systemCode)) { + recorded = { ok: true, status: 'absent' }; + } + // A failed marker does not need an artifact hash to invalidate old + // evidence. Non-regular or changing paths are never followed. + } + if (!recorded) { + const provenancePath = state?.provenancePath || deliveryProvenancePath(stableOutput); + let aliasesProtectedPath; + try { + aliasesProtectedPath = (input && pathsAlias(provenancePath, input)) + || pathsAlias(provenancePath, stableOutput); + } catch (aliasError) { + recorded = { + ok: false, + status: 'unrecorded', + diagnostic: diagnostic({ + code: 'delivery/provenance-path-resolution', + message: 'Delivery failure provenance could not resolve its target safely.', + subject: { output: stableOutput, provenance: provenancePath }, + evidence: { + reason: aliasError.message, + ...(aliasError?.code ? { systemCode: aliasError.code } : {}), + }, + supportedFixes: ['remove the sidecar path conflict or symbolic-link cycle, then rerun deliver'], + }), + }; + } + if (!recorded && aliasesProtectedPath) { + recorded = { + ok: false, + status: 'unrecorded', + diagnostic: diagnostic({ + code: 'delivery/provenance-target-alias', + message: 'Delivery failure provenance could not be recorded without replacing an input or the artifact.', + subject: { output: stableOutput, provenance: provenancePath }, + evidence: { ...(input ? { input: path.resolve(input) } : {}) }, + supportedFixes: ['choose an output whose .delivery.json sidecar is distinct from every input and the HTML artifact'], + }), + }; + } + if (!recorded) { + try { + writeCapturedDeliveryProvenance(provenancePath, { + schemaVersion: 1, + receiptId, + status: 'failed', + command: 'deliver', + stage, + input, + output: stableOutput, + ...(artifact ? { artifact: artifactIdentity(artifact) } : {}), + error, + }, { + beforeCommit: () => { + assertDeliveryOwnership(ownership, 'record failed delivery provenance', { + allowInitializing: true, + pending: state?.pendingIdentity ? 'owned' : 'ignore', + }); + if (artifactBinding) { + const verification = verifyRegularFileBinding(artifactBinding, { + filePath: stableOutput, + expectedLinks: 1, + }); + if (verification.status !== 'match') { + throw deliveryTargetStateError( + stableOutput, + 'failed delivery artifact', + verification, + ); + } + } + }, + capture: state?.deliveryTargets?.provenance, + verifyAtomicOutput, + fileBindingRuntime, + }); + recorded = { ok: true, status: 'failed' }; + } catch (writeError) { + if (writeError.deliveryOwnershipCode === 'delivery/ownership-lost') { + recorded = { ok: false, status: 'unrecorded', ownershipError: writeError }; + } else if (writeError.deliveryProvenanceRecovery?.recoveryRequired) { + const recovery = writeError.deliveryProvenanceRecovery; + recorded = { + ok: false, + status: 'unrecorded', + provenanceRecovery: writeError, + diagnostic: diagnostic({ + code: 'delivery/provenance-recovery-required', + message: 'Failed-delivery provenance could not be rolled back safely.', + subject: { output: stableOutput, provenance: provenancePath }, + evidence: { + reason: writeError.message, + recoveryRequired: true, + ...(recovery.recoveryDirectory ? { recoveryDirectory: recovery.recoveryDirectory } : {}), + ...(recovery.recoveryFiles?.length ? { recoveryFiles: recovery.recoveryFiles } : {}), + ...(recovery.rollbackErrors?.length ? { rollbackErrors: recovery.rollbackErrors } : {}), + }, + supportedFixes: ['leave the delivery lock and journal untouched, inspect the retained recovery files, then recover serially'], + }), + }; + } else if (writeError.deliveryTargetState) { + recorded = { + ok: false, + status: 'unrecorded', + diagnostic: writeError.archifyDiagnostics?.[0], + }; + } else { + recorded = { + ok: false, + status: 'unrecorded', + diagnostic: diagnostic({ + code: 'delivery/provenance-write', + message: 'The failed delivery could not persist its stale-artifact marker.', + subject: { output: stableOutput, provenance: provenancePath }, + evidence: { + reason: writeError.message, + ...(writeError?.code ? { systemCode: writeError.code } : {}), + failureJournalRecorded: !journalError, + ...(journalError ? { journalError: journalError.message } : {}), + }, + supportedFixes: ['restore write access to the output directory, then rerun deliver before trusting the artifact'], + }), + }; + } + } + } + } + } finally { + if (artifactBinding && typeof releaseRegularFileBinding === 'function') { + releaseRegularFileBinding(artifactBinding); + } + } + } + if (recorded?.journalRecovery || recorded?.provenanceRecovery) return recorded; + if (acquiredHere || shouldReleaseSuppliedOwnership) { + try { + releaseDeliveryOwnership(ownership, { allowInitializing: true }); + } catch (lockError) { + if (lockError.deliveryOwnershipCode === 'delivery/ownership-lost') { + const ownershipError = recorded.ownershipError || lockError; + if (options.recoveryDirectory) { + ownershipError.deliveryCommitDetails = { + ...(ownershipError.deliveryCommitDetails || {}), + recoveryRequired: true, + recoveryDirectory: options.recoveryDirectory, + recoverableBackups: [], + }; + } + return { + ...recorded, + ok: false, + status: 'unrecorded', + ownershipError, + lockError, + }; + } + return { ...recorded, lockError: recorded.ownershipError || lockError }; + } + } + return recorded; +} + +function deliverySuccessProvenance(receipt) { + return { + schemaVersion: 1, + receiptId: receipt.receiptId, + status: 'current', + command: 'deliver', + type: receipt.type, + input: receipt.input, + output: receipt.output, + specification: receipt.specification, + artifact: receipt.artifact, + }; +} + +function inspectDeliveryProvenance(artifactPath, artifact, { + requireProvenance = false, + pathIdentityRuntime, + expectedSidecar, + inspection, +} = {}) { + let sidecar = deliveryProvenancePath(artifactPath); + const rawHeadSidecar = rawHeadDeliveryProvenancePath(artifactPath); + const pending = deliveryPendingPath(artifactPath); + const rawHeadPending = rawHeadDeliveryPendingPath(artifactPath); + const legacyExtensionPending = legacyExtensionDeliverySidecarPath( + artifactPath, + '.delivery-pending.json', + ); + const legacyExtensionSidecar = legacyExtensionDeliverySidecarPath( + artifactPath, + '.delivery.json', + ); + const stableOutput = canonicalExistingDeliveryOutput(artifactPath); + const locks = [ + deliveryLockPath(stableOutput), + ...legacyDeliveryLockPaths([artifactPath, stableOutput]), + ]; + let foundPending; + try { + for (const candidate of [...new Set([ + pending, + rawHeadPending, + legacyExtensionPending, + ].filter(Boolean))]) { + if (pathEntryExists(candidate)) { + foundPending = candidate; + break; + } + } + if (foundPending) { + return { + ok: false, status: 'failed', + diagnostics: [diagnostic({ + code: 'delivery/provenance-failed', + message: 'A delivery attempt is unfinished or failed; complete a successful deliver before trusting this artifact.', + subject: { artifact: path.resolve(artifactPath), provenance: sidecar }, + evidence: { pendingJournal: foundPending, found: true }, + supportedFixes: ['finish any active delivery, then rerun deliver successfully; retain the pending journal until recovery succeeds'], + })], + }; + } + } catch (error) { + return invalidProvenance(artifactPath, foundPending || pending, error.message); + } + for (const lock of locks) { + try { + if (pathEntryExists(lock)) { + return { + ok: false, status: 'locked', + diagnostics: [diagnostic({ + code: 'delivery/provenance-locked', + message: 'Delivery ownership is unresolved while the output lock remains; complete or recover delivery before trusting this artifact.', + subject: { artifact: path.resolve(artifactPath), lock }, + evidence: { deliveryLock: lock, found: true }, + supportedFixes: ['finish the active delivery or inspect and recover the preserved lock, then rerun deliver successfully'], + })], + }; + } + } catch (error) { + return invalidProvenance(artifactPath, lock, error.message); + } + } + let found; + try { + found = pathEntryExists(sidecar); + } catch (error) { + return invalidProvenance(artifactPath, sidecar, error.message); + } + if (!found && rawHeadSidecar) { + try { + found = pathEntryExists(rawHeadSidecar); + if (found) sidecar = rawHeadSidecar; + } catch (error) { + return invalidProvenance(artifactPath, rawHeadSidecar, error.message); + } + } + if (!found && legacyExtensionSidecar) { + try { + found = pathEntryExists(legacyExtensionSidecar); + if (found) sidecar = legacyExtensionSidecar; + } catch (error) { + return invalidProvenance(artifactPath, legacyExtensionSidecar, error.message); + } + } + if (!found) { + if (!requireProvenance) return { ok: true, status: 'unknown' }; + const message = 'Delivery provenance is required, but the artifact has no .delivery.json sidecar.'; + return { + ok: false, + status: 'unknown', + diagnostics: [diagnostic({ + code: 'delivery/provenance-required', + message, + subject: { artifact: path.resolve(artifactPath), provenance: sidecar }, + evidence: { required: true, found: false }, + supportedFixes: ['rerun deliver for the current specification, then retry with --require-provenance'], + })], + }; + } + let receipt; + let sidecarCapture; + try { + const { + captureRegularFileBinding, + releaseRegularFileBinding, + } = pathIdentityRuntime || {}; + if (typeof captureRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw new Error('The regular-file binding runtime is unavailable.'); + } + sidecarCapture = captureRegularFileBinding(sidecar, { + subject: 'delivery-provenance', + ...(expectedSidecar ? { + expectedIdentity: expectedSidecar.identity, + expectedSha256: expectedSidecar.content.sha256, + expectedBytes: expectedSidecar.content.bytes, + expectedMode: expectedSidecar.mode, + } : {}), + expectedLinks: 1, + includeContent: true, + }); + if (sidecarCapture.status !== 'captured') { + if (sidecarCapture.reason?.code === 'delivery-provenance-hardlinked') { + return { + ok: false, + status: 'unsupported', + diagnostics: [diagnostic({ + code: 'delivery/provenance-hardlink-unsupported', + message: 'Delivery provenance is hard-linked and cannot prove that every linked name was updated atomically.', + subject: { artifact: path.resolve(artifactPath), provenance: sidecar }, + evidence: { links: sidecarCapture.reason.links }, + supportedFixes: ['remove extra hard links to the provenance sidecar, then rerun deliver'], + })], + }; + } + throw new Error(`Delivery provenance is not a stable regular file (${sidecarCapture.reason?.code || 'unknown target state'}).`); + } + receipt = JSON.parse(sidecarCapture.content.buffer.toString('utf8')); + const identityValid = (value) => value && typeof value.sha256 === 'string' && /^[0-9a-f]{64}$/.test(value.sha256) + && Number.isSafeInteger(value.bytes) && value.bytes >= 0; + if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) + || receipt.schemaVersion !== 1 || receipt.command !== 'deliver' + || typeof receipt.receiptId !== 'string' + || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(receipt.receiptId) + || typeof receipt.output !== 'string' || !path.isAbsolute(receipt.output) + || typeof receipt.input !== 'string' || !path.isAbsolute(receipt.input) + || !['current', 'failed'].includes(receipt.status) + || (receipt.status === 'current' && (!TYPES.has(receipt.type) + || !identityValid(receipt.specification) || !identityValid(receipt.artifact))) + || (receipt.status === 'failed' && typeof receipt.stage !== 'string')) { + throw new Error('Delivery provenance does not conform to the supported receipt schema.'); + } + if (inspection) { + inspection.sidecar = { + path: sidecar, + identity: { + device: sidecarCapture.identity.device, + inode: sidecarCapture.identity.inode, + }, + content: { + sha256: sidecarCapture.content.sha256, + bytes: sidecarCapture.content.bytes, + }, + mode: sidecarCapture.mode, + }; + } + } catch (error) { + const message = `Could not read delivery provenance: ${error.message}`; + return { + ok: false, + status: 'invalid', + diagnostics: [diagnostic({ + code: 'delivery/provenance-invalid', + message, + subject: { artifact: path.resolve(artifactPath), provenance: sidecar }, + evidence: { reason: error.message }, + supportedFixes: ['rerun deliver to replace the invalid provenance sidecar'], + })], + }; + } finally { + if (sidecarCapture?.binding) { + pathIdentityRuntime.releaseRegularFileBinding(sidecarCapture.binding); + } + } + let outputIdentity; + if (typeof pathIdentityRuntime?.sameEntry !== 'function') { + const error = pathIdentityRuntime?.error; + outputIdentity = { + status: 'unknown', + reason: { + code: 'path-semantics-runtime-unavailable', + ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}), + }, + }; + } else { + try { + outputIdentity = pathIdentityRuntime.sameEntry(receipt.output, artifactPath); + } catch (error) { + outputIdentity = { + status: 'unknown', + reason: { + code: 'path-identity-check-failed', + ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}), + }, + }; + } + } + if (outputIdentity.status === 'unknown') { + return { + ok: false, + status: 'invalid', + diagnostics: [diagnostic({ + code: 'delivery/provenance-output-indeterminate', + message: 'Delivery provenance could not determine whether its recorded output identifies the inspected artifact.', + subject: { + artifact: path.resolve(artifactPath), + provenance: sidecar, + recordedOutput: receipt.output, + }, + evidence: { pathIdentity: outputIdentity.reason }, + supportedFixes: ['restore access to both the artifact and its recorded output path, then rerun deliver successfully'], + })], + }; + } + if (outputIdentity.status !== 'match') { + return invalidProvenance( + artifactPath, + sidecar, + 'The recorded delivery output does not identify this artifact.', + { recordedOutput: receipt.output, pathIdentity: outputIdentity.reason }, + ); + } + const actualSha256 = createHash('sha256').update(artifact).digest('hex'); + if (receipt.status === 'failed') { + const message = 'The artifact is stale because the latest delivery attempt failed.'; + return { + ok: false, + status: 'failed', + receiptId: receipt.receiptId, + diagnostics: [diagnostic({ + code: 'delivery/provenance-failed', + message, + subject: { artifact: path.resolve(artifactPath), provenance: sidecar }, + evidence: { stage: receipt.stage, deliveryReceiptId: receipt.receiptId }, + supportedFixes: ['repair the specification and complete a successful deliver before checking this artifact'], + })], + }; + } + if (receipt.artifact?.sha256 !== actualSha256 || receipt.artifact?.bytes !== artifact.byteLength) { + const message = 'The artifact bytes do not match their delivery provenance.'; + return { + ok: false, + status: 'mismatch', + receiptId: receipt.receiptId, + diagnostics: [diagnostic({ + code: 'delivery/provenance-mismatch', + message, + subject: { artifact: path.resolve(artifactPath), provenance: sidecar }, + evidence: { + expectedSha256: receipt.artifact?.sha256, + actualSha256, + expectedBytes: receipt.artifact?.bytes, + actualBytes: artifact.byteLength, + }, + supportedFixes: ['restore the delivered artifact or rerun deliver to create a matching artifact and provenance pair'], + })], + }; + } + return { ok: true, status: 'current', receiptId: receipt.receiptId, artifact: receipt.artifact }; +} + +function invalidProvenance(artifactPath, sidecar, reason, evidence = {}) { + return { + ok: false, status: 'invalid', + diagnostics: [diagnostic({ + code: 'delivery/provenance-invalid', message: 'Delivery provenance could not be verified.', + subject: { artifact: artifactPath, provenance: sidecar }, evidence: { reason, ...evidence }, + supportedFixes: ['restore access to the delivery records, then rerun deliver successfully'], + })], + }; +} + +function usage() { + return `Usage: + archify render [output.html] [--quality standard|showcase] [--repo-root path] + archify compare architecture [output.html] [--receipt path] [--json] [--quality standard|showcase] [--repo-root path] + archify deliver [output.html] [--json] [--open] [--quality standard|showcase] [--repo-root path] + archify finalize [--json] [--receipt path] [--out-dir ] [--quality standard|showcase] [--repo-root path] [--candidate-sha256 hex] + archify preview [output.html] [--no-open] [--quality standard|showcase] [--repo-root path] + archify validate [--json] [--layout-json] [--quality standard|showcase] [--repo-root path] + archify migrate workflow --to-schema 2 [--output portable.html] [--json] [--repo-root path] + archify inspect + archify check [--json] [--require-provenance] + archify browser-check [--json|--summary] [--require-provenance] [--out-dir ] + archify visual-check [--json|--summary] [--require-provenance] [--out-dir ] + archify guide [scenario or question] [--json] [--lang en|zh] + archify brands [name, alias, domain, or category] [--json] + archify brands capture [--json] + archify examples + archify doctor + archify demo [output-directory] + +Types: + architecture, workflow, sequence, dataflow, lifecycle +`; +} + +function fail(message, code = 2) { + console.error(message); + process.exit(code); +} + +function rejectCliArgument(message, details = {}) { + const error = new Error(message); + error.archifyArgument = { + code: details.code || 'cli/invalid-arguments', + subject: details.subject || {}, + evidence: details.evidence || {}, + supportedFixes: details.supportedFixes || ['correct the command arguments and retry'], + }; + throw error; +} + +function rendererPath(type) { + if (!TYPES.has(type)) { + rejectCliArgument(`Unknown diagram type "${type}". Expected one of: ${[...TYPES].join(', ')}`, { + code: 'cli/unknown-diagram-type', + subject: { type }, + evidence: { supportedTypes: [...TYPES] }, + supportedFixes: [`use one of: ${[...TYPES].join(', ')}`], + }); + } + return path.join(skillRoot, 'renderers', type, `render-${type}.mjs`); +} + +function runNode(args, options = {}) { + return spawnSync(process.execPath, args, { + cwd: options.cwd || process.cwd(), + encoding: 'utf8', + stdio: options.stdio || 'inherit', + env: options.env ? { ...process.env, ...options.env } : process.env, + }); +} + +function extractQualityArgs(args) { + const rest = []; + let quality; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--quality') { + quality = args[index + 1]; + if (!quality || quality.startsWith('--')) rejectCliArgument('--quality requires standard or showcase.', { + code: 'cli/missing-option-value', + subject: { option: '--quality' }, + supportedFixes: ['provide --quality standard or --quality showcase'], + }); + index += 1; + continue; + } + if (arg.startsWith('--quality=')) { + quality = arg.slice('--quality='.length); + if (!quality) rejectCliArgument('--quality requires standard or showcase.', { + code: 'cli/missing-option-value', + subject: { option: '--quality' }, + supportedFixes: ['provide --quality standard or --quality showcase'], + }); + continue; + } + rest.push(arg); + } + if (quality !== undefined && !['standard', 'showcase'].includes(quality)) { + rejectCliArgument(`Unknown quality profile "${quality}". Expected standard or showcase.`, { + code: 'cli/invalid-option-value', + subject: { option: '--quality' }, + evidence: { value: quality, supportedValues: ['standard', 'showcase'] }, + supportedFixes: ['use --quality standard or --quality showcase'], + }); + } + return { rest, quality }; +} + +function extractRepoRootArgs(args) { + const rest = []; + let repoRoot; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--repo-root') { + repoRoot = args[index + 1]; + if (!repoRoot || repoRoot.startsWith('--')) rejectCliArgument('--repo-root requires a repository path.', { + code: 'cli/missing-option-value', + subject: { option: '--repo-root' }, + supportedFixes: ['provide one repository path after --repo-root'], + }); + index += 1; + continue; + } + if (arg.startsWith('--repo-root=')) { + repoRoot = arg.slice('--repo-root='.length); + if (!repoRoot) rejectCliArgument('--repo-root requires a repository path.', { + code: 'cli/missing-option-value', + subject: { option: '--repo-root' }, + supportedFixes: ['provide one repository path after --repo-root'], + }); + continue; + } + rest.push(arg); + } + return { rest, repoRoot: repoRoot ? path.resolve(repoRoot) : undefined }; +} + +function extractOutDirArgs(args) { + const rest = []; + let outDir; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--out-dir') { + outDir = args[index + 1]; + if (!outDir || outDir.startsWith('--')) fail('--out-dir requires a directory path.'); + index += 1; + continue; + } + if (arg.startsWith('--out-dir=')) { + outDir = arg.slice('--out-dir='.length); + if (!outDir) fail('--out-dir requires a directory path.'); + continue; + } + rest.push(arg); + } + return { rest, outDir }; +} + +function rendererEnv(quality, repoRoot, diagnosticJson = false) { + return { + ...(quality ? { ARCHIFY_QUALITY_PROFILE: quality } : {}), + ...(repoRoot ? { ARCHIFY_REPO_ROOT: repoRoot } : {}), + ...(diagnosticJson ? { ARCHIFY_DIAGNOSTIC_FORMAT: 'json' } : {}), + }; +} + +function diagnostic({ code, message, subject = {}, evidence = {}, supportedFixes = [], severity = 'error' }) { + return { + code, + severity, + message, + subject, + evidence, + supportedFixes, + }; +} + +function deliveryLockFailureDiagnostic(output, error) { + const code = error.deliveryOwnershipCode || error.deliveryLockCode || 'delivery/lock-acquire'; + if (code === 'delivery/legacy-pending') { + return diagnostic({ + code, + message: `A pre-namespace delivery journal blocks delivery for "${output}".`, + subject: { output, journal: error.deliveryLockPath }, + evidence: { pendingJournal: error.deliveryLockPath, found: true }, + supportedFixes: ['recover the unfinished delivery and remove only its journal after recovery succeeds'], + }); + } + const lock = error.deliveryOwnershipDetails?.lock + || error.deliveryLockPath + || deliveryLockPath(output); + return diagnostic({ + code, + message: code === 'delivery/lock-release' + ? `Delivery lock could not be released for "${output}": ${error.message}` + : code === 'delivery/ownership-lost' + ? `Delivery ownership was lost for "${output}": ${error.message}` + : `Could not start delivery for "${output}": ${error.message}`, + subject: { output, lock }, + evidence: { + reason: error.message, + ...(error.code ? { systemCode: error.code } : {}), + ...(error.lockCleanupError ? { cleanupError: error.lockCleanupError } : {}), + ...(error.deliveryLockOwner || {}), + ...(error.deliveryOwnershipDetails || {}), + ...(error.deliveryCommitDetails || {}), + }, + supportedFixes: code === 'delivery/concurrent-attempt' + ? ['wait for the active delivery to finish, then retry'] + : code === 'delivery/lock-stale' + ? [ + 'confirm no delivery attempt is active for this output', + `remove only the preserved lock "${lock}", then rerun deliver successfully`, + ] + : code === 'delivery/lock-path-conflict' + ? ['choose an output whose lock path is distinct from the input specification'] + : code === 'delivery/lock-invalid' + ? ['inspect and preserve the unrecognized lock entry, or choose another output path'] + : code === 'delivery/ownership-lost' + ? ['leave the replacement lock and journal untouched, inspect the reported owner, then retry only after that attempt finishes or is recovered'] + : code === 'delivery/lock-release' + ? ['preserve the reported lock, restore permission to remove that exact entry, then recover serially before retrying'] + : ['resolve the reported filesystem error and any lock cleanup error, then retry'], + }); +} + +function deliveryJournalRecoveryDiagnostic(output, error) { + const recovery = error.deliveryJournalRecovery || {}; + const location = recovery.recoveryDirectory + ? ` Recovery material was retained at "${recovery.recoveryDirectory}".` + : ''; + return diagnostic({ + code: 'delivery/journal-recovery-required', + message: `Delivery journal publication could not be rolled back safely.${location}`, + subject: { output, journal: recovery.journal || deliveryPendingPath(output) }, + evidence: { + reason: error.message, + recoveryRequired: true, + ...(recovery.recoveryDirectory ? { recoveryDirectory: recovery.recoveryDirectory } : {}), + ...(recovery.recoveryFiles?.length ? { recoveryFiles: recovery.recoveryFiles } : {}), + ...(recovery.rollbackErrors?.length ? { rollbackErrors: recovery.rollbackErrors } : {}), + }, + supportedFixes: recovery.recoveryDirectory + ? [ + 'leave the delivery lock and current journal claimant untouched', + `inspect every recovery file in ${JSON.stringify(recovery.recoveryDirectory)} and restore the previous journal only after the claimant is resolved`, + ] + : ['leave the delivery lock and journal untouched, inspect their current owners, then recover serially'], + }); +} + +function deliveryCommitRecoveryDiagnostic(output, error) { + const recovery = error.deliveryCommitDetails || {}; + return diagnostic({ + code: 'delivery/commit-recovery-required', + message: `Delivery rollback could not safely restore the previous output pair for "${output}".`, + subject: { output }, + evidence: { + reason: recovery.reason || error.message, + recoveryRequired: true, + ...(recovery.recoveryDirectory ? { recoveryDirectory: recovery.recoveryDirectory } : {}), + ...(recovery.recoverableBackups?.length + ? { recoverableBackups: recovery.recoverableBackups } + : {}), + ...(recovery.rollbackErrors?.length ? { rollbackErrors: recovery.rollbackErrors } : {}), + }, + supportedFixes: [ + 'leave the delivery lock, journal, current claimants, and retained backups untouched', + 'inspect the reported files and restore the previous pair only after resolving every claimant', + ], + }); +} + +function deliveryTargetStateDiagnostic(output, role, result) { + const reason = result?.reason || { code: 'target-state-unavailable' }; + const hardlinked = typeof reason.code === 'string' && reason.code.endsWith('-hardlinked'); + const provenanceTarget = /provenance/iu.test(role); + const code = hardlinked + ? provenanceTarget ? 'delivery/provenance-hardlink-unsupported' : 'output/target-hardlinked' + : ['target-not-regular-file', 'requested-entry-symbolic-link', 'requested-entry-not-regular-file'].includes(reason.code) + ? 'output/target-not-regular-file' + : result?.status === 'different' + ? 'output/target-changed' + : 'output/target-indeterminate'; + const message = hardlinked + ? `The ${role} target is hard-linked and cannot be replaced atomically.` + : code === 'output/target-not-regular-file' + ? `The ${role} target is not a regular file.` + : code === 'output/target-changed' + ? `The ${role} target changed while the verified delivery pair was being prepared.` + : `The ${role} target identity could not be determined safely.`; + return diagnostic({ + code, + message, + subject: { output: path.resolve(output), role }, + evidence: { targetState: reason }, + supportedFixes: hardlinked + ? ['remove extra hard links or choose a new output path, then rerun deliver'] + : code === 'output/target-not-regular-file' + ? ['choose an absent or regular-file target, then rerun deliver'] + : code === 'output/target-changed' + ? ['retry only after other processes stop creating, replacing, retargeting, or changing the output pair'] + : ['use an ordinary local filesystem path with stable file identity, then retry'], + }); +} + +function deliveryTargetStateError(output, role, result) { + const issue = deliveryTargetStateDiagnostic(output, role, result); + const error = new Error(issue.message); + error.archifyDiagnostics = [issue]; + error.deliveryTargetState = result; + return error; +} + +function captureDeliveryTargetState({ + state, + requestedOutput, + pathsAlias, + captureAtomicOutput, + verifyAtomicOutput, + preparedTargets, +}) { + if (typeof captureAtomicOutput !== 'function' || typeof verifyAtomicOutput !== 'function') { + throw deliveryTargetStateError(requestedOutput, 'delivery pair', { + status: 'unknown', + reason: { code: 'atomic-output-runtime-unavailable' }, + }); + } + const artifactTarget = preparedTargets?.artifact || { + requestedPath: requestedOutput, + ...captureAtomicOutput(requestedOutput), + }; + if (artifactTarget.status !== 'captured') { + throw deliveryTargetStateError(requestedOutput, 'HTML artifact', artifactTarget); + } + if (!pathsAlias(artifactTarget.commitPath, state.output)) { + throw deliveryTargetStateError(requestedOutput, 'HTML artifact', { + status: 'different', + reason: { + code: 'write-slot-changed-before-snapshot', + plannedCommitPath: state.output, + currentCommitPath: artifactTarget.commitPath, + }, + }); + } + const requestedProvenancePath = preparedTargets?.provenance?.requestedPath + || deliveryProvenancePath(artifactTarget.commitPath); + const provenanceTarget = preparedTargets?.provenance || { + requestedPath: requestedProvenancePath, + ...captureAtomicOutput(requestedProvenancePath, { + requestedEntryPolicy: 'regular-or-absent', + }), + }; + if (provenanceTarget.status !== 'captured') { + throw deliveryTargetStateError(requestedProvenancePath, 'delivery provenance', provenanceTarget); + } + for (const [role, target] of [ + ['HTML artifact', artifactTarget], + ['delivery provenance', provenanceTarget], + ]) { + const verification = verifyAtomicOutput(target.snapshot); + if (verification.status !== 'match') { + throw deliveryTargetStateError(target.requestedPath, role, verification); + } + } + state.output = artifactTarget.commitPath; + state.provenancePath = provenanceTarget.commitPath; + state.pendingPath = deliveryPendingPath(artifactTarget.commitPath); + state.verifyAtomicOutput = verifyAtomicOutput; + state.deliveryTargets = { + artifact: artifactTarget, + provenance: provenanceTarget, + }; + return state.deliveryTargets; +} + +function inputDiagnostic(error, inputPath) { + const isSyntax = error instanceof SyntaxError; + return diagnostic({ + code: isSyntax ? 'input/json-parse' : 'input/read', + message: isSyntax + ? `Input JSON could not be parsed: ${error.message}` + : `Input could not be read: ${error.message}`, + subject: { input: inputPath }, + evidence: { + ...(error?.code ? { systemCode: error.code } : {}), + reason: error.message, + }, + supportedFixes: [isSyntax + ? 'repair the JSON syntax and run validation again' + : 'provide one readable JSON input file'], + }); +} + +function rendererFailure(result) { + if (result.error) { + return { + error: 'Renderer process could not start.', + diagnostics: [diagnostic({ + code: 'internal/renderer-process', + message: 'Renderer process could not start.', + evidence: { reason: result.error.message }, + })], + }; + } + try { + const payload = JSON.parse((result.stderr || '').trim()); + if (payload?.ok === false && Array.isArray(payload.diagnostics) && payload.diagnostics.length) { + return { + error: payload.error || payload.diagnostics[0].message, + diagnostics: payload.diagnostics, + }; + } + } catch { + // The diagnostic boundary is intentionally fail-closed. Never copy a raw + // Node stack into a machine receipt when a renderer exits unexpectedly. + } + return { + error: 'Renderer failed before emitting a structured diagnostic.', + diagnostics: [diagnostic({ + code: 'internal/unclassified', + message: 'Renderer failed before emitting a structured diagnostic.', + evidence: { exitCode: result.status ?? 1 }, + })], + }; +} + +const COMPOSITION_CHECKS = new Set([ + 'label_route_clearance', + 'relationship_crossings', + 'relationship_corridors', + 'container_border_runs', + 'route_rhythm', +]); + +const CHECK_FIXES = { + single_svg: ['remove additional SVG roots so the artifact contains exactly one diagram SVG'], + finite_svg: ['replace non-finite coordinates before rendering again'], + orthogonal_arrows: ['use renderer-supported orthogonal routing controls'], + legend_clearance: ['move the route or enlarge the viewBox so relationships do not enter the legend'], +}; + +const COMPOSITION_FIXES = { + 'composition/proper-crossing': ['if authored via/route/channelX/channelY controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and adjust route/via or channel coordinates so unrelated relationships use separate corridors'], + 'composition/ambiguous-corridor': ['if authored via/route/channelX/channelY controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and adjust route/via or channel coordinates so unrelated relationships do not visually merge'], + 'composition/container-border-run': ['if authored via/route/channelX/channelY controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and route across the frame perpendicularly through a clear opening'], + 'composition/label-route-clearance': ['if authored labelAt/labelDx/labelDy/labelSegment controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and adjust labelAt, labelDx, labelDy, labelSegment, message y, or the other relationship route'], + 'composition/label-canvas-containment': ['if authored labelAt/labelDx/labelDy/labelSegment controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and adjust labelAt, labelDx, labelDy, or labelSegment so the label rect stays inside the viewBox, or enlarge meta.viewBox'], + 'composition/micro-segment': ['if authored via/route/channelX/channelY controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and move the route/channel/via point so every visible segment is at least 8px'], + 'composition/short-interior-segment': ['if authored via/route/channelX/channelY controls exist and are not required by the user, remove them to let the renderer re-plan; otherwise preserve that intent and move the route/channel/via point so every interior turn has at least 16px'], +}; + +function checkerDiagnostics(checker) { + const diagnostics = []; + for (const issue of checker?.composition?.issues || []) { + if (issue.severity !== 'error') continue; + const { severity, code, relationship, nodeId, ...evidence } = issue; + diagnostics.push(diagnostic({ + code, + severity, + message: `Final artifact failed ${code}.`, + subject: relationship ? { relationship } : { check: 'composition', ...(nodeId ? { nodeId } : {}) }, + evidence, + supportedFixes: compositionFixes(issue), + })); + } + for (const check of checker?.checks || []) { + if (check.ok || COMPOSITION_CHECKS.has(check.name)) continue; + diagnostics.push(diagnostic({ + code: `artifact/${check.name.replaceAll('_', '-')}`, + message: (check.details || []).find(Boolean) || `Final artifact failed ${check.name}.`, + subject: { check: check.name }, + evidence: { details: check.details || [] }, + supportedFixes: CHECK_FIXES[check.name] || [], + })); + } + return diagnostics.length ? diagnostics : [diagnostic({ + code: 'artifact/check-failed', + message: 'Final artifact check failed without a classified diagnostic.', + subject: { check: 'unknown' }, + evidence: {}, + })]; +} + +function compositionFixes(issue) { + if (issue.code === 'composition/viewport-height') return [String(issue.detail || '').replace(/^\[[^\]]+\]\s*/, '')]; + if (issue.code !== 'composition/desktop-readability') return COMPOSITION_FIXES[issue.code] || []; + const sourceFontPx = Number(issue.sourceFontPx); + const actualBudgetPx = Number(issue.availableDiagramWidth); + const hardFloorPx = Number(issue.minimumProjectedFontPx); + const viewBoxWidth = Number(issue.viewBoxWidth); + const preserveIntent = 'Preserve the semantic text and any supplied coordinates, routes, sides, channels, and labels.'; + const readerCap = issue.budgetBasis === 'recognized-declared-wide' + ? ` The declared Reader reports a ${issue.budgetLimit} limit and ${actualBudgetPx}px actual diagram budget; do not assume an uncapped viewport.` + : ''; + if (![sourceFontPx, actualBudgetPx, hardFloorPx, viewBoxWidth].every(Number.isFinite) + || actualBudgetPx <= 0 || hardFloorPx <= 0 || viewBoxWidth <= 0) { + return [`${preserveIntent} Repair the measured source font, desktop budget, or complete viewBox width; position-only label controls do not change projected text size.`]; + } + if (sourceFontPx < hardFloorPx) { + return [`${preserveIntent} The diagnosed ${sourceFontPx}px source text is below the ${hardFloorPx}px hard floor even at scale 1, so use a renderer-supported semantic text-size setting or renderer-level fix. Position-only label controls cannot repair its projection.${readerCap}`]; + } + const maximumViewBoxWidth = Math.floor((sourceFontPx * actualBudgetPx) / hardFloorPx); + return [`${preserveIntent} Compactly reflow automatic spacing and empty corridors so the complete viewBox width is at most ${maximumViewBoxWidth}px (current ${viewBoxWidth}px; ${sourceFontPx}px source text at ${actualBudgetPx}px desktop budget). If supplied geometry fixes that width, use a renderer-supported semantic text-size setting or renderer-level fix instead. Position-only label controls cannot repair its projection.${readerCap}`]; +} + +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'); +} + +function exitFrom(result) { + if (result.error) fail(result.error.message, 1); + process.exit(result.status ?? 1); +} + +function reportCompareFailure({ json, stage, error, code = 'delta/internal', details = {}, status = 1 }) { + const receipt = { + schemaVersion: 1, + ok: false, + command: 'compare', + type: 'architecture', + stage, + error, + diagnostics: [{ + code, + severity: 'error', + message: error, + subject: details.side ? { side: details.side, ...(details.path ? { path: details.path } : {}) } : {}, + evidence: Object.fromEntries(Object.entries(details).filter(([key]) => !['side', 'path', 'supportedFixes'].includes(key))), + supportedFixes: details.supportedFixes || [], + }], + }; + if (json) console.log(JSON.stringify(receipt, null, 2)); + else console.error(formatDiagnostics(error, receipt.diagnostics)); + process.exitCode = status; +} + +function extractCompareOptions(args) { + const positional = []; + let receipt; + let json = false; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--json') { + json = true; + continue; + } + if (arg === '--receipt') { + receipt = args[index + 1]; + if (!receipt || receipt.startsWith('--')) fail('--receipt requires a JSON output path.'); + index += 1; + continue; + } + if (arg.startsWith('--receipt=')) { + receipt = arg.slice('--receipt='.length); + if (!receipt) fail('--receipt requires a JSON output path.'); + continue; + } + if (arg.startsWith('--')) fail(`Unknown compare option "${arg}".`); + positional.push(arg); + } + return { positional, receipt, json }; +} + +function compareReceiptPath(outputPath) { + const { directory, stem, options } = deliverySidecarNamespace(outputPath); + return path.join( + directory, + `${boundedSidecarStem(stem, COMPARE_SIDECAR_SUFFIXES, options)}.receipt.json`, + ); +} + +function compareCommitError(message, code, details = {}) { + const error = new Error(message); + error.compareStage = 'commit'; + error.compareCode = code; + error.compareDetails = details; + return error; +} + +function compareAtomicOutputFailure(result, label, requestedPath, stage) { + const reason = result.reason || { code: 'atomic-output-unknown' }; + let code = 'output/target-indeterminate'; + let message = `The compare ${label} target identity could not be verified safely.`; + let supportedFixes = ['use an ordinary local filesystem path with stable file identity, then retry']; + if (typeof reason.code === 'string' && reason.code.endsWith('-hardlinked')) { + code = 'output/target-hardlinked'; + message = `The compare ${label} target has multiple hard links and cannot be replaced atomically.`; + supportedFixes = [`choose a ${label} path that is absent or names a regular file with one link`]; + } else if (reason.code === 'target-not-regular-file') { + code = 'output/target-not-regular-file'; + message = `The compare ${label} target is not a regular file.`; + supportedFixes = [`choose an absent or regular-file path for the ${label}`]; + } else if (result.status === 'different') { + code = 'output/target-changed'; + message = `The compare ${label} target changed while the artifact pair was being prepared.`; + supportedFixes = ['retry after other processes stop creating, replacing, or redirecting compare outputs']; + } + return { + stage, + code, + message, + details: { + side: label, + path: requestedPath, + atomicOutput: reason, + supportedFixes, + }, + }; +} + +function commitComparePair({ + htmlCandidate, + htmlContent, + receiptCandidate, + receiptContent, + outputCapture, + receiptCapture, + stagingDirectory, + verifyAtomicOutput, + fileBindingRuntime, +}) { + const { + backupPublicRegularFileBinding, + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + const targets = [ + { + label: 'HTML artifact', capture: outputCapture, candidate: htmlCandidate, content: htmlContent, backup: path.join(stagingDirectory, '.previous-output'), + }, + { + label: 'receipt', capture: receiptCapture, candidate: receiptCandidate, content: receiptContent, backup: path.join(stagingDirectory, '.previous-receipt'), + }, + ]; + const targetChanged = (item, result) => { + const normalized = typeof result === 'string' + ? { status: 'different', reason: { code: result } } + : result; + const failure = compareAtomicOutputFailure( + normalized, + item.label, + item.capture.snapshot.requestedPath, + 'commit', + ); + const error = compareCommitError(failure.message, failure.code, failure.details); + error.compareFailure = failure; + return error; + }; + + const backedUp = []; + const committed = []; + let retainSuccessfulBackupBindings = false; + try { + if (typeof backupPublicRegularFileBinding !== 'function' + || typeof captureRegularFileBinding !== 'function' + || typeof quarantineRemoveRegularFileBinding !== 'function' + || typeof verifyRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw targetChanged(targets[0], { + status: 'unknown', + reason: { code: 'atomic-output-runtime-unavailable' }, + }); + } + // Re-resolve and bind both requested write slots and candidate inodes + // before moving either target. The held handles keep bytes, mode and + // identity authoritative across every publish/finalize transition. + for (const item of targets) { + const verification = verifyAtomicOutput(item.capture.snapshot); + if (verification.status !== 'match') throw targetChanged(item, verification); + if (item.capture.mode !== null) fs.chmodSync(item.candidate, item.capture.mode); + const candidate = captureRegularFileBinding(item.candidate, { + subject: 'compare-candidate', + expectedSha256: item.content.sha256, + expectedBytes: item.content.bytes, + ...(item.capture.mode === null ? {} : { expectedMode: item.capture.mode }), + expectedLinks: 1, + }); + if (candidate.status !== 'captured') throw targetChanged(item, candidate); + item.candidateBinding = candidate.binding; + item.candidatePresent = true; + item.candidateIdentity = { + dev: candidate.identity.device, + ino: candidate.identity.inode, + }; + item.candidateMode = candidate.mode; + if (item.capture.snapshot.target.kind === 'file') { + const expected = item.capture.snapshot.target; + const previous = captureRegularFileBinding(item.capture.commitPath, { + subject: 'previous-compare-target', + expectedIdentity: { device: expected.device, inode: expected.inode }, + expectedMode: expected.mode, + expectedLinks: 1, + }); + if (previous.status !== 'captured') throw targetChanged(item, previous); + item.previousBinding = previous.binding; + } + } + for (const item of targets) { + if (item.capture.snapshot.target.kind !== 'file') continue; + const moved = backupPublicRegularFileBinding( + item.previousBinding, + item.capture.commitPath, + item.backup, + { subject: 'previous-compare-target' }, + ); + item.backupPresent = moved.backupCreated === true; + item.backupVerified = moved.backupVerified === true; + if (item.backupPresent) backedUp.push(item); + if (moved.status === 'recovery-required' && moved.recoveryFile) { + item.backupRecovery = moved.recoveryFile; + } + if (moved.status !== 'backed-up') throw targetChanged(item, moved); + } + for (const item of targets) { + const candidate = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.candidate, + expectedLinks: 1, + }); + if (candidate.status !== 'match') throw targetChanged(item, candidate); + try { + // linkSync is the no-clobber publish primitive: unlike renameSync, it + // fails with EEXIST if another writer claims an absent slot after the + // final snapshot verification. + fs.linkSync(item.candidate, item.capture.commitPath); + } catch (error) { + if (error?.code === 'EEXIST') { + throw targetChanged( + item, + 'target-claimed-during-publish', + ); + } + throw error; + } + item.committedIdentity = item.candidateIdentity; + committed.push(item); + const linked = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.capture.commitPath, + expectedLinks: 2, + }); + if (linked.status !== 'match') throw targetChanged(item, linked); + } + for (const item of committed) { + const current = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.capture.commitPath, + expectedLinks: 2, + }); + if (current.status !== 'match') throw targetChanged(item, current); + } + // The target remains the durable link. Removing the staging links inside + // the transaction keeps successful outputs at nlink=1 even if the outer + // best-effort staging-directory cleanup later fails. + for (const item of committed) { + const staged = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.candidate, + expectedLinks: 2, + }); + if (staged.status !== 'match') throw targetChanged(item, staged); + const retired = quarantineRemoveRegularFileBinding( + item.candidateBinding, + item.candidate, + { subject: 'compare-candidate', expectedLinks: 2 }, + ); + if (retired.status !== 'removed') throw targetChanged(item, retired); + item.candidatePresent = false; + } + for (const item of committed) { + const finalized = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.capture.commitPath, + expectedLinks: 1, + }); + if (finalized.status !== 'match') throw targetChanged(item, finalized); + } + retainSuccessfulBackupBindings = true; + return { + ownedBackupEntries: backedUp + .filter((item) => item.backupPresent) + .map((item) => ({ + filePath: item.backup, + binding: item.previousBinding, + subject: 'previous-compare-backup', + expectedLinks: 1, + })), + }; + } catch (cause) { + const rollbackErrors = []; + const recoveryFiles = targets + .filter((item) => item.backupRecovery) + .map((item) => ({ + backup: item.backupRecovery, + target: item.capture.commitPath, + verifiedPrevious: false, + })); + for (const item of [...committed].reverse()) { + try { + const removed = quarantineRemoveRegularFileBinding( + item.candidateBinding, + item.capture.commitPath, + { + subject: 'published-compare-target', + expectedLinks: item.candidatePresent ? 2 : 1, + }, + ); + if (removed.status !== 'removed') { + if (removed.status === 'recovery-required') { + recoveryFiles.push({ + backup: removed.recoveryFile, + target: item.capture.commitPath, + }); + } + throw new Error(`the committed target could not be removed safely (${removed.reason?.code || 'unknown target state'})`); + } + } catch (error) { + rollbackErrors.push(`${item.label}: remove failed (${error.message})`); + } + } + for (const item of [...backedUp].reverse()) { + try { + if (pathEntryExists(item.capture.commitPath)) { + const current = item.candidateBinding + ? verifyRegularFileBinding(item.candidateBinding, { + filePath: item.capture.commitPath, + expectedLinks: item.candidatePresent ? 2 : 1, + }) + : { status: 'different' }; + if (current.status !== 'match') { + throw new Error(`a changed target claimant prevents restoring the previous file (${current.reason?.code || 'unknown target state'})`); + } + const removed = quarantineRemoveRegularFileBinding( + item.candidateBinding, + item.capture.commitPath, + { + subject: 'published-compare-target', + expectedLinks: item.candidatePresent ? 2 : 1, + }, + ); + if (removed.status !== 'removed') { + if (removed.status === 'recovery-required') { + recoveryFiles.push({ + backup: removed.recoveryFile, + target: item.capture.commitPath, + }); + } + throw new Error(`the committed target could not be removed safely (${removed.reason?.code || 'unknown target state'})`); + } + } + if (item.backupVerified !== true) { + throw new Error('the backup entry was never verified as the previous file'); + } + const currentBackup = verifyRegularFileBinding(item.previousBinding, { + filePath: item.backup, + expectedLinks: 1, + }); + if (currentBackup.status !== 'match') { + item.backupVerified = false; + throw new Error('the recoverable backup was replaced before rollback'); + } + // Publish the previous file without clobbering a claimant that appears + // after the absence check. The backup remains the recovery source until + // the restored hard link has been identity-verified. + fs.linkSync(item.backup, item.capture.commitPath); + for (const restoredPath of [item.capture.commitPath, item.backup]) { + const restored = verifyRegularFileBinding(item.previousBinding, { + filePath: restoredPath, + expectedLinks: 2, + }); + if (restored.status !== 'match') { + throw new Error('the restored target identity could not be verified'); + } + } + const retired = quarantineRemoveRegularFileBinding( + item.previousBinding, + item.backup, + { subject: 'previous-compare-target', expectedLinks: 2 }, + ); + if (retired.status !== 'removed') { + throw new Error(`the restored backup could not be retired safely (${retired.reason?.code || 'unknown target state'})`); + } + item.backupPresent = false; + const finalized = verifyRegularFileBinding(item.previousBinding, { + filePath: item.capture.commitPath, + expectedLinks: 1, + }); + if (finalized.status !== 'match') { + throw new Error('the restored target changed before rollback finalized'); + } + } catch (error) { + rollbackErrors.push(`${item.label}: restore failed (${error.message})`); + // Track failed restoration rather than probing existence: a permission + // error must not make cleanup discard a potentially recoverable backup. + if (item.backupPresent !== false) { + recoveryFiles.push({ + backup: item.backup, + target: item.capture.commitPath, + verifiedPrevious: item.backupVerified === true, + }); + } + } + } + if (!rollbackErrors.length && cause.compareFailure) { + throw compareCommitError( + cause.message, + cause.compareFailure.code, + { + ...cause.compareFailure.details, + reason: cause.message, + }, + ); + } + throw compareCommitError( + (rollbackErrors.length + ? 'Architecture Delta pair commit failed and its previous files could not be fully restored.' + : 'Architecture Delta pair commit failed; the previous files were restored.') + + (recoveryFiles.length + ? ` Recovery directory retained at ${path.dirname(recoveryFiles[0].backup)}.` + : ''), + rollbackErrors.length ? 'delta/commit-rollback-failed' : 'delta/commit-failed', + { + reason: cause.message, + ...(rollbackErrors.length ? { rollbackErrors } : {}), + ...(recoveryFiles.length ? { + recoveryDirectory: path.dirname(recoveryFiles[0].backup), + recoveryDirectories: [...new Set(recoveryFiles.map(({ backup }) => path.dirname(backup)))], + recoveryFiles, + } : {}), + supportedFixes: recoveryFiles.length + ? [ + ...recoveryFiles.map(({ backup, target, verifiedPrevious }) => verifiedPrevious + ? `resolve the filesystem error, inspect the current target, then restore ${JSON.stringify(backup)} to ${JSON.stringify(target)} before retrying` + : `preserve and inspect the unverified entry at ${JSON.stringify(backup)}; do not treat it as the previous file for ${JSON.stringify(target)}`), + 'remove the recovery directory only after the previous files have been recovered and verified', + ] + : ['check that both output paths are writable regular files, then retry'], + }, + ); + } finally { + for (const item of targets) { + if (item.candidateBinding) releaseRegularFileBinding(item.candidateBinding); + if (item.previousBinding + && !(retainSuccessfulBackupBindings && item.backupPresent)) { + releaseRegularFileBinding(item.previousBinding); + } + } + } +} + +function commitDeliveryPair({ + htmlCandidate, + htmlContent, + provenanceCandidate, + provenanceContent, + stagingDirectory, + ownership, + verifyAtomicOutput, + fileBindingRuntime, +}) { + const { + backupPublicRegularFileBinding, + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + } = fileBindingRuntime || {}; + const state = assertDeliveryOwnership(ownership, 'start the delivery pair commit', { pending: 'owned' }); + const snapshots = state.deliveryTargets; + if (!snapshots?.artifact || !snapshots?.provenance) { + throw deliveryTargetStateError(state.output, 'delivery pair', { + status: 'unknown', + reason: { code: 'target-snapshot-missing' }, + }); + } + const targets = [ + { + label: 'HTML artifact', + target: snapshots.artifact.commitPath, + candidate: htmlCandidate, + content: htmlContent, + backup: path.join(stagingDirectory, '.previous-output'), + capture: snapshots.artifact, + }, + { + label: 'delivery provenance', + target: snapshots.provenance.commitPath, + candidate: provenanceCandidate, + content: provenanceContent, + backup: path.join(stagingDirectory, '.previous-provenance'), + capture: snapshots.provenance, + }, + ]; + const backedUp = []; + const retainedBackups = new Set(); + const publicRecoveryBackups = []; + let retainSuccessfulBackupBindings = false; + const committed = []; + try { + if (typeof backupPublicRegularFileBinding !== 'function' + || typeof captureRegularFileBinding !== 'function' + || typeof quarantineRemoveRegularFileBinding !== 'function' + || typeof verifyRegularFileBinding !== 'function' + || typeof releaseRegularFileBinding !== 'function') { + throw deliveryTargetStateError(state.output, 'delivery pair', { + status: 'unknown', + reason: { code: 'atomic-output-runtime-unavailable' }, + }); + } + for (const item of targets) { + const verification = verifyAtomicOutput(item.capture.snapshot); + if (verification.status !== 'match') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, verification); + } + if (item.capture.mode !== null) fs.chmodSync(item.candidate, item.capture.mode); + const candidate = captureRegularFileBinding(item.candidate, { + subject: 'delivery-candidate', + expectedSha256: item.content.sha256, + expectedBytes: item.content.bytes, + ...(item.capture.mode === null ? {} : { expectedMode: item.capture.mode }), + expectedLinks: 1, + }); + if (candidate.status !== 'captured') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, candidate); + } + item.candidateBinding = candidate.binding; + item.candidatePresent = true; + item.candidateIdentity = { + dev: candidate.identity.device, + ino: candidate.identity.inode, + }; + item.candidateMode = candidate.mode; + if (item.capture.snapshot.target.kind === 'file') { + const expected = item.capture.snapshot.target; + const previous = captureRegularFileBinding(item.target, { + subject: 'previous-delivery-target', + expectedIdentity: { device: expected.device, inode: expected.inode }, + expectedMode: expected.mode, + expectedLinks: 1, + }); + if (previous.status !== 'captured') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, previous); + } + item.previousBinding = previous.binding; + } + } + for (const item of targets) { + if (item.capture.snapshot.target.kind !== 'file') continue; + assertDeliveryOwnership(ownership, `back up the ${item.label}`, { pending: 'owned' }); + const moved = backupPublicRegularFileBinding( + item.previousBinding, + item.target, + item.backup, + { subject: 'previous-delivery-target' }, + ); + item.backupPresent = moved.backupCreated === true; + item.backupVerified = moved.backupVerified === true; + if (item.backupPresent) backedUp.push(item); + if (moved.status === 'recovery-required' && moved.recoveryFile) { + publicRecoveryBackups.push({ + label: `preserved replacement for ${item.label}`, + path: moved.recoveryFile, + target: item.target, + }); + } + if (moved.status !== 'backed-up') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, moved); + } + retainedBackups.add(item); + } + for (const item of targets) { + assertDeliveryOwnership(ownership, `commit the ${item.label}`, { pending: 'owned' }); + const candidate = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.candidate, + expectedLinks: 1, + }); + if (candidate.status !== 'match') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, candidate); + } + try { + fs.linkSync(item.candidate, item.target); + } catch (error) { + if (error?.code === 'EEXIST') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, { + status: 'different', + reason: { code: 'target-claimed-during-publish' }, + }); + } + throw error; + } + item.committedIdentity = item.candidateIdentity; + committed.push(item); + const linked = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.target, + expectedLinks: 2, + }); + if (linked.status !== 'match') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, linked); + } + } + for (const item of committed) { + assertDeliveryOwnership(ownership, `verify the committed ${item.label}`, { pending: 'owned' }); + const current = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.target, + expectedLinks: 2, + }); + if (current.status !== 'match') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, current); + } + } + for (const item of committed) { + const staged = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.candidate, + expectedLinks: 2, + }); + if (staged.status !== 'match') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, staged); + } + const retired = quarantineRemoveRegularFileBinding( + item.candidateBinding, + item.candidate, + { subject: 'delivery-candidate', expectedLinks: 2 }, + ); + if (retired.status !== 'removed') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, retired); + } + item.candidatePresent = false; + } + for (const item of committed) { + const finalized = verifyRegularFileBinding(item.candidateBinding, { + filePath: item.target, + expectedLinks: 1, + }); + if (finalized.status !== 'match') { + throw deliveryTargetStateError(item.capture.requestedPath, item.label, finalized); + } + } + // Finalization is part of the commit: keep the old files recoverable until + // the journal has been removed and checkers can accept the new artifact. + assertDeliveryOwnership(ownership, 'finalize the delivery journal', { pending: 'owned' }); + quarantineRemoveOwnedDeliveryEntry(state, { + filePath: state.pendingPath, + identity: state.pendingIdentity, + content: state.pendingContent, + subject: 'delivery-journal', + operation: 'finalize the delivery journal', + evidenceKey: 'journal', + }); + assertDeliveryOwnership(ownership, 'finish finalizing the delivery journal', { pending: 'absent' }); + state.pendingIdentity = undefined; + state.pendingContent = undefined; + state.phase = 'finalized'; + retainSuccessfulBackupBindings = true; + return { + recoverableBackups: [ + ...[...retainedBackups].map((item) => ({ label: item.label, path: item.backup })), + ...publicRecoveryBackups, + ], + ownedBackupEntries: backedUp + .filter((item) => item.backupPresent) + .map((item) => ({ + filePath: item.backup, + binding: item.previousBinding, + subject: 'previous-delivery-backup', + expectedLinks: 1, + })), + }; + } catch (cause) { + const recoveryDetails = () => ({ + recoveryRequired: true, + recoveryDirectory: stagingDirectory, + recoverableBackups: [ + ...[...retainedBackups].map((item) => ({ label: item.label, path: item.backup })), + ...publicRecoveryBackups, + ], + }); + if (cause.deliveryOwnershipCode === 'delivery/ownership-lost') { + cause.deliveryCommitDetails = { ...(cause.deliveryCommitDetails || {}), ...recoveryDetails() }; + throw cause; + } + const rollbackErrors = []; + const recoverableBackups = []; + try { + for (const item of [...committed].reverse()) { + try { + assertDeliveryOwnership(ownership, `roll back the ${item.label}`, { pending: 'identity' }); + const removed = quarantineRemoveRegularFileBinding(item.candidateBinding, item.target, { + subject: 'published-delivery-target', + expectedLinks: item.candidatePresent ? 2 : 1, + }); + if (removed.status !== 'removed') { + if (removed.status === 'recovery-required') { + publicRecoveryBackups.push({ + label: `preserved replacement for ${item.label}`, + path: removed.recoveryFile, + target: item.target, + }); + } + throw new Error(`the committed target could not be removed safely (${removed.reason?.code || 'unknown target state'})`); + } + } catch (error) { + if (error.deliveryOwnershipCode === 'delivery/ownership-lost') throw error; + rollbackErrors.push(`${item.label}: remove failed (${error.message})`); + } + } + for (const item of [...backedUp].reverse()) { + try { + assertDeliveryOwnership(ownership, `restore the previous ${item.label}`, { pending: 'identity' }); + if (pathEntryExists(item.target)) { + const current = item.candidateBinding + ? verifyRegularFileBinding(item.candidateBinding, { + filePath: item.target, + expectedLinks: item.candidatePresent ? 2 : 1, + }) + : { status: 'different' }; + if (current.status !== 'match') { + throw new Error(`a changed target claimant prevents restoring the previous file (${current.reason?.code || 'unknown target state'})`); + } + const removed = quarantineRemoveRegularFileBinding(item.candidateBinding, item.target, { + subject: 'published-delivery-target', + expectedLinks: item.candidatePresent ? 2 : 1, + }); + if (removed.status !== 'removed') { + if (removed.status === 'recovery-required') { + publicRecoveryBackups.push({ + label: `preserved replacement for ${item.label}`, + path: removed.recoveryFile, + target: item.target, + }); + } + throw new Error(`the committed target could not be removed safely (${removed.reason?.code || 'unknown target state'})`); + } + } + assertDeliveryOwnership(ownership, `restore the previous ${item.label}`, { pending: 'identity' }); + if (item.backupVerified !== true) { + throw new Error('the backup entry was never verified as the previous delivery file'); + } + const currentBackup = verifyRegularFileBinding(item.previousBinding, { + filePath: item.backup, + expectedLinks: 1, + }); + if (currentBackup.status !== 'match') { + item.backupVerified = false; + retainedBackups.delete(item); + throw new Error('the previous delivery backup identity changed'); + } + try { + fs.linkSync(item.backup, item.target); + } catch (error) { + if (error?.code === 'EEXIST') { + throw new Error('a new target claimant prevents restoring the previous file'); + } + throw error; + } + for (const restoredPath of [item.target, item.backup]) { + const restored = verifyRegularFileBinding(item.previousBinding, { + filePath: restoredPath, + expectedLinks: 2, + }); + if (restored.status !== 'match') { + throw new Error('the restored delivery target identity could not be verified'); + } + } + const retired = quarantineRemoveRegularFileBinding( + item.previousBinding, + item.backup, + { subject: 'previous-delivery-target', expectedLinks: 2 }, + ); + if (retired.status !== 'removed') { + throw new Error(`the restored delivery backup could not be retired safely (${retired.reason?.code || 'unknown target state'})`); + } + item.backupPresent = false; + const restoredFinal = verifyRegularFileBinding(item.previousBinding, { + filePath: item.target, + expectedLinks: 1, + }); + if (restoredFinal.status !== 'match') { + throw new Error('the restored delivery target link count could not be verified'); + } + retainedBackups.delete(item); + } catch (error) { + if (error.deliveryOwnershipCode === 'delivery/ownership-lost') throw error; + rollbackErrors.push(`${item.label}: restore failed (${error.message})`); + // A failed restoration leaves this exact backup at its source path. + // Record it without probing the failing recovery path again. + if (item.backupPresent !== false) { + recoverableBackups.push({ + label: item.backupVerified === true + ? item.label + : `unverified preserved backup entry for ${item.label}`, + path: item.backup, + target: item.target, + verifiedPrevious: item.backupVerified === true, + }); + } + } + } + assertDeliveryOwnership(ownership, 'finish the delivery rollback', { pending: 'identity' }); + } catch (rollbackError) { + if (rollbackError.deliveryOwnershipCode === 'delivery/ownership-lost') { + rollbackError.deliveryCommitDetails = { + ...(rollbackError.deliveryCommitDetails || {}), + commitError: { + reason: cause.message, + ...(cause?.code ? { systemCode: cause.code } : {}), + }, + ...recoveryDetails(), + }; + throw rollbackError; + } + rollbackErrors.push(rollbackError.message); + } + const error = new Error(rollbackErrors.length + ? 'Delivery pair commit failed and its previous files could not be fully restored.' + : 'Delivery pair commit failed; the previous files were restored.'); + error.deliveryCommitDetails = { + reason: cause.message, + ...(rollbackErrors.length ? { + rollbackErrors, + recoveryRequired: true, + recoveryDirectory: stagingDirectory, + recoverableBackups: [...recoverableBackups, ...publicRecoveryBackups], + } : {}), + }; + if (cause.deliveryTargetState) { + error.deliveryTargetState = cause.deliveryTargetState; + error.archifyDiagnostics = cause.archifyDiagnostics; + } + throw error; + } finally { + for (const item of targets) { + if (item.candidateBinding) releaseRegularFileBinding(item.candidateBinding); + if (item.previousBinding + && !(retainSuccessfulBackupBindings && item.backupPresent)) { + releaseRegularFileBinding(item.previousBinding); + } + } + } +} + +function renderValidatedArchitecture(inputPath, outputPath, quality, repoRoot, captureStagingFile) { + const render = runNode([rendererPath('architecture'), inputPath, outputPath], { + stdio: 'pipe', + env: rendererEnv(quality, repoRoot, true), + }); + if (render.status !== 0) { + const failure = rendererFailure(render); + const error = new Error(failure.error); + error.compareStage = 'input'; + error.compareStatus = render.status ?? 1; + error.diagnostics = failure.diagnostics; + throw error; + } + let artifact; + if (captureStagingFile) { + artifact = fs.readFileSync(outputPath); + captureStagingFile(outputPath, artifactIdentity(artifact)); + } + const check = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), outputPath], { stdio: 'pipe' }); + if (check.status !== 0) { + const error = new Error('Validated snapshot failed final artifact checks.'); + error.compareStage = 'check'; + error.compareStatus = check.status ?? 1; + try { + error.checker = JSON.parse(check.stdout); + error.diagnostics = checkerDiagnostics(error.checker); + } catch { + error.diagnostics = []; + } + throw error; + } + artifact ||= fs.readFileSync(outputPath); + return { + artifact, + html: artifact.toString('utf8'), + checks: JSON.parse(check.stdout), + sourceEvidence: sourceEvidenceFromArtifact(artifact), + }; +} + +async function commandCompare(args) { + await loadSidecarPathRuntime(); + const { + canonicalFuturePath, + resolveOutputPath, + validateAuthoredOutputPath, + } = await import('../renderers/shared/output-path.mjs'); + const { + captureAtomicOutput, + verifyAtomicOutput, + backupPublicRegularFileBinding, + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + } = await import('../renderers/shared/atomic-output.mjs'); + const fileBindingRuntime = { + backupPublicRegularFileBinding, + captureRegularFileBinding, + quarantineRemoveRegularFileBinding, + verifyRegularFileBinding, + releaseRegularFileBinding, + }; + const { + sameParent, + sidecarNamespaceComponentKey, + } = await import('../renderers/shared/path-semantics.mjs'); + sidecarNamespaceComponentKeyRuntime = sidecarNamespaceComponentKey; + const qualityArgs = extractQualityArgs(args); + const repoArgs = extractRepoRootArgs(qualityArgs.rest); + const options = extractCompareOptions(repoArgs.rest); + const [type, baseInput, headInput, requestedOutput] = options.positional; + if (type !== 'architecture' || !baseInput || !headInput || options.positional.length > 4) fail(usage()); + let deltaRuntime; + try { + deltaRuntime = await import(pathToFileURL(path.join(skillRoot, 'delta/architecture-delta.mjs')).href); + } catch (error) { + reportCompareFailure({ json: options.json, stage: 'prepare', error: 'Architecture compare runtime is unavailable.', code: 'delta/runtime-missing', details: { reason: error.message, supportedFixes: ['install the complete Archify skill package'] } }); + return; + } + const { + ArchitectureDeltaError, + annotateArchitectureSideSvg, + buildDeltaSvg, + canonicalArchitecture, + canonicalArchitectureJson, + compareArchitecture, + extractArchitectureSvg, + extractArtifactCss, + renderArchitectureDeltaHtml, + validateArchitectureDeltaHtml, + } = deltaRuntime; + + const basePath = path.resolve(baseInput); + const headPath = path.resolve(headInput); + const explicitReceiptTarget = options.receipt ? path.resolve(options.receipt) : undefined; + const rawOutputTarget = requestedOutput || 'architecture-delta.html'; + const rawOutputExtension = path.extname(rawOutputTarget); + // Preserve the established alias-first diagnostic for an already-invalid + // CLI extension without letting this legacy spelling select the real + // sidecar namespace for any valid compare output. + const invalidOutputAliasPreflight = !explicitReceiptTarget + && rawOutputExtension.toLowerCase() !== '.html' + ? path.resolve(rawOutputExtension + ? `${rawOutputTarget.slice(0, -rawOutputExtension.length)}.receipt.json` + : `${rawOutputTarget}.receipt.json`) + : undefined; + let outputPath; + try { + ({ outputPath } = resolveOutputPath({ + requestedOutput, + defaultOutput: 'architecture-delta.html', + inputPaths: [basePath, headPath], + otherOutputPaths: explicitReceiptTarget + ? [explicitReceiptTarget] + : (invalidOutputAliasPreflight ? [invalidOutputAliasPreflight] : []), + })); + } catch (error) { + const outputDiagnostic = error.archifyDiagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: error.message, + code: outputDiagnostic?.code || 'output/path-resolution', + details: { + ...(outputDiagnostic?.subject || {}), + ...(outputDiagnostic?.evidence || {}), + supportedFixes: outputDiagnostic?.supportedFixes || ['choose a safe output path and retry'], + }, + }); + return; + } + const requestedOutputPath = outputPath; + let receiptPath; + let requestedReceiptPath; + if (explicitReceiptTarget) { + try { + ({ outputPath: receiptPath } = resolveOutputPath({ + requestedOutput: options.receipt, + defaultOutput: options.receipt, + requiredExtension: '.json', + inputPaths: [basePath, headPath], + otherOutputPaths: [outputPath], + })); + requestedReceiptPath = receiptPath; + } catch (error) { + const outputDiagnostic = error.archifyDiagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: error.message, + code: outputDiagnostic?.code || 'output/path-resolution', + details: { + ...(outputDiagnostic?.subject || {}), + ...(outputDiagnostic?.evidence || {}), + supportedFixes: outputDiagnostic?.supportedFixes || ['choose a safe receipt path and retry'], + }, + }); + return; + } + } + try { + outputPath = canonicalFuturePath(requestedOutputPath); + if (requestedReceiptPath) receiptPath = canonicalFuturePath(requestedReceiptPath); + } catch (error) { + const outputDiagnostic = error.archifyDiagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: error.message, + code: outputDiagnostic?.code || 'output/path-resolution', + details: { + ...(outputDiagnostic?.subject || {}), + ...(outputDiagnostic?.evidence || {}), + supportedFixes: outputDiagnostic?.supportedFixes || ['choose safe compare output paths and retry'], + }, + }); + return; + } + let baseBuffer; + let headBuffer; + let base; + let head; + try { + baseBuffer = fs.readFileSync(basePath); + base = JSON.parse(baseBuffer.toString('utf8')); + } catch (error) { + reportCompareFailure({ json: options.json, stage: 'input', error: `Could not read base input: ${error.message}`, code: 'delta/base-input', details: { side: 'base', reason: error.message } }); + return; + } + try { + headBuffer = fs.readFileSync(headPath); + head = JSON.parse(headBuffer.toString('utf8')); + } catch (error) { + reportCompareFailure({ json: options.json, stage: 'input', error: `Could not read head input: ${error.message}`, code: 'delta/head-input', details: { side: 'head', reason: error.message } }); + return; + } + + for (const [side, document] of [['base', base], ['head', head]]) { + try { + validateAuthoredOutputPath(document?.meta?.output); + } catch (error) { + const diagnosticEntry = error.archifyDiagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: 'input', + error: `${side === 'base' ? 'Base' : 'Head'} snapshot failed validation: ${error.message}`, + code: diagnosticEntry?.code || `delta/${side}-validation`, + details: { + side, + ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), + ...(diagnosticEntry?.evidence || {}), + supportedFixes: diagnosticEntry?.supportedFixes || [], + }, + }); + return; + } + } + + let outputDirectory = path.dirname(outputPath); + if (receiptPath) { + const explicitReceiptDirectoryIdentity = sameParent(outputPath, receiptPath); + if (explicitReceiptDirectoryIdentity.status === 'unknown') { + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: 'The compare receipt directory identity could not be verified.', + code: 'delta/receipt-directory-identity-indeterminate', + details: { + artifactDirectory: outputDirectory, + receiptDirectory: path.dirname(receiptPath), + pathIdentity: explicitReceiptDirectoryIdentity.reason, + supportedFixes: ['restore access to both output directories or choose paths in one verifiable directory'], + }, + }); + return; + } + if (explicitReceiptDirectoryIdentity.status !== 'match') { + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: 'The compare receipt must be written beside the HTML artifact.', + code: 'delta/receipt-directory', + details: { + artifactDirectory: outputDirectory, + receiptDirectory: path.dirname(receiptPath), + pathIdentity: explicitReceiptDirectoryIdentity.reason, + supportedFixes: ['choose a --receipt path in the same directory as output.html'], + }, + }); + return; + } + } + try { + fs.mkdirSync(outputDirectory, { recursive: true }); + } catch (error) { + reportCompareFailure({ json: options.json, stage: 'prepare', error: `Could not create compare output directory: ${error.message}`, code: 'delta/output-directory', details: { reason: error.message } }); + return; + } + + if (!receiptPath) { + try { + const defaultReceiptPath = compareReceiptPath(outputPath); + ({ outputPath: receiptPath } = resolveOutputPath({ + requestedOutput: defaultReceiptPath, + defaultOutput: defaultReceiptPath, + requiredExtension: '.json', + inputPaths: [basePath, headPath], + otherOutputPaths: [outputPath], + })); + requestedReceiptPath = receiptPath; + receiptPath = canonicalFuturePath(requestedReceiptPath); + } catch (error) { + const outputDiagnostic = error.archifyDiagnostics?.[0]; + const namespaceIndeterminate = error?.code === 'ARCHIFY_SIDECAR_NAMESPACE_INDETERMINATE'; + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: error.message, + code: namespaceIndeterminate + ? 'delta/receipt-namespace-indeterminate' + : outputDiagnostic?.code || 'output/path-resolution', + details: { + ...(outputDiagnostic?.subject || {}), + ...(outputDiagnostic?.evidence || {}), + ...(namespaceIndeterminate ? { pathIdentity: error.sidecarNamespaceReason } : {}), + supportedFixes: outputDiagnostic?.supportedFixes + || ['choose a safe receipt path beside the compare artifact and retry'], + }, + }); + return; + } + } + + const receiptDirectoryIdentity = sameParent(outputPath, receiptPath); + if (receiptDirectoryIdentity.status === 'unknown') { + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: 'The compare receipt directory identity could not be verified.', + code: 'delta/receipt-directory-identity-indeterminate', + details: { + artifactDirectory: outputDirectory, + receiptDirectory: path.dirname(receiptPath), + pathIdentity: receiptDirectoryIdentity.reason, + supportedFixes: ['restore access to both output directories or choose paths in one verifiable directory'], + }, + }); + return; + } + if (receiptDirectoryIdentity.status !== 'match') { + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: 'The compare receipt must be written beside the HTML artifact.', + code: 'delta/receipt-directory', + details: { + artifactDirectory: outputDirectory, + receiptDirectory: path.dirname(receiptPath), + pathIdentity: receiptDirectoryIdentity.reason, + supportedFixes: ['choose a --receipt path in the same directory as output.html'], + }, + }); + return; + } + + const outputCapture = captureAtomicOutput(requestedOutputPath); + const receiptCapture = captureAtomicOutput(requestedReceiptPath); + for (const [capture, label, requestedPath] of [ + [outputCapture, 'HTML artifact', requestedOutputPath], + [receiptCapture, 'receipt', requestedReceiptPath], + ]) { + if (capture.status === 'captured') continue; + const failure = compareAtomicOutputFailure(capture, label, requestedPath, 'prepare'); + reportCompareFailure({ + json: options.json, + stage: failure.stage, + error: failure.message, + code: failure.code, + details: failure.details, + }); + return; + } + const outputParent = outputCapture.snapshot.slot; + const receiptParent = receiptCapture.snapshot.slot; + if (outputParent.parentDevice !== receiptParent.parentDevice + || outputParent.parentInode !== receiptParent.parentInode) { + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: 'The compare receipt must be written beside the HTML artifact.', + code: 'delta/receipt-directory', + details: { + artifactDirectory: outputParent.parentPath, + receiptDirectory: receiptParent.parentPath, + supportedFixes: ['choose a --receipt path in the same physical directory as output.html'], + }, + }); + return; + } + outputPath = outputCapture.commitPath; + receiptPath = receiptCapture.commitPath; + outputDirectory = path.dirname(outputPath); + + let stagingDirectory; + let stagingIdentity; + try { + const staging = createOwnedEmptyStagingDirectory( + path.join(outputDirectory, '.archify-compare-'), + ); + stagingDirectory = staging.directory; + stagingIdentity = staging.identity; + } catch (error) { + reportCompareFailure({ json: options.json, stage: 'prepare', error: `Could not create compare candidate: ${error.message}`, code: 'delta/candidate-directory', details: { reason: error.message } }); + return; + } + + const baseCandidate = path.join(stagingDirectory, 'base.html'); + const headCandidate = path.join(stagingDirectory, 'head.html'); + const rawBaseCandidate = path.join(stagingDirectory, 'base.raw.html'); + const rawHeadCandidate = path.join(stagingDirectory, 'head.raw.html'); + const rawBaseInput = path.join(stagingDirectory, 'base.snapshot.json'); + const rawHeadInput = path.join(stagingDirectory, 'head.snapshot.json'); + const canonicalBaseInput = path.join(stagingDirectory, 'base.architecture.json'); + const canonicalHeadInput = path.join(stagingDirectory, 'head.architecture.json'); + const htmlCandidate = path.join(stagingDirectory, path.basename(outputPath)); + const receiptCandidate = path.join(stagingDirectory, path.basename(receiptPath)); + let preserveRecoveryDirectory = false; + const stagingOwnership = []; + const captureCompareStagingFile = (filePath, content) => captureOwnedStagingFile( + stagingOwnership, + filePath, + fileBindingRuntime, + { subject: 'compare-staging-entry', content }, + ); + + try { + let baseResult; + let headResult; + for (const { side, snapshotPath, buffer } of [ + { side: 'base', snapshotPath: rawBaseInput, buffer: baseBuffer }, + { side: 'head', snapshotPath: rawHeadInput, buffer: headBuffer }, + ]) { + try { + fs.writeFileSync(snapshotPath, buffer, { flag: 'wx' }); + captureCompareStagingFile(snapshotPath, artifactIdentity(buffer)); + } catch (error) { + const message = `Could not freeze ${side} compare snapshot: ${error.message}`; + reportCompareFailure({ + json: options.json, + stage: 'prepare', + error: message, + code: 'delta/freeze-snapshot', + details: { + side, + ...(error?.code ? { systemCode: error.code } : {}), + reason: error.message, + supportedFixes: ['choose a writable compare output directory on the target filesystem'], + }, + }); + return; + } + } + try { + renderValidatedArchitecture( + rawBaseInput, + rawBaseCandidate, + qualityArgs.quality, + repoArgs.repoRoot, + captureCompareStagingFile, + ); + } catch (error) { + const diagnosticEntry = error.diagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: error.compareStage || 'validate', + error: `Base snapshot failed validation: ${error.message}`, + code: diagnosticEntry?.code || 'delta/base-validation', + details: { side: 'base', ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), ...(diagnosticEntry?.evidence || {}), supportedFixes: diagnosticEntry?.supportedFixes || [] }, + status: error.compareStatus || 1, + }); + return; + } + try { + renderValidatedArchitecture( + rawHeadInput, + rawHeadCandidate, + qualityArgs.quality, + repoArgs.repoRoot, + captureCompareStagingFile, + ); + } catch (error) { + const diagnosticEntry = error.diagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: error.compareStage || 'validate', + error: `Head snapshot failed validation: ${error.message}`, + code: diagnosticEntry?.code || 'delta/head-validation', + details: { side: 'head', ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), ...(diagnosticEntry?.evidence || {}), supportedFixes: diagnosticEntry?.supportedFixes || [] }, + status: error.compareStatus || 1, + }); + return; + } + + // Validation must see the exact authored inputs. Only after both sides + // pass do we canonicalize their collection order for deterministic SVG + // geometry and stable artifact bytes. + const canonicalBase = canonicalArchitecture(base); + const canonicalHead = canonicalArchitecture(head); + canonicalBase.meta.output = base.meta.output; + canonicalHead.meta.output = head.meta.output; + const canonicalBaseBytes = Buffer.from(JSON.stringify(canonicalBase)); + const canonicalHeadBytes = Buffer.from(JSON.stringify(canonicalHead)); + fs.writeFileSync(canonicalBaseInput, canonicalBaseBytes); + captureCompareStagingFile(canonicalBaseInput, artifactIdentity(canonicalBaseBytes)); + fs.writeFileSync(canonicalHeadInput, canonicalHeadBytes); + captureCompareStagingFile(canonicalHeadInput, artifactIdentity(canonicalHeadBytes)); + baseResult = renderValidatedArchitecture( + canonicalBaseInput, + baseCandidate, + qualityArgs.quality, + repoArgs.repoRoot, + captureCompareStagingFile, + ); + headResult = renderValidatedArchitecture( + canonicalHeadInput, + headCandidate, + qualityArgs.quality, + repoArgs.repoRoot, + captureCompareStagingFile, + ); + + const semanticHash = (diagram) => createHash('sha256').update(canonicalArchitectureJson(diagram)).digest('hex'); + let compareIr; + try { + compareIr = compareArchitecture(base, head, { + baseRawSha256: createHash('sha256').update(baseBuffer).digest('hex'), + headRawSha256: createHash('sha256').update(headBuffer).digest('hex'), + baseSemanticSha256: semanticHash(base), + headSemanticSha256: semanticHash(head), + baseBytes: baseBuffer.byteLength, + headBytes: headBuffer.byteLength, + baseVerified: Boolean(baseResult.sourceEvidence), + headVerified: Boolean(headResult.sourceEvidence), + }); + } catch (error) { + if (!(error instanceof ArchitectureDeltaError)) throw error; + reportCompareFailure({ json: options.json, stage: 'compare', error: error.message, code: error.code, details: error.details }); + return; + } + + const baseSourceSvg = extractArchitectureSvg(baseResult.html); + const headSourceSvg = extractArchitectureSvg(headResult.html); + const baseSvg = annotateArchitectureSideSvg(baseSourceSvg, compareIr, 'base'); + const headSvg = annotateArchitectureSideSvg(headSourceSvg, compareIr, 'head'); + const deltaSvg = buildDeltaSvg(baseSourceSvg, headSourceSvg, compareIr); + // Raw input hashes and byte counts belong in the sidecar receipt, not the + // artifact. Keeping them out makes formatting-only input rewrites produce + // the exact same canonical review HTML and artifact hash. + const artifactIr = { + ...compareIr, + base: Object.fromEntries(Object.entries(compareIr.base).filter(([key]) => !['rawSha256', 'bytes'].includes(key))), + head: Object.fromEntries(Object.entries(compareIr.head).filter(([key]) => !['rawSha256', 'bytes'].includes(key))), + }; + const html = renderArchitectureDeltaHtml({ + receipt: artifactIr, + baseSvg, + deltaSvg, + headSvg, + baseHtml: baseResult.html, + headHtml: headResult.html, + artifactCss: extractArtifactCss(headResult.html), + }); + const deltaValidation = validateArchitectureDeltaHtml(html, artifactIr); + fs.writeFileSync(htmlCandidate, html); + if (outputCapture.mode !== null) fs.chmodSync(htmlCandidate, outputCapture.mode); + const artifact = fs.readFileSync(htmlCandidate); + captureCompareStagingFile(htmlCandidate, artifactIdentity(artifact)); + const baseChecks = baseResult.checks.checks.filter((check) => check.ok).length; + const headChecks = headResult.checks.checks.filter((check) => check.ok).length; + const finalReceipt = { + ...compareIr, + artifact: artifactIdentity(artifact), + validation: { + checksPassed: baseChecks + headChecks + deltaValidation.checksPassed, + checkCount: baseResult.checks.checks.length + headResult.checks.checks.length + deltaValidation.checkCount, + baseComposition: baseResult.checks.composition.status, + headComposition: headResult.checks.composition.status, + }, + }; + const receiptBytes = Buffer.from(`${JSON.stringify(finalReceipt, null, 2)}\n`); + fs.writeFileSync(receiptCandidate, receiptBytes); + if (receiptCapture.mode !== null) fs.chmodSync(receiptCandidate, receiptCapture.mode); + captureCompareStagingFile(receiptCandidate, artifactIdentity(receiptBytes)); + + try { + const currentOutput = resolveOutputPath({ + requestedOutput, + defaultOutput: 'architecture-delta.html', + inputPaths: [basePath, headPath], + otherOutputPaths: [requestedReceiptPath], + }).outputPath; + resolveOutputPath({ + requestedOutput: options.receipt || compareReceiptPath(currentOutput), + defaultOutput: compareReceiptPath(currentOutput), + requiredExtension: '.json', + inputPaths: [basePath, headPath], + otherOutputPaths: [currentOutput], + }); + } catch (error) { + const outputDiagnostic = error.archifyDiagnostics?.[0]; + reportCompareFailure({ + json: options.json, + stage: 'commit', + error: error.message, + code: outputDiagnostic?.code || 'output/path-resolution', + details: { + ...(outputDiagnostic?.subject || {}), + ...(outputDiagnostic?.evidence || {}), + supportedFixes: outputDiagnostic?.supportedFixes || ['restore safe output paths and retry'], + }, + }); + return; + } + + const committed = commitComparePair({ + htmlCandidate, + htmlContent: finalReceipt.artifact, + receiptCandidate, + receiptContent: artifactIdentity(receiptBytes), + outputCapture, + receiptCapture, + stagingDirectory, + verifyAtomicOutput, + fileBindingRuntime, + }); + stagingOwnership.push(...committed.ownedBackupEntries); + if (options.json) console.log(JSON.stringify(finalReceipt, null, 2)); + else { + console.log(`compared architecture ${requestedOutputPath}`); + console.log(`${finalReceipt.validation.checksPassed}/${finalReceipt.validation.checkCount} checks; completeness ${finalReceipt.completeness}; ${finalReceipt.proofLevel}; sha256 ${finalReceipt.artifact.sha256.slice(0, 12)}`); + console.log(`receipt ${requestedReceiptPath}`); + } + } catch (error) { + if (error instanceof ArchitectureDeltaError) { + reportCompareFailure({ json: options.json, stage: 'artifact', error: error.message, code: error.code, details: error.details }); + } else if (error.compareStage === 'commit') { + preserveRecoveryDirectory = Boolean(error.compareDetails?.recoveryFiles?.length); + reportCompareFailure({ + json: options.json, + stage: error.compareStage, + error: error.message, + code: error.compareCode, + details: error.compareDetails, + }); + } else { + reportCompareFailure({ json: options.json, stage: 'internal', error: 'Architecture compare failed before commit.', code: 'delta/internal', details: { reason: error.message } }); + } + } finally { + try { + if (!preserveRecoveryDirectory) { + cleanupOwnedStagingDirectory( + stagingDirectory, + stagingIdentity, + stagingOwnership, + fileBindingRuntime, + ); + } else { + releaseOwnedStagingBindings(stagingOwnership, fileBindingRuntime); + } + } catch (error) { + console.error(`Warning: could not remove compare staging directory: ${error.message}`); + } + } +} + +function commandRender(args) { + const qualityArgs = extractQualityArgs(args); + const repoArgs = extractRepoRootArgs(qualityArgs.rest); + // render takes no options of its own once --quality and --repo-root are + // stripped, so anything left starting with -- is a typo. Without this a + // mistyped flag was taken as the output path: `render architecture spec.json + // --json out.html` wrote a file literally named `--json` and never wrote + // out.html, exiting 0. Every sibling subcommand already guards this. + const unknown = repoArgs.rest.filter((arg) => arg.startsWith('--')); + if (unknown.length) fail(`Unknown render option "${unknown[0]}".`); + const [type, input, output] = repoArgs.rest; + if (!type || !input || repoArgs.rest.length > 3) fail(usage()); + const result = runNode([rendererPath(type), input, ...(output ? [output] : [])], { + env: rendererEnv(qualityArgs.quality, repoArgs.repoRoot), + }); + if (result.status !== 0) exitFrom(result); +} + +function reportArtifactFailure({ command, json, stage, type, input, output, error, diagnostics = [], status = 1, checker, receiptId, provenance, update }) { + const receipt = { + schemaVersion: 1, + ok: false, + command, + ...(receiptId ? { receiptId } : {}), + stage, + type, + input, + ...(output === undefined ? {} : { output }), + error, + diagnostics, + ...(provenance ? { provenance } : {}), + ...(checker ? { checker } : {}), + ...(update ? { update } : {}), + }; + if (json) console.log(JSON.stringify(receipt, null, 2)); + else { + console.error(formatDiagnostics(error, diagnostics)); + if (update?.noticeRequired) console.log(update.noticeText); + } + process.exitCode = status; +} + +function writeDeliveryFailureReceipt(options) { + const receiptId = options.receiptId || randomUUID(); + const recorded = recordDeliveryFailure({ ...options, receiptId }); + const ownershipOrLockError = recorded.ownershipError || recorded.lockError; + if (ownershipOrLockError) { + const failureWasRecorded = recorded.status && recorded.status !== 'unrecorded'; + const lockOrOwnershipFailure = Boolean( + ownershipOrLockError.deliveryLockCode + || ownershipOrLockError.deliveryOwnershipCode, + ); + reportArtifactFailure({ + ...options, + command: 'deliver', + receiptId, + status: 1, + error: lockOrOwnershipFailure + ? `Could not safely continue delivery for "${options.output}": ${ownershipOrLockError.message}` + : options.error, + ...(failureWasRecorded ? { provenance: recorded.status } : {}), + diagnostics: lockOrOwnershipFailure + ? [ + deliveryLockFailureDiagnostic(options.output, ownershipOrLockError), + ...(options.diagnostics || []), + ...(recorded.diagnostic ? [recorded.diagnostic] : []), + ] + : [ + ...(options.diagnostics || []), + deliveryLockFailureDiagnostic(options.output, ownershipOrLockError), + ...(recorded.diagnostic ? [recorded.diagnostic] : []), + ], + }); + return recorded; + } + const recoveryDiagnosticFirst = Boolean(recorded.journalRecovery || recorded.provenanceRecovery); + const diagnostics = recoveryDiagnosticFirst + ? [ + ...(recorded.diagnostic ? [recorded.diagnostic] : []), + ...(options.diagnostics || []), + ] + : [ + ...(options.diagnostics || []), + ...(recorded.diagnostic ? [recorded.diagnostic] : []), + ]; + reportArtifactFailure({ + ...options, + command: 'deliver', + receiptId, + provenance: recorded.status, + diagnostics, + }); + return recorded; +} + +function reportValidateFailure(options) { + reportArtifactFailure({ ...options, command: 'validate' }); +} + +function reportArtifactArgumentFailure(command, error) { + const details = error.archifyArgument || {}; + reportArtifactFailure({ + command, + json: true, + stage: 'arguments', + error: error.message, + diagnostics: [diagnostic({ + code: details.code || 'cli/invalid-arguments', + message: error.message, + subject: { command, ...(details.subject || {}) }, + evidence: details.evidence || {}, + supportedFixes: details.supportedFixes || ['correct the command arguments and retry'], + })], + status: 2, + }); +} + +function sourceEvidenceFromArtifact(artifact) { + const html = artifact.toString('utf8'); + const match = html.match(/ + +`; +} + +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(`