diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 46c09ee9..c688b604 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -24,6 +24,16 @@ updates:
update-types: ["version-update:semver-major"]
- dependency-name: "typescript"
update-types: ["version-update:semver-major"]
+ # vue-router 5 peers `vite: ^7.3.0 || ^8.0.0` and expects a Vite
+ # toolchain. Measured, not assumed, and the result is SPLIT: integriq and
+ # zaakafhandelapp fail their build on `Can't resolve 'vue-router'`, while
+ # openregister and learniq build clean on 5.3.0. Held because a bump that
+ # breaks some apps and not others cannot be merged unattended, and the
+ # difference is not yet understood. Adopting it fleet-wide is a
+ # Vite migration, not a bump. versioniq is already on Vite and is the
+ # natural pilot.
+ - dependency-name: "vue-router"
+ update-types: ["version-update:semver-major"]
cooldown:
default-days: 1
include:
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 41313c3c..3ab2489c 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -156,6 +156,7 @@ permissions:
jobs:
quality:
+ if: (github.event_name != 'pull_request' || github.head_ref != 'development')
uses: ConductionNL/.github/.github/workflows/quality.yml@main
with:
app-name: hermiq
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 163c53cc..caa8ad17 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -12,7 +12,7 @@
- 0.1.40-unstable.20260830174612
+ 0.1.47-unstable.20260905095638
EUPL-1.2
Conduction
Hermiq
@@ -132,6 +132,32 @@
silently reading null.
-->
OCA\Hermiq\Repair\RenameDutchColumns
+
+
+ OCA\Hermiq\Repair\PruneRetiredAgentFlowSchemas
+
+
+ OCA\Hermiq\Repair\MirrorPendingApprovalsToTasks
+
+
+ OCA\Hermiq\Repair\MirrorSchedulesToEngineFlows
OCA\Hermiq\Repair\CheckOpenRegisterCompatibility
@@ -161,6 +187,13 @@
+
+
+ OCA\Hermiq\Command\RollbackApprovalTaskMirror
+
+ OCA\Hermiq\Command\RollbackScheduleFlowMirror
+
+
OCA\Hermiq\Settings\AdminSettings
OCA\Hermiq\Sections\SettingsSection
diff --git a/composer.json b/composer.json
index f84e9078..e50d8bc7 100644
--- a/composer.json
+++ b/composer.json
@@ -90,7 +90,8 @@
"optimize-autoloader": true,
"sort-packages": true,
"platform": {
- "php": "8.3"
+ "php": "8.3",
+ "ext-xsl": "1"
}
},
"extra": {
diff --git a/composer.lock b/composer.lock
index 68009277..2ba4ac05 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "92cd2af65375bbd49b1252be717fe90c",
+ "content-hash": "0e671e30b4ea1cc8df53fdc8e07cd42d",
"packages": [
{
"name": "cweagans/composer-configurable-plugin",
@@ -2300,16 +2300,16 @@
},
{
"name": "conduction/hydra-gates",
- "version": "v1.10.0",
+ "version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/ConductionNL/.github.git",
- "reference": "d143bc27aecbb44a66c843dba89d374628bf9eef"
+ "reference": "0bc214023be78aac94142035a9988986cfccccbc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ConductionNL/.github/zipball/d143bc27aecbb44a66c843dba89d374628bf9eef",
- "reference": "d143bc27aecbb44a66c843dba89d374628bf9eef",
+ "url": "https://api.github.com/repos/ConductionNL/.github/zipball/0bc214023be78aac94142035a9988986cfccccbc",
+ "reference": "0bc214023be78aac94142035a9988986cfccccbc",
"shasum": ""
},
"require": {
@@ -2348,9 +2348,9 @@
"support": {
"docs": "https://github.com/ConductionNL/.github/blob/main/hydra-gates/README.md",
"issues": "https://github.com/ConductionNL/.github/issues",
- "source": "https://github.com/ConductionNL/.github/tree/v1.10.0"
+ "source": "https://github.com/ConductionNL/.github/tree/v1.15.0"
},
- "time": "2026-08-27T16:18:04+00:00"
+ "time": "2026-09-04T16:24:04+00:00"
},
{
"name": "consolidation/annotated-command",
@@ -5008,11 +5008,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.2.9",
+ "version": "2.2.12",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/13d6b4f347bad222da436580c8304fa6f83e6bd0",
- "reference": "13d6b4f347bad222da436580c8304fa6f83e6bd0",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/174b0d88710f00a42598886504dd7a146f91ace5",
+ "reference": "174b0d88710f00a42598886504dd7a146f91ace5",
"shasum": ""
},
"require": {
@@ -5068,7 +5068,7 @@
"type": "github"
}
],
- "time": "2026-08-22T07:38:16+00:00"
+ "time": "2026-08-31T19:09:43+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -9508,7 +9508,8 @@
},
"platform-dev": {},
"platform-overrides": {
- "php": "8.3"
+ "php": "8.3",
+ "ext-xsl": "1"
},
"plugin-api-version": "2.9.0"
}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 9bad6e2d..c7c8ac1e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -144,11 +144,32 @@ export default [
'no-console': 'off',
'n/no-process-exit': 'off',
'n/hashbang': 'off',
- // `_` / `__` as a deliberate throwaway binding — `catch (_)`, a
- // discarded destructuring slot. Narrow on purpose: the pattern matches
- // UNDERSCORES ONLY, so a real name that happens to start with `_` is
- // still reported. v9 drives plain `.js` through the CORE rule (the
- // `@typescript-eslint` swap is per-file-type), so it is set here.
+ // Tests import devDependencies by definition; this rule is about what
+ // ships in the published package, which tests/ never does.
+ 'n/no-unpublished-import': 'off',
+ },
+ },
+
+ {
+ // `_` / `__` as a deliberate throwaway binding — `catch (_)`, a discarded
+ // destructuring slot. Narrow on purpose: the pattern matches UNDERSCORES
+ // ONLY, so a real name that happens to start with `_` is still reported.
+ //
+ // 🔴 `.js` / `.mjs` ONLY, NOT `.ts`. The CORE rule is not TypeScript-aware:
+ // applied to a `.ts` file it reads the parameter NAMES inside a function
+ // TYPE as bindings and reports them unused. Measured on humaniq —
+ //
+ // t?: (app: string, key: string) => string
+ //
+ // produced four `no-unused-vars` errors for `app` and `key`, which are
+ // documentation, not variables. The same mis-scoping made every unused
+ // `catch (e)` in a `.ts` spec report TWICE, once per rule.
+ //
+ // v9 already turns the core rule off for `.ts` and drives
+ // `@typescript-eslint/no-unused-vars` instead; naming `.ts` here switched
+ // it back on. TypeScript files are handled by the block below.
+ files: ['tests/**/*.js', 'tests/**/*.mjs'],
+ rules: {
'no-unused-vars': [
'error',
{
@@ -165,12 +186,117 @@ export default [
ignoreRestSiblings: true,
},
],
- // Tests import devDependencies by definition; this rule is about what
- // ships in the published package, which tests/ never does.
- 'n/no-unpublished-import': 'off',
},
},
+ {
+ // The TypeScript half of the block above. Same intent, same patterns, on
+ // the rule that actually understands the language: it knows a name inside
+ // a function type is not a binding, so type annotations stay quiet while a
+ // genuinely dead local is still reported.
+ files: ['tests/**/*.ts', 'tests/**/*.tsx'],
+ rules: {
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ varsIgnorePattern: '^_+$',
+ caughtErrors: 'all',
+ caughtErrorsIgnorePattern: '^_+$',
+ argsIgnorePattern: '^_',
+ ignoreRestSiblings: true,
+ },
+ ],
+ },
+ },
+
+ {
+ // 🔴 Node-side CLI tooling under `scripts/`, which is COMMONJS. Flat
+ // config defaults every `.js` to ESM with browser-ish globals, so without
+ // this block eslint reports the CommonJS wrapper itself as undefined
+ // identifiers. Measured on this app: 52 of the 233 errors under
+ // `tests/` + `scripts/` were `no-undef`, ALL of them in `scripts/`, and
+ // all five names were the environment rather than a typo — `process` 23,
+ // `require` 20, `__dirname` 6, `__filename` 2, `module` 1.
+ //
+ // This is describing the environment, not relaxing a rule, and it is the
+ // same argument the test-globals block below makes: declaring them keeps
+ // `no-undef` able to do its real job, which is catching a genuinely
+ // misspelled identifier. Suppressing the rule instead would bury that.
+ //
+ // `no-console` is off because printing its report is what a CLI checker
+ // is FOR.
+ //
+ // 🔴 NO `n/*` ENTRIES HERE, DELIBERATELY. `eslint-plugin-n` is NOT
+ // registered for these files under eslint 10 + @nextcloud/eslint-config
+ // 9, so `'n/no-process-exit': 'off'` would be dead config that reads as
+ // if it were doing something. Measured both ways on this app: 0 `n/`
+ // findings with the entries and 0 without.
+ //
+ // What DID report was the opposite — four `scripts/*.js` carried
+ // `/* eslint-disable n/no-process-exit */` and `/* eslint-disable
+ // n/shebang */` left over from the eslintrc era, and an inline disable
+ // naming an unregistered plugin is itself an error ("Definition for rule
+ // 'n/shebang' was not found"). Those 8 comments are removed; do not add
+ // `n/*` rules back to replace them.
+ //
+ // ⚠️ `.js` and `.cjs` ONLY. A `scripts/*.mjs` is genuinely ESM and must
+ // keep the default `sourceType`, or `import` stops parsing there.
+ files: ['scripts/**/*.js', 'scripts/**/*.cjs'],
+ languageOptions: {
+ sourceType: 'commonjs',
+ globals: {
+ require: 'readonly',
+ module: 'writable',
+ exports: 'writable',
+ process: 'readonly',
+ __dirname: 'readonly',
+ __filename: 'readonly',
+ console: 'readonly',
+ Buffer: 'readonly',
+ global: 'readonly',
+ URL: 'readonly',
+ TextEncoder: 'readonly',
+ TextDecoder: 'readonly',
+ },
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
+
+ {
+ // The ESM half of the block above. A `scripts/*.mjs` is genuinely a module
+ // and must keep the default `sourceType`, so it gets Node's globals but
+ // none of the CommonJS wrapper. Measured: `process` reported undefined 2x
+ // in hermiq's generate-opengemeenten-icons.mjs and 4x in openregister's
+ // l10n/runtime-check.mjs, which the `.js`/`.cjs` block deliberately does
+ // not match.
+ files: ['scripts/**/*.mjs', 'tests/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ process: 'readonly',
+ console: 'readonly',
+ Buffer: 'readonly',
+ global: 'readonly',
+ URL: 'readonly',
+ TextEncoder: 'readonly',
+ TextDecoder: 'readonly',
+ },
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
+
+ {
+ // eslint must not try to PARSE a shell script. `tests/e2e/seed.test.sh`
+ // matches the `**/*.test.*` glob some presets use, and eslint then reads
+ // it as JavaScript and reports "Parsing error: Unexpected character" —
+ // a finding about a file it should never have opened.
+ ignores: ['**/*.sh', '**/*.bash'],
+ },
+
+
{
// Test globals. Several apps keep their spec files INSIDE `src/`, which the
// lint script scans, and neither `@nextcloud/eslint-config` nor the runner
diff --git a/l10n/en.js b/l10n/en.js
index 6edb1f41..d946af7f 100644
--- a/l10n/en.js
+++ b/l10n/en.js
@@ -1,6 +1,14 @@
OC.L10N.register(
"hermiq",
{
+ "Load example data?": "Load example data?",
+ "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.",
+ "Load the example data": "Load the example data",
+ "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.",
+ "None, I will set this up myself": "None, I will set this up myself",
+ "Nothing is imported. You start with an empty app and add your own data.": "Nothing is imported. You start with an empty app and add your own data.",
+ "Example data": "Example data",
+ "Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.": "Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.",
"\"Satisfied\" means Hermiq can evidence this control from its own governance data — it is not a certification of compliance by a qualified auditor.": "\"Satisfied\" means Hermiq can evidence this control from its own governance data — it is not a certification of compliance by a qualified auditor.",
"%n case": "%n case",
"%n cases": "%n cases",
@@ -1307,7 +1315,10 @@ OC.L10N.register(
"Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.": "Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.",
"Where the automation lives": "Where the automation lives",
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.",
- "Open Flows in the menu": "Open Flows in the menu"
+ "Open Flows in the menu": "Open Flows in the menu",
+ "Reports": "Reports",
+ "Pick a report to open it.": "Pick a report to open it.",
+ "What the agents did, and what a human still has to answer for.": "What the agents did, and what a human still has to answer for."
},
"nplurals=2; plural=(n != 1);"
)
diff --git a/l10n/en.json b/l10n/en.json
index 5570cf39..8b87dbe9 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -1,5 +1,13 @@
{
"translations": {
+ "Load example data?": "Load example data?",
+ "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.",
+ "Load the example data": "Load the example data",
+ "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.",
+ "None, I will set this up myself": "None, I will set this up myself",
+ "Nothing is imported. You start with an empty app and add your own data.": "Nothing is imported. You start with an empty app and add your own data.",
+ "Example data": "Example data",
+ "Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.": "Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.",
"\"Satisfied\" means Hermiq can evidence this control from its own governance data — it is not a certification of compliance by a qualified auditor.": "\"Satisfied\" means Hermiq can evidence this control from its own governance data — it is not a certification of compliance by a qualified auditor.",
"%n case": "%n case",
"%n cases": "%n cases",
@@ -1318,7 +1326,10 @@
"Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.": "Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.",
"Where the automation lives": "Where the automation lives",
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.",
- "Open Flows in the menu": "Open Flows in the menu"
+ "Open Flows in the menu": "Open Flows in the menu",
+ "Reports": "Reports",
+ "Pick a report to open it.": "Pick a report to open it.",
+ "What the agents did, and what a human still has to answer for.": "What the agents did, and what a human still has to answer for."
},
"plurals": "",
"pluralForm": "nplurals=2; plural=(n != 1);"
diff --git a/l10n/nl.js b/l10n/nl.js
index 8f020403..e80c4e15 100644
--- a/l10n/nl.js
+++ b/l10n/nl.js
@@ -1,6 +1,14 @@
OC.L10N.register(
"hermiq",
{
+ "Load example data?": "Voorbeeldgegevens laden?",
+ "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
+ "Load the example data": "Laad de voorbeeldgegevens",
+ "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Laadt wat je koos. De gegevens zijn herkenbaar voorbeeldgegevens, je kunt dit meer dan een keer uitvoeren en je kunt ze daarna verwijderen.",
+ "None, I will set this up myself": "Geen, ik richt dit zelf in",
+ "Nothing is imported. You start with an empty app and add your own data.": "Er wordt niets geïmporteerd. Je begint met een lege app en voegt zelf gegevens toe.",
+ "Example data": "Voorbeeldgegevens",
+ "Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.": "Voorbeeldwaarden voor elk schema dat deze app levert, gegenereerd uit de schema’s zelf. Het laat de lijsten, detailpagina’s en dashboards werkend zien in plaats van een verhaal te vertellen. Veilig om vaker uit te voeren, en je kunt het daarna verwijderen.",
"Demo data (optional)": "Demovoorbeelddata (optioneel)",
"Load a small example dataset of agents, skills and conversations, so the lists and detail pages show a working product straight away. The data is obviously sample data, it is safe to run more than once, and it can be removed afterwards. Skip this on a production install.": "Laad een kleine voorbeeldset met agents, skills en gesprekken, zodat de lijsten en detailpagina's meteen een werkend product laten zien. De data is duidelijk voorbeelddata, veilig om meerdere keren uit te voeren en achteraf te verwijderen. Sla dit over op een productie-installatie.",
"%n step": ["%n stap","%n stappen"],
@@ -1388,7 +1396,10 @@ OC.L10N.register(
"Open the documentation to keep going": "Open de documentatie om verder te gaan",
"Where the automation lives": "Waar de automatisering zit",
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows zijn wat er gebeurt zonder dat iemand klikt: een agent die op een schema draait, een overdracht die afgaat zodra een taak klaar is. Hier leest en bewerkt u ze. U hoeft nu niets te bouwen.",
- "Open Flows in the menu": "Open Flows in het menu"
+ "Open Flows in the menu": "Open Flows in het menu",
+ "Reports": "Rapportages",
+ "Pick a report to open it.": "Kies een rapport om het te openen.",
+ "What the agents did, and what a human still has to answer for.": "Wat de agents deden, en waar een mens nog verantwoording over aflegt."
},
"nplurals=2; plural=(n != 1);"
)
diff --git a/l10n/nl.json b/l10n/nl.json
index f7ac8de7..6f788493 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -1,5 +1,13 @@
{
"translations": {
+ "Load example data?": "Voorbeeldgegevens laden?",
+ "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
+ "Load the example data": "Laad de voorbeeldgegevens",
+ "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards.": "Laadt wat je koos. De gegevens zijn herkenbaar voorbeeldgegevens, je kunt dit meer dan een keer uitvoeren en je kunt ze daarna verwijderen.",
+ "None, I will set this up myself": "Geen, ik richt dit zelf in",
+ "Nothing is imported. You start with an empty app and add your own data.": "Er wordt niets geïmporteerd. Je begint met een lege app en voegt zelf gegevens toe.",
+ "Example data": "Voorbeeldgegevens",
+ "Sample values for every schema this app supplies, generated from the schemas themselves. It shows the lists, detail pages and dashboards working rather than telling a story. Safe to run more than once, and you can delete it afterwards.": "Voorbeeldwaarden voor elk schema dat deze app levert, gegenereerd uit de schema’s zelf. Het laat de lijsten, detailpagina’s en dashboards werkend zien in plaats van een verhaal te vertellen. Veilig om vaker uit te voeren, en je kunt het daarna verwijderen.",
"Demo data (optional)": "Demovoorbeelddata (optioneel)",
"Load a small example dataset of agents, skills and conversations, so the lists and detail pages show a working product straight away. The data is obviously sample data, it is safe to run more than once, and it can be removed afterwards. Skip this on a production install.": "Laad een kleine voorbeeldset met agents, skills en gesprekken, zodat de lijsten en detailpagina's meteen een werkend product laten zien. De data is duidelijk voorbeelddata, veilig om meerdere keren uit te voeren en achteraf te verwijderen. Sla dit over op een productie-installatie.",
"%n step": [
@@ -1411,7 +1419,10 @@
"Open the documentation to keep going": "Open de documentatie om verder te gaan",
"Where the automation lives": "Waar de automatisering zit",
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows zijn wat er gebeurt zonder dat iemand klikt: een agent die op een schema draait, een overdracht die afgaat zodra een taak klaar is. Hier leest en bewerkt u ze. U hoeft nu niets te bouwen.",
- "Open Flows in the menu": "Open Flows in het menu"
+ "Open Flows in the menu": "Open Flows in het menu",
+ "Reports": "Rapportages",
+ "Pick a report to open it.": "Kies een rapport om het te openen.",
+ "What the agents did, and what a human still has to answer for.": "Wat de agents deden, en waar een mens nog verantwoording over aflegt."
},
"plurals": "",
"pluralForm": "nplurals=2; plural=(n != 1);"
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index c3a27328..293127d3 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -159,6 +159,20 @@ public function register(IRegistrationContext $context): void {
);
}
+ // Oversight (schedules-onto-engine-triggers, EU AI Act Art. 14): contribute
+ // hermiq's per-organisation kill switch to the engine's oversight registry,
+ // so a `hermiq.*` hop of an engaged organisation's run is vetoed BEFORE it
+ // executes — the operator's stop control reaches engine-timed and
+ // canvas-authored runs, not only the app-local tick. Guarded on the event
+ // class existing so an instance whose OpenRegister predates the oversight
+ // registry still boots.
+ if (class_exists(\OCA\OpenRegister\Service\Flow\RegisterFlowOversightEvent::class) === true) {
+ $context->registerEventListener(
+ \OCA\OpenRegister\Service\Flow\RegisterFlowOversightEvent::class,
+ \OCA\Hermiq\Listener\FlowOversightRegistrationListener::class
+ );
+ }
+
// Agent render leaf (agent-object-leaf, ADR-019 + ADR-066): contribute the
// `hermiq-agent` leaf to OpenRegister's cross-app leaf catalogue via the
// sibling-app leaf-registration hook (RegisterLeafProvidersEvent). This makes
@@ -239,6 +253,18 @@ public function register(IRegistrationContext $context): void {
listener: TalkBotInvokeListener::class
);
+ // Approval-task convergence (phase 1): every pending Approval is
+ // mirrored as an OpenRegister task, and a task completed in
+ // OpenRegister's shared inbox decides the Approval here. Registered by
+ // event NAME (the filinq#988 pattern), deliberately: at register()
+ // time the OCA\OpenRegister\ prefix may not be autoloadable yet, so a
+ // class_exists() guard could silently disable the whole surface, while
+ // registering for a name that is never dispatched is harmless.
+ $context->registerEventListener(
+ event: \OCA\Hermiq\Listener\TaskTerminalListener::EVENT_TASK_TERMINAL,
+ listener: \OCA\Hermiq\Listener\TaskTerminalListener::class
+ );
+
// Approvals decided by reaction (talk-approval-reactions): spreed
// invokes bots on reactions with the SAME BotInvokeEvent, so this is a
// second listener on the same event — each ignores the invocation types
diff --git a/lib/BackgroundJob/ScheduleTask.php b/lib/BackgroundJob/ScheduleTask.php
index 72b5e6e1..87a74c68 100644
--- a/lib/BackgroundJob/ScheduleTask.php
+++ b/lib/BackgroundJob/ScheduleTask.php
@@ -26,10 +26,13 @@
namespace OCA\Hermiq\BackgroundJob;
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
use OCA\Hermiq\Service\ScheduleService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
+use Psr\Log\LoggerInterface;
+use Throwable;
/**
* Background job that dispatches due Hermiq schedules.
@@ -50,12 +53,19 @@ class ScheduleTask extends TimedJob {
*
* @param ITimeFactory $time Time factory for TimedJob scheduling.
* @param ScheduleService $scheduleService Service that dispatches due schedules.
+ * @param ScheduleFlowBridge $flowBridge Arms eligible schedules on the engine's
+ * schedule trigger before each tick
+ * (schedules-onto-engine-triggers).
+ * @param LoggerInterface $logger Isolates a bridge failure from the tick.
*
* @spec openspec/changes/agent-schedule-dispatcher/tasks.md#task-2-2
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
*/
public function __construct(
ITimeFactory $time,
private readonly ScheduleService $scheduleService,
+ private readonly ScheduleFlowBridge $flowBridge,
+ private readonly LoggerInterface $logger,
) {
parent::__construct(time: $time);
@@ -85,8 +95,22 @@ public function __construct(
* @phpstan-param mixed $argument
*
* @spec openspec/changes/agent-schedule-dispatcher/tasks.md#task-2-2
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
*/
public function run(mixed $argument): void {
+ // Arm first: a schedule created since the last tick delegates its clock
+ // to the engine within one poll interval, and a mirrored schedule whose
+ // timing drifted is refreshed. Failure-isolated on purpose — a bridge
+ // error must never block the tick, whose schedules still fire locally.
+ try {
+ $this->flowBridge->syncAll();
+ } catch (Throwable $e) {
+ $this->logger->error(
+ '[hermiq] Schedule flow sync failed; this tick dispatches locally: ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ }
+
$this->scheduleService->run();
}//end run()
diff --git a/lib/Command/RollbackApprovalTaskMirror.php b/lib/Command/RollbackApprovalTaskMirror.php
new file mode 100644
index 00000000..a3ccef69
--- /dev/null
+++ b/lib/Command/RollbackApprovalTaskMirror.php
@@ -0,0 +1,200 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Command;
+
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
+
+/**
+ * Terminates the mirror tasks of pending approvals and clears the backlinks.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+class RollbackApprovalTaskMirror extends Command {
+
+ /**
+ * OpenRegister register slug that holds Hermiq objects.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'hermiq';
+
+ /**
+ * OpenRegister schema slug for approval objects.
+ *
+ * @var string
+ */
+ private const APPROVAL_SCHEMA = 'approval';
+
+ /**
+ * Constructor.
+ *
+ * @param ObjectService $objectService Reads pending Approvals and clears the backlinks.
+ * @param ApprovalTaskBridge $taskBridge Terminates the mirror tasks (never throws).
+ * @param LoggerInterface $logger Diagnostics.
+ */
+ public function __construct(
+ private readonly ObjectService $objectService,
+ private readonly ApprovalTaskBridge $taskBridge,
+ private readonly LoggerInterface $logger,
+ ) {
+ parent::__construct();
+ }//end __construct()
+
+ /**
+ * Configure the command.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+ protected function configure(): void {
+ $this->setName(name: 'hermiq:approvals:rollback-task-mirror')
+ ->setDescription(
+ description: 'Terminate the OpenRegister mirror tasks of pending hermiq approvals and clear the taskUuid backlinks'
+ )
+ ->addOption(
+ name: 'dry-run',
+ shortcut: null,
+ mode: InputOption::VALUE_NONE,
+ description: 'Report what would be rolled back without changing anything'
+ );
+ }//end configure()
+
+ /**
+ * Execute the rollback.
+ *
+ * @param InputInterface $input Command input.
+ * @param OutputInterface $output Command output.
+ *
+ * @return int Zero on success, one when any approval could not be rolled back.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $dryRun = ((bool)$input->getOption('dry-run') === true);
+
+ try {
+ $objects = $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::APPROVAL_SCHEMA)
+ ->findAll(
+ config: ['filters' => ['status' => 'pending']],
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (Throwable $e) {
+ $output->writeln('Could not read approvals: ' . $e->getMessage() . '');
+
+ return 1;
+ }
+
+ $rolledBack = 0;
+ $failed = [];
+ foreach ($objects as $object) {
+ if (($object instanceof ObjectEntity) === false) {
+ continue;
+ }
+
+ $data = $object->getObject();
+ $taskUuid = trim((string)($data['taskUuid'] ?? ''));
+ if ((string)($data['status'] ?? '') !== 'pending' || $taskUuid === '') {
+ continue;
+ }
+
+ $approvalUuid = (string)$object->getUuid();
+ if ($dryRun === true) {
+ $output->writeln('Would roll back approval ' . $approvalUuid . ' (task ' . $taskUuid . ').');
+ $rolledBack++;
+ continue;
+ }
+
+ // Terminate the mirror first: a cleared backlink with a live task
+ // would leave an undecidable row in someone's inbox, while a
+ // terminated task with a stale backlink is release-idempotent.
+ $this->taskBridge->releaseFor(
+ approval: $object,
+ decision: 'rolled-back',
+ deciderUid: 'occ:rollback-task-mirror'
+ );
+
+ try {
+ $data['taskUuid'] = null;
+ $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::APPROVAL_SCHEMA)
+ ->saveObject(
+ object: $data,
+ register: self::REGISTER_SLUG,
+ schema: self::APPROVAL_SCHEMA,
+ uuid: $approvalUuid,
+ _rbac: false,
+ _multitenancy: false
+ );
+ $rolledBack++;
+ $output->writeln('Rolled back approval ' . $approvalUuid . ' (task ' . $taskUuid . ').');
+ } catch (Throwable $e) {
+ $failed[] = $approvalUuid;
+ $this->logger->warning(
+ 'RollbackApprovalTaskMirror could not clear taskUuid on approval '
+ . $approvalUuid . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ $output->writeln(
+ 'Could not clear the backlink on approval ' . $approvalUuid
+ . ': ' . $e->getMessage() . ''
+ );
+ }//end try
+ }//end foreach
+
+ $output->writeln(
+ sprintf('Rollback: %d approvals rolled back, %d failed.', $rolledBack, count($failed))
+ );
+
+ if ($failed !== []) {
+ return 1;
+ }
+
+ return 0;
+
+ }//end execute()
+}//end class
diff --git a/lib/Command/RollbackScheduleFlowMirror.php b/lib/Command/RollbackScheduleFlowMirror.php
new file mode 100644
index 00000000..58f29836
--- /dev/null
+++ b/lib/Command/RollbackScheduleFlowMirror.php
@@ -0,0 +1,181 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Command;
+
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
+use OCA\Hermiq\Service\ScheduleService;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
+
+/**
+ * Clears delegation markers and deletes the mirror flows.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+class RollbackScheduleFlowMirror extends Command {
+
+ /**
+ * OpenRegister register slug that holds hermiq objects.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'hermiq';
+
+ /**
+ * OpenRegister schema slug for schedule objects.
+ *
+ * @var string
+ */
+ private const SCHEMA_SLUG = 'schedule';
+
+ /**
+ * Constructor.
+ *
+ * @param ObjectService $objectService Reads delegated schedules.
+ * @param ScheduleService $scheduleService Clears the markers through the sanitised persist.
+ * @param ScheduleFlowBridge $flowBridge Deletes the mirror flows.
+ * @param LoggerInterface $logger Diagnostics.
+ */
+ public function __construct(
+ private readonly ObjectService $objectService,
+ private readonly ScheduleService $scheduleService,
+ private readonly ScheduleFlowBridge $flowBridge,
+ private readonly LoggerInterface $logger,
+ ) {
+ parent::__construct();
+ }//end __construct()
+
+ /**
+ * Configure the command.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ protected function configure(): void {
+ $this->setName(name: 'hermiq:schedules:rollback-flow-mirror')
+ ->setDescription(
+ description: 'Return every delegated schedule clock to hermiq: clear engineFlowId markers and delete the mirror flows'
+ )
+ ->addOption(
+ name: 'dry-run',
+ shortcut: null,
+ mode: InputOption::VALUE_NONE,
+ description: 'Report what would be rolled back without changing anything'
+ );
+ }//end configure()
+
+ /**
+ * Execute the rollback.
+ *
+ * @param InputInterface $input Command input.
+ * @param OutputInterface $output Command output.
+ *
+ * @return int Zero on success, one when any schedule could not be rolled back.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $dryRun = ((bool)$input->getOption('dry-run') === true);
+
+ try {
+ $objects = $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::SCHEMA_SLUG)
+ ->findAll(config: ['limit' => 1000], _rbac: false, _multitenancy: false);
+ } catch (Throwable $e) {
+ $output->writeln('Could not read schedules: ' . $e->getMessage() . '');
+
+ return 1;
+ }
+
+ $rolledBack = 0;
+ $failed = [];
+ foreach ($objects as $object) {
+ if (($object instanceof ObjectEntity) === false) {
+ continue;
+ }
+
+ $flowId = trim((string)($object->getObject()['engineFlowId'] ?? ''));
+ if ($flowId === '') {
+ continue;
+ }
+
+ $scheduleUuid = (string)$object->getUuid();
+ if ($dryRun === true) {
+ $output->writeln('Would roll back schedule ' . $scheduleUuid . ' (flow ' . $flowId . ').');
+ $rolledBack++;
+ continue;
+ }
+
+ try {
+ // Marker first, flow second: from the marker write on, the
+ // local dispatcher owns the clock; a crash before the delete
+ // leaves a mirror whose delegation check refuses to fire.
+ $this->scheduleService->clearEngineDelegation(schedule: $object);
+ $this->flowBridge->deleteFlow(flowId: $flowId);
+ $rolledBack++;
+ $output->writeln('Rolled back schedule ' . $scheduleUuid . ' (flow ' . $flowId . ').');
+ } catch (Throwable $e) {
+ $failed[] = $scheduleUuid;
+ $this->logger->warning(
+ 'RollbackScheduleFlowMirror could not roll back schedule '
+ . $scheduleUuid . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ $output->writeln(
+ 'Could not roll back schedule ' . $scheduleUuid
+ . ': ' . $e->getMessage() . ''
+ );
+ }//end try
+ }//end foreach
+
+ $output->writeln(
+ sprintf('Rollback: %d schedules rolled back, %d failed.', $rolledBack, count($failed))
+ );
+
+ if ($failed !== []) {
+ return 1;
+ }
+
+ return 0;
+
+ }//end execute()
+}//end class
diff --git a/lib/Controller/ChatHealthController.php b/lib/Controller/ChatHealthController.php
index 3efb15ea..f9d434c8 100644
--- a/lib/Controller/ChatHealthController.php
+++ b/lib/Controller/ChatHealthController.php
@@ -40,11 +40,14 @@
use Throwable;
/**
- * Provides GET /api/chat/health — an unauthenticated endpoint the nextcloud-vue
+ * Provides GET /api/chat/health, an unauthenticated endpoint the nextcloud-vue
* widget probes once at mount time to decide whether to render the AI companion
* button. Returns HTTP 200 + {status:"ok",capabilities:["chat","stream"]} when at
- * least one LLM chat provider is configured; HTTP 503 + {status:"no_provider"}
- * otherwise; HTTP 503 + {status:"config_error"} when the config read itself fails.
+ * least one LLM chat provider is configured; HTTP 200 + {status:"unconfigured",
+ * capabilities:[]} when none is, because an unconfigured app is healthy, and a
+ * 5xx here trips every co-installed app's no-5xx e2e guard; HTTP 503 +
+ * {status:"config_error"} when the config read itself fails, which IS the app
+ * being broken.
*
* @spec openspec/changes/agent-engine-port/tasks.md#task-4-1
*/
@@ -69,12 +72,14 @@ public function __construct(
/**
* Health probe for the AI chat backend.
*
- * Returns 200 when a chat provider is configured, 503 otherwise. Annotated
- * as PublicPage so the widget can probe without authentication (mirrors
- * OR's ChatHealthController::health()). A failing config read is reported
- * as `config_error` (not `no_provider`) so operators can tell a fresh
- * instance from a broken config service; the widget treats any non-200 as
- * "no AI" without branching.
+ * Answers 200 whenever the app itself is healthy. A missing chat provider
+ * is a configuration state, not an outage: it answers 200 with
+ * `status: unconfigured` and an empty capability list, so a consumer
+ * decides on the body, and a strict no-5xx guard on a co-installed app
+ * (the rig's dossiq KPI spec) does not fail on a fresh hermiq. Reserve
+ * 5xx for the app being broken: a failing config read stays 503
+ * `config_error`. Annotated as PublicPage so the widget can probe without
+ * authentication.
*
* The rate limit is deliberately generous: monitoring polls this on a short
* interval, and a ceiling that trips on a normal probe cadence turns the
@@ -99,14 +104,19 @@ public function health(): JSONResponse {
if (empty($chatProvider) === true) {
return new JSONResponse(
- data: ['status' => 'no_provider'],
- statusCode: 503
+ data: [
+ 'status' => 'unconfigured',
+ 'configured' => false,
+ 'capabilities' => [],
+ ],
+ statusCode: 200
);
}
return new JSONResponse(
data: [
'status' => 'ok',
+ 'configured' => true,
'capabilities' => ['chat', 'stream'],
],
statusCode: 200
diff --git a/lib/Controller/SetupController.php b/lib/Controller/SetupController.php
index 56bcd61e..ef67e204 100644
--- a/lib/Controller/SetupController.php
+++ b/lib/Controller/SetupController.php
@@ -79,7 +79,20 @@ class SetupController extends Controller {
*
* @var array
*/
- private const WRITABLE_KEYS = ['llmendpoint'];
+ private const WRITABLE_KEYS = ['llmendpoint', self::DATASET_KEY];
+
+ /**
+ * App-config key holding the dataset the operator picked.
+ *
+ * The wizard's `choice` step writes it through `POST /api/setup/config`, and
+ * the `run-action` step that follows reads it back. Two steps rather than
+ * one because `CnSetupWizard::runAction()` posts to
+ * `/api/setup/action/{action}` with no body: an action cannot carry the
+ * answer, so the answer has to be stored before the action runs.
+ *
+ * @var string
+ */
+ private const DATASET_KEY = 'demo_dataset';
/**
* Default Ollama endpoint when the admin has not configured one yet.
@@ -140,6 +153,7 @@ public function __construct(
public function status(): JSONResponse {
$llmTested = $this->config(key: 'setup_llm_tested') === '1';
$demoDecided = $this->appConfig->getValueString(Application::APP_ID, self::DEMO_DECIDED_KEY, '') !== '';
+ $pickedDataset = $this->appConfig->getValueString(Application::APP_ID, self::DATASET_KEY, '');
// `completed` stays the REQUIRED step alone. Demo data is optional, so
// it must not gate the app — it only needs to be reportable, so the
// wizard can stop asking once it has an answer.
@@ -153,13 +167,22 @@ public function status(): JSONResponse {
data: [
'version' => self::SETUP_VERSION,
'completed' => $completed,
+ // The choice step reads its options from here: it declares
+ // `optionsSource: datasets` and no options of its own, so a
+ // dataset missing from this list is a dataset nobody can pick.
+ 'datasets' => $this->demoDataService->listChoices(),
'steps' => [
// DEALT WITH, not "objects exist" — see DEMO_DECIDED_KEY.
// A step the wizard can never mark done keeps the dialog
// open over every page, which since nextcloud-vue 2.21 is
// enough on its own: an OUTSTANDING OPTIONAL step opens the
// wizard (nextcloud-vue#806).
- 'demo-data' => ['done' => $demoDecided],
+ 'demo-data' => ['done' => ($pickedDataset !== '')],
+ // "None" is an ANSWER, so the load step is finished the
+ // moment it is chosen: there is nothing left to run.
+ 'load-demo-data' => [
+ 'done' => ($demoDecided === true || $pickedDataset === DemoDataService::NONE_DATASET),
+ ],
'test-llm' => ['done' => $llmTested],
],
]
@@ -182,6 +205,27 @@ public function status(): JSONResponse {
*/
#[AuthorizedAdminSetting(AdminSettings::class)]
public function saveConfig(): JSONResponse {
+ // 🔴 THE DATASET IS VALIDATED BEFORE IT IS STORED. The allow-list above
+ // keeps this endpoint from writing arbitrary keys; it says nothing
+ // about the VALUE. The load step reads this one back and hands it to
+ // the importer, so an unknown value would surface one step later as a
+ // failed import with no clue why.
+ $dataset = $this->request->getParam(self::DATASET_KEY);
+ if ($dataset !== null) {
+ $named = 'that';
+ if (is_scalar($dataset) === true) {
+ $named = (string)$dataset;
+ }
+
+ $known = array_column($this->demoDataService->listChoices(), 'id');
+ if (in_array($named, $known, true) === false) {
+ return new JSONResponse(
+ data: ['success' => false, 'message' => 'No dataset is called "' . $named . '".'],
+ statusCode: Http::STATUS_BAD_REQUEST,
+ );
+ }
+ }
+
foreach ($this->request->getParams() as $key => $value) {
if (in_array($key, self::WRITABLE_KEYS, true) === false) {
continue;
@@ -222,16 +266,24 @@ public function runAction(string $actionId): JSONResponse {
return $this->testLlm();
}
- if ($actionId === 'install-demo-data') {
- return $this->installDemoData();
+ // `install-demo-data` is the id the step used before it asked WHICH
+ // dataset, and it still means "import the one this app ships". Kept so
+ // an older manifest, a runbook or a script that posts it keeps working.
+ if ($actionId === 'load-demo-data' || $actionId === 'install-demo-data') {
+ return $this->loadDataset(actionId: $actionId);
}
// DECLINING IS AN ANSWER. Without this the wizard re-offers the import
// on every visit, so "no thanks" is not expressible and the step never
// closes — which is also what keeps the dialog over the page.
+ //
+ // 🔴 AND IT ANSWERS *BOTH* STEPS now that the question is a choice
+ // followed by a run-action: closing only the second leaves the first
+ // outstanding, and that is enough to keep the wizard open.
if ($actionId === 'skip-demo-data') {
+ $this->appConfig->setValueString(Application::APP_ID, self::DATASET_KEY, DemoDataService::NONE_DATASET);
$this->appConfig->setValueString(Application::APP_ID, self::DEMO_DECIDED_KEY, 'skipped');
- return new JSONResponse(data: ['success' => true, 'message' => 'Demo data skipped.']);
+ return new JSONResponse(data: ['success' => true, 'message' => 'No example data was loaded.']);
}
return new JSONResponse(
@@ -242,17 +294,43 @@ public function runAction(string $actionId): JSONResponse {
}//end runAction()
/**
- * Import the shipped demo dataset.
+ * Import the dataset the operator picked in the previous step.
*
* Reports the FAILURE, rather than a quiet success: an operator who asked
* for demo data and got none must be told, and `DemoDataService::install()`
* throws instead of returning an empty result for exactly that reason.
*
+ * @param string $actionId The action that asked, which decides whether an
+ * unanswered choice is refused or means the shipped set.
+ *
* @return JSONResponse `{ success, message }`.
*
* @spec exclude First-time-setup action dispatch; no behavioural spec.
*/
- private function installDemoData(): JSONResponse {
+ private function loadDataset(string $actionId): JSONResponse {
+ $picked = $this->appConfig->getValueString(Application::APP_ID, self::DATASET_KEY, '');
+
+ // The legacy id carries no answer, so it means the shipped dataset. A
+ // caller that posts it has said which one by posting it.
+ if ($actionId === 'install-demo-data' && $picked === '') {
+ $picked = DemoDataService::DEMO_DATASET;
+ }
+
+ // 🔴 NO SILENT DEFAULT. Importing here because the operator clicked Run
+ // one step early would plant example objects nobody asked for.
+ if ($picked === '') {
+ return new JSONResponse(
+ data: ['success' => false, 'message' => 'Pick a dataset first.'],
+ statusCode: Http::STATUS_BAD_REQUEST,
+ );
+ }
+
+ if ($picked === DemoDataService::NONE_DATASET) {
+ $this->appConfig->setValueString(Application::APP_ID, self::DEMO_DECIDED_KEY, 'skipped');
+
+ return new JSONResponse(data: ['success' => true, 'message' => 'No example data was loaded.']);
+ }
+
try {
$imported = $this->demoDataService->install();
} catch (\Throwable $e) {
@@ -275,7 +353,7 @@ private function installDemoData(): JSONResponse {
]
);
- }//end installDemoData()
+ }//end loadDataset()
/**
* Probe the configured LLM endpoint (Ollama /api/tags), record the first
diff --git a/lib/Flow/HermiqAgentNode.php b/lib/Flow/HermiqAgentNode.php
index efe2b3f9..9a2186e6 100644
--- a/lib/Flow/HermiqAgentNode.php
+++ b/lib/Flow/HermiqAgentNode.php
@@ -25,7 +25,7 @@
*
* @link https://conduction.nl
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
declare(strict_types=1);
@@ -65,7 +65,7 @@
* dressed as a safety property. Remove the fallback once the fleet's target
* instances are known to carry #2835.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
class HermiqAgentNode implements IFlowNode, IFlowNodeLogActions {
@@ -97,7 +97,7 @@ public function __construct(
*
* @return string The id.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function getId(): string {
return 'hermiq.agent-step';
@@ -108,7 +108,7 @@ public function getId(): string {
*
* @return string The display name.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function getDisplayName(): string {
return $this->l10n->t('Agent step');
@@ -119,7 +119,7 @@ public function getDisplayName(): string {
*
* @return string The description.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function getDescription(): string {
return $this->l10n->t('Run an agent turn and put its answer on the item.');
@@ -130,7 +130,7 @@ public function getDescription(): string {
*
* @return string The icon URL.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function getIcon(): string {
return $this->urls->imagePath('hermiq', 'app-dark.svg');
@@ -156,7 +156,7 @@ public function getIcon(): string {
*
* @return array The links.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function logActions(array $entry): array {
$items = (array)($entry['output']['items'] ?? []);
@@ -183,7 +183,7 @@ public function logActions(array $entry): array {
*
* @return boolean Whether it is available.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function isAvailableForScope(int $scope): bool {
return in_array($scope, [IManager::SCOPE_ADMIN, IManager::SCOPE_USER], true);
@@ -198,7 +198,7 @@ public function isAvailableForScope(int $scope): bool {
*
* @throws UnexpectedValueException When no agent is named.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function validateConfig(array $config): void {
if (trim((string)($config['agentId'] ?? ($config['agent'] ?? ''))) === '') {
@@ -230,7 +230,7 @@ public function validateConfig(array $config): void {
* @throws Throwable When the agent turn fails; the step's `onError` policy
* decides what the run then does.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function execute(array $items, array $config, array $context): array {
// Third shape of the same defect: a step that did not run must not
@@ -339,7 +339,7 @@ public function execute(array $items, array $config, array $context): array {
*
* @return string The rendered prompt.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
private function render(string $template, array $json): string {
return (string)preg_replace_callback(
@@ -386,7 +386,7 @@ static function (array $matches) use ($json): string {
*
* @throws UnexpectedValueException When `expectJson` is set and the answer is not JSON.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
private function decode(array $config, string $answer) {
if (($config['expectJson'] ?? false) !== true) {
diff --git a/lib/Flow/HermiqFlowNodeListener.php b/lib/Flow/HermiqFlowNodeListener.php
index cb342d1c..be64240c 100644
--- a/lib/Flow/HermiqFlowNodeListener.php
+++ b/lib/Flow/HermiqFlowNodeListener.php
@@ -15,7 +15,7 @@
*
* @link https://conduction.nl
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
declare(strict_types=1);
@@ -31,7 +31,7 @@
*
* @template-implements IEventListener
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
class HermiqFlowNodeListener implements IEventListener {
/**
@@ -40,11 +40,15 @@ class HermiqFlowNodeListener implements IEventListener {
* @param HermiqAgentNode $agentNode The agent step node.
* @param HermiqWorkloadNode $workloadNode The workload step node.
* @param HermiqWorkloadCollectNode $collectNode Collects a workload started asynchronously.
+ * @param HermiqScheduleDispatchNode $scheduleDispatchNode Fires one schedule occurrence
+ * through the governed dispatch
+ * path (schedules-onto-engine-triggers).
*/
public function __construct(
private readonly HermiqAgentNode $agentNode,
private readonly HermiqWorkloadNode $workloadNode,
private readonly HermiqWorkloadCollectNode $collectNode,
+ private readonly HermiqScheduleDispatchNode $scheduleDispatchNode,
) {
}//end __construct()
@@ -59,7 +63,7 @@ public function __construct(
*
* @return void
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function handle(Event $event): void {
if (($event instanceof RegisterFlowNodesEvent) === false) {
@@ -69,6 +73,7 @@ public function handle(Event $event): void {
$event->registerNode(node: $this->agentNode);
$event->registerNode(node: $this->workloadNode);
$event->registerNode(node: $this->collectNode);
+ $event->registerNode(node: $this->scheduleDispatchNode);
}//end handle()
}//end class
diff --git a/lib/Flow/HermiqScheduleDispatchNode.php b/lib/Flow/HermiqScheduleDispatchNode.php
new file mode 100644
index 00000000..893664e7
--- /dev/null
+++ b/lib/Flow/HermiqScheduleDispatchNode.php
@@ -0,0 +1,352 @@
+ hermiq.schedule-dispatch -> openregister.end`,
+ * and this step re-enters `ScheduleService::runNow()` — the same private
+ * dispatch path a local tick used. So the kill switch, the budget hard cap,
+ * the approval gate, commit-before-run, retry bookkeeping, delivery and the
+ * per-run audit entry all apply byte-for-byte (schedules-onto-engine-triggers).
+ *
+ * The alternative — triggering straight into `hermiq.agent-step` — was
+ * rejected in the change's design: it would drop the approval gate, the
+ * budget cap, delivery and retry for exactly the runs that had them.
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Flow
+ * @package OCA\Hermiq\Flow
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Flow;
+
+use OCA\Hermiq\Service\ScheduleService;
+use OCA\OpenRegister\Db\FlowMapper;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\Flow\IFlowNode;
+use OCA\OpenRegister\Service\ObjectService;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\WorkflowEngine\IManager;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+use UnexpectedValueException;
+
+/**
+ * Fires one hermiq schedule occurrence from an engine-timed flow.
+ *
+ * WHY A STALE DELEGATION REFUSES INSTEAD OF RUNNING. The schedule stores the
+ * uuid of the ONE flow that owns its clock (`engineFlowId`). A firing flow
+ * whose uuid the schedule no longer carries is a leftover: a crashed
+ * rollback, a re-mirror, or a schedule someone deleted and recreated. Running
+ * it would give the schedule two clocks — the exact double-fire the marker
+ * exists to prevent — so the step disables the stale flow and fails loudly.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+class HermiqScheduleDispatchNode implements IFlowNode {
+
+ /**
+ * OpenRegister register slug that holds hermiq schedule objects.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'hermiq';
+
+ /**
+ * OpenRegister schema slug for schedule objects.
+ *
+ * @var string
+ */
+ private const SCHEMA_SLUG = 'schedule';
+
+ /**
+ * Constructor.
+ *
+ * @param ScheduleService $scheduleService The governed dispatch path (runNow).
+ * @param ObjectService $objectService Loads the schedule object.
+ * @param ContainerInterface $container Lazy FlowMapper resolution, so a stale
+ * mirror can be disabled without making
+ * the flow store a hard constructor
+ * dependency of every node resolution.
+ * @param LoggerInterface $logger Diagnostics.
+ * @param IL10N $l10n Translations.
+ * @param IURLGenerator $urls For the palette icon.
+ */
+ public function __construct(
+ private readonly ScheduleService $scheduleService,
+ private readonly ObjectService $objectService,
+ private readonly ContainerInterface $container,
+ private readonly LoggerInterface $logger,
+ private readonly IL10N $l10n,
+ private readonly IURLGenerator $urls,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * The step type.
+ *
+ * @return string The id.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function getId(): string {
+ return 'hermiq.schedule-dispatch';
+ }//end getId()
+
+ /**
+ * Palette name.
+ *
+ * @return string The display name.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function getDisplayName(): string {
+ return $this->l10n->t('Run a schedule');
+ }//end getDisplayName()
+
+ /**
+ * Palette description.
+ *
+ * @return string The description.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function getDescription(): string {
+ return $this->l10n->t('Fire one occurrence of a hermiq schedule. Every gate the schedule has still applies.');
+ }//end getDescription()
+
+ /**
+ * Palette icon.
+ *
+ * @return string The icon URL.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function getIcon(): string {
+ return $this->urls->imagePath('hermiq', 'app-dark.svg');
+ }//end getIcon()
+
+ /**
+ * Available in both scopes; the dispatch path enforces its own gates.
+ *
+ * @param int $scope The scope constant.
+ *
+ * @return boolean Whether it is available.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function isAvailableForScope(int $scope): bool {
+ return in_array($scope, [IManager::SCOPE_ADMIN, IManager::SCOPE_USER], true);
+ }//end isAvailableForScope()
+
+ /**
+ * Reject a dispatch step that names no schedule.
+ *
+ * @param array $config The step configuration.
+ *
+ * @return void
+ *
+ * @throws UnexpectedValueException When no schedule is named.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function validateConfig(array $config): void {
+ if (trim((string)($config['scheduleId'] ?? '')) === '') {
+ throw new UnexpectedValueException($this->l10n->t('A schedule dispatch step needs a schedule.'));
+ }
+
+ }//end validateConfig()
+
+ /**
+ * Fire the schedule's occurrence through the governed dispatch path.
+ *
+ * One occurrence per FIRING, not per item: a schedule trigger starts its
+ * run with a single empty item, and a schedule is not a collection to fan
+ * out over. The occurrence's outcome (`ok`, `error`, `awaiting_approval`,
+ * `skipped_killswitch`, `skipped_budget`) is put on each item as
+ * `scheduleStatus` so the run log shows what the occurrence did.
+ *
+ * A DOMAIN-RECORDED failure is a step SUCCESS on purpose. `runNow()`
+ * catches an agent-turn error, records `lastStatus='error'` on the
+ * schedule, writes the audit entry and arms the schedule's own
+ * retry/dead-letter machinery. Failing the step too would double-report
+ * the same failure and mark the flow run failed for an occurrence the
+ * domain layer is already handling. Only a failure that escapes
+ * `runNow()` — one the domain layer could NOT record — fails the step.
+ *
+ * @param array $items The input items.
+ * @param array $config The step configuration.
+ * @param array $context Run-level metadata (carries the firing flow's id).
+ *
+ * @return array The items, each carrying the occurrence's status.
+ *
+ * @throws UnexpectedValueException When the schedule is missing, disabled,
+ * or no longer delegated to this flow.
+ * @throws Throwable When the dispatch itself fails catastrophically.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function execute(array $items, array $config, array $context): array {
+ $scheduleId = trim((string)($config['scheduleId'] ?? ''));
+ if ($scheduleId === '') {
+ // Same defect class as the agent step: a flow seeded or imported
+ // past validateConfig() must not turn this into a silent pass.
+ throw new UnexpectedValueException($this->l10n->t('A schedule dispatch step needs a schedule.'));
+ }
+
+ $firingFlowId = trim((string)($context['payload']['flowId'] ?? ''));
+
+ $schedule = $this->loadSchedule(scheduleId: $scheduleId);
+ if ($schedule === null) {
+ $this->disableStaleFlow(flowId: $firingFlowId, reason: 'its schedule no longer exists');
+ throw new UnexpectedValueException(
+ $this->l10n->t('Schedule %s no longer exists. The mirror flow has been disabled.', [$scheduleId])
+ );
+ }
+
+ $data = $schedule->getObject();
+ $delegatedTo = trim((string)($data['engineFlowId'] ?? ''));
+
+ // The schedule names the ONE flow that owns its clock. A firing flow it
+ // does not name is stale (crashed rollback, re-mirror): running it
+ // would be a second clock. Refuse, and switch the leftover off.
+ if ($delegatedTo === '' || ($firingFlowId !== '' && $firingFlowId !== $delegatedTo)) {
+ $this->disableStaleFlow(flowId: $firingFlowId, reason: 'the schedule is no longer delegated to it');
+ throw new UnexpectedValueException(
+ $this->l10n->t('Schedule %s is not delegated to this flow any more.', [$scheduleId])
+ );
+ }
+
+ if (($data['enabled'] ?? false) !== true) {
+ // The bridge's next sync pass disables the mirror flow; until then
+ // a paused schedule must simply not run.
+ throw new UnexpectedValueException(
+ $this->l10n->t('Schedule %s is disabled.', [$scheduleId])
+ );
+ }
+
+ $this->scheduleService->runNow(schedule: $schedule);
+
+ $status = $this->refreshedStatus(scheduleId: $scheduleId);
+
+ $out = [];
+ foreach ($items as $index => $item) {
+ $json = (array)($item['json'] ?? []);
+ $json['scheduleId'] = $scheduleId;
+ $json['scheduleStatus'] = $status;
+
+ $out[] = [
+ 'json' => $json,
+ 'binary' => (array)($item['binary'] ?? []),
+ 'pairedItem' => ['item' => $index],
+ ];
+ }
+
+ return $out;
+ }//end execute()
+
+ /**
+ * Load the schedule object, or null when it cannot be found.
+ *
+ * @param string $scheduleId The schedule uuid.
+ *
+ * @return ObjectEntity|null The schedule, or null.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ private function loadSchedule(string $scheduleId): ?ObjectEntity {
+ try {
+ $object = $this->objectService->find(
+ id: $scheduleId,
+ register: self::REGISTER_SLUG,
+ schema: self::SCHEMA_SLUG,
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (Throwable $e) {
+ return null;
+ }
+
+ if (($object instanceof ObjectEntity) === false) {
+ return null;
+ }
+
+ return $object;
+ }//end loadSchedule()
+
+ /**
+ * The schedule's lastStatus after the dispatch, re-read from the store.
+ *
+ * Re-read rather than taken from the in-memory entity: `runNow()` persists
+ * its outcome through ObjectService and the entity this step loaded does
+ * not see that write. `unknown` when the re-read fails; the audit entry on
+ * the schedule is the record, this value is a run-log convenience.
+ *
+ * @param string $scheduleId The schedule uuid.
+ *
+ * @return string The refreshed lastStatus, or 'unknown'.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ private function refreshedStatus(string $scheduleId): string {
+ $fresh = $this->loadSchedule(scheduleId: $scheduleId);
+ if ($fresh === null) {
+ return 'unknown';
+ }
+
+ return (string)($fresh->getObject()['lastStatus'] ?? 'unknown');
+ }//end refreshedStatus()
+
+ /**
+ * Switch a stale mirror flow off so it stops firing forever.
+ *
+ * Best-effort by contract: the refusal this step throws is the control,
+ * the disable is hygiene. A disable failure is logged, never thrown over
+ * the refusal it accompanies.
+ *
+ * @param string $flowId The firing flow's uuid ('' when unknown).
+ * @param string $reason Why the flow is stale, for the log line.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ private function disableStaleFlow(string $flowId, string $reason): void {
+ if ($flowId === '') {
+ return;
+ }
+
+ try {
+ $mapper = $this->container->get(FlowMapper::class);
+ $flow = $mapper->findByUuid($flowId);
+ $flow->setEnabled(false);
+ $mapper->update($flow);
+ $this->logger->warning(
+ sprintf('[hermiq] Disabled stale schedule mirror flow %s: %s.', $flowId, $reason)
+ );
+ } catch (Throwable $e) {
+ $this->logger->warning(
+ sprintf('[hermiq] Could not disable stale schedule mirror flow %s: %s', $flowId, $e->getMessage()),
+ ['exception' => $e]
+ );
+ }
+
+ }//end disableStaleFlow()
+}//end class
diff --git a/lib/Flow/TenantKillSwitchCheck.php b/lib/Flow/TenantKillSwitchCheck.php
new file mode 100644
index 00000000..8c4c9d37
--- /dev/null
+++ b/lib/Flow/TenantKillSwitchCheck.php
@@ -0,0 +1,219 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Flow
+ * @package OCA\Hermiq\Flow
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Flow;
+
+use OCA\OpenRegister\Db\FlowRunMapper;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\Flow\IFlowOversightCheck;
+use OCA\OpenRegister\Service\ObjectService;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Vetoes hermiq hops for organisations whose kill switch is engaged.
+ *
+ * THE FAIL MODES ARE DELIBERATELY ASYMMETRIC:
+ *
+ * - A TenantControl READ failure consents (logged). This mirrors the
+ * dispatcher's documented choice in `loadEngagedOrganisations()`: a
+ * transient read error must not silently halt every tenant's runs.
+ * - An UNATTRIBUTABLE hermiq hop while any switch is engaged is VETOED. A
+ * kill switch that cannot tell whose run it is looking at must stop the
+ * run, not wave it through; over-blocking during an engaged emergency
+ * beats under-blocking during one.
+ *
+ * Scoped to `hermiq.*` node types only: other apps' hops are not hermiq's
+ * to veto, and the registry consults every check on every hop of every run.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+class TenantKillSwitchCheck implements IFlowOversightCheck {
+
+ /**
+ * OpenRegister register slug that holds hermiq objects.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'hermiq';
+
+ /**
+ * OpenRegister schema slug for tenant-control (kill switch) objects.
+ *
+ * @var string
+ */
+ private const TENANT_CONTROL_SCHEMA = 'tenantcontrol';
+
+ /**
+ * Constructor.
+ *
+ * @param ObjectService $objectService Reads engaged TenantControl objects.
+ * @param ContainerInterface $container Lazy FlowRunMapper resolution for the
+ * run's organisation; lazy so the check
+ * stays constructible on an OpenRegister
+ * without the flow store.
+ * @param LoggerInterface $logger Diagnostics.
+ */
+ public function __construct(
+ private readonly ObjectService $objectService,
+ private readonly ContainerInterface $container,
+ private readonly LoggerInterface $logger,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * The check's id, as recorded on a refusal.
+ *
+ * @return string The id.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function getId(): string {
+ return 'hermiq.tenant-killswitch';
+ }//end getId()
+
+ /**
+ * Veto a hermiq hop whose organisation has an engaged kill switch.
+ *
+ * @param array $context The hop context (`nodeType`, `runUuid`).
+ *
+ * @return string|null The refusal reason, or null to consent.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function veto(array $context): ?string {
+ $nodeType = (string)($context['nodeType'] ?? '');
+ if (str_starts_with($nodeType, 'hermiq.') === false) {
+ return null;
+ }
+
+ $engaged = $this->engagedOrganisations();
+ if ($engaged === []) {
+ return null;
+ }
+
+ $organisation = $this->runOrganisation(runUuid: (string)($context['runUuid'] ?? ''));
+
+ if ($organisation === null) {
+ return 'A hermiq kill switch is engaged and this run\'s organisation could not be established. '
+ . 'The hop is refused rather than run unattributed.';
+ }
+
+ if (in_array($organisation, $engaged, true) === true) {
+ return 'The hermiq kill switch for this run\'s organisation is engaged. '
+ . 'Disengage it in hermiq\'s tenant controls to let agent work continue.';
+ }
+
+ return null;
+ }//end veto()
+
+ /**
+ * The organisations whose kill switch is currently engaged.
+ *
+ * A read failure answers the EMPTY set, logged: the dispatcher's own
+ * documented fail-open-on-read choice, so a transient TenantControl
+ * outage never halts every tenant. Engagement itself is then a hard veto.
+ *
+ * @return array The engaged organisation identifiers.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ private function engagedOrganisations(): array {
+ try {
+ $objects = $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::TENANT_CONTROL_SCHEMA)
+ ->findAll(
+ config: ['filters' => ['engaged' => true]],
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (Throwable $e) {
+ $this->logger->error(
+ '[hermiq] Oversight check could not load engaged kill switches: ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ return [];
+ }
+
+ $organisations = [];
+ foreach ($objects as $object) {
+ if (($object instanceof ObjectEntity) === false) {
+ continue;
+ }
+
+ if (($object->getObject()['engaged'] ?? false) !== true) {
+ continue;
+ }
+
+ $organisation = (string)($object->getOrganisation() ?? '');
+ if ($organisation !== '') {
+ $organisations[] = $organisation;
+ }
+ }
+
+ return array_values(array_unique($organisations));
+ }//end engagedOrganisations()
+
+ /**
+ * The organisation the identified run belongs to, or null when unknown.
+ *
+ * Read from the FlowRun row rather than the context: the run's stored
+ * attribution is server-written, where a context value would be
+ * caller-supplied at queue time.
+ *
+ * @param string $runUuid The run uuid from the hop context.
+ *
+ * @return string|null The organisation, or null when it cannot be established.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ private function runOrganisation(string $runUuid): ?string {
+ if ($runUuid === '') {
+ return null;
+ }
+
+ try {
+ $mapper = $this->container->get(FlowRunMapper::class);
+ $run = $mapper->findByUuid($runUuid);
+ } catch (Throwable $e) {
+ return null;
+ }
+
+ $organisation = trim((string)($run->getOrganisation() ?? ''));
+ if ($organisation === '') {
+ return null;
+ }
+
+ return $organisation;
+ }//end runOrganisation()
+}//end class
diff --git a/lib/Listener/FlowOversightRegistrationListener.php b/lib/Listener/FlowOversightRegistrationListener.php
new file mode 100644
index 00000000..b2e4648f
--- /dev/null
+++ b/lib/Listener/FlowOversightRegistrationListener.php
@@ -0,0 +1,71 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Listener
+ * @package OCA\Hermiq\Listener
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Listener;
+
+use OCA\Hermiq\Flow\TenantKillSwitchCheck;
+use OCA\OpenRegister\Service\Flow\RegisterFlowOversightEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+/**
+ * Registers the tenant kill switch when the oversight registry discovers.
+ *
+ * Mirrors `HermiqFlowNodeListener`: the registry dispatches
+ * `RegisterFlowOversightEvent` before its first use, and every check
+ * contributed here is asked before every hop of every flow run.
+ *
+ * @template-implements IEventListener
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+class FlowOversightRegistrationListener implements IEventListener {
+
+ /**
+ * Constructor.
+ *
+ * @param TenantKillSwitchCheck $killSwitchCheck The per-organisation stop control.
+ */
+ public function __construct(
+ private readonly TenantKillSwitchCheck $killSwitchCheck,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * Contribute hermiq's oversight check.
+ *
+ * @param Event $event The dispatched event.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function handle(Event $event): void {
+ if (($event instanceof RegisterFlowOversightEvent) === false) {
+ return;
+ }
+
+ $event->registerCheck(check: $this->killSwitchCheck);
+
+ }//end handle()
+}//end class
diff --git a/lib/Listener/TaskTerminalListener.php b/lib/Listener/TaskTerminalListener.php
new file mode 100644
index 00000000..17e421d4
--- /dev/null
+++ b/lib/Listener/TaskTerminalListener.php
@@ -0,0 +1,247 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Listener;
+
+use OCA\Hermiq\Service\ApprovalService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Applies decided OpenRegister mirror tasks onto hermiq Approvals.
+ *
+ * @implements IEventListener
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+class TaskTerminalListener implements IEventListener {
+
+ /**
+ * FQN of OpenRegister's terminal-task event, as a string on purpose: a
+ * cross-app class name is a runtime lookup, and a literal cannot
+ * accidentally autoload or hard-couple (the filinq#988 pattern).
+ *
+ * @var string
+ */
+ public const EVENT_TASK_TERMINAL = 'OCA\\OpenRegister\\Event\\TaskTerminalEvent';
+
+ /**
+ * The task state meaning "the work finished with an explicit outcome".
+ * Terminated and disabled tasks are deliberately NOT decisions: a mirror
+ * cancelled in OpenRegister leaves the Approval pending and decidable in
+ * hermiq (design.md Decision 4).
+ *
+ * @var string
+ */
+ private const STATE_COMPLETED = 'completed';
+
+ /**
+ * OpenRegister's published rejecting-outcome vocabulary, mirrored from
+ * `TaskState::REJECTING_OUTCOMES` (a value copy, not an import: the
+ * listener must route even when that class cannot be resolved).
+ *
+ * @var array
+ */
+ private const REJECTING_OUTCOMES = ['rejected', 'returned', 'declined', 'denied'];
+
+ /**
+ * Constructor.
+ *
+ * @param ApprovalService $approvalService The approval gate's decision write-path.
+ * @param LoggerInterface $logger PSR-3 logger.
+ */
+ public function __construct(
+ private readonly ApprovalService $approvalService,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * Handle a committed terminal-task event for a hermiq mirror task.
+ *
+ * @param Event $event The dispatched event.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+ public function handle(Event $event): void {
+ if (get_class($event) !== self::EVENT_TASK_TERMINAL) {
+ return;
+ }
+
+ // Only the after-commit dispatch may resume work (OpenRegister's own
+ // contract for run continuation); the in-transaction dispatch is for
+ // timer bookkeeping and would resume a run inside a foreign transaction.
+ if ($this->read(subject: $event, getter: 'isCommitted') !== true) {
+ return;
+ }
+
+ $task = $this->read(subject: $event, getter: 'getTask');
+ if (is_object($task) === false) {
+ return;
+ }
+
+ if ((string)$this->read(subject: $task, getter: 'getState') !== self::STATE_COMPLETED) {
+ return;
+ }
+
+ $approvalUuid = $this->approvalUuidOf(task: $task);
+ if ($approvalUuid === '') {
+ return;
+ }
+
+ try {
+ $this->apply(task: $task, approvalUuid: $approvalUuid);
+ } catch (Throwable $e) {
+ $this->logger->error(
+ 'Hermiq could not apply the decision of mirror task '
+ . ((string)$this->read(subject: $task, getter: 'getUuid'))
+ . ' onto approval ' . $approvalUuid . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ }
+
+ }//end handle()
+
+ /**
+ * Apply a completed mirror task's outcome onto its pending Approval.
+ *
+ * @param object $task The completed task.
+ * @param string $approvalUuid The linked Approval's uuid.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+ private function apply(object $task, string $approvalUuid): void {
+ $approval = $this->approvalService->loadApproval(uuid: $approvalUuid);
+ if ($approval === null) {
+ $this->logger->warning(
+ 'Hermiq mirror task ' . ((string)$this->read(subject: $task, getter: 'getUuid'))
+ . ' names approval ' . $approvalUuid . ', which does not resolve.'
+ );
+
+ return;
+ }
+
+ $decider = trim((string)$this->read(subject: $task, getter: 'getCompletedBy'));
+ if ($decider === '') {
+ $decider = trim((string)$this->read(subject: $task, getter: 'getAssignee'));
+ }
+
+ if ($decider === '') {
+ // Never decide as nobody: the decision stays on the task until a
+ // person owns it, and the Approval stays pending in hermiq.
+ $this->logger->warning(
+ 'Hermiq skipped mirror task ' . ((string)$this->read(subject: $task, getter: 'getUuid'))
+ . ' for approval ' . $approvalUuid . ': it carries no completing identity.'
+ );
+
+ return;
+ }
+
+ $outcome = strtolower(trim((string)$this->read(subject: $task, getter: 'getOutcome')));
+ if (in_array($outcome, self::REJECTING_OUTCOMES, true) === true) {
+ $reason = trim((string)$this->read(subject: $task, getter: 'getComment'));
+ if ($reason === '') {
+ $reason = trim((string)$this->read(subject: $task, getter: 'getResultText'));
+ }
+
+ $this->approvalService->deny(approval: $approval, deciderUid: $decider, reason: $reason);
+
+ return;
+ }
+
+ $this->approvalService->approve(approval: $approval, deciderUid: $decider);
+
+ }//end apply()
+
+ /**
+ * The hermiq ownership marker: `metadata.hermiq.approvalUuid`.
+ *
+ * @param object $task The terminal task.
+ *
+ * @return string The linked Approval uuid, or '' for a foreign task.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+ private function approvalUuidOf(object $task): string {
+ $metadata = $this->read(subject: $task, getter: 'getMetadata');
+ if (is_array($metadata) === false) {
+ return '';
+ }
+
+ $hermiq = ($metadata['hermiq'] ?? null);
+ if (is_array($hermiq) === false) {
+ return '';
+ }
+
+ return trim((string)($hermiq['approvalUuid'] ?? ''));
+
+ }//end approvalUuidOf()
+
+ /**
+ * Read a value through a dynamic getter that works for real methods and
+ * for NC's `Entity::__call` magic getters alike.
+ *
+ * @param object $subject The event or task.
+ * @param string $getter The getter name.
+ *
+ * @return mixed The value, or null when unreadable.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+ private function read(object $subject, string $getter): mixed {
+ $callable = [$subject, $getter];
+ if (is_callable($callable) === false) {
+ return null;
+ }
+
+ try {
+ return $callable();
+ } catch (Throwable) {
+ return null;
+ }
+
+ }//end read()
+}//end class
diff --git a/lib/Repair/MirrorPendingApprovalsToTasks.php b/lib/Repair/MirrorPendingApprovalsToTasks.php
new file mode 100644
index 00000000..a259c959
--- /dev/null
+++ b/lib/Repair/MirrorPendingApprovalsToTasks.php
@@ -0,0 +1,191 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Repair;
+
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Mirrors pending Approvals that predate the convergence seam.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+class MirrorPendingApprovalsToTasks implements IRepairStep {
+
+ /**
+ * OpenRegister register slug that holds Hermiq objects.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'hermiq';
+
+ /**
+ * OpenRegister schema slug for approval objects.
+ *
+ * @var string
+ */
+ private const APPROVAL_SCHEMA = 'approval';
+
+ /**
+ * Constructor.
+ *
+ * @param ObjectService $objectService Reads pending Approvals and writes the backlink.
+ * @param ApprovalTaskBridge $taskBridge Creates the mirror tasks (never throws).
+ * @param LoggerInterface $logger Diagnostics.
+ */
+ public function __construct(
+ private readonly ObjectService $objectService,
+ private readonly ApprovalTaskBridge $taskBridge,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * The step's display name.
+ *
+ * @return string The name shown during upgrade.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+ public function getName(): string {
+ return 'Mirror in-flight hermiq approvals as OpenRegister tasks';
+ }//end getName()
+
+ /**
+ * Mirror every pending Approval that has no task yet.
+ *
+ * @param IOutput $output Upgrade progress output.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+ public function run(IOutput $output): void {
+ try {
+ $objects = $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::APPROVAL_SCHEMA)
+ ->findAll(
+ config: ['filters' => ['status' => 'pending']],
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (Throwable $e) {
+ // A fresh install has no register yet; an upgrade whose descriptor
+ // import has not run this boot is in the same position. The seam
+ // mirrors every approval created from here on, so nothing is lost.
+ $output->info('Hermiq approval mirroring skipped: ' . $e->getMessage());
+ $this->logger->info(
+ 'MirrorPendingApprovalsToTasks skipped: ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+
+ return;
+ }//end try
+
+ $mirrored = 0;
+ $already = 0;
+ $failed = [];
+ foreach ($objects as $object) {
+ if (($object instanceof ObjectEntity) === false) {
+ continue;
+ }
+
+ $data = $object->getObject();
+ if ((string)($data['status'] ?? '') !== 'pending') {
+ continue;
+ }
+
+ if (trim((string)($data['taskUuid'] ?? '')) !== '') {
+ $already++;
+ continue;
+ }
+
+ $taskUuid = $this->taskBridge->ensureTaskFor(approval: $object);
+ if ($taskUuid === null) {
+ $failed[] = (string)$object->getUuid();
+ continue;
+ }
+
+ try {
+ $data['taskUuid'] = $taskUuid;
+ $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::APPROVAL_SCHEMA)
+ ->saveObject(
+ object: $data,
+ register: self::REGISTER_SLUG,
+ schema: self::APPROVAL_SCHEMA,
+ uuid: (string)$object->getUuid(),
+ _rbac: false,
+ _multitenancy: false
+ );
+ $mirrored++;
+ } catch (Throwable $e) {
+ $failed[] = (string)$object->getUuid();
+ $this->logger->warning(
+ 'MirrorPendingApprovalsToTasks could not write taskUuid onto approval '
+ . ((string)$object->getUuid()) . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ }
+ }//end foreach
+
+ $output->info(
+ sprintf(
+ 'Hermiq approval mirroring: %d mirrored, %d already mirrored, %d failed.',
+ $mirrored,
+ $already,
+ count($failed)
+ )
+ );
+
+ if ($failed !== []) {
+ // Named, loudly, but never fatal: the unmirrored approvals stay
+ // decidable on hermiq's own surface, and a re-run picks them up.
+ $output->warning('Unmirrored approvals: ' . implode(', ', $failed));
+ }
+
+ }//end run()
+}//end class
diff --git a/lib/Repair/MirrorSchedulesToEngineFlows.php b/lib/Repair/MirrorSchedulesToEngineFlows.php
new file mode 100644
index 00000000..474c46af
--- /dev/null
+++ b/lib/Repair/MirrorSchedulesToEngineFlows.php
@@ -0,0 +1,110 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Repair;
+
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Mirrors eligible schedules that predate the engine delegation seam.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+class MirrorSchedulesToEngineFlows implements IRepairStep {
+
+ /**
+ * Constructor.
+ *
+ * @param ScheduleFlowBridge $flowBridge The sync pass (mirror/refresh/retire).
+ * @param LoggerInterface $logger Diagnostics.
+ */
+ public function __construct(
+ private readonly ScheduleFlowBridge $flowBridge,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * The step's display name.
+ *
+ * @return string The name shown during upgrade.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function getName(): string {
+ return 'Delegate eligible hermiq schedules to OpenRegister\'s schedule trigger';
+ }//end getName()
+
+ /**
+ * Run one sync pass over every schedule.
+ *
+ * @param IOutput $output Upgrade progress output.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function run(IOutput $output): void {
+ try {
+ $stats = $this->flowBridge->syncAll();
+ } catch (Throwable $e) {
+ // A fresh install has no register yet; an OpenRegister without the
+ // flow store answers the same way. The per-tick sync arms every
+ // schedule from here on, so nothing is lost.
+ $output->info('Hermiq schedule delegation skipped: ' . $e->getMessage());
+ $this->logger->info(
+ 'MirrorSchedulesToEngineFlows skipped: ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ return;
+ }
+
+ $output->info(
+ sprintf(
+ 'Hermiq schedule delegation: %d mirrored, %d refreshed, %d retired, %d stayed local, %d failed.',
+ $stats['mirrored'],
+ $stats['refreshed'],
+ $stats['retired'],
+ $stats['skipped'],
+ $stats['failed']
+ )
+ );
+
+ }//end run()
+}//end class
diff --git a/lib/Repair/PruneRetiredAgentFlowSchemas.php b/lib/Repair/PruneRetiredAgentFlowSchemas.php
new file mode 100644
index 00000000..8deb0135
--- /dev/null
+++ b/lib/Repair/PruneRetiredAgentFlowSchemas.php
@@ -0,0 +1,201 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Repair;
+
+use OCA\Hermiq\AppInfo\Application;
+use OCA\OpenRegister\Db\RegisterMapper;
+use OCA\OpenRegister\Db\Schema;
+use OCA\OpenRegister\Db\SchemaMapper;
+use OCA\OpenRegister\Service\SchemaDeletionService;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Deletes the retired agentflow object store's leftovers from the instance.
+ *
+ * Flows are authored in OpenRegister's native flow store (REQ-FA-002); the
+ * `agentflow` / `agentflowrun` object store that predated it is retired — its
+ * runner, resolver and frontend are gone, and this release drops the two
+ * schemas from `hermiq_register.json`. Dropping them from the descriptor is
+ * only half the retirement: OpenRegister's import UNIONS schema ids into the
+ * register and never removes one, so on every instance that ever imported the
+ * old descriptor both schemas keep their rows, their magic tables and their
+ * place in the register's `schemas` array forever. This step is the other
+ * half, mirroring `occ openregister:schemas:prune-retired` (the command this
+ * borrows its mechanics from) so no operator has to remember to run it.
+ *
+ * IT DELETES ROWS, DELIBERATELY. Any surviving object is unreachable: no
+ * hermiq code reads or writes the schemas any more, so what remains is demo
+ * seed data and pre-pivot copies of flows that were long since re-seeded into
+ * the native flow store. Keeping the rows would keep the schemas, and with
+ * them the retired store's claim on the `agentflow` slug.
+ *
+ * App-scoped by construction: schemas are resolved with
+ * `findByApplicationAndSlug(..., application: 'hermiq')`, so a same-slug
+ * schema another app owns is out of reach.
+ *
+ * Idempotent: once pruned (or on a fresh install that never imported the old
+ * descriptor) the lookup finds nothing and the step is a logged no-op. Never
+ * raises — a repair step that aborts the upgrade over stale rows would trade
+ * dead data for an instance that will not start.
+ *
+ * @spec openspec/specs/flow-authoring/spec.md#requirement-a-flow-is-stored-once-by-openregister-req-fa-002
+ */
+class PruneRetiredAgentFlowSchemas implements IRepairStep {
+
+ /**
+ * The retired schema slugs, exactly as the old descriptor declared them.
+ *
+ * @var string[]
+ */
+ public const RETIRED_SLUGS = [
+ 'agentflow',
+ 'agentflowrun',
+ ];
+
+ /**
+ * Constructor.
+ *
+ * @param ContainerInterface $container Server container for lazy OpenRegister resolution
+ * (OpenRegister may be absent, or predate the deletion service).
+ * @param LoggerInterface $logger PSR-3 logger.
+ */
+ public function __construct(
+ private readonly ContainerInterface $container,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * Repair-step name, as shown by `occ upgrade`.
+ *
+ * @return string The name.
+ *
+ * @spec openspec/specs/flow-authoring/spec.md#requirement-a-flow-is-stored-once-by-openregister-req-fa-002
+ */
+ public function getName(): string {
+ return 'Prune the retired agentflow / agentflowrun schemas from OpenRegister';
+ }//end getName()
+
+ /**
+ * Resolve each retired slug within hermiq's ownership and cascade-delete it.
+ *
+ * Unlink-before-delete, in the prune command's order: a register left
+ * pointing at a deleted schema id makes every later slug resolution in
+ * that register scan a dangling reference.
+ *
+ * @param IOutput $output Repair output channel.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/flow-authoring/spec.md#requirement-a-flow-is-stored-once-by-openregister-req-fa-002
+ */
+ public function run(IOutput $output): void {
+ try {
+ $schemaMapper = $this->container->get(SchemaMapper::class);
+ $registerMapper = $this->container->get(RegisterMapper::class);
+ $deletionService = $this->container->get(SchemaDeletionService::class);
+ } catch (Throwable $e) {
+ // OpenRegister absent, or too old to carry the deletion service.
+ // Recoverable either way: the step re-runs on the next upgrade.
+ $output->info('OpenRegister schema pruning unavailable — skipping the agentflow prune.');
+ $this->logger->info('[hermiq] Agentflow schema prune skipped: ' . $e->getMessage());
+ return;
+ }
+
+ foreach (self::RETIRED_SLUGS as $slug) {
+ try {
+ $schema = $schemaMapper->findByApplicationAndSlug(slug: $slug, application: Application::APP_ID);
+ if ($schema === null) {
+ // Already pruned, or never imported. This is the idempotent path.
+ $output->info('Retired schema "' . $slug . '" is not present — nothing to prune.');
+ continue;
+ }
+
+ $this->unlinkFromRegisters(registerMapper: $registerMapper, schema: $schema, output: $output);
+
+ $result = $deletionService->cascadeDeleteSchema(schema: $schema);
+ $output->info(
+ 'Pruned retired schema "' . $slug . '" (objects removed: '
+ . (int)$result['deletedCount'] . ').'
+ );
+ } catch (Throwable $e) {
+ // Reported, not raised — see the class docblock.
+ $this->logger->warning(
+ '[hermiq] Could not prune the retired schema "' . $slug . '": '
+ . $e::class . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ $output->warning('Could not prune retired schema "' . $slug . '": ' . $e->getMessage());
+ }//end try
+ }//end foreach
+
+ }//end run()
+
+ /**
+ * Drop the schema's id from every register that references it.
+ *
+ * Same coercion rule as `PruneRetiredSchemasCommand::unlinkSchemaId()`: the
+ * stored list holds ids as ints or strings depending on which import era
+ * wrote them, so `"74"` and `74` are the same reference and both go, while
+ * non-numeric entries are preserved untouched.
+ *
+ * @param RegisterMapper $registerMapper The register store.
+ * @param Schema $schema The schema being pruned.
+ * @param IOutput $output Repair output channel.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/flow-authoring/spec.md#requirement-a-flow-is-stored-once-by-openregister-req-fa-002
+ */
+ private function unlinkFromRegisters(RegisterMapper $registerMapper, Schema $schema, IOutput $output): void {
+ $schemaId = (int)$schema->getId();
+
+ foreach ($registerMapper->findAll(_rbac: false, _multitenancy: false) as $register) {
+ $remaining = [];
+ $changed = false;
+ foreach ($register->getSchemas() as $ref) {
+ if ((is_int($ref) === true || (is_string($ref) === true && is_numeric($ref) === true))
+ && (int)$ref === $schemaId
+ ) {
+ $changed = true;
+ continue;
+ }
+
+ $remaining[] = $ref;
+ }
+
+ if ($changed === false) {
+ continue;
+ }
+
+ $register->setSchemas($remaining);
+ $registerMapper->update($register);
+ $output->info(
+ 'Unlinked retired schema id ' . $schemaId . ' from register "'
+ . (string)$register->getSlug() . '".'
+ );
+ }//end foreach
+
+ }//end unlinkFromRegisters()
+}//end class
diff --git a/lib/Repair/SeedHydraTriageFlow.php b/lib/Repair/SeedHydraTriageFlow.php
index 979456c6..761297f6 100644
--- a/lib/Repair/SeedHydraTriageFlow.php
+++ b/lib/Repair/SeedHydraTriageFlow.php
@@ -106,13 +106,6 @@ class SeedHydraTriageFlow implements IRepairStep {
*/
public const REGISTER_SLUG = 'hermiq';
- /**
- * Schema slug for agentflow objects.
- *
- * @var string
- */
- public const FLOW_SCHEMA = 'agentflow';
-
/**
* The seeded flow's name — also the idempotency key.
*
diff --git a/lib/Service/Approval/ApprovalTaskBridge.php b/lib/Service/Approval/ApprovalTaskBridge.php
new file mode 100644
index 00000000..5cfd2d13
--- /dev/null
+++ b/lib/Service/Approval/ApprovalTaskBridge.php
@@ -0,0 +1,289 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Service\Approval;
+
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\Task\TaskService;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Mirrors pending Approvals as OpenRegister tasks and releases the mirrors.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+class ApprovalTaskBridge {
+
+ /**
+ * FQN of OpenRegister's task service, as a string on purpose: a cross-app
+ * class name is a runtime lookup, and an import would hard-couple hermiq's
+ * boot to a namespace an older OpenRegister does not ship.
+ *
+ * @var string
+ */
+ public const TASK_SERVICE_CLASS = 'OCA\\OpenRegister\\Service\\Task\\TaskService';
+
+ /**
+ * The system seat recorded as the mirror task's requester. Never the
+ * reviewer: OpenRegister's separation-of-duties guard must not mistake
+ * the initiating system for the decider (design.md Decision 3).
+ *
+ * @var string
+ */
+ public const REQUESTER = 'hermiq:approval-gate';
+
+ /**
+ * The actor recorded when hermiq releases a mirror after its own decision.
+ *
+ * @var string
+ */
+ public const RELEASE_SOURCE = 'hermiq:approval-gate';
+
+ /**
+ * Constructor.
+ *
+ * @param ContainerInterface $container Lazy, call-time TaskService resolution.
+ * @param LoggerInterface $logger PSR-3 logger (degraded-surface diagnostics).
+ */
+ public function __construct(
+ private readonly ContainerInterface $container,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * Ensure the OpenRegister mirror task for a pending Approval, returning
+ * its uuid (the caller persists it onto the Approval as `taskUuid`).
+ *
+ * Idempotent through the caller's guard: an Approval already carrying a
+ * `taskUuid` is returned as-is without touching the task surface. Never
+ * throws: a missing or older task surface logs a warning and returns
+ * null, and the approval write-path proceeds unmirrored.
+ *
+ * @param ObjectEntity $approval The just-persisted pending Approval.
+ *
+ * @return string|null The mirror task's uuid, or null when unmirrored.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+ public function ensureTaskFor(ObjectEntity $approval): ?string {
+ $data = $approval->getObject();
+ $existing = trim((string)($data['taskUuid'] ?? ''));
+ if ($existing !== '') {
+ return $existing;
+ }
+
+ if ((string)($data['status'] ?? '') !== 'pending') {
+ return null;
+ }
+
+ $approvalUuid = (string)$approval->getUuid();
+ if ($approvalUuid === '') {
+ return null;
+ }
+
+ try {
+ $tasks = $this->taskService();
+ if ($tasks === null) {
+ return null;
+ }
+
+ $task = $tasks->import(
+ data: $this->taskData(approvalUuid: $approvalUuid, data: $data),
+ actor: self::REQUESTER
+ );
+
+ $uuid = (string)$task->getUuid();
+ if ($uuid === '') {
+ return null;
+ }
+
+ return $uuid;
+ } catch (Throwable $e) {
+ $this->logger->warning(
+ 'Hermiq could not mirror approval ' . $approvalUuid
+ . ' as an OpenRegister task: ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+
+ return null;
+ }//end try
+
+ }//end ensureTaskFor()
+
+ /**
+ * Release an Approval's mirror after a decision on hermiq's own surface:
+ * the task is terminated as moot so it leaves every OpenRegister inbox.
+ *
+ * Idempotent both ways: no `taskUuid` is a no-op, and OpenRegister's
+ * `terminateAsMoot()` returns an already-terminal task untouched, which
+ * is exactly the case when the decision CAME from the task (design.md
+ * Decision 1). Never throws.
+ *
+ * @param ObjectEntity $approval The decided Approval.
+ * @param string $decision The decision applied (`approved`|`denied`).
+ * @param string $deciderUid The deciding user.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-hermiq-decision-releases-the-mirror
+ */
+ public function releaseFor(ObjectEntity $approval, string $decision, string $deciderUid): void {
+ $data = $approval->getObject();
+ $taskUuid = trim((string)($data['taskUuid'] ?? ''));
+ if ($taskUuid === '') {
+ return;
+ }
+
+ try {
+ $tasks = $this->taskService();
+ if ($tasks === null) {
+ return;
+ }
+
+ $tasks->terminateAsMoot(
+ uuid: $taskUuid,
+ reason: 'Decided in hermiq: ' . $decision . ' by ' . $deciderUid,
+ source: self::RELEASE_SOURCE
+ );
+ } catch (Throwable $e) {
+ $this->logger->warning(
+ 'Hermiq could not release mirror task ' . $taskUuid
+ . ' for approval ' . ((string)$approval->getUuid()) . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ }
+
+ }//end releaseFor()
+
+ /**
+ * Build the mirror task's creation payload (the UserTaskConfig
+ * conventions: an assigned user reviewer makes an `active` task, a group
+ * reviewer an `enabled` one offered to the group).
+ *
+ * @param string $approvalUuid The Approval's uuid (backlink + anchor).
+ * @param array $data The Approval's payload.
+ *
+ * @return array The `TaskService::import()` data.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+ private function taskData(string $approvalUuid, array $data): array {
+ $sourceType = (string)($data['sourceType'] ?? 'schedule');
+ $reviewer = trim((string)($data['reviewer'] ?? ''));
+ $reviewerType = (string)($data['reviewerType'] ?? 'user');
+
+ $assignee = null;
+ $candidateGroups = null;
+ $state = 'enabled';
+ $performerType = 'group';
+ if ($reviewerType !== 'group' && $reviewer !== '') {
+ $assignee = $reviewer;
+ $candidateGroups = null;
+ $state = 'active';
+ $performerType = 'user';
+ }
+
+ if ($reviewerType === 'group' && $reviewer !== '') {
+ $candidateGroups = [$reviewer];
+ }
+
+ $agentId = trim((string)($data['agentId'] ?? ''));
+ $title = 'Hermiq approval: ' . $sourceType;
+ if ($agentId !== '') {
+ $title .= ' (agent ' . $agentId . ')';
+ }
+
+ $prompt = trim((string)($data['prompt'] ?? ''));
+ $description = 'A gated hermiq ' . $sourceType . ' action awaits your approval.';
+ if ($prompt !== '') {
+ $description .= "\n\n" . $prompt;
+ }
+
+ return [
+ 'title' => $title,
+ 'description' => $description,
+ 'state' => $state,
+ 'performerType' => $performerType,
+ 'priority' => 'high',
+ 'assignee' => $assignee,
+ 'candidateGroups' => $candidateGroups,
+ 'requester' => self::REQUESTER,
+ 'objectUuid' => $approvalUuid,
+ 'metadata' => [
+ 'hermiq' => [
+ 'approvalUuid' => $approvalUuid,
+ 'sourceType' => $sourceType,
+ ],
+ ],
+ ];
+
+ }//end taskData()
+
+ /**
+ * Resolve OpenRegister's TaskService, or null when this OpenRegister does
+ * not ship the task surface. Deliberately quiet on ContainerInterface
+ * misses (the degraded install IS the documented branch); anything else
+ * bubbles to the callers' logged catch.
+ *
+ * @return TaskService|null The task service, or null when unavailable.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+ private function taskService(): ?TaskService {
+ if (class_exists(self::TASK_SERVICE_CLASS) === false) {
+ $this->logger->warning(
+ 'Hermiq approval-task mirroring is inactive: this OpenRegister does not ship '
+ . self::TASK_SERVICE_CLASS . '.'
+ );
+
+ return null;
+ }
+
+ $service = $this->container->get(self::TASK_SERVICE_CLASS);
+ if (($service instanceof TaskService) === false) {
+ return null;
+ }
+
+ return $service;
+
+ }//end taskService()
+}//end class
diff --git a/lib/Service/ApprovalService.php b/lib/Service/ApprovalService.php
index 91985858..7573cbef 100644
--- a/lib/Service/ApprovalService.php
+++ b/lib/Service/ApprovalService.php
@@ -87,6 +87,7 @@
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
use OCA\OpenRegister\Db\AuditTrailMapper;
use OCA\OpenRegister\Db\ObjectEntity;
use OCA\OpenRegister\Service\ObjectService;
@@ -170,6 +171,8 @@ class ApprovalService {
* @param RedactionService $redactionService Masks the (user-supplied) reason before the audit write.
* @param ContainerInterface $container Server container for lazy ScheduleService resolution.
* @param LoggerInterface $logger PSR-3 logger (non-fatal gate diagnostics).
+ * @param ApprovalTaskBridge $taskBridge Mirrors pending Approvals as OpenRegister tasks
+ * and releases the mirrors on decision (approval-task convergence, phase 1).
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList) Constructor DI: each parameter is
* a distinct injected collaborator, not a logic-bearing argument list.
@@ -184,6 +187,7 @@ public function __construct(
private readonly RedactionService $redactionService,
private readonly ContainerInterface $container,
private readonly LoggerInterface $logger,
+ private readonly ApprovalTaskBridge $taskBridge,
) {
}//end __construct()
@@ -1277,6 +1281,12 @@ public function approve(ObjectEntity $approval, string $deciderUid): array {
);
$this->writeDecisionAudit(approval: $approval, action: 'approve', reason: '');
+ // Release the OpenRegister mirror BEFORE resuming: a resume crash must
+ // not leave a decided approval dangling in anyone's task inbox. When
+ // the decision CAME from the task, the mirror is already terminal and
+ // this is a no-op (design.md Decision 1).
+ $this->taskBridge->releaseFor(approval: $approval, decision: 'approved', deciderUid: $deciderUid);
+
$sourceType = (string)($data['sourceType'] ?? 'schedule');
$ran = $this->resumeGatedRun(sourceType: $sourceType, data: $data);
@@ -1382,6 +1392,10 @@ public function deny(ObjectEntity $approval, string $deciderUid, ?string $reason
);
$this->writeDecisionAudit(approval: $approval, action: 'deny', reason: $cleanReason);
+ // Release the OpenRegister mirror: idempotent when the denial came
+ // from the task itself (design.md Decision 1).
+ $this->taskBridge->releaseFor(approval: $approval, decision: 'denied', deciderUid: $deciderUid);
+
if ((string)($data['sourceType'] ?? '') === 'skill-draft') {
// Skill-self-improvement: denial from ANY surface reconciles the draft
// to `rejected` (idempotent — a draft already settled stays settled).
@@ -1683,7 +1697,7 @@ private function persistApproval(array $data, ?string $uuid, string $owner): Obj
}
try {
- return $this->objectService->saveObject(
+ $saved = $this->objectService->saveObject(
object: $data,
register: self::REGISTER_SLUG,
schema: self::APPROVAL_SCHEMA,
@@ -1691,12 +1705,71 @@ private function persistApproval(array $data, ?string $uuid, string $owner): Obj
_rbac: false,
_multitenancy: false
);
+
+ // Approval-task convergence, phase 1 (design.md Decision 2): the
+ // CREATION of a pending Approval is the one choke point every
+ // gated shape flows through, so the OpenRegister mirror task is
+ // ensured here and nowhere else. Decision saves (uuid known)
+ // never mirror. Still inside the owner impersonation on purpose:
+ // the backlink write-back must act as the same identity as the
+ // save it amends.
+ if ($uuid === null && (string)($data['status'] ?? '') === 'pending') {
+ $saved = $this->mirrorPendingApproval(approval: $saved, data: $data);
+ }
+
+ return $saved;
} finally {
$this->userSession->setUser($priorUser);
}
}//end persistApproval()
+ /**
+ * Mirror a just-created pending Approval as an OpenRegister task and
+ * write the mirror's uuid back onto the Approval as `taskUuid`.
+ *
+ * Non-fatal by contract, in both halves: the bridge never throws (an
+ * absent task surface is a logged no-op and the Approval stays
+ * authoritative), and a failed backlink write-back is logged rather than
+ * raised — the mirror still routes through its own metadata backlink,
+ * only release and rollback lose their shortcut.
+ *
+ * @param ObjectEntity $approval The just-persisted pending Approval.
+ * @param array $data The Approval payload as saved.
+ *
+ * @return ObjectEntity The Approval, re-saved with `taskUuid` when mirrored.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+ private function mirrorPendingApproval(ObjectEntity $approval, array $data): ObjectEntity {
+ $taskUuid = $this->taskBridge->ensureTaskFor(approval: $approval);
+ if ($taskUuid === null) {
+ return $approval;
+ }
+
+ try {
+ $data['taskUuid'] = $taskUuid;
+
+ return $this->objectService->saveObject(
+ object: $data,
+ register: self::REGISTER_SLUG,
+ schema: self::APPROVAL_SCHEMA,
+ uuid: (string)$approval->getUuid(),
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (Throwable $e) {
+ $this->logger->warning(
+ 'Hermiq could not write taskUuid ' . $taskUuid . ' back onto approval '
+ . ((string)$approval->getUuid()) . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+
+ return $approval;
+ }
+
+ }//end mirrorPendingApproval()
+
/**
* Write the decision AuditTrail entry via OpenRegister (redaction-before-persist).
*
diff --git a/lib/Service/DemoDataService.php b/lib/Service/DemoDataService.php
index 21adeb0a..6fb683f1 100644
--- a/lib/Service/DemoDataService.php
+++ b/lib/Service/DemoDataService.php
@@ -87,8 +87,110 @@ public function isAvailable(): bool {
}//end isAvailable()
/**
- * Import the demo dataset.
+ * The answer that means "plant nothing".
+ *
+ * 🔴 NOT THE ABSENCE OF AN ANSWER. An operator who declines has FINISHED the
+ * step; a step that can never be marked done reopens the wizard over every
+ * page (nextcloud-vue#806).
+ *
+ * @var string
+ */
+ public const NONE_DATASET = 'none';
+
+ /**
+ * The id of the dataset this app ships.
+ *
+ * @var string
+ */
+ public const DEMO_DATASET = 'demo';
+
+ /**
+ * Every answer the wizard's choice step may offer, declining included.
+ *
+ * 🔴 THE SERVER OWNS THIS LIST, AND THAT IS THE POINT. The step declares
+ * `optionsSource: datasets` and no options of its own, so the label, the
+ * description and the object count come from the descriptor that will
+ * actually be imported. A manifest that restated them could disagree with
+ * what lands, and nothing would notice.
+ *
+ * @return array The answers.
+ *
+ * @spec exclude Demo-data choice list; ADR-111 rule 1 has no per-app behavioural spec.
+ */
+ public function listChoices(): array {
+ $choices = [
+ [
+ 'id' => self::NONE_DATASET,
+ 'label' => 'None, I will set this up myself',
+ 'description' => 'Nothing is imported. You start with an empty app and add your own data.',
+ 'objectCount' => 0,
+ 'icon' => 'CloseCircleOutline',
+ ],
+ ];
+
+ $objects = $this->shippedObjectCount();
+ if ($objects !== null) {
+ $choices[] = [
+ 'id' => self::DEMO_DATASET,
+ 'label' => 'Example data',
+ // 🔴 NO NUMBER IN THIS SENTENCE. The wizard runs a card's
+ // description through the app's translation function, which is a
+ // literal lookup, so an interpolated count would make the string
+ // untranslatable and leave a Dutch operator reading English. The
+ // count travels as `objectCount` and the card renders it as a
+ // stat, with a label the library translates.
+ 'description' => (
+ 'Sample values for every schema this app supplies, generated from the schemas '
+ . 'themselves. It shows the lists, detail pages and dashboards working rather '
+ . 'than telling a story. Safe to run more than once, and you can delete it '
+ . 'afterwards.'
+ ),
+ 'objectCount' => $objects,
+ 'icon' => 'DatabaseOutline',
+ ];
+ }
+
+ return $choices;
+
+ }//end listChoices()
+
+ /**
+ * How many objects the shipped descriptor carries, or null when it ships none.
+ *
+ * Counted from the FILE, so the card promises the number that will actually
+ * be imported. A missing or malformed descriptor returns null and the app
+ * then offers only "None" — honest, rather than an import that cannot run.
*
+ * @return integer|null The object count, or null when there is no usable descriptor.
+ */
+ private function shippedObjectCount(): ?int {
+ $path = $this->descriptorPath();
+ if (is_file($path) === false) {
+ return null;
+ }
+
+ $raw = file_get_contents($path);
+ if ($raw === false) {
+ return null;
+ }
+
+ $data = json_decode($raw, true);
+ if (is_array($data) === false) {
+ return null;
+ }
+
+ $components = ($data['components'] ?? []);
+ if (is_array($components) === false || is_array(($components['objects'] ?? null)) === false) {
+ return 0;
+ }
+
+ return count($components['objects']);
+
+ }//end shippedObjectCount()
+
+ /**
+ * Import the demo dataset.
+ *''
* 🔴 THROWS RATHER THAN RETURNING A QUIET FAILURE. The caller reports the
* outcome to an operator who just asked for this, so "nothing happened"
* must not be presentable as success.
diff --git a/lib/Service/Schedule/ScheduleCadenceMapper.php b/lib/Service/Schedule/ScheduleCadenceMapper.php
new file mode 100644
index 00000000..f046d15a
--- /dev/null
+++ b/lib/Service/Schedule/ScheduleCadenceMapper.php
@@ -0,0 +1,257 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Service
+ * @package OCA\Hermiq\Service\Schedule
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Service\Schedule;
+
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCP\IConfig;
+
+/**
+ * The shape map, as one small decision surface.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+class ScheduleCadenceMapper {
+
+ /**
+ * Constructor.
+ *
+ * @param IConfig $config Reads owner/instance timezone for the safety check.
+ */
+ public function __construct(
+ private readonly IConfig $config,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * The cron expression the engine should fire this schedule on, or null.
+ *
+ * `kind=cron` passes through when it has 5 fields AND the timezone
+ * difference between the owner and the server cannot change its meaning.
+ * `kind=interval` derives a pure-cadence expression when the minutes
+ * divide an hour, or whole hours divide a day; a cadence survives any
+ * fixed offset, so it needs no timezone check. `kind=once` never maps:
+ * a deadline is FlowTimerService's primitive, staged in phase 2.
+ *
+ * @param ObjectEntity $schedule The schedule.
+ *
+ * @return string|null The 5-field cron, or null when no safe form exists.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function mappedCron(ObjectEntity $schedule): ?string {
+ $data = $schedule->getObject();
+ $kind = (string)($data['kind'] ?? '');
+
+ if ($kind === 'cron') {
+ return $this->passthroughCron(
+ cron: trim((string)($data['cronExpr'] ?? '')),
+ owner: (string)($schedule->getOwner() ?? '')
+ );
+ }
+
+ if ($kind === 'interval') {
+ return $this->cronForInterval(minutes: (int)($data['intervalMinutes'] ?? 0));
+ }
+
+ return null;
+ }//end mappedCron()
+
+ /**
+ * Name the staged phase that covers this schedule's timing shape.
+ *
+ * @param array $data The schedule payload.
+ *
+ * @return string The reason the shape stays local.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function gapReason(array $data): string {
+ $kind = (string)($data['kind'] ?? '');
+
+ if ($kind === 'once') {
+ return 'once schedules stay local until the FlowTimerService one-shot lands (staged phase 2)';
+ }
+
+ if ($kind === 'interval') {
+ return 'this interval has no 5-field cron form; it stays local (staged phase 2)';
+ }
+
+ return 'this cron is not a 5-field timezone-safe expression the engine can evaluate; it stays local (staged phase 2)';
+ }//end gapReason()
+
+ /**
+ * A `kind=cron` expression, when it is safe to hand to the engine.
+ *
+ * @param string $cron The stored expression.
+ * @param string $owner The schedule owner uid.
+ *
+ * @return string|null The expression, or null when it must stay local.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function passthroughCron(string $cron, string $owner): ?string {
+ $fields = preg_split('/\s+/', $cron);
+ if (is_array($fields) === false || count($fields) !== 5) {
+ return null;
+ }
+
+ if ($this->timezoneSafe(fields: $fields, owner: $owner) === false) {
+ return null;
+ }
+
+ return $cron;
+ }//end passthroughCron()
+
+ /**
+ * Whether owner-vs-server timezone difference can change this cron's meaning.
+ *
+ * The dispatcher evaluated cron in the OWNER's timezone; the engine
+ * evaluates in the server default. Safe when the two are the same zone,
+ * or when the expression is a pure minute cadence (minute `*` or a step,
+ * every other field `*`). An hour-anchored cron for an owner in another
+ * zone would silently shift, so it stays local. Shifting the hour field
+ * by the offset was rejected in design.md: DST makes that rewrite wrong
+ * twice a year.
+ *
+ * @param array $fields The 5 cron fields.
+ * @param string $owner The schedule owner uid.
+ *
+ * @return boolean Whether the expression is timezone-safe to mirror.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function timezoneSafe(array $fields, string $owner): bool {
+ if ($this->ownerTimezoneName(owner: $owner) === date_default_timezone_get()) {
+ return true;
+ }
+
+ $minuteIsCadence = ($fields[0] === '*' || preg_match('/^\*\/\d+$/', $fields[0]) === 1);
+
+ return $minuteIsCadence
+ && $fields[1] === '*'
+ && $fields[2] === '*'
+ && $fields[3] === '*'
+ && $fields[4] === '*';
+ }//end timezoneSafe()
+
+ /**
+ * The owner's resolved timezone name, mirroring the dispatcher's rule.
+ *
+ * Owner's Nextcloud timezone, else the instance default, else UTC. The
+ * SAME fallback chain `ScheduleService::resolveTimezone()` applies, so
+ * the safety check compares the zones the two clocks would actually use.
+ *
+ * @param string $owner The owner uid.
+ *
+ * @return string The timezone name.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function ownerTimezoneName(string $owner): string {
+ $tz = '';
+ if ($owner !== '') {
+ $tz = (string)$this->config->getUserValue($owner, 'core', 'timezone', '');
+ }
+
+ if ($tz === '') {
+ $tz = (string)$this->config->getSystemValueString('default_timezone', 'UTC');
+ }
+
+ return $tz;
+ }//end ownerTimezoneName()
+
+ /**
+ * The 5-field cadence for an interval, or null when none expresses it.
+ *
+ * The mirror fires ALIGNED (every N minutes on the wall clock) where the
+ * poller fired ROLLING (N minutes after the last run). The cadence is
+ * preserved; the phase may shift once at migration. Accepted in
+ * design.md: rolling was an artefact of the poller, not a promise.
+ *
+ * @param int $minutes The interval in minutes.
+ *
+ * @return string|null The cron, or null.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function cronForInterval(int $minutes): ?string {
+ if ($minutes >= 1 && $minutes < 60) {
+ return $this->minuteCadence(minutes: $minutes);
+ }
+
+ if ($minutes >= 60 && ($minutes % 60) === 0) {
+ return $this->hourCadence(hours: intdiv($minutes, 60));
+ }
+
+ return null;
+ }//end cronForInterval()
+
+ /**
+ * A sub-hour cadence, when the minutes divide an hour evenly.
+ *
+ * @param int $minutes The interval in minutes (1..59).
+ *
+ * @return string|null The cron, or null.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function minuteCadence(int $minutes): ?string {
+ if ((60 % $minutes) !== 0) {
+ return null;
+ }
+
+ if ($minutes === 1) {
+ return '* * * * *';
+ }
+
+ return '*/' . $minutes . ' * * * *';
+ }//end minuteCadence()
+
+ /**
+ * A whole-hour cadence, when the hours divide a day evenly.
+ *
+ * @param int $hours The interval in whole hours.
+ *
+ * @return string|null The cron, or null.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function hourCadence(int $hours): ?string {
+ if ($hours === 1) {
+ return '0 * * * *';
+ }
+
+ if ($hours <= 23 && (24 % $hours) === 0) {
+ return '0 */' . $hours . ' * * *';
+ }
+
+ return null;
+ }//end hourCadence()
+}//end class
diff --git a/lib/Service/Schedule/ScheduleFlowBridge.php b/lib/Service/Schedule/ScheduleFlowBridge.php
new file mode 100644
index 00000000..d918208a
--- /dev/null
+++ b/lib/Service/Schedule/ScheduleFlowBridge.php
@@ -0,0 +1,570 @@
+
+ * `hermiq.schedule-dispatch` -> `openregister.end`) in step with the
+ * schedule, and writes the flow's uuid back onto the schedule as
+ * `engineFlowId`. From that moment the ENGINE owns the schedule's clock and
+ * the app-local dispatcher stops selecting it as due by nextRun; the
+ * schedule's governance (kill switch, budget, approval, retry, delivery,
+ * audit) is untouched because the dispatch node re-enters
+ * `ScheduleService::runNow()`.
+ *
+ * Shapes the engine cannot time yet (once, inexpressible intervals,
+ * timezone-sensitive crons) are left on the local dispatcher on purpose;
+ * the change's design.md carries the full map and the staged phases.
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Service
+ * @package OCA\Hermiq\Service\Schedule
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Service\Schedule;
+
+use DateTime;
+use OCA\Hermiq\AppInfo\Application;
+use OCA\Hermiq\Service\ScheduleService;
+use OCA\OpenRegister\Db\Flow;
+use OCA\OpenRegister\Db\FlowMapper;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use OCP\IUserManager;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+/**
+ * Keeps each eligible schedule's mirror flow in step with the schedule.
+ *
+ * ELIGIBILITY IS THE WHOLE DECISION SURFACE of this class, so it is one
+ * method (`ineligibilityReason()`) that answers with a reason instead of a
+ * boolean: the sync log then says WHY a schedule stayed local, which is the
+ * difference between a staged phase and a silent no-op.
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The bridge coordinates the
+ * schedule store, the flow store, identity resolution and the delegation
+ * marker; each collaborator is a distinct seam.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+class ScheduleFlowBridge {
+
+ /**
+ * OpenRegister register slug that holds hermiq schedule objects.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'hermiq';
+
+ /**
+ * OpenRegister schema slug for schedule objects.
+ *
+ * @var string
+ */
+ private const SCHEMA_SLUG = 'schedule';
+
+ /**
+ * OpenRegister schema slug for agent objects (actingUser resolution).
+ *
+ * @var string
+ */
+ private const AGENT_SCHEMA = 'agent';
+
+ /**
+ * The applicationSlug every mirror flow carries, so mirrors are
+ * selectable as one family and never mistaken for authored flows.
+ *
+ * @var string
+ */
+ private const APPLICATION_SLUG = 'hermiq-schedules';
+
+ /**
+ * Constructor.
+ *
+ * @param ObjectService $objectService Reads schedules and agents.
+ * @param ScheduleService $scheduleService Writes/clears the delegation marker
+ * through the sanitised persist.
+ * @param ScheduleCadenceMapper $cadenceMapper The shape map: which timing
+ * shapes the engine can carry,
+ * and as which cron.
+ * @param ContainerInterface $container Lazy FlowMapper resolution: an instance
+ * whose OpenRegister predates the flow
+ * store must keep booting, so the flow
+ * classes are never hard constructor
+ * dependencies.
+ * @param ScheduleFlowPublisher $publisher Publishes mirror heads so the
+ * engine's version pin accepts them.
+ * @param ScheduleMirrorDefinition $definition The mirror flow's shape:
+ * nodes, edges, uuid, runAs.
+ * @param IUserManager $userManager Proves a resolved runAs names a live user.
+ * @param LoggerInterface $logger Sync diagnostics.
+ */
+ public function __construct(
+ private readonly ObjectService $objectService,
+ private readonly ScheduleService $scheduleService,
+ private readonly ScheduleCadenceMapper $cadenceMapper,
+ private readonly ContainerInterface $container,
+ private readonly ScheduleFlowPublisher $publisher,
+ private readonly ScheduleMirrorDefinition $definition,
+ private readonly IUserManager $userManager,
+ private readonly LoggerInterface $logger,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * Bring every schedule's delegation in step with the engine, once.
+ *
+ * Called from the dispatch tick (arming: a schedule created since the
+ * last tick is mirrored within one poll interval) and from the
+ * migration repair step (in-flight schedules on upgrade). Idempotent by
+ * construction: a mirrored, undrifted schedule is a no-op.
+ *
+ * Per-schedule isolation mirrors the tick loop: one bad schedule must
+ * not block the sync for the rest.
+ *
+ * @return array{mirrored:int,refreshed:int,retired:int,skipped:int,failed:int} Counts of what the pass did.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function syncAll(): array {
+ $stats = ['mirrored' => 0, 'refreshed' => 0, 'retired' => 0, 'skipped' => 0, 'failed' => 0];
+
+ if (class_exists(FlowMapper::class) === false) {
+ // An OpenRegister without the flow store: every schedule stays on
+ // the local dispatcher, which still works exactly as before.
+ return $stats;
+ }
+
+ try {
+ $schedules = $this->objectService
+ ->setRegister(self::REGISTER_SLUG)
+ ->setSchema(self::SCHEMA_SLUG)
+ ->findAll(config: ['limit' => 1000], _rbac: false, _multitenancy: false);
+ } catch (Throwable $e) {
+ $this->logger->error(
+ '[hermiq] Schedule flow sync could not load schedules: ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ $stats['failed']++;
+ return $stats;
+ }
+
+ foreach ($schedules as $schedule) {
+ if (($schedule instanceof ObjectEntity) === false) {
+ continue;
+ }
+
+ try {
+ $outcome = $this->syncOne(schedule: $schedule);
+ $stats[$outcome]++;
+ } catch (Throwable $e) {
+ $stats['failed']++;
+ $this->logger->warning(
+ sprintf(
+ '[hermiq] Schedule flow sync failed for %s: %s',
+ (string)$schedule->getUuid(),
+ $e->getMessage()
+ ),
+ ['exception' => $e]
+ );
+ }
+ }
+
+ return $stats;
+ }//end syncAll()
+
+ /**
+ * Sync one schedule: mirror, refresh, retire, or leave alone.
+ *
+ * @param ObjectEntity $schedule The schedule to sync.
+ *
+ * @return string One of mirrored|refreshed|retired|skipped.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function syncOne(ObjectEntity $schedule): string {
+ $data = $schedule->getObject();
+ $delegatedTo = trim((string)($data['engineFlowId'] ?? ''));
+ $reason = $this->ineligibilityReason(schedule: $schedule);
+
+ if ($reason !== null) {
+ if ($delegatedTo !== '') {
+ // The schedule was edited into a shape the engine cannot time:
+ // take the clock back. Marker first, flow second, so a crash
+ // in between leaves a refusing mirror, never a double clock.
+ $this->retire(schedule: $schedule, flowId: $delegatedTo);
+ return 'retired';
+ }
+
+ return 'skipped';
+ }
+
+ if ($delegatedTo === '') {
+ $this->mirror(schedule: $schedule);
+ return 'mirrored';
+ }
+
+ return $this->refresh(schedule: $schedule, flowId: $delegatedTo);
+ }//end syncOne()
+
+ /**
+ * Why this schedule cannot delegate its clock, or null when it can.
+ *
+ * @param ObjectEntity $schedule The schedule to judge.
+ *
+ * @return string|null The reason it stays local, or null when eligible.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function ineligibilityReason(ObjectEntity $schedule): ?string {
+ $data = $schedule->getObject();
+
+ if (trim((string)($schedule->getOrganisation() ?? '')) === '') {
+ // A flow row without an organisation is invisible to every tenant
+ // forever (hermiq#140). Refuse to create one.
+ return 'the schedule has no organisation, and an organisation-less flow row is unreachable';
+ }
+
+ if ($this->resolveRunAs(schedule: $schedule) === null) {
+ return 'no live acting identity resolves (agent actingUser and owner both name nobody)';
+ }
+
+ if ($this->mappedCron(schedule: $schedule) === null) {
+ return $this->cadenceMapper->gapReason(data: $data);
+ }
+
+ return null;
+ }//end ineligibilityReason()
+
+ /**
+ * The cron expression the engine should fire this schedule on, or null.
+ *
+ * Delegates to the ScheduleCadenceMapper, which owns the shape map; kept
+ * on the bridge so its callers (the sync pass, the tests) keep one seam.
+ *
+ * @param ObjectEntity $schedule The schedule.
+ *
+ * @return string|null The 5-field cron, or null when no safe form exists.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function mappedCron(ObjectEntity $schedule): ?string {
+ return $this->cadenceMapper->mappedCron(schedule: $schedule);
+ }//end mappedCron()
+
+ /**
+ * The identity a mirrored run acts as, or null when nobody live resolves.
+ *
+ * The bound agent's `actingUser` when it names a live, enabled user,
+ * otherwise the schedule's owner: exactly the resolution the dispatch
+ * path applies at run time, computed here at mirror time so the trigger
+ * node can declare it. This mirrors EXISTING consent (the owner authored
+ * the schedule as a standing unattended instruction); it never widens
+ * (ADR-099). OpenRegister re-resolves at every firing and fails closed
+ * on a gone or disabled user, and `runNow()` re-resolves the acting user
+ * again, so a drifted declaration can delay a run but never widen one.
+ *
+ * @param ObjectEntity $schedule The schedule.
+ *
+ * @return string|null The uid to declare as runAs, or null.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function resolveRunAs(ObjectEntity $schedule): ?string {
+ $data = $schedule->getObject();
+
+ $actingUser = $this->agentActingUser(agentId: (string)($data['agentId'] ?? ''));
+ if ($actingUser !== null && $this->isLiveUser(uid: $actingUser) === true) {
+ return $actingUser;
+ }
+
+ $owner = trim((string)($schedule->getOwner() ?? ''));
+ if ($owner !== '' && $this->isLiveUser(uid: $owner) === true) {
+ return $owner;
+ }
+
+ return null;
+ }//end resolveRunAs()
+
+ /**
+ * The bound agent's raw actingUser, or null when unset or unreadable.
+ *
+ * @param string $agentId The agent uuid.
+ *
+ * @return string|null The actingUser, or null.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ private function agentActingUser(string $agentId): ?string {
+ if ($agentId === '') {
+ return null;
+ }
+
+ try {
+ $agent = $this->objectService->find(
+ id: $agentId,
+ register: self::REGISTER_SLUG,
+ schema: self::AGENT_SCHEMA,
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (Throwable $e) {
+ return null;
+ }
+
+ if ($agent === null) {
+ return null;
+ }
+
+ $actingUser = trim((string)($agent->getObject()['actingUser'] ?? ''));
+ if ($actingUser === '') {
+ return null;
+ }
+
+ return $actingUser;
+ }//end agentActingUser()
+
+ /**
+ * Whether the uid names an existing, enabled user.
+ *
+ * @param string $uid The uid to check.
+ *
+ * @return boolean Whether the user is live.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ private function isLiveUser(string $uid): bool {
+ $user = $this->userManager->get($uid);
+
+ return ($user !== null && $user->isEnabled() === true);
+ }//end isLiveUser()
+
+ /**
+ * Create the mirror flow, publish it, and hand the clock to the engine.
+ *
+ * Insert FIRST, publish SECOND, mark LAST. The engine's
+ * `FlowRunVersionPin` refuses every dispatch of a flow with no published
+ * version, so a mirror that is only inserted is a clock that never ticks:
+ * the schedule would leave the local dispatcher and gain nothing in
+ * return. Publishing before the marker means a publish failure leaves the
+ * schedule on its local clock (the flow is deleted again and the failure
+ * is counted), and a crash between publish and mark leaves a published
+ * flow whose dispatch node refuses to fire (the schedule does not name
+ * it). The failure mode stays a loud dead flow, never a dead clock and
+ * never a double one.
+ *
+ * @param ObjectEntity $schedule The eligible, unmirrored schedule.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function mirror(ObjectEntity $schedule): void {
+ $mapper = $this->container->get(FlowMapper::class);
+ $data = $schedule->getObject();
+ $cron = (string)$this->mappedCron(schedule: $schedule);
+ $runAs = (string)$this->resolveRunAs(schedule: $schedule);
+ $uuid = $this->definition->newUuid();
+
+ $flow = new Flow();
+ $flow->setUuid($uuid);
+ $flow->setApp(Application::APP_ID);
+ $flow->setName('Schedule mirror: ' . (string)($data['name'] ?? (string)$schedule->getUuid()));
+ $flow->setDescription(
+ 'Mirror of hermiq schedule ' . (string)$schedule->getUuid()
+ . ' (schedules-onto-engine-triggers). The engine owns the clock; the dispatch node'
+ . ' re-enters the governed run path. Managed by the ScheduleFlowBridge: edits here'
+ . ' are overwritten on its next sync pass.'
+ );
+ $flow->setTrigger('schedule');
+ $flow->setTriggerRegister(null);
+ $flow->setTriggerSchema(null);
+ $flow->setCron($cron);
+ $flow->setNodes($this->definition->nodes(scheduleUuid: (string)$schedule->getUuid(), cron: $cron, runAs: $runAs));
+ $flow->setEdges($this->definition->edges());
+ $flow->setLimits(['maxNodes' => 5, 'maxIterations' => 5]);
+ $flow->setApplicationSlug(self::APPLICATION_SLUG);
+ $flow->setEnabled((($data['enabled'] ?? false) === true));
+ $flow->setOwner((string)($schedule->getOwner() ?? ''));
+ $flow->setOrganisation((string)$schedule->getOrganisation());
+ $flow->setCreated(new DateTime());
+ $flow->setUpdated(new DateTime());
+
+ $mapper->insert($flow);
+
+ try {
+ $this->publisher->publishHead(flow: $flow);
+ } catch (Throwable $e) {
+ // An unpublished mirror is a clock the pin refuses forever. Take
+ // the flow back out so the schedule stays on the local dispatcher
+ // (the marker was never written), and let syncAll count the miss.
+ $this->deleteFlow(flowId: $uuid);
+ throw $e;
+ }
+
+ $this->scheduleService->markEngineDelegation(schedule: $schedule, flowId: $uuid);
+
+ $this->logger->info(
+ sprintf('[hermiq] Schedule %s delegated its clock to flow %s.', (string)$schedule->getUuid(), $uuid)
+ );
+
+ }//end mirror()
+
+ /**
+ * Refresh a mirrored schedule's flow when its timing or state drifted.
+ *
+ * A mirror whose flow row is GONE (crashed rollback, manual deletion) is
+ * re-mirrored: the marker is cleared through the sanitised persist and
+ * the next sync pass creates a fresh flow, rather than leaving a
+ * delegated schedule with no clock at all.
+ *
+ * @param ObjectEntity $schedule The mirrored schedule.
+ * @param string $flowId The mirror flow's uuid.
+ *
+ * @return string One of refreshed|skipped.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ private function refresh(ObjectEntity $schedule, string $flowId): string {
+ $mapper = $this->container->get(FlowMapper::class);
+ $data = $schedule->getObject();
+
+ try {
+ $flow = $mapper->findByUuid($flowId);
+ } catch (Throwable $e) {
+ // The delegated clock does not exist: take the schedule back so
+ // the local dispatcher covers it, and let the next pass re-mirror.
+ $this->scheduleService->clearEngineDelegation(schedule: $schedule);
+ $this->logger->warning(
+ sprintf(
+ '[hermiq] Schedule %s was delegated to missing flow %s; the local clock takes over until the next sync.',
+ (string)$schedule->getUuid(),
+ $flowId
+ )
+ );
+
+ return 'refreshed';
+ }
+
+ $cron = (string)$this->mappedCron(schedule: $schedule);
+ $runAs = (string)$this->resolveRunAs(schedule: $schedule);
+ $enabled = (($data['enabled'] ?? false) === true);
+
+ $storedCron = trim((string)($flow->getCron() ?? ''));
+ $storedRunAs = $this->definition->triggerRunAs(nodes: $flow->getNodes());
+ $storedEnabled = ($flow->getEnabled() === true);
+
+ if ($storedCron === $cron && $storedRunAs === $runAs && $storedEnabled === $enabled) {
+ if ($this->publisher->lacksPublishedVersion(flow: $flow) === true) {
+ // The definition is current but the pin still refuses it: a
+ // mirror created before publishing existed, or a crash
+ // between an update and its republish. Publish the head so
+ // the engine's clock actually ticks.
+ $this->publisher->publishHead(flow: $flow);
+ $this->logger->info(
+ sprintf('[hermiq] Mirror flow %s had no published version; published its head.', $flowId)
+ );
+
+ return 'refreshed';
+ }
+
+ return 'skipped';
+ }
+
+ // Draft FIRST, update SECOND, publish LAST. `FlowRunVersionPin` runs
+ // the PUBLISHED version, not the flow row, so an update without a
+ // republish would keep firing the old cadence forever. Drafting
+ // before the update flips the head off `published`, which is what
+ // lets the no-drift branch above heal a crash between the update and
+ // the publish; the old published version keeps serving in between.
+ $this->publisher->draftForRedefinition(flow: $flow);
+
+ $flow->setCron($cron);
+ $flow->setNodes($this->definition->nodes(scheduleUuid: (string)$schedule->getUuid(), cron: $cron, runAs: $runAs));
+ $flow->setEdges($this->definition->edges());
+ $flow->setEnabled($enabled);
+ $flow->setUpdated(new DateTime());
+ $mapper->update($flow);
+
+ $this->publisher->publishHead(flow: $flow);
+
+ return 'refreshed';
+ }//end refresh()
+
+ /**
+ * Take a schedule's clock back and delete its mirror flow.
+ *
+ * Marker first, flow second (see syncOne). A flow that cannot be
+ * deleted is disabled instead, and either way the schedule is already
+ * back on the local clock.
+ *
+ * @param ObjectEntity $schedule The schedule to take back.
+ * @param string $flowId The mirror flow's uuid.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function retire(ObjectEntity $schedule, string $flowId): void {
+ $this->scheduleService->clearEngineDelegation(schedule: $schedule);
+ $this->deleteFlow(flowId: $flowId);
+
+ }//end retire()
+
+ /**
+ * Delete a mirror flow row; fall back to disabling it.
+ *
+ * @param string $flowId The flow uuid.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function deleteFlow(string $flowId): void {
+ try {
+ $mapper = $this->container->get(FlowMapper::class);
+ $flow = $mapper->findByUuid($flowId);
+ } catch (Throwable $e) {
+ // Already gone: exactly the end state a delete wants.
+ return;
+ }
+
+ try {
+ $mapper->delete($flow);
+ } catch (Throwable $e) {
+ $this->logger->warning(
+ sprintf('[hermiq] Could not delete mirror flow %s, disabling it instead: %s', $flowId, $e->getMessage()),
+ ['exception' => $e]
+ );
+ try {
+ $flow->setEnabled(false);
+ $mapper->update($flow);
+ } catch (Throwable $inner) {
+ $this->logger->error(
+ sprintf('[hermiq] Could not disable mirror flow %s either: %s', $flowId, $inner->getMessage()),
+ ['exception' => $inner]
+ );
+ }
+ }//end try
+
+ }//end deleteFlow()
+
+}//end class
diff --git a/lib/Service/Schedule/ScheduleFlowPublisher.php b/lib/Service/Schedule/ScheduleFlowPublisher.php
new file mode 100644
index 00000000..3ffd96d0
--- /dev/null
+++ b/lib/Service/Schedule/ScheduleFlowPublisher.php
@@ -0,0 +1,143 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Service
+ * @package OCA\Hermiq\Service\Schedule
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Service\Schedule;
+
+use OCA\OpenRegister\Db\Flow;
+use OCA\OpenRegister\Db\FlowVersion;
+use OCA\OpenRegister\Service\Flow\FlowVersionService;
+use Psr\Container\ContainerInterface;
+
+/**
+ * The lifecycle seam between the schedule bridge and flow versioning.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+class ScheduleFlowPublisher {
+
+ /**
+ * Constructor.
+ *
+ * @param ContainerInterface $container Lazy FlowVersionService resolution:
+ * an OpenRegister without flow
+ * versioning must keep booting, so
+ * the version classes are never hard
+ * constructor dependencies.
+ */
+ public function __construct(
+ private readonly ContainerInterface $container,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * Publish the flow's head so the engine's version pin accepts it.
+ *
+ * A head that is not a draft (a deprecated mirror, or a null lifecycle
+ * column from before versioning) is drafted first, because the version
+ * service refuses to publish anything else. A failure propagates: the
+ * caller decides whether that unmirrors the flow (create) or leaves a
+ * draft head for the next pass to heal (refresh). Swallowing it here
+ * would recreate the defect this class exists to close, a mirror the pin
+ * refuses on every tick.
+ *
+ * @param Flow $flow The mirror flow whose head must be published.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function publishHead(Flow $flow): void {
+ if (class_exists(FlowVersionService::class) === false) {
+ return;
+ }
+
+ $versions = $this->container->get(FlowVersionService::class);
+
+ $state = $flow->getLifecycleStatus();
+ if ($state === FlowVersion::STATUS_PUBLISHED) {
+ return;
+ }
+
+ if ($state !== FlowVersion::STATUS_DRAFT) {
+ $versions->createDraft(flow: $flow);
+ }
+
+ // No interactive user publishes a mirror: the bridge does, as the
+ // standing consequence of the owner's schedule. `publishedBy` null is
+ // the honest record of that.
+ $versions->publish(flow: $flow, publishedBy: null);
+
+ }//end publishHead()
+
+ /**
+ * Draft the head of a published flow before its definition changes.
+ *
+ * The engine runs the pinned published version, not the flow row, so a
+ * redefinition must land as draft, update, publish. Drafting first flips
+ * the head off `published`, which makes a crash between the update and
+ * the republish visible as an unpublished head the next sync pass heals;
+ * the old published version keeps serving in between.
+ *
+ * @param Flow $flow The mirror flow about to be redefined.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function draftForRedefinition(Flow $flow): void {
+ if (class_exists(FlowVersionService::class) === false) {
+ return;
+ }
+
+ if ($flow->getLifecycleStatus() !== FlowVersion::STATUS_PUBLISHED) {
+ return;
+ }
+
+ $this->container->get(FlowVersionService::class)->createDraft(flow: $flow);
+
+ }//end draftForRedefinition()
+
+ /**
+ * Whether the engine's version pin would refuse this flow right now.
+ *
+ * @param Flow $flow The mirror flow.
+ *
+ * @return boolean True when versioning exists and the head is unpublished.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function lacksPublishedVersion(Flow $flow): bool {
+ return (class_exists(FlowVersionService::class) === true
+ && $flow->getLifecycleStatus() !== FlowVersion::STATUS_PUBLISHED);
+
+ }//end lacksPublishedVersion()
+}//end class
diff --git a/lib/Service/Schedule/ScheduleMirrorDefinition.php b/lib/Service/Schedule/ScheduleMirrorDefinition.php
new file mode 100644
index 00000000..ae1c5dcc
--- /dev/null
+++ b/lib/Service/Schedule/ScheduleMirrorDefinition.php
@@ -0,0 +1,137 @@
+ `hermiq.schedule-dispatch` ->
+ * `openregister.end`), the two edges between them, the uuid a fresh mirror
+ * row gets, and how to read the declared runAs back out of a stored node
+ * list. The ScheduleFlowBridge decides WHEN a mirror exists; this class says
+ * what it contains.
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Service
+ * @package OCA\Hermiq\Service\Schedule
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Service\Schedule;
+
+/**
+ * Builds and reads the mirror flow's node and edge lists.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+class ScheduleMirrorDefinition {
+
+ /**
+ * The mirror flow's three nodes.
+ *
+ * @param string $scheduleUuid The schedule uuid the dispatch node fires.
+ * @param string $cron The 5-field cron expression.
+ * @param string $runAs The declared acting identity.
+ *
+ * @return array> The nodes.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function nodes(string $scheduleUuid, string $cron, string $runAs): array {
+ return [
+ [
+ 'id' => 'trigger',
+ 'position' => ['x' => 0, 'y' => 0],
+ 'type' => 'openregister.trigger-schedule',
+ 'config' => ['cron' => $cron, 'runAs' => $runAs],
+ ],
+ [
+ 'id' => 'dispatch',
+ 'position' => ['x' => 260, 'y' => 0],
+ 'type' => 'hermiq.schedule-dispatch',
+ 'config' => ['scheduleId' => $scheduleUuid],
+ ],
+ [
+ 'id' => 'done',
+ 'position' => ['x' => 520, 'y' => 0],
+ 'type' => 'openregister.end',
+ 'config' => [],
+ ],
+ ];
+ }//end nodes()
+
+ /**
+ * The mirror flow's two edges.
+ *
+ * @return array> The edges.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function edges(): array {
+ return [
+ [
+ 'id' => 'trigger-dispatch',
+ 'from' => 'trigger',
+ 'to' => 'dispatch',
+ 'title' => 'due',
+ ],
+ [
+ 'id' => 'dispatch-done',
+ 'from' => 'dispatch',
+ 'to' => 'done',
+ 'title' => 'dispatched',
+ ],
+ ];
+ }//end edges()
+
+ /**
+ * The trigger node's declared runAs in a stored node list.
+ *
+ * @param mixed $nodes The stored nodes.
+ *
+ * @return string The declared runAs, or ''.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function triggerRunAs(mixed $nodes): string {
+ if (is_array($nodes) === false) {
+ return '';
+ }
+
+ foreach ($nodes as $node) {
+ if (is_array($node) === true && ($node['type'] ?? null) === 'openregister.trigger-schedule') {
+ $config = ($node['config'] ?? []);
+ if (is_array($config) === true) {
+ return trim((string)($config['runAs'] ?? ''));
+ }
+ }
+ }
+
+ return '';
+ }//end triggerRunAs()
+
+ /**
+ * A fresh v4 uuid for a mirror flow row.
+ *
+ * @return string The uuid.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function newUuid(): string {
+ $bytes = random_bytes(16);
+ $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
+ $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
+
+ return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
+ }//end newUuid()
+}//end class
diff --git a/lib/Service/ScheduleService.php b/lib/Service/ScheduleService.php
index 66740a59..667437a3 100644
--- a/lib/Service/ScheduleService.php
+++ b/lib/Service/ScheduleService.php
@@ -228,7 +228,7 @@ class ScheduleService {
*
* @return string The conversation uuid, or '' when there is none.
*
- * @spec openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
+ * @spec openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
*/
public function lastRunConversationUuid(): string {
return $this->lastRunConversationUuid;
@@ -724,6 +724,51 @@ public function runNow(ObjectEntity $schedule, bool $bypassApprovalGate = false)
}//end runNow()
+ /**
+ * Record that an OpenRegister flow now owns this schedule's clock.
+ *
+ * Writes the mirror flow's uuid into `engineFlowId` through the SAME
+ * sanitised persist every dispatcher write uses, so the full-object save
+ * survives OpenRegister's own read-side artifacts (date format, nullable
+ * repeat). While the marker is set, findDueSchedules() no longer selects
+ * the schedule as due by nextRun; only the engine fires its occurrences.
+ *
+ * @param ObjectEntity $schedule The schedule being delegated.
+ * @param string $flowId The mirror flow's uuid.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function markEngineDelegation(ObjectEntity $schedule, string $flowId): void {
+ $data = $schedule->getObject();
+ $data['engineFlowId'] = trim($flowId);
+ $this->persist(schedule: $schedule, data: $data);
+
+ }//end markEngineDelegation()
+
+ /**
+ * Return this schedule's clock to the app-local dispatcher.
+ *
+ * Clears `engineFlowId` through the sanitised persist, after which
+ * findDueSchedules() selects the schedule as due by nextRun again. The
+ * caller (bridge retire, rollback command) deletes the mirror flow AFTER
+ * this write: a crash between the two leaves a mirror whose delegation
+ * check refuses to fire, never a double clock.
+ *
+ * @param ObjectEntity $schedule The schedule being taken back.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function clearEngineDelegation(ObjectEntity $schedule): void {
+ $data = $schedule->getObject();
+ $data['engineFlowId'] = '';
+ $this->persist(schedule: $schedule, data: $data);
+
+ }//end clearEngineDelegation()
+
/**
* Find enabled schedules that are due at or before the given moment.
*
@@ -741,6 +786,7 @@ public function runNow(ObjectEntity $schedule, bool $bypassApprovalGate = false)
*
* @spec openspec/changes/agent-schedule-dispatcher/tasks.md#3-scheduleservice-dispatch-logic
* @spec openspec/specs/agent-schedule/spec.md#requirement-per-schedule-opt-in-bounded-retry-with-exponential-backoff-mvp
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
*/
private function findDueSchedules(DateTimeImmutable $now): array {
$objects = $this->objectService
@@ -763,17 +809,7 @@ private function findDueSchedules(DateTimeImmutable $now): array {
continue;
}
- $nextRun = $this->parseDate(value: (string)($data['nextRun'] ?? ''));
- $dueByNextRun = ($nextRun === null || $nextRun <= $now);
-
- $dueByRetry = false;
- $retryState = $this->normaliseRetryState(raw: ($data['retryState'] ?? null));
- if ($retryState !== null) {
- $nextAttemptAt = $this->parseDate(value: $retryState['nextAttemptAt']);
- $dueByRetry = ($nextAttemptAt === null || $nextAttemptAt <= $now);
- }
-
- if ($dueByNextRun === true || $dueByRetry === true) {
+ if ($this->isDueAt(data: $data, now: $now) === true) {
$due[] = $object;
}
}//end foreach
@@ -781,6 +817,41 @@ private function findDueSchedules(DateTimeImmutable $now): array {
return $due;
}//end findDueSchedules()
+ /**
+ * Whether one enabled schedule is due at the given moment.
+ *
+ * Due by `nextRun` UNLESS the clock is delegated to the engine
+ * (schedules-onto-engine-triggers): a schedule carrying `engineFlowId`
+ * has its occurrences fired by the OpenRegister flow that owns it, so
+ * selecting it here too would be a double clock. Due by a pending
+ * retry's `nextAttemptAt` REGARDLESS of delegation, so an open retry
+ * sequence is never silently dropped by handing the clock over.
+ *
+ * @param array $data The schedule payload.
+ * @param DateTimeImmutable $now The current UTC moment.
+ *
+ * @return bool Whether the schedule should fire this tick.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ * @spec openspec/specs/agent-schedule/spec.md#requirement-per-schedule-opt-in-bounded-retry-with-exponential-backoff-mvp
+ */
+ private function isDueAt(array $data, DateTimeImmutable $now): bool {
+ $delegated = (trim((string)($data['engineFlowId'] ?? '')) !== '');
+
+ $nextRun = $this->parseDate(value: (string)($data['nextRun'] ?? ''));
+ if ($delegated === false && ($nextRun === null || $nextRun <= $now)) {
+ return true;
+ }
+
+ $retryState = $this->normaliseRetryState(raw: ($data['retryState'] ?? null));
+ if ($retryState !== null) {
+ $nextAttemptAt = $this->parseDate(value: $retryState['nextAttemptAt']);
+ return ($nextAttemptAt === null || $nextAttemptAt <= $now);
+ }
+
+ return false;
+ }//end isDueAt()
+
/**
* Process one due schedule, applying the synchronous oversight gates first.
*
diff --git a/lib/Settings/hermiq_mock_register.json b/lib/Settings/hermiq_mock_register.json
index 40918f74..6053e11c 100644
--- a/lib/Settings/hermiq_mock_register.json
+++ b/lib/Settings/hermiq_mock_register.json
@@ -16,7 +16,7 @@
"hermiq": {
"slug": "hermiq",
"title": "Hermiq (demo)",
- "version": "0.29.0",
+ "version": "0.30.0",
"description": "Demo data for Hermiq. Generated from the register's own schemas — see hydra-gates/scripts/lib/generate_mock_register.py."
}
},
@@ -114,102 +114,6 @@
"isPrivate": true,
"invitedUsers": []
},
- {
- "@self": {
- "register": "hermiq",
- "schema": "agentflow",
- "slug": "agentflow-voorbeeld-name-1-1"
- },
- "name": "Voorbeeld Name 1",
- "triggerRegister": "Voorbeeld Triggerregister 1",
- "triggerSchema": "Voorbeeld Triggerschema 1",
- "owner": "Voorbeeld Owner 1",
- "trigger": "object.updated",
- "cron": "Voorbeeld Cron 1",
- "enabled": true,
- "nodes": [],
- "edges": [],
- "limits": {},
- "description": "Voorbeeld Description 1",
- "notes": "Voorbeeld Notes 1"
- },
- {
- "@self": {
- "register": "hermiq",
- "schema": "agentflow",
- "slug": "agentflow-voorbeeld-name-2-2"
- },
- "name": "Voorbeeld Name 2",
- "triggerRegister": "Voorbeeld Triggerregister 2",
- "triggerSchema": "Voorbeeld Triggerschema 2",
- "owner": "Voorbeeld Owner 2",
- "trigger": "object.updated",
- "cron": "Voorbeeld Cron 2",
- "enabled": true,
- "nodes": [],
- "edges": [],
- "limits": {},
- "description": "Voorbeeld Description 2",
- "notes": "Voorbeeld Notes 2"
- },
- {
- "@self": {
- "register": "hermiq",
- "schema": "agentflow",
- "slug": "agentflow-voorbeeld-name-3-3"
- },
- "name": "Voorbeeld Name 3",
- "triggerRegister": "Voorbeeld Triggerregister 3",
- "triggerSchema": "Voorbeeld Triggerschema 3",
- "owner": "Voorbeeld Owner 3",
- "trigger": "object.updated",
- "cron": "Voorbeeld Cron 3",
- "enabled": true,
- "nodes": [],
- "edges": [],
- "limits": {},
- "description": "Voorbeeld Description 3",
- "notes": "Voorbeeld Notes 3"
- },
- {
- "@self": {
- "register": "hermiq",
- "schema": "agentflowrun",
- "slug": "agentflowrun-agentflowrun-1-1"
- },
- "agentflow": "00000000-0000-4000-8000-000000000000",
- "correlationId": "Voorbeeld Correlationid 1",
- "status": "Voorbeeld Status 1",
- "steps": [],
- "state": {},
- "usage": {}
- },
- {
- "@self": {
- "register": "hermiq",
- "schema": "agentflowrun",
- "slug": "agentflowrun-agentflowrun-2-2"
- },
- "agentflow": "00000000-0000-4000-8000-000000000001",
- "correlationId": "Voorbeeld Correlationid 2",
- "status": "Voorbeeld Status 2",
- "steps": [],
- "state": {},
- "usage": {}
- },
- {
- "@self": {
- "register": "hermiq",
- "schema": "agentflowrun",
- "slug": "agentflowrun-agentflowrun-3-3"
- },
- "agentflow": "00000000-0000-4000-8000-000000000002",
- "correlationId": "Voorbeeld Correlationid 3",
- "status": "Voorbeeld Status 3",
- "steps": [],
- "state": {},
- "usage": {}
- },
{
"@self": {
"register": "hermiq",
diff --git a/lib/Settings/hermiq_register.json b/lib/Settings/hermiq_register.json
index fe9074f5..d673d55c 100644
--- a/lib/Settings/hermiq_register.json
+++ b/lib/Settings/hermiq_register.json
@@ -2,8 +2,8 @@
"openapi": "3.0.0",
"info": {
"title": "Hermiq Register",
- "description": "Hermiq register v0.29.0 — speech-services: Agent gains four OPTIONAL voice* fields — voiceInputEngine and voiceOutputEngine (auto|browser|local|off), voiceSilenceTimeout (ms), voiceConversationEnabled. They decide whether a given agent's dictation goes to the browser's recogniser (in Chrome: Google's servers) or to this instance's whisper, and a 'local' agent NEVER falls back to the browser. ⚠️ voice* rather than talk*: talkEnabled already exists and means Nextcloud Talk rooms, not microphones. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.28.0 — hermiq-agent-application-slug: Agent gains the OPTIONAL applicationSlug (the owning external Application's slug, e.g. hydra-console for the four Hydra-* agents), mirroring Flow.applicationSlug (hydra-flow-application-slug). Empty for an agent hermiq owns outright; never required. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.27.0 — Hermiq register v0.25.0 — tool-discovery-and-access-requests: adds the ToolAccessRequest schema. An agent can now discover a capability it does not hold and ASK for it; the request notifies the agent owner and grants nothing on its own. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.24.0 — Schedule autonomous AI agents in Nextcloud. v0.24.0 — agent-object-owner-authorization: Agent gains an authorization block granting read to authenticated users. create/update/delete are OMITTED ON PURPOSE — OpenRegister fails closed on an action a non-empty block does not list, so omission is what makes writes owner-only (owners and admins are admitted before that check). Do NOT 'complete' the block by adding write actions: that reopens a reproduced hole in which any authenticated user could rewrite any agent's tool grants, prompt, model and delegationAllowlist. `scope: private` was rejected — it covers every action and would close reads for invited users too. Previously v0.23.0 — v0.23.0 — talk-agent-sessions: Conversation gains the OPTIONAL talkRoomOrigin ('created' when Hermiq created the room for this session, 'bound' when Hermiq was invited into or delivered into a room somebody else owns). It decides whether the agent treats every human message in the room as a turn or applies the mention gate, so the value is STORED rather than inferred from the room's shape — inferring it would silently flip that behaviour the moment a second person is invited. Unset reads as 'bound', which",
- "version": "0.29.0"
+ "description": "Hermiq register v0.31.0, schedules-onto-engine-triggers: Schedule gains the OPTIONAL, derived engineFlowId — the uuid of the OpenRegister flow that now owns this schedule's clock. Written by the ScheduleFlowBridge when a schedule is mirrored onto the engine's schedule trigger, cleared by occ hermiq:schedules:rollback-flow-mirror. While set, the app-local dispatcher no longer selects the schedule as due by nextRun (a pending retry still fires locally). ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.30.0, finish-agentflow-retirement: the AgentFlow and AgentFlowRun schemas leave the descriptor. The object store they described is retired: hermiq authors flows in OpenRegister's native flow store, and the agentflow runner, resolver and frontend are already gone. The import unions schema ids and never removes one, so the PruneRetiredAgentFlowSchemas repair step deletes the leftover rows, their objects and their tables. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.29.0 — speech-services: Agent gains four OPTIONAL voice* fields — voiceInputEngine and voiceOutputEngine (auto|browser|local|off), voiceSilenceTimeout (ms), voiceConversationEnabled. They decide whether a given agent's dictation goes to the browser's recogniser (in Chrome: Google's servers) or to this instance's whisper, and a 'local' agent NEVER falls back to the browser. ⚠️ voice* rather than talk*: talkEnabled already exists and means Nextcloud Talk rooms, not microphones. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.28.0 — hermiq-agent-application-slug: Agent gains the OPTIONAL applicationSlug (the owning external Application's slug, e.g. hydra-console for the four Hydra-* agents), mirroring Flow.applicationSlug (hydra-flow-application-slug). Empty for an agent hermiq owns outright; never required. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.27.0 — Hermiq register v0.25.0 — tool-discovery-and-access-requests: adds the ToolAccessRequest schema. An agent can now discover a capability it does not hold and ASK for it; the request notifies the agent owner and grants nothing on its own. ⚠️ THE VERSION BUMP IS THE POINT: the import is gated on info.version, so a schema shipped without one is inert on every existing install with no error anywhere. Previous v0.24.0 — Schedule autonomous AI agents in Nextcloud. v0.24.0 — agent-object-owner-authorization: Agent gains an authorization block granting read to authenticated users. create/update/delete are OMITTED ON PURPOSE — OpenRegister fails closed on an action a non-empty block does not list, so omission is what makes writes owner-only (owners and admins are admitted before that check). Do NOT 'complete' the block by adding write actions: that reopens a reproduced hole in which any authenticated user could rewrite any agent's tool grants, prompt, model and delegationAllowlist. `scope: private` was rejected — it covers every action and would close reads for invited users too. Previously v0.23.0 — v0.23.0 — talk-agent-sessions: Conversation gains the OPTIONAL talkRoomOrigin ('created' when Hermiq created the room for this session, 'bound' when Hermiq was invited into or delivered into a room somebody else owns). It decides whether the agent treats every human message in the room as a turn or applies the mention gate, so the value is STORED rather than inferred from the room's shape — inferring it would silently flip that behaviour the moment a second person is invited. Unset reads as 'bound', which",
+ "version": "0.31.0"
},
"x-openregister": {
"type": "application",
@@ -19,7 +19,7 @@
"hermiq": {
"slug": "hermiq",
"title": "Hermiq",
- "version": "0.29.0",
+ "version": "0.31.0",
"description": "Schedule autonomous AI agents in Nextcloud",
"schemas": [
"agent",
@@ -27,8 +27,6 @@
"agentbudget",
"agentcompliancecontrol",
"agentcontrolframework",
- "agentflow",
- "agentflowrun",
"agentsession",
"agentsessionturn",
"agentskill",
@@ -59,7 +57,7 @@
"Schedule": {
"slug": "schedule",
"icon": "ClockOutline",
- "version": "0.1.1",
+ "version": "0.2.0",
"title": "Schedule",
"description": "A schedule that fires an OpenRegister agent on a cron expression, a fixed interval, or once at a future time.",
"type": "object",
@@ -292,13 +290,18 @@
"format": "date-time",
"title": "Webhook secret rotated at",
"description": "Derived: when the webhook signing secret was last minted or rotated; null when no secret has ever been minted."
+ },
+ "engineFlowId": {
+ "type": "string",
+ "title": "Engine flow",
+ "description": "Derived: uuid of the OpenRegister flow that owns this schedule's clock (schedules-onto-engine-triggers). Written by the ScheduleFlowBridge when the schedule is mirrored onto the engine's schedule trigger; cleared by occ hermiq:schedules:rollback-flow-mirror. While set, the app-local dispatcher no longer selects this schedule as due by nextRun. Empty or unset means the local dispatcher owns the clock."
}
}
},
"Approval": {
"slug": "approval",
"icon": "CheckDecagramOutline",
- "version": "0.5.0",
+ "version": "0.6.0",
"title": "Approval",
"description": "A human-oversight approval request for a gated agent run or tool invocation (EU AI Act Art. 14); its status is the audited pending→approved/denied decision. Gates a scheduled run (sourceType=schedule, the original shape), a flow-triggered run (sourceType=flow, from OpenRegister's AgentRunRequestedEvent), a webhook-triggered run (sourceType=webhook, from an external system calling the agent's inbound webhook), an un-granted destructive tool invocation attempted mid-run (sourceType=tool, agent-tool-governance-and-disclosure), or a guardrail-policy confirm-classified tool call (sourceType=toolcall, agent-guardrails — distinct from sourceType=tool: keyed by agentId+toolId+arguments and single-use/TTL-bounded rather than a permanent per-(agentId,toolId) decision).",
"type": "object",
@@ -430,6 +433,11 @@
"title": "Reason",
"description": "Optional decision reason (typically supplied on denial)."
},
+ "taskUuid": {
+ "type": "string",
+ "title": "Mirror task",
+ "description": "Uuid of the OpenRegister task mirroring this pending approval (approval-task convergence, phase 1). Written back right after creation so a hermiq decision can release the mirror and the rollback command can find it. Empty or unset means the approval was never mirrored (older OpenRegister without the task surface) and is decidable only on hermiq's own surface."
+ },
"talkMessageId": {
"type": "string",
"title": "Talk message",
@@ -1021,7 +1029,7 @@
},
"Budget": {
"slug": "agentbudget",
- "icon": "CashMultiple",
+ "icon": "CalculatorVariantOutline",
"version": "0.1.1",
"title": "Budget",
"description": "Per-organisation or per-agent spend guardrail (tokens and/or EUR per period): a soft threshold that warns without blocking, and a hard cap that blocks NEW runs for the budgeted scope at the same dispatch gate the kill-switch and human-approval gate use (never aborts a run already in progress). Current-period usage is computed on read from the existing run AuditTrail — never a stored counter.",
@@ -3626,150 +3634,6 @@
}
}
},
- "AgentFlow": {
- "slug": "agentflow",
- "icon": "Sitemap",
- "version": "0.1.3",
- "title": "Agent flow",
- "description": "An authored agent graph: nodes/edges triggered by a Nextcloud event or a cron schedule, walked by OpenRegister's flow engine (ADR-065). Authored through nc-vue's CnFlowDetail. v0.1.3 adds cron — a flow could already declare trigger 'schedule', but the expression that says WHEN had no property to live in, so every cron value was silently dropped on save and the flow was scheduled for never. Previously v0.1.2 added triggerRegister (read when matching triggers but undeclared, so it was silently dropped on save) and owner (the NC UID a trigger-fired run is attributed to).",
- "type": "object",
- "required": [
- "name"
- ],
- "authorization": {
- "read": [
- "authenticated"
- ]
- },
- "properties": {
- "name": {
- "type": "string",
- "title": "Name",
- "description": "What this flow is called — how it is listed and how a run refers to it."
- },
- "triggerRegister": {
- "type": "string",
- "title": "Trigger register",
- "description": "OpenRegister register id/slug whose object events start this graph. Empty means any register. Trigger matching already used this field; it was missing from the schema, so the value was dropped on save and every flow matched every register."
- },
- "triggerSchema": {
- "type": "string",
- "title": "Trigger schema",
- "description": "OpenRegister schema id/slug whose object events start this graph. Empty means any schema."
- },
- "owner": {
- "type": "string",
- "title": "Owner",
- "description": "Nextcloud UID of the person who authored and activated this flow. A trigger fires with no acting user, so this is who the run is attributed to and whose identity its steps execute as. A flow with no owner MUST NOT dispatch — never defaulted to an empty or system owner."
- },
- "trigger": {
- "type": "string",
- "title": "Trigger",
- "description": "Event-catalog trigger id (object.created/updated/deleted), manual for a flow run only on request, or schedule to run it on the cron below.",
- "default": "object.updated"
- },
- "cron": {
- "type": "string",
- "title": "Cron schedule",
- "description": "For a schedule trigger: the cron expression that decides when the flow runs (e.g. \"*/5 * * * *\" = every five minutes, \"0 9 * * 1\" = 09:00 every Monday). Ignored for other triggers. Without this property a flow could declare trigger 'schedule' and the value saying WHEN was dropped on save, so the flow was scheduled for never."
- },
- "enabled": {
- "type": "boolean",
- "title": "Enabled",
- "description": "Whether this flow may run. A disabled flow is never started by a trigger or by the schedule, however due it is.",
- "default": true
- },
- "nodes": {
- "type": "array",
- "default": [],
- "title": "Nodes",
- "description": "Graph nodes {id,type,config,position}.",
- "items": {
- "type": "object"
- }
- },
- "edges": {
- "type": "array",
- "default": [],
- "title": "Edges",
- "description": "Graph edges {id,source,target,when?}.",
- "items": {
- "type": "object"
- }
- },
- "limits": {
- "type": "object",
- "title": "Limits",
- "description": "Execution bounds {maxNodes,maxIterations}.",
- "default": {}
- },
- "description": {
- "type": "string",
- "title": "Description",
- "description": "What this flow does, in a sentence — shown wherever the flow is listed."
- },
- "notes": {
- "type": "string",
- "title": "Notes",
- "description": "Free-form working notes for whoever maintains the flow. Not read by the engine."
- }
- }
- },
- "AgentFlowRun": {
- "slug": "agentflowrun",
- "icon": "History",
- "version": "0.1.0",
- "title": "Agent flow run",
- "description": "A record of one agent-graph run: ordered node steps, state checkpoints, status and usage.",
- "type": "object",
- "required": [],
- "authorization": {
- "read": [
- "authenticated"
- ]
- },
- "properties": {
- "agentflow": {
- "type": "string",
- "format": "uuid",
- "title": "Agent flow",
- "description": "The authored agent graph this run walked.",
- "$ref": "agentflow"
- },
- "correlationId": {
- "readOnly": true,
- "type": "string",
- "title": "Correlation id",
- "description": "Ties this run to the triggering event or schedule, so every record produced by one trigger can be found together."
- },
- "status": {
- "type": "string",
- "title": "Status",
- "description": "Where the run got to: queued, running, succeeded, failed or cancelled."
- },
- "steps": {
- "type": "array",
- "default": [],
- "title": "Steps",
- "description": "The node steps in the order they executed, each with what it received, returned and logged.",
- "items": {
- "type": "object"
- }
- },
- "state": {
- "type": "object",
- "title": "State",
- "description": "The run's state checkpoint, so a resumed or replayed run picks up where this one stopped.",
- "default": {}
- },
- "usage": {
- "type": "object",
- "title": "Usage",
- "description": "Token and cost totals accumulated across the run's LLM calls.",
- "default": {}
- }
- }
- },
"ToolAccessRequest": {
"type": "object",
"authorization": {
diff --git a/openspec/changes/approval-task-convergence/design.md b/openspec/changes/approval-task-convergence/design.md
new file mode 100644
index 00000000..5cacab30
--- /dev/null
+++ b/openspec/changes/approval-task-convergence/design.md
@@ -0,0 +1,45 @@
+# Design: approval-task convergence, phase 1
+
+## Decision 1: mirror, do not move (yet)
+
+The Approval object stays the authoritative record in phase 1; the OpenRegister task is a mirror that adds the shared decision surface. Moving authority to the task in the same PR would mean rewriting all five source-type resume paths, the Talk reaction listener, the skill-draft applier and the compliance reads at once. The mirror gives the convergence payoff now (one inbox, one decision surface, OR notifications and VTODO for free) while every existing caller keeps working. Authority flips in phase 2.
+
+Consequence: dual state exists by design, and both directions reconcile:
+
+- OR decides first: `TaskTerminalListener` applies `approve()`/`deny()`; hermiq's own release call then finds the task already terminal and does nothing (OpenRegister's `terminateAsMoot()` is idempotent on terminal tasks).
+- hermiq decides first: `approve()`/`deny()` release the mirror via `terminateAsMoot()`; the terminal event that raises fires with outcome `terminated`, which the listener ignores (it only acts on state `completed`), and even a completed replay is safe because `approve()`/`deny()` no-op on non-pending approvals.
+
+## Decision 2: one creation choke point
+
+The mirror is created inside `ApprovalService::persistApproval()`, only when the save is a CREATION of a PENDING approval (uuid null, status pending). All six ensure-pending shapes flow through that method, so no per-shape wiring exists to drift. The task uuid is written back onto the Approval in a second save; the write-back requires the `taskUuid` property to be DECLARED on the approval schema, because OpenRegister silently drops undeclared properties on save.
+
+## Decision 3: the bridge is the only file that touches the task surface
+
+`ApprovalTaskBridge` resolves `TaskService` from the container by FQN string at call time and reduces everything else to scalars. It never throws to its caller: a missing or older OpenRegister task surface logs a warning and returns null, and the approval write-path proceeds without a mirror. This is NOT a fail-open authorization path: no decision is ever derived from the bridge's absence; the gate itself (the pending Approval) is created before the bridge is consulted and remains decidable on hermiq's own guarded surface.
+
+Task shape (the `UserTaskConfig` conventions):
+
+- `state`: `active` when the reviewer is a user (assigned), `enabled` when the reviewer is a group (candidates only).
+- `assignee` / `candidateGroups`: from the Approval's `reviewer`/`reviewerType`.
+- `requester`: `hermiq:approval-gate`, a system seat, never the reviewer, so OpenRegister's separation-of-duties guard cannot mistake the initiator for the decider.
+- `objectUuid`: the Approval's uuid (the task's anchor object).
+- `metadata.hermiq`: `{approvalUuid, sourceType}`, the ownership marker the listener filters on.
+- `title`/`description`: derived from sourceType, agentId and prompt.
+
+## Decision 4: events by string literal, values by dynamic getter
+
+`TaskTerminalEvent` is registered and routed by FQN string literal (filinq#988): during hermiq's own `register()` the `OCA\OpenRegister\` prefix may not be autoloadable yet, so neither `::class` on an import nor a `class_exists()` probe is reliable, and registering a listener for an event name that never dispatches is harmless. Inside the listener every value is read through an `is_callable` dynamic getter, so hermiq tolerates OpenRegister older, newer or absent.
+
+The listener acts only on: committed events, state `completed`, a `metadata.hermiq.approvalUuid` it can resolve to a PENDING approval. The decider is the task's `completedBy` (falling back to `assignee`); with neither, the event is logged and skipped rather than deciding as nobody. Rejecting outcomes are OpenRegister's published vocabulary (`rejected`, `returned`, `declined`, `denied`); everything else on a completed task approves. A task terminated or cancelled in OpenRegister leaves the approval pending and decidable in hermiq.
+
+## Decision 5: migration is a mirror pass, not a rewrite
+
+In-flight state is pending Approval objects. The repair step (`MirrorPendingApprovalsToTasks`) walks every pending approval and ensures a mirror task exists, guarded on the stored `taskUuid` so a second run changes nothing. It runs after `CheckOpenRegisterCompatibility` in post-migration only (a fresh install has no approvals). Decided approvals are history, not work: they get no task (OpenRegister's own migration imported closed approvals because tasks BECAME the record there; here the Approval remains the record).
+
+Rollback (`occ hermiq:approvals:rollback-task-mirror`) terminates every still-open mirror task as moot and clears the `taskUuid` backlinks, restoring the pre-convergence world without touching any decision already made. Both directions log per-object and report counts; a partial pass names what failed.
+
+## Alternatives considered
+
+- Full cutover in one PR (ApprovalService becomes a thin delegate): rejected for blast radius; five resume paths, the Talk binding and the skill-draft applier would all change under one review. Staged as phase 2/3.
+- `TaskSequenceService::provision()` per approval: rejected; hermiq approvals are single-decider gates, not multi-step chains. A one-task sequence buys consolidation semantics nothing here. Sequences become relevant in phase 3 if hermiq grows multi-step approval.
+- Hard constructor injection of `TaskService` into the bridge: rejected; it would make every hermiq boot construct the OR task surface and would fatal on an older OpenRegister that has `ObjectService` but no `Task` namespace.
diff --git a/openspec/changes/approval-task-convergence/proposal.md b/openspec/changes/approval-task-convergence/proposal.md
new file mode 100644
index 00000000..46eb7cbe
--- /dev/null
+++ b/openspec/changes/approval-task-convergence/proposal.md
@@ -0,0 +1,24 @@
+# Approval-task convergence
+
+## Why
+
+The fleet audit flagged hermiq's app-local `ApprovalService` as a duplicate of the engine. OpenRegister now owns the fleet task entity and the approval layer: `openregister_tasks`, `TaskService` (with a trusted `import()` path), `TaskInboxService`, `TaskSequenceService`, task notifications and the CalDAV VTODO projection. Every approval hermiq keeps to itself is an approval the shared inbox, the calendar projection and the sequence machinery cannot see.
+
+hermiq's `ApprovalService` is 1,746 lines gating five action shapes (schedule, flow, webhook, tool, toolcall) plus skill drafts, with its own reviewer routing, its own inbox (`ApprovalController::index`), its own Talk delivery and its own decision write-path. filinq already made this move for signing (filinq#988): it consumes OpenRegister's task events through string-literal FQNs and retired its step listener.
+
+## What changes
+
+Phase 1 (this change) delivers the adoption seam and the data migration, not the full retirement:
+
+- Every pending hermiq Approval is mirrored as ONE OpenRegister task, created on the trusted `TaskService::import()` path (hermiq is the initiator), carrying a `metadata.hermiq.approvalUuid` backlink and anchored on the Approval object. The Approval object stores the mirror's `taskUuid`.
+- A new `TaskTerminalListener` consumes OpenRegister's committed `TaskTerminalEvent` (registered by string-literal FQN, the filinq#988 pattern, so registration survives bootstrap ordering): a task completed with an approving outcome applies `ApprovalService::approve()`, a rejecting outcome applies `deny()`. A decision made in OpenRegister's inbox therefore resumes the gated run exactly as a decision made in hermiq.
+- A decision made on hermiq's own surface releases the mirror: the linked task is terminated as moot, so it never dangles in anyone's inbox.
+- A repair step mirrors every already-pending Approval on upgrade (idempotent, guarded on the stored `taskUuid`), and an occ command rolls the mirror back (terminates mirror tasks, clears the backlinks), following OpenRegister's `MigrateApprovalChainsToTasks` + rollback-command pattern.
+
+Phase 2 and 3 (staged in tasks.md, not in this change's code): route hermiq's approval inbox reads through `TaskInboxService`, retire the app-local reviewer notification fan-out in favour of task notifications, and reduce `ApprovalService`'s decision surface to a thin delegate over task verbs.
+
+## Impact
+
+- Affected specs: `human-approval-gate` (decision surface widens to OpenRegister's task inbox; hermiq's gate semantics are unchanged).
+- Affected code: `lib/Service/ApprovalService.php` (bridge seam), new `lib/Service/Approval/ApprovalTaskBridge.php`, new `lib/Listener/TaskTerminalListener.php`, `lib/AppInfo/Application.php`, new `lib/Repair/MirrorPendingApprovalsToTasks.php`, new `lib/Command/RollbackApprovalTaskMirror.php`, `appinfo/info.xml`, `lib/Settings/hermiq_register.json` + mock (new `taskUuid` property on the approval schema).
+- Approval-gate behaviour is unchanged when OpenRegister's task surface is absent or older: the bridge degrades to a logged no-op and the Approval object remains authoritative.
diff --git a/openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md b/openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md
new file mode 100644
index 00000000..e3312d4b
--- /dev/null
+++ b/openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md
@@ -0,0 +1,69 @@
+# Approval-task convergence
+
+## ADDED Requirements
+
+### Requirement: every pending approval is mirrored as one OpenRegister task
+
+When hermiq creates a pending Approval, it SHALL ensure exactly one OpenRegister task exists for it, created on the trusted `TaskService::import()` path, carrying `metadata.hermiq.approvalUuid`, anchored on the Approval object, and routed to the Approval's reviewer (assignee for a user reviewer, candidate group for a group reviewer). The Approval SHALL store the mirror's uuid in its `taskUuid` property. When OpenRegister's task surface is absent or older, the approval write-path SHALL proceed without a mirror and log a warning.
+
+#### Scenario: a gated schedule produces a task mirror
+
+- **WHEN** the dispatcher creates a pending Approval for a gated schedule
+- **THEN** one OpenRegister task exists whose metadata names the Approval's uuid, whose anchor is the Approval, and whose assignee or candidate group is the resolved reviewer
+- **AND** the Approval's `taskUuid` names that task
+
+@e2e exclude {backend-only seam; the mirror write-path is exercised by ApprovalTaskBridgeTest and ApprovalServiceTest, and the e2e surface (OpenRegister's task inbox) lives in openregister}
+
+#### Scenario: the task surface is unavailable
+
+- **WHEN** a pending Approval is created and `TaskService` cannot be resolved
+- **THEN** the Approval is created and decidable on hermiq's own surface, with no mirror and a logged warning
+
+@e2e exclude {degraded-install branch, unit-covered in ApprovalTaskBridgeTest}
+
+### Requirement: a task decision is an approval decision
+
+hermiq SHALL consume OpenRegister's committed `TaskTerminalEvent` through a listener registered by string-literal FQN. For a task completed with `metadata.hermiq.approvalUuid` resolving to a pending Approval, a rejecting outcome (`rejected`, `returned`, `declined`, `denied`) SHALL apply `ApprovalService::deny()` and any other outcome SHALL apply `ApprovalService::approve()`, attributed to the task's `completedBy` (falling back to its assignee). An event without a resolvable decider SHALL be skipped and logged, never decided as nobody. Uncommitted events, non-completed terminal states and foreign tasks SHALL be ignored.
+
+#### Scenario: approving from the OpenRegister inbox resumes the gated run
+
+- **WHEN** a reviewer completes a mirror task in OpenRegister with an approving outcome
+- **THEN** the linked Approval transitions to `approved` with the completer as decider, and the gated run resumes exactly as if approved on hermiq's surface
+
+@e2e exclude {cross-app event path; unit-covered in TaskTerminalListenerTest, and the inbox UI belongs to openregister}
+
+#### Scenario: rejecting from the OpenRegister inbox denies
+
+- **WHEN** a reviewer completes a mirror task with outcome `rejected` and a comment
+- **THEN** the linked Approval transitions to `denied` with the comment as reason, and the gated run never executes
+
+@e2e exclude {cross-app event path, unit-covered in TaskTerminalListenerTest}
+
+### Requirement: a hermiq decision releases the mirror
+
+When an Approval is decided on hermiq's own surface, hermiq SHALL terminate the linked mirror task as moot so it leaves every OpenRegister inbox. Releasing an already-terminal task SHALL be a no-op, and a decision applied FROM a task terminal event SHALL NOT loop (the approval's non-pending status makes the replay a no-op).
+
+#### Scenario: deciding in hermiq clears the OpenRegister inbox
+
+- **WHEN** a reviewer approves a mirrored Approval through hermiq's own controller
+- **THEN** the mirror task is terminated as moot with a reason naming the hermiq decision
+
+@e2e exclude {cross-app write, unit-covered in ApprovalTaskBridgeTest}
+
+### Requirement: in-flight approvals migrate and roll back
+
+On upgrade, a repair step SHALL mirror every already-pending Approval that has no `taskUuid`, idempotently (a second run changes nothing), reporting counts and naming failures. An occ command SHALL roll the mirror back: every still-open mirror task is terminated as moot and every `taskUuid` backlink cleared, without touching decisions already made.
+
+#### Scenario: upgrade mirrors pending approvals once
+
+- **WHEN** the repair step runs twice on an instance with pending approvals
+- **THEN** after the first run every pending Approval carries a `taskUuid` naming an existing task, and the second run creates nothing
+
+@e2e exclude {repair-step path, unit-covered in MirrorPendingApprovalsToTasksTest}
+
+#### Scenario: rollback restores the pre-convergence world
+
+- **WHEN** `occ hermiq:approvals:rollback-task-mirror` runs after the mirror pass
+- **THEN** every mirror task for a still-pending Approval is terminal and every pending Approval's `taskUuid` is cleared
+
+@e2e exclude {occ path, unit-covered in RollbackApprovalTaskMirrorTest}
diff --git a/openspec/changes/approval-task-convergence/tasks.md b/openspec/changes/approval-task-convergence/tasks.md
new file mode 100644
index 00000000..8a9cd771
--- /dev/null
+++ b/openspec/changes/approval-task-convergence/tasks.md
@@ -0,0 +1,26 @@
+# Tasks: approval-task convergence
+
+## Phase 1: adoption seam and migration (this change)
+
+- [x] 1.1 Declare `taskUuid` on the approval schema in `lib/Settings/hermiq_register.json` and the mock register (OpenRegister drops undeclared properties on save), bump the schema version
+- [x] 1.2 `ApprovalTaskBridge`: `ensureTaskFor()` creates the mirror via `TaskService::import()` (lazy FQN-string resolution, scalar-only data, never throws to the caller), `releaseFor()` terminates the mirror as moot
+- [x] 1.3 Seam in `ApprovalService`: mirror on pending-approval creation inside `persistApproval()` (one choke point), release in `approve()` and `deny()`
+- [x] 1.4 `TaskTerminalListener` consuming committed `TaskTerminalEvent` by string-literal FQN; register it in `Application::register()`
+- [x] 1.5 `MirrorPendingApprovalsToTasks` repair step (idempotent, post-migration only) registered in `appinfo/info.xml`
+- [x] 1.6 `occ hermiq:approvals:rollback-task-mirror` command registered in `appinfo/info.xml`
+- [x] 1.7 Test stubs matching the REAL OpenRegister development signatures: `OCA\OpenRegister\Db\Task`, `OCA\OpenRegister\Service\Task\TaskService`, `OCA\OpenRegister\Event\TaskTerminalEvent`
+- [x] 1.8 Unit tests: bridge, listener, seam, repair step, rollback command
+
+## Phase 2: one inbox (staged, next change)
+
+- [ ] 2.1 `ApprovalController::index()` reads pending work through OpenRegister's `TaskInboxService` (mirror tasks joined onto Approval payloads), retiring the app-local pending query as the inbox source
+- [ ] 2.2 Retire `DeliveryService`'s reviewer notification fan-out for approvals in favour of OpenRegister task notifications (Talk delivery stays until 2.3)
+- [ ] 2.3 Repoint `TalkApprovalReactionListener` and `Talk/TalkApprovalBinding` onto the mirror task (a Talk reaction completes the TASK, so both surfaces converge on one decision path)
+- [ ] 2.4 Flip authority: the task becomes the record of the decision, the Approval keeps only hermiq-specific resume context
+
+## Phase 3: thin delegate (staged)
+
+- [ ] 3.1 `ApprovalService::approve()`/`deny()` become delegates over `TaskService::complete()`; the terminal listener becomes the ONLY decision applier
+- [ ] 3.2 Evaluate `TaskSequenceService::provision()` for multi-step approval (skill drafts with maintainer + owner sign-off); adopt or record the rejection
+- [ ] 3.3 Adopt `expiresAt`/`onTimeout` for TTL-bounded `toolcall` approvals once OpenRegister's `task-expiry-and-outcomes` change lands, replacing the app-local TTL constant
+- [ ] 3.4 Retire the approval schema's decision fields that the task now owns; `occ openregister:schemas:prune-retired` for anything dropped
diff --git a/openspec/changes/agent-graph-builder/proposal.md b/openspec/changes/archive/2026-09-02-agent-graph-builder/proposal.md
similarity index 94%
rename from openspec/changes/agent-graph-builder/proposal.md
rename to openspec/changes/archive/2026-09-02-agent-graph-builder/proposal.md
index c2f25947..1a6f0bd8 100644
--- a/openspec/changes/agent-graph-builder/proposal.md
+++ b/openspec/changes/archive/2026-09-02-agent-graph-builder/proposal.md
@@ -4,6 +4,11 @@ kind: code
# Proposal: agent-graph-builder
+> Archived 2026-09-02 (finish-agentflow-retirement), superseded. The graph builder
+> this change described was built on the `agentflow` object store, and that store
+> is retired: its runner, resolver and frontend are gone. Flow authoring lives in
+> OpenRegister's native flow store and canvas instead (REQ-FA-001/002).
+
## Why
Hermiq can run a single governed agent turn triggered by a Nextcloud event and
diff --git a/openspec/changes/agent-graph-builder/specs/agent-graph/spec.md b/openspec/changes/archive/2026-09-02-agent-graph-builder/specs/agent-graph/spec.md
similarity index 100%
rename from openspec/changes/agent-graph-builder/specs/agent-graph/spec.md
rename to openspec/changes/archive/2026-09-02-agent-graph-builder/specs/agent-graph/spec.md
diff --git a/openspec/changes/agent-graph-builder/tasks.md b/openspec/changes/archive/2026-09-02-agent-graph-builder/tasks.md
similarity index 100%
rename from openspec/changes/agent-graph-builder/tasks.md
rename to openspec/changes/archive/2026-09-02-agent-graph-builder/tasks.md
diff --git a/openspec/changes/consume-or-flow-engine/.openspec.yaml b/openspec/changes/archive/2026-09-02-consume-or-flow-engine/.openspec.yaml
similarity index 100%
rename from openspec/changes/consume-or-flow-engine/.openspec.yaml
rename to openspec/changes/archive/2026-09-02-consume-or-flow-engine/.openspec.yaml
diff --git a/openspec/changes/consume-or-flow-engine/proposal.md b/openspec/changes/archive/2026-09-02-consume-or-flow-engine/proposal.md
similarity index 86%
rename from openspec/changes/consume-or-flow-engine/proposal.md
rename to openspec/changes/archive/2026-09-02-consume-or-flow-engine/proposal.md
index 702c154a..f25220c2 100644
--- a/openspec/changes/consume-or-flow-engine/proposal.md
+++ b/openspec/changes/archive/2026-09-02-consume-or-flow-engine/proposal.md
@@ -1,5 +1,11 @@
# Proposal: consume-or-flow-engine
+> Archived 2026-09-02 (finish-agentflow-retirement). The contribution shipped and
+> lives on: `HermiqAgentNode` still provides the agent step, registered through
+> `HermiqFlowNodeListener`. The `agentflow` object store this change consumed is
+> retired. Flows live in OpenRegister's native flow store (REQ-FA-002), the
+> resolver is gone, and the schemas left the register descriptor in v0.30.0.
+
## Summary
Make hermiq a CONSUMER of OpenRegister's flow engine instead of the owner of a
diff --git a/openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md b/openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
similarity index 100%
rename from openspec/changes/consume-or-flow-engine/specs/or-flow-consumer/spec.md
rename to openspec/changes/archive/2026-09-02-consume-or-flow-engine/specs/or-flow-consumer/spec.md
diff --git a/openspec/changes/consume-or-flow-engine/tasks.md b/openspec/changes/archive/2026-09-02-consume-or-flow-engine/tasks.md
similarity index 100%
rename from openspec/changes/consume-or-flow-engine/tasks.md
rename to openspec/changes/archive/2026-09-02-consume-or-flow-engine/tasks.md
diff --git a/openspec/changes/hermiq-schedule-source/.openspec.yaml b/openspec/changes/archive/2026-09-02-hermiq-schedule-source/.openspec.yaml
similarity index 100%
rename from openspec/changes/hermiq-schedule-source/.openspec.yaml
rename to openspec/changes/archive/2026-09-02-hermiq-schedule-source/.openspec.yaml
diff --git a/openspec/changes/hermiq-schedule-source/proposal.md b/openspec/changes/archive/2026-09-02-hermiq-schedule-source/proposal.md
similarity index 87%
rename from openspec/changes/hermiq-schedule-source/proposal.md
rename to openspec/changes/archive/2026-09-02-hermiq-schedule-source/proposal.md
index 5b06aff7..4de34672 100644
--- a/openspec/changes/hermiq-schedule-source/proposal.md
+++ b/openspec/changes/archive/2026-09-02-hermiq-schedule-source/proposal.md
@@ -1,5 +1,10 @@
# Proposal: hermiq-schedule-source
+> Archived 2026-09-02 (finish-agentflow-retirement), superseded. The scheduled-flow
+> enumeration and the `agentflow.cron` property this change added lived on the
+> retired `agentflow` object store. Scheduling now belongs to OpenRegister's
+> native flow store, which carries `cron` itself (REQ-FA-002).
+
## Summary
Make hermiq's scheduled agentflows actually fire: answer OpenRegister's new
diff --git a/openspec/changes/hermiq-schedule-source/specs/or-flow-consumer/spec.md b/openspec/changes/archive/2026-09-02-hermiq-schedule-source/specs/or-flow-consumer/spec.md
similarity index 100%
rename from openspec/changes/hermiq-schedule-source/specs/or-flow-consumer/spec.md
rename to openspec/changes/archive/2026-09-02-hermiq-schedule-source/specs/or-flow-consumer/spec.md
diff --git a/openspec/changes/hermiq-schedule-source/tasks.md b/openspec/changes/archive/2026-09-02-hermiq-schedule-source/tasks.md
similarity index 100%
rename from openspec/changes/hermiq-schedule-source/tasks.md
rename to openspec/changes/archive/2026-09-02-hermiq-schedule-source/tasks.md
diff --git a/openspec/changes/schedules-onto-engine-triggers/design.md b/openspec/changes/schedules-onto-engine-triggers/design.md
new file mode 100644
index 00000000..46bf72e8
--- /dev/null
+++ b/openspec/changes/schedules-onto-engine-triggers/design.md
@@ -0,0 +1,185 @@
+# Design: schedules-onto-engine-triggers
+
+## The shape map
+
+hermiq's Schedule schema has three timing kinds. Each maps, or deliberately
+does not, onto an engine primitive:
+
+| Schedule shape | Engine primitive | Phase |
+|---|---|---|
+| `kind=cron`, 5-field expression, timezone-safe | `openregister.trigger-schedule` (`cron`, `runAs`) | 1 (this change) |
+| `kind=interval`, minutes divide 60, or whole hours dividing 24 | `openregister.trigger-schedule` with the derived cron (`*/N * * * *` or `0 */H * * *`) | 1 (this change) |
+| `kind=once` (`runAt`) | `FlowTimerService` one-shot timer: a deadline, which is exactly the timer service's primitive | staged |
+| `kind=cron`, 6-field or timezone-sensitive | none yet; the engine evaluates cron in the server timezone and takes 5 fields only | staged |
+| `kind=interval`, inexpressible (for example 90 minutes) | none; would need timer re-arm chains | staged |
+| A pending retry attempt (`retryState.nextAttemptAt`) | none; retry backoff is domain bookkeeping, not a cadence | stays local |
+
+A schedule that maps stays governed by everything it had, because the mirror
+flow's work node is not the agent step. It is a new, thinner node:
+
+```
+openregister.trigger-schedule ──> hermiq.schedule-dispatch ──> openregister.end
+ (cron, runAs) (scheduleId)
+```
+
+`hermiq.schedule-dispatch` loads the Schedule object and calls
+`ScheduleService::runNow()`. That is the same private dispatch path a tick
+used: kill switch, budget cap, approval gate, commit-before-run, retry
+bookkeeping, delivery, audit. Nothing is reimplemented and nothing is lost.
+The alternative (trigger straight into `hermiq.agent-step`) was rejected: it
+would drop the approval gate, the budget hard cap, delivery and retry for
+exactly the runs that had them.
+
+### Timezone rule
+
+hermiq evaluates cron in the schedule owner's timezone;
+`FlowScheduleService::isDue()` evaluates in the server default timezone. A
+cron is mirrored only when that difference cannot change its meaning:
+
+- the owner's resolved timezone equals the server default timezone, or
+- the expression is a pure cadence: minute field `*` or `*/n`, every other
+ field `*`.
+
+Anything else (a "09:00 daily" owned by someone in another timezone) stays on
+the local tick until the trigger node learns timezones. Shifting hour fields
+by an offset was rejected: DST makes the rewrite wrong twice a year, silently.
+
+### Interval alignment
+
+An interval schedule fired rolling ("N minutes after the last run"). Its
+mirror fires aligned ("every N minutes on the wall clock"). The cadence is
+preserved; the phase may shift once at migration. This is documented and
+accepted: rolling semantics were an artefact of the poller, not a promise the
+schema made.
+
+## Whose identity a mirrored run uses (runAs)
+
+`TriggerScheduleNode` refuses a flow whose trigger names nobody (ADR-099).
+The bridge writes `runAs` as **the identity the run already acts as today**:
+the bound agent's `actingUser` when it names a live user, otherwise the
+schedule's owner. That is `ScheduleService::resolveActingUser()`'s exact
+resolution, computed at mirror time.
+
+This is not a widening. The owner authored the schedule as a standing
+instruction to run unattended; mirroring reproduces that consent, it does not
+create it. Two safety properties hold on top:
+
+- OpenRegister re-resolves `runAs` at every firing and fails closed when the
+ user is gone or disabled, matching hermiq's own offboarding pause.
+- The dispatch node re-enters `runNow()`, which re-resolves the acting user
+ itself. If `actingUser` changed after mirroring, the run still acts as the
+ correct current identity; the bridge refreshes the trigger's `runAs` on its
+ next sync pass.
+
+A schedule whose resolved identity names no live user is not mirrored.
+
+## Arming and the two-way sync
+
+"Creation delegates to the engine" happens on the tick: `ScheduleTask` calls
+`ScheduleFlowBridge::syncAll()` before dispatching. For every enabled
+schedule the bridge:
+
+- mirrors an eligible, unmirrored schedule (insert flow, write `engineFlowId`
+ back onto the schedule);
+- refreshes a mirrored schedule whose cron, `runAs` or enabled flag drifted;
+- retires the mirror (delete flow, clear `engineFlowId`) when the schedule
+ was edited into a shape the engine cannot time.
+
+The write-back property `engineFlowId` is declared in the Schedule schema
+first, because OpenRegister silently drops undeclared properties on save.
+
+A schedule deleted from the UI leaves its mirror orphaned until the node next
+fires; the node then finds no delegated schedule, disables the flow and fails
+the step loudly. Sweeping from the flow side is staged.
+
+### Publishing and the version pin
+
+OpenRegister versions flow definitions, and its `FlowRunVersionPin` refuses
+every scheduled dispatch of a flow with no published version. A mirror that is
+only inserted is therefore a clock that never ticks: the schedule leaves the
+local dispatcher and gains nothing in return. Proven on the rig, where the
+whole dual-app chain worked after one manual publish.
+
+The bridge publishes server-side through `FlowVersionService`, the same class
+the publish endpoint uses, resolved lazily from the container and guarded on
+the class existing (an OpenRegister without versioning has no pin and nothing
+to publish). `publishedBy` stays null: no person pressed publish, the bridge
+did, as the standing consequence of the owner's schedule.
+
+The chosen semantics per path:
+
+- **Create: insert, publish, mark.** A publish failure deletes the flow and
+ never writes the marker, so the schedule keeps its local clock. A crash
+ between publish and mark leaves a published flow the dispatch node refuses
+ (the schedule does not name it): a loud dead flow, never a dead clock.
+- **Refresh: draft, update, publish.** The engine runs the pinned published
+ version, not the flow row, so an update without a republish would fire the
+ old cadence forever. Drafting first flips the head off `published`, which
+ makes a crash between the update and the publish detectable; the old
+ published version keeps serving until the new one lands.
+- **Heal: an undrifted mirror without a published version is published.**
+ This catches mirrors created before the bridge published, and any crash the
+ refresh path left behind, on the next sync pass instead of waiting for a
+ manual publish.
+
+### Double-fire safety
+
+At-most-once holds because the clock has exactly one owner per schedule:
+
+- `engineFlowId` set: `findDueSchedules()` ignores `nextRun` for it; only the
+ engine fires the occurrence. A pending retry is the one exception; it
+ stays local so an open retry sequence is never silently dropped.
+- `engineFlowId` empty: the local tick owns it, as before.
+
+The rollback command restores the second state atomically per schedule:
+clear the marker, then delete the flow. A crash between the two leaves a
+mirror whose schedule is no longer delegated; the node's delegation check
+(schedule's `engineFlowId` must equal the firing flow) refuses to run it, so
+the failure mode is a loud dead flow, never a double fire.
+
+## Oversight registration
+
+hermiq contributes `hermiq.tenant-killswitch` through
+`RegisterFlowOversightEvent`, guarded on the event class existing. The check:
+
+- consents to any hop that is not a `hermiq.*` node type (other apps' flows
+ are not hermiq's to veto);
+- consents when no TenantControl kill switch is engaged (the cheap common
+ path, one read);
+- vetoes a `hermiq.*` hop whose run belongs to an engaged organisation (the
+ run's organisation, read from the `FlowRun` by `runUuid`);
+- vetoes a `hermiq.*` hop whose organisation it cannot establish while any
+ kill switch is engaged. A kill switch that cannot attribute fails closed.
+
+A TenantControl read error consents (logged), mirroring the dispatcher's
+documented choice: a transient read failure must not halt every tenant. The
+registry itself already fails closed on a check that throws.
+
+This gives the kill switch three layers for a scheduled run: the engine-side
+check on every hop, `runNow()`'s own gate 1, and OpenRegister's instance-wide
+`KillSwitchCheck`. The redundancy is deliberate; each layer covers a path the
+others do not (canvas-authored agent steps, legacy tick runs, instance halt).
+
+## Migration and rollback
+
+- `MirrorSchedulesToEngineFlows` (post-migration repair step): calls
+ `syncAll()`. Idempotent by construction: the `engineFlowId` guard makes a
+ second run a no-op, the `MirrorPendingApprovalsToTasks` pattern. Never
+ throws; failures are counted and logged.
+- `occ hermiq:schedules:rollback-flow-mirror`: for every schedule carrying
+ `engineFlowId`, clear the marker, then delete the flow. `--dry-run`
+ supported. After rollback the local tick owns every clock again.
+
+## Risks
+
+- **hermiq CI has no OpenRegister.** Flow classes resolve to
+ `tests/Stubs`; the stubs mirror the real signatures (`FlowMapper`
+ `findByUuid`/`insert`/`update`/`delete`, `Flow` magic accessors) and the
+ standing rule applies: verified live where both apps are installed.
+- **Organisation-less schedules.** A flow row without an organisation is
+ invisible to every tenant forever (hermiq#140). The bridge refuses to
+ mirror a schedule without one; it stays on the local tick.
+- **Engine cadence.** `FlowScheduleService` fires from its own sweep; a
+ mirrored schedule's effective granularity is the engine's, not the tick's
+ 5 minutes. Both are coarse clocks; the schema never promised sub-5-minute
+ precision.
diff --git a/openspec/changes/schedules-onto-engine-triggers/proposal.md b/openspec/changes/schedules-onto-engine-triggers/proposal.md
new file mode 100644
index 00000000..d163be59
--- /dev/null
+++ b/openspec/changes/schedules-onto-engine-triggers/proposal.md
@@ -0,0 +1,64 @@
+# Proposal: schedules-onto-engine-triggers
+
+## Summary
+
+Hand the schedule clock to OpenRegister's flow engine. Every schedule whose
+cadence a cron expression can carry gets a mirror flow: a schedule trigger
+node with an explicit `runAs`, and one hermiq dispatch node that re-enters the
+proven governed dispatch path. The app-local dispatcher stops polling those
+schedules and keeps only what the engine cannot time yet. hermiq's scheduled
+runs also become visible to the engine's oversight surface: a contributed
+check lets the tenant kill switch stop them at every hop.
+
+## Why
+
+The fleet audit flagged hermiq's `ScheduleService` + `ScheduleTask` as a
+duplicate scheduler. OpenRegister owns fleet scheduling (`TriggerScheduleNode`,
+`FlowScheduleService`); hermiq owns agent execution. Today hermiq owns both:
+a 5-minute `TimedJob` polls Schedule objects, computes cron/interval/once
+due-ness itself, and fires. That clock is the duplicate. The domain around it
+(kill switch, budget cap, approval gate, retry and dead-letter, delivery, the
+run audit) is not a duplicate: no engine primitive carries it, and deleting it
+would delete governance.
+
+There is a second gap this closes. hermiq contributes `hermiq.agent-step` to
+the engine, and the engine's oversight registry asks contributed checks before
+every hop. hermiq never contributed one. So an agent step in a canvas-authored
+flow runs even while a tenant's kill switch is engaged. For an AI-agent app
+under EU AI Act Art. 14 that is a hole, not a nuance.
+
+## What changes
+
+- **The engine gets the clock.** A new `ScheduleFlowBridge` mirrors each
+ eligible schedule as one OpenRegister flow:
+ `openregister.trigger-schedule` (cron + `runAs`) into a new
+ `hermiq.schedule-dispatch` node into `openregister.end`. The dispatch node
+ calls `ScheduleService::runNow()`, so every gate, retry, delivery and audit
+ behaviour is byte-for-byte the one a tick applied.
+- **The dispatcher thins.** A mirrored schedule (marked by the new
+ `engineFlowId` property) is no longer due by `nextRun` on the local tick.
+ The tick keeps two jobs: arming (sync schedules to their mirror flows) and
+ the shapes the engine cannot time yet (`once`, inexpressible intervals,
+ pending retries).
+- **Oversight reaches hermiq.** A contributed `IFlowOversightCheck` vetoes any
+ `hermiq.*` hop for an organisation whose TenantControl kill switch is
+ engaged.
+- **In-flight schedules migrate.** An idempotent repair step mirrors existing
+ eligible schedules on upgrade; `occ hermiq:schedules:rollback-flow-mirror`
+ deletes the mirrors and returns the clock to the app-local dispatcher.
+
+## What stays domain
+
+Agent execution itself: impersonation, guardrails, budget accounting, the
+approval gate, retry and dead-letter bookkeeping, delivery and the per-run
+audit entry all stay in hermiq, reached through the dispatch node. The engine
+times the run; hermiq runs it.
+
+## Out of scope (staged in tasks.md)
+
+- `once` schedules onto `FlowTimerService` one-shot timers.
+- Intervals no 5-field cron can express (for example every 90 minutes).
+- Retry re-arming through the engine (a pending retry still fires from the
+ local tick).
+- Deleting `ScheduleService`'s due-selection wholesale. The selection thins
+ here and retires only when every shape has an engine home.
diff --git a/openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md b/openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md
new file mode 100644
index 00000000..46c33675
--- /dev/null
+++ b/openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md
@@ -0,0 +1,152 @@
+# Delta: schedule-engine-delegation
+
+## ADDED Requirements
+
+### Requirement: Eligible schedules delegate their clock to the engine
+
+Every enabled Schedule whose cadence a 5-field, timezone-safe cron expression
+can carry SHALL be mirrored as one OpenRegister flow consisting of a schedule
+trigger node, a `hermiq.schedule-dispatch` node and an end node. The mirrored
+schedule SHALL carry the flow's uuid in `engineFlowId`, and the app-local
+dispatcher SHALL NOT select it as due by `nextRun` while that marker is set.
+
+The bridge SHALL publish the mirror flow's head when OpenRegister carries flow
+versioning: the engine's version pin refuses every scheduled dispatch of an
+unpublished flow, so an unpublished mirror is a clock that never ticks. The
+marker SHALL be written only after a successful publish; a publish failure
+SHALL remove the flow again and leave the schedule on its local clock. When a
+refresh changes the mirrored definition, the bridge SHALL republish, because
+the engine runs the pinned published version, not the flow row. A sync pass
+SHALL publish the head of any mirrored, undrifted flow that lacks a published
+version, so mirrors created before publishing existed heal without a manual
+publish.
+
+#### Scenario: A cron schedule is mirrored and leaves the local clock
+
+- **GIVEN** an enabled `kind=cron` schedule with a 5-field cadence expression
+- **WHEN** the bridge syncs
+- **THEN** a flow exists with `trigger=schedule`, the schedule's cron and a
+ trigger-node `runAs`, and the schedule stores that flow's uuid in
+ `engineFlowId`
+- **AND** the local dispatcher no longer selects the schedule as due by
+ `nextRun`
+
+#### Scenario: A pending retry still fires locally
+
+- **GIVEN** a mirrored schedule with an open `retryState` whose
+ `nextAttemptAt` has passed
+- **WHEN** the local tick selects due schedules
+- **THEN** the schedule is selected, so an open retry sequence is never
+ dropped by delegation
+
+#### Scenario: An inexpressible shape stays local
+
+- **GIVEN** an enabled `kind=once` schedule, or an interval no 5-field cron
+ expresses
+- **WHEN** the bridge syncs
+- **THEN** no flow is created and the schedule stays on the local dispatcher
+
+#### Scenario: A mirrored flow is published so the engine will run it
+
+- **GIVEN** an eligible, unmirrored schedule on an OpenRegister with flow
+ versioning
+- **WHEN** the bridge mirrors it
+- **THEN** the mirror flow has a published version before `engineFlowId` is
+ written, so the version pin accepts the first trigger tick
+- **AND** if publishing fails, the flow is removed, no marker is written, and
+ the schedule stays on the local dispatcher
+
+#### Scenario: A changed cadence lands as a new published version
+
+- **GIVEN** a mirrored schedule whose cron, `runAs` or enabled flag drifted
+ from its flow
+- **WHEN** the bridge refreshes the mirror
+- **THEN** the flow's head is republished, so the engine fires the new
+ definition instead of the old pinned version
+
+@e2e exclude {backend delegation seam; requires a live OpenRegister flow
+engine, which CI does not install. Verified by unit tests against
+signature-matched stubs and live on a dual-app instance.}
+
+### Requirement: A mirrored run declares and re-enters its governed identity
+
+The bridge SHALL write the trigger node's `runAs` as the identity the run
+already acts as: the bound agent's `actingUser` when it names a live user,
+otherwise the schedule owner. A schedule whose resolved identity names no
+live user SHALL NOT be mirrored. The dispatch node SHALL execute through
+`ScheduleService::runNow()`, so the kill switch, budget cap, approval gate,
+retry bookkeeping, delivery and run audit apply unchanged.
+
+#### Scenario: runAs is the resolved acting identity
+
+- **GIVEN** a schedule owned by `alice` bound to an agent whose `actingUser`
+ is the live user `svc-runner`
+- **WHEN** the bridge mirrors it
+- **THEN** the trigger node's `runAs` is `svc-runner`
+
+#### Scenario: A gated occurrence still gates
+
+- **GIVEN** a mirrored schedule with `requiresApproval=true`
+- **WHEN** the engine fires the dispatch node
+- **THEN** the run does not execute and a single pending Approval is ensured,
+ exactly as on a tick
+
+#### Scenario: A stale mirror refuses to fire
+
+- **GIVEN** a flow whose schedule no longer carries that flow's uuid in
+ `engineFlowId`
+- **WHEN** the dispatch node executes
+- **THEN** the step fails loudly and does not run the agent
+
+@e2e exclude {backend delegation seam; requires a live OpenRegister flow
+engine, which CI does not install. Verified by unit tests against
+signature-matched stubs and live on a dual-app instance.}
+
+### Requirement: The tenant kill switch reaches every hermiq flow hop
+
+hermiq SHALL register an oversight check with OpenRegister's
+`FlowOversightRegistry` discovery event. The check SHALL veto any `hermiq.*`
+node hop whose run belongs to an organisation with an engaged TenantControl
+kill switch, SHALL veto a `hermiq.*` hop it cannot attribute while any kill
+switch is engaged, and SHALL consent to all other hops.
+
+#### Scenario: An engaged organisation's hop is vetoed
+
+- **GIVEN** an organisation whose TenantControl kill switch is engaged
+- **WHEN** the engine asks the check before a `hermiq.agent-step` hop of that
+ organisation's run
+- **THEN** the check vetoes with a reason naming the kill switch
+
+#### Scenario: Other apps' hops are not hermiq's to veto
+
+- **GIVEN** any engaged kill switch
+- **WHEN** the engine asks the check before a non-hermiq node hop
+- **THEN** the check consents
+
+@e2e exclude {oversight veto seam inside OpenRegister's engine walk; CI does
+not install the engine. Verified by unit tests against signature-matched
+stubs and live on a dual-app instance.}
+
+### Requirement: In-flight schedules migrate and roll back
+
+An idempotent post-migration repair step SHALL mirror every already-eligible
+schedule, skipping any schedule that carries `engineFlowId`. The command
+`occ hermiq:schedules:rollback-flow-mirror` SHALL clear each schedule's
+marker and then delete its mirror flow, returning the clock to the app-local
+dispatcher.
+
+#### Scenario: The repair step is idempotent
+
+- **GIVEN** schedules already mirrored by a previous run
+- **WHEN** the repair step runs again
+- **THEN** no second flow is created for any of them
+
+#### Scenario: Rollback restores the local clock
+
+- **GIVEN** mirrored schedules
+- **WHEN** the rollback command runs
+- **THEN** every `engineFlowId` is cleared, every mirror flow is deleted, and
+ the local dispatcher selects the schedules as due by `nextRun` again
+
+@e2e exclude {upgrade-time repair step and occ command; no browser surface.
+Verified by unit tests.}
diff --git a/openspec/changes/schedules-onto-engine-triggers/tasks.md b/openspec/changes/schedules-onto-engine-triggers/tasks.md
new file mode 100644
index 00000000..1b775643
--- /dev/null
+++ b/openspec/changes/schedules-onto-engine-triggers/tasks.md
@@ -0,0 +1,59 @@
+# Tasks: schedules-onto-engine-triggers
+
+## Phase 1: the seam, the clock hand-off, oversight (this change)
+
+- [x] 1.1 Declare `engineFlowId` on the Schedule schema (bump schema version)
+ so the write-back survives OpenRegister's undeclared-property drop.
+- [x] 1.2 `hermiq.schedule-dispatch` flow node: loads the schedule, refuses a
+ stale or disabled delegation, re-enters `ScheduleService::runNow()`.
+ Registered in `HermiqFlowNodeListener`.
+- [x] 1.3 `ScheduleFlowBridge`: eligibility (5-field timezone-safe cron,
+ expressible intervals, resolvable identity, organisation present),
+ mirror/refresh/retire, `syncAll()`. Flow rows carry app, organisation,
+ owner and `applicationSlug=hermiq-schedules`.
+- [x] 1.4 `ScheduleService`: `markEngineDelegation()` / `clearEngineDelegation()`
+ reusing the existing sanitised persist; `findDueSchedules()` skips
+ `nextRun` due-ness for delegated schedules (retry due-ness kept).
+- [x] 1.5 `ScheduleTask` arms on every tick: `syncAll()` before dispatch,
+ failure-isolated so a bridge error never blocks the tick.
+- [x] 1.6 Oversight: `TenantKillSwitchCheck` + registration listener on
+ `RegisterFlowOversightEvent`, guarded on the class existing.
+- [x] 1.7 Migration: `MirrorSchedulesToEngineFlows` repair step (idempotent)
+ and `occ hermiq:schedules:rollback-flow-mirror` (with `--dry-run`),
+ both registered in `appinfo/info.xml`.
+- [x] 1.8 Stubs matched to the real OpenRegister signatures (`FlowMapper`
+ delete/findByUuid, `RegisterFlowOversightEvent`, `IFlowOversightCheck`,
+ `FlowRun`/`FlowRunMapper`) and unit tests for the bridge, the node, the
+ check, the repair step and the rollback command.
+- [x] 1.9 Publish the mirror flow's head (`FlowVersionService` via the
+ container, guarded on the class existing). Found on the rig: the engine's
+ `FlowRunVersionPin` refuses every scheduled dispatch of an unpublished
+ flow, so a mirror that is only inserted never ticks. Create path: insert,
+ publish, mark; a publish failure deletes the flow and the schedule keeps
+ its local clock. Refresh path: draft, update, publish, because the engine
+ runs the pinned published version, not the flow row; a crash in between
+ leaves an unpublished head the next pass heals. The undrifted branch
+ publishes any unpublished head, healing pre-publish mirrors. Stubs
+ (`FlowVersion`, `FlowVersionService`) model the pin's contract so a
+ bridge that skips publishing fails its tests.
+
+## Phase 2: the shapes the engine cannot time yet (staged)
+
+- [ ] 2.1 `kind=once` onto `FlowTimerService`: arm a one-shot timer at
+ `runAt` whose expiry queues the mirror flow; cancel on schedule
+ disable/delete. Needs a subject contract for ownerless timers.
+- [ ] 2.2 Timezone-sensitive crons: either a `timezone` config key on
+ `openregister.trigger-schedule` (OpenRegister change) or per-firing
+ evaluation in the owner's timezone; until then they stay local.
+- [ ] 2.3 Inexpressible intervals via timer re-arm chains.
+- [ ] 2.4 Retry re-arming through the engine, so a delegated schedule's
+ backoff no longer needs the local tick.
+
+## Phase 3: retire the local clock (staged)
+
+- [ ] 3.1 Sweep orphaned mirror flows from the flow side (schedule deleted in
+ the UI) instead of waiting for the node's loud refusal.
+- [ ] 3.2 When every shape has an engine home, drop `nextRun` due-selection
+ from `findDueSchedules()` and shrink `ScheduleTask` to the sync pass.
+- [ ] 3.3 Move `nextRun` display derivation to the engine's schedule state so
+ the UI reads one clock.
diff --git a/package-lock.json b/package-lock.json
index fb123cae..7cdec62e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,11 @@
"version": "0.1.0",
"license": "EUPL-1.2",
"dependencies": {
- "@babel/core": "^8.0.1",
- "@babel/preset-env": "^8.0.2",
- "@conduction/nextcloud-vue": "^2.24.3",
+ "@babel/core": "^7.22.9",
+ "@babel/preset-env": "^7.29.7",
+ "@conduction/nextcloud-vue": "^2.37.0",
"@nextcloud/auth": "^2.5.0",
- "@nextcloud/axios": "~2.5.2",
+ "@nextcloud/axios": "~2.6.0",
"@nextcloud/dialogs": "^7.5.0",
"@nextcloud/event-bus": "^3.3.3",
"@nextcloud/initial-state": "^3.0.0",
@@ -40,12 +40,12 @@
"@nextcloud/stylelint-config": "^3.2.2",
"@nextcloud/webpack-vue-config": "^7.0.4",
"@playwright/test": "^1.61.1",
- "@typescript-eslint/parser": "^8.68.0",
+ "@typescript-eslint/parser": "^8.69.0",
"@vue/compiler-sfc": "^3.5.42",
"@vue/test-utils": "^2.5.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "css-loader": "~7.1.1",
+ "css-loader": "~7.1.5",
"eslint": "^10.9.1",
"eslint-config-prettier": "^10.1.8",
"postcss-html": "^2.0.0",
@@ -73,7 +73,6 @@
"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",
@@ -85,471 +84,260 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz",
- "integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"license": "MIT",
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz",
- "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/generator": "^8.0.0",
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helpers": "^8.0.0",
- "@babel/parser": "^8.0.0",
- "@babel/template": "^8.0.0",
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0",
- "@types/gensync": "^1.0.5",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
- "empathic": "^2.0.1",
+ "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
- "import-meta-resolve": "^4.2.0",
"json5": "^2.2.3",
- "obug": "^2.1.1",
- "semver": "^7.7.3"
+ "semver": "^6.3.1"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
- "node_modules/@babel/core/node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "license": "MIT"
- },
"node_modules/@babel/generator": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
- "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^8.0.0",
- "@babel/types": "^8.0.0",
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
- "@types/jsesc": "^2.5.0",
"jsesc": "^3.0.2"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-annotate-as-pure": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz",
- "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz",
+ "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz",
- "integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^8.0.0",
- "@babel/helper-validator-option": "^8.0.0",
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
- "lru-cache": "^11.0.0",
- "semver": "^7.7.3"
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-8.0.1.tgz",
- "integrity": "sha512-++t3ZktzlLmASAxIlxeXQK9Z2YwUafYGYcvGBFevqOqt16HozVHStUoQvWD09fzAZOb/uJGpUTBuGK41AJAuOA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
+ "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^8.0.0",
- "@babel/helper-member-expression-to-functions": "^8.0.0",
- "@babel/helper-optimise-call-expression": "^8.0.0",
- "@babel/helper-replace-supers": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0",
- "@babel/traverse": "^8.0.0",
- "semver": "^7.7.3"
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "semver": "^6.3.1"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-regexp-features-plugin": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-8.0.1.tgz",
- "integrity": "sha512-PydTbcVTiIfVweHMeY1u3MslaD/ZzvnaTNhJp+7ghofelLWshF66Ckc/ZsjStfvRQIKQ4uVG0yEJucyDtyrWgw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz",
+ "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^8.0.0",
+ "@babel/helper-annotate-as-pure": "^7.29.7",
"regexpu-core": "^6.3.1",
- "semver": "^7.7.3"
+ "semver": "^6.3.1"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-1.0.0.tgz",
- "integrity": "sha512-9jzVaTeZyXRDKTgUnNzcPQMO8y0ga3o+Z4fKjNet9Fcx7slgKa83qRbz0EwROSd6qO6CoEe/HQszqSPKb5lhkw==",
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
+ "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helper-plugin-utils": "^8.0.0",
- "lodash.debounce": "^4.0.8"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "debug": "^4.4.3",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.22.11"
},
"peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0"
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/helper-globals": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz",
- "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"license": "MIT",
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-8.0.0.tgz",
- "integrity": "sha512-xkXrMbtk87Gk7+oKBVmBc6EORg/Qwx++AHESldmHkpvG8wgccdhJJFwrzqlF382Fk8wfXhJHWE/g/43QvEGNPQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz",
+ "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-optimise-call-expression": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-8.0.0.tgz",
- "integrity": "sha512-3W6satvtPuCUkUx63S2jMoW9EQNYkADgs1HTfufmL7gCmAulHMKupA/12WNz4A0GMMFn/YnWWwqOT9IZrJHQjg==",
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^8.0.0"
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
+ "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz",
- "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
"license": "MIT",
"engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-remap-async-to-generator": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-8.0.1.tgz",
- "integrity": "sha512-baAKuLEMmu6BCSY3tuiU7qglM1qOZt6F1SrFScA241oNqksxkxfEZEKztlGRmoVns9AQ5UgArH7RsUEjxWnzgQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz",
+ "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^8.0.0",
- "@babel/helper-wrap-function": "^8.0.0",
- "@babel/traverse": "^8.0.0"
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-wrap-function": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-replace-supers": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-8.0.1.tgz",
- "integrity": "sha512-B1SZADIcy3tmH8CmWvj4SHi/oAPom4UL3uknTc2QRNsPVLFk/sPnZvQL/8kj7Y5omvjMqie0vklvs6XM4OLW5Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
+ "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-member-expression-to-functions": "^8.0.0",
- "@babel/helper-optimise-call-expression": "^8.0.0",
- "@babel/traverse": "^8.0.0"
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-8.0.0.tgz",
- "integrity": "sha512-xmCA9kP3IhySsqhzwIdWGlDN/1A4cCKNBO/uwZx/3YzmDoMePwno2Q5/Bq0q+tYaKbeF940YiKV/kaW8Mzvpjw==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
+ "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
@@ -571,101 +359,39 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz",
- "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"license": "MIT",
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helper-wrap-function": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-8.0.0.tgz",
- "integrity": "sha512-Qpm8+wi5xfDkBfollanwriCcKniFfBmMmaKB01GVM6VGzKXo1fdxosZp04qEr5HM+LKhwr3hG1yRy8+ORsficA==",
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^8.0.0",
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz",
+ "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz",
- "integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==",
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
@@ -684,1310 +410,1060 @@
}
},
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-8.0.1.tgz",
- "integrity": "sha512-Ytgjjne4RnG3Oig7ik+NfY4ebRY30BPptVkkyu1f72eINJXRM3/bkU++tIc5aPvyLmo4KH20avq0xJ2o+9aEnw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz",
+ "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-8.0.1.tgz",
- "integrity": "sha512-X7pAMBhuKluA7UfwZNvKN0XVVu/AGeo84Z75eJl85rcb8J2aBzLK92btahM1X5h0oi0QIrbe0qIMA/0+4Buk7w==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz",
+ "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-8.0.1.tgz",
- "integrity": "sha512-DJviKTxYfH0hFwnMiW4dnPyMGzS3Hrr4zUfXl1zwQ0QiGlGlNYklLoPSYEQr8S7nau0/K7NdQjTh0qbYuyFjCA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz",
+ "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-8.0.1.tgz",
- "integrity": "sha512-DmR/N+B9+4PbURFj4+zdnWj49/PFAnK2bn8+E4ZAmwn3J5QCxnbG7Ep6aRfz9M8Aw+rBro0kIJQycvzFpl4buQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz",
+ "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-8.0.1.tgz",
- "integrity": "sha512-x8bi0LFVD2xkULjfNn+hCMg16yAFHAM9fS/ThSFeYBi+0MP9K6qcY2BZb4urUwC7PYtEy5wPe6TKjOEjXrCGFA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz",
+ "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0",
- "@babel/plugin-transform-optional-chaining": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/plugin-transform-optional-chaining": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.13.0"
}
},
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-8.0.1.tgz",
- "integrity": "sha512-P8+RN2n7ts2s1vnE+lXdHYf+dhnmcGSen/kWzBsVluT9Sey5AqmcRXYWlHqgQxaNlKTD5YMa1tf5z4d1v8W88w==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz",
+ "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-8.0.1.tgz",
- "integrity": "sha512-o/gr7kRlq3PKLLuYth4udOsrC7geBerti+QtwPeyxMOsEQO1d8kDHqk9r2PtMx2y9i8FG7tzyTerfv1yMLSMsQ==",
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
- },
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-async-generator-functions": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-8.0.1.tgz",
- "integrity": "sha512-kqnSMF1YHBzuiQrl68675i5Ma1oljvo+SJsNEZFZVBu5BUrVIZm9KId3ui2PdtLK2sv2zM8sJnjPDfgLxQlEqQ==",
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz",
+ "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-remap-async-to-generator": "^8.0.1",
- "@babel/traverse": "^8.0.0"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-async-to-generator": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-8.0.1.tgz",
- "integrity": "sha512-e1jmmEU4p2Lx64sA1+EF8e8/RxPuegzbXcEbmFp5alDyLE+f2ViUpZ77bRWMXzihTwgVVmn/TOpqDbAuS5g1Ew==",
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz",
+ "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^8.0.0",
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-remap-async-to-generator": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-module-imports": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz",
- "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==",
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
+ "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz",
+ "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-remap-async-to-generator": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz",
+ "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-remap-async-to-generator": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-8.0.1.tgz",
- "integrity": "sha512-0V97/gcf7LIgPieEiK1YT0eXa18XJFSLOTZjzEZhA9SJIqZhD/IwGUrCitBzXSmnGCP7hchwC6svHtJ/Eidcpg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz",
+ "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-8.0.1.tgz",
- "integrity": "sha512-HxiQvKsSCs2jOmMhjDrooHaZYOy6W8bqwXp/zjdgPjsNrda6tK9/CH3a/cVIeg6ge3hSS02ALqvqgIo4rTsuSg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
+ "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-class-properties": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-8.0.1.tgz",
- "integrity": "sha512-tORnYiVhIHnKj90TgbSZXrO24f9oEpA6MgFxpIDSKKlHv7AzBIRhkMlYevanueLNYaQXqZWarfCgXM4bWTfNiw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz",
+ "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-class-static-block": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-8.0.1.tgz",
- "integrity": "sha512-NEVK+L0Le8h8tJ+IK0CGS5y9Yi1ZHxLj6M5PeanhMFuq9aSo0XI+Wtmbuyop6fTNukOm7ORNntf/kwid891vqQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz",
+ "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.12.0"
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-8.0.1.tgz",
- "integrity": "sha512-phwyCES8kIMAdVOFw25ztmgAvkl2G+TvUv7azUYyrlR1Qoo3eLJC/MU3MGUKFZ4BWtsJ1NTJM1lKRLzKbswg7w==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
+ "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^8.0.0",
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helper-globals": "^8.0.0",
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-replace-supers": "^8.0.1",
- "@babel/traverse": "^8.0.0"
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-computed-properties": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-8.0.1.tgz",
- "integrity": "sha512-i4l3OGLO8DUDcwdnyraOvILbhqdUf4QgfzhVxSOSzRy49XKXrY7pwaSg9gDSKmhZfNPrEMciBSJSciQh/CjB1A==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz",
+ "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/template": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-destructuring": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-8.0.1.tgz",
- "integrity": "sha512-RtR8uLDl0QcCmqMNIkM8gmDeYZ3rS0ZH+sa+I6sfc09yFoqfp9AEPgBstq9KyfVb0lFCVSRFfJXCI70FIl5ccw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz",
+ "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-dotall-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-8.0.1.tgz",
- "integrity": "sha512-czOUoSaZljJ92yu+bYlXqb/UBN8K9daNCob/B6/7nthSvfGP6YhCnfqD64XWfyb2dN4ypxALNplApoJrsMd4fw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz",
+ "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-8.0.1.tgz",
- "integrity": "sha512-kNnVLkxFUEcTtCyB5PFVQ5Xoy88Bk1lU/ZgDu97CW8eNhRH2Wsiy8Sq5l5dFnwtIUYjzsXHU77jUy1W5AtGSIw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz",
+ "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-8.0.1.tgz",
- "integrity": "sha512-Tv43P47o6fuHgBL7HLHQg3WKXohW9CEUGjLtnCDW27yJLK0zKUdTTqREbZbycNHA83hewMjde5tF6ekrHu9bAA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz",
+ "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-transform-dynamic-import": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-8.0.1.tgz",
- "integrity": "sha512-AS9GlgKc43tJNRu7yOvLaTko4qmdOb+8M69uNS8i421WLO20eVez7LdG5khKdi8E0LIQpYzzzdGIrdXWnO753g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz",
+ "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-explicit-resource-management": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-8.0.1.tgz",
- "integrity": "sha512-VzDIYwBlLCpV6mJfloRdJm8HmYnMqs7O+bGha8yfg2kP7jAdxeCw6yZBVBeaKKQUThtSU52iy+3lB7DhYsbOBA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz",
+ "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/plugin-transform-destructuring": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-8.0.1.tgz",
- "integrity": "sha512-DsZvUUklUmDQ7d2vp+VjqgUWD51mGxhZZ1FPdPP9Hcj0vsgGUKX+zEBGp/vzB1O5PZUxWT/Euq5fu39M9dm9wg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz",
+ "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-8.0.1.tgz",
- "integrity": "sha512-bFzznm46bvWGaTYKle3iolbBJ+oPBfUjwCPesxlFE3SQ7DaY9EHf/8Y5ZzrodKJi8JDdcAyaVWaDUSVyhULh0g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz",
+ "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-for-of": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-8.0.1.tgz",
- "integrity": "sha512-rpeXtgELjpIBQH/+YmyFlD9timPEVCyqY+TNednzoeoTYvXSBEeUvYnYE+BK8rB8m6hHiNK7aL9QWKhGifEJCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-function-name": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-8.0.1.tgz",
- "integrity": "sha512-H1L/JfPf3CqmubuaiZaquXKQ8MRs4YWSsgRllkTviM8TafcCNnlvc4/fJZ3rXP8HmFM+/Bg+TlsPehUI9BtDFA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-json-strings": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-8.0.1.tgz",
- "integrity": "sha512-Mowp8X0J6p7ZehLU82B5e65te2uuSeDHyxrEROwEAS2VKXNXssfw5ZMqhY7k9iXTsOv1Xs/49G3lDCj9Vvw8qQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-literals": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-8.0.1.tgz",
- "integrity": "sha512-ai7kfPRcfyUV1EszXoF1PvL3IuJoCuH08WSEPoRcJTWfZZ55VL/rcfvbVY16QLA3jjbzzSneQSoCtD3L6OyUjw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-logical-assignment-operators": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-8.0.1.tgz",
- "integrity": "sha512-Emvtr5zkEGyCNAmt+qKD5EUh8G0RbxV9EZWrDdX0LuVy5tBq1B3fOIslvVF9aCJmpnwS/AvAT53b9LxAZyXlng==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-member-expression-literals": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-8.0.1.tgz",
- "integrity": "sha512-3Axi9abnyGsm/hh6DsKPZ1Cr9fTtKqS7w0Ig5g12mU269YclpH8pV3xMln2vPLexXgUp6S6L+I06d9/YOLfRKA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-8.0.1.tgz",
- "integrity": "sha512-FDdhET8y1YFDNRuoynqSf23WTzbBBpbIB2oRrlFX7YYm9uWtFvJDSD1r/epBSjfPkOjeaaLgRW9xNnt3JGx46A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-transforms": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-module-imports": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz",
- "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-module-transforms": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.1.tgz",
- "integrity": "sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.0",
- "@babel/traverse": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-8.0.1.tgz",
- "integrity": "sha512-PMuzulWrrzFNmY3lXSk/tV9NRb7y0eZZLJY4UEo2TKszroxvUZHAPPi+T9FDyrQhod+TQA+t+8/QYaaMpiEuhA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-transforms": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-imports": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz",
- "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-transforms": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.1.tgz",
- "integrity": "sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.0",
- "@babel/traverse": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-8.0.1.tgz",
- "integrity": "sha512-0NEHanXmnFEnfT2dLKTXnu7m8GXFsnxRgteBC2aH21hYMBwAgxu5dcTdi/Eg+ToI1HbZe0CHwz4XRLgRNQhYoQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz",
+ "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-validator-identifier": "^8.0.0"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-module-imports": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz",
- "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-module-transforms": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.1.tgz",
- "integrity": "sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==",
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz",
+ "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.0",
- "@babel/traverse": "^8.0.0"
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz",
+ "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz",
+ "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-umd": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-8.0.1.tgz",
- "integrity": "sha512-XKTa2J2MdkmbVEeChq9f7Or0VYcsF0NyVBgytRyeN9F+J+ETAB2SHhfkG4toz/ssuU0i+h/QgJ6ddo5YakSQcA==",
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz",
+ "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-transforms": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-module-imports": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz",
- "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==",
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz",
+ "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-module-transforms": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.1.tgz",
- "integrity": "sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==",
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz",
+ "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.0",
- "@babel/traverse": "^8.0.0"
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
+ "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz",
+ "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==",
"license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.8"
+ },
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-modules-umd/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz",
+ "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-8.0.1.tgz",
- "integrity": "sha512-zCHu+Jr2gTdJE48lN9SV/kXueCW2M79mKtKJc/ttfzzr/jvgdQdCd17RADMqFRQc/25MLxdtjTmlD0HSAMOlIQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz",
+ "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-transform-new-target": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-8.0.1.tgz",
- "integrity": "sha512-QSQxVg1x4PuOuhWUs4Y9u+x9Y+ER8z6G3tC+bDLBzvoOrNLJrEBQLRnwrTP8e5klihAw6Z+e9X5RjdAKcAGapA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz",
+ "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-8.0.1.tgz",
- "integrity": "sha512-AgCJAmQLF7+PtsK79wJqr4xJ2StHCXlz7JL5CVFP4HejJx25Tk6yl1ZrXvi0cKh3VGDVnfVxefxnrpsBirgpyQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz",
+ "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-numeric-separator": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-8.0.1.tgz",
- "integrity": "sha512-it2DmUyLIA1GQUXlFDEnI+/G89mTgxndnAiZYpW8xYR6LboblfirMqiWJeTna5uypQJg7viTT4D1iEURRtFcfw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz",
+ "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-8.0.1.tgz",
- "integrity": "sha512-VmxkDu6bBdbxRzqn6E93hYucug4OVa6svSO19W//vVzNUGAmQzk3QRyHyyEtfcjSLR3NWfRsWwVM9zExLmd+2w==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz",
+ "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/plugin-transform-destructuring": "^8.0.1",
- "@babel/plugin-transform-parameters": "^8.0.1"
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7",
+ "@babel/plugin-transform-parameters": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-object-super": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-8.0.1.tgz",
- "integrity": "sha512-fDkPXRTRKGm25bAq01q82UM4ypPqdVXCwphUUm4t1dL01fGIG0v8KRvT+4BjhMAtRxtPuI34t5Vs7yjRgs3ZgQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz",
+ "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-replace-supers": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-8.0.1.tgz",
- "integrity": "sha512-b2OQ74uGliyATcasTjxGy2O/86UI/n+EN4juB4EMfEwTi9j9uq70PuP0L8fW77vfRY66gO/YoTo/WbIdQ/Si1g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz",
+ "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-8.0.1.tgz",
- "integrity": "sha512-WtRS1c94lZGpGHxYLXMEWeoMVcuv8nkiyr8BTs6OYZv7N3Y9xVE8nbdFIl4lDJH6aH8/pLhqAQOL69d/WI9WdA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz",
+ "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-parameters": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-8.0.1.tgz",
- "integrity": "sha512-IIwRqroW0CYQwR6+3pnmu27z+H98poScWdnov8z6osumMeEsFxAFBBsDS2CFk2jFpPlGqVr89jK/HXO6i5DzxQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz",
+ "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-private-methods": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-8.0.1.tgz",
- "integrity": "sha512-TrFCGcXaVDh6S5IRhmLSRTY9H80VTCMQWnZtzBRg4RWg3KCLmdmsmj4M15kZAPZfoPkWL/SJb4em3Py/vOiX8g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz",
+ "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-private-property-in-object": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-8.0.1.tgz",
- "integrity": "sha512-e+yfOqSYBZaf3PARpiQkjZrpWYgmcFLhK+1tevh2CpHR1O9/36IdyPnAZusESX5nzVV/XZTDAtQBRLa8HPT5Dw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz",
+ "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-annotate-as-pure": "^8.0.0",
- "@babel/helper-create-class-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-property-literals": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-8.0.1.tgz",
- "integrity": "sha512-Z/qx4cxUtYR1nt7XWRutObPxDks98fEYsjWbVeKEqZH6y3AGknmgzCqmHf2FHWZCl1DfoPeuJY+3hZ+35D+2tg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz",
+ "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-regenerator": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-8.0.2.tgz",
- "integrity": "sha512-aFfsjCRYducRV4dPnpsBbdRkLjboca9FVDg6HZCgy0Ahvk2ZQ/2exmCRC5qS9P6rsWwrmIheNaIM6A1j2F8KMA==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz",
+ "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-regexp-modifiers": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-8.0.1.tgz",
- "integrity": "sha512-02ITRDBesPdTYU0oShAzERwEPzozOUQSXlz3qrt8JGuhalBJQv9z5NjgHJPC9sS3Fsam8gDtfAEpBnqZwUIdjQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz",
+ "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-transform-reserved-words": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-8.0.1.tgz",
- "integrity": "sha512-+aykZi7ZP3U84veqfJXm3HhPZGddWFi64g7jr0ni6tb1zel+1ey+SL+IRKPoZXFyFqvYEsoqrmx4PyEJRlHl/Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz",
+ "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-shorthand-properties": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-8.0.1.tgz",
- "integrity": "sha512-JddANd9yPVH8dYgVoNkqAH5BftnsDxFpG51Zas7sc6F3poz5QWcejHNGO8a/57IX5ByjGSzEmYk9Z7ZMa5MWaw==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
+ "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-8.0.1.tgz",
- "integrity": "sha512-O9Bw9FyxlSw1SlMg3S82/GKNZ0x77RPbHezotEy1JTlIM/vk6WO8jW1iF+iTiKLOXNvi+b+LZ9t77Gi+Q0FhGg==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz",
+ "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0"
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-sticky-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-8.0.1.tgz",
- "integrity": "sha512-IsVP6WrZZQdaG2zLmeKwWiI+ua2NB5L1+f77C2/8z2NCDz7uxlIA/lnwocYOJk9PXcOC2sZgRls3LN4XpNduzQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz",
+ "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-template-literals": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-8.0.1.tgz",
- "integrity": "sha512-JXvtj5+BJA9Qv3prDzW2z2DkGTJNmG0BObTdUD03STiu1Jr4fNQkQy3hYZgPL46a2RjcuhwBMYf49BOuJ98gnA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
+ "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-8.0.1.tgz",
- "integrity": "sha512-+wJoxgxP2gtey0UMUOMhzMMji2XHO/Uu6MXUh/r5Yhc2jngKzK/wFxY2WNe4UCaRcMvCb4gcnB8wIgFXJsocXg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz",
+ "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-8.0.1.tgz",
- "integrity": "sha512-TAXJepIJ6vZphytTwcf+LuXi2M2ZWI43VCqNw+1ZZLPP/38Z1A8j4Mahvg8kqDgMOSM/cakk+hedTJCiw3jQuQ==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz",
+ "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-8.0.1.tgz",
- "integrity": "sha512-zjBN9tSMSuomNDfurL69Gf7+v4D2t5uI1mSZaYJDo88SKpbduhCXqtxH7Tx66iCF6caWYwnBzSM0tnCozmQq5Q==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz",
+ "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-unicode-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-8.0.1.tgz",
- "integrity": "sha512-v0oO83cvT5lwbcIVRShpx4vaHD8AvM9IBowsQuTeP+kGmhh3recJQs33Bl6dlo3/2g9amlznLbFGn4VJbPCJqA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz",
+ "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-8.0.1.tgz",
- "integrity": "sha512-MlQeyS0K7gh0XNeLBMS/3Z07HjDOKhA7xm2L18GyxOXyiFHI9E+ZuQ4mFYmcLjluXsE/Wf6dABIqZvKpKw0Z3w==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz",
+ "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1"
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/preset-env": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-8.0.2.tgz",
- "integrity": "sha512-CUGLn9hNBCF/eXnwdFAWERbniCcXCRvnKwLV9fegeUEIqv7YlU2MepsWMMM54GcILx5XYMnRh+JAL+K5G+mK6g==",
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^8.0.0",
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/helper-validator-option": "^8.0.0",
- "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^8.0.1",
- "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^8.0.1",
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^8.0.1",
- "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^8.0.1",
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^8.0.1",
- "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^8.0.1",
- "@babel/plugin-transform-arrow-functions": "^8.0.1",
- "@babel/plugin-transform-async-generator-functions": "^8.0.1",
- "@babel/plugin-transform-async-to-generator": "^8.0.1",
- "@babel/plugin-transform-block-scoped-functions": "^8.0.1",
- "@babel/plugin-transform-block-scoping": "^8.0.1",
- "@babel/plugin-transform-class-properties": "^8.0.1",
- "@babel/plugin-transform-class-static-block": "^8.0.1",
- "@babel/plugin-transform-classes": "^8.0.1",
- "@babel/plugin-transform-computed-properties": "^8.0.1",
- "@babel/plugin-transform-destructuring": "^8.0.1",
- "@babel/plugin-transform-dotall-regex": "^8.0.1",
- "@babel/plugin-transform-duplicate-keys": "^8.0.1",
- "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^8.0.1",
- "@babel/plugin-transform-dynamic-import": "^8.0.1",
- "@babel/plugin-transform-explicit-resource-management": "^8.0.1",
- "@babel/plugin-transform-exponentiation-operator": "^8.0.1",
- "@babel/plugin-transform-export-namespace-from": "^8.0.1",
- "@babel/plugin-transform-for-of": "^8.0.1",
- "@babel/plugin-transform-function-name": "^8.0.1",
- "@babel/plugin-transform-json-strings": "^8.0.1",
- "@babel/plugin-transform-literals": "^8.0.1",
- "@babel/plugin-transform-logical-assignment-operators": "^8.0.1",
- "@babel/plugin-transform-member-expression-literals": "^8.0.1",
- "@babel/plugin-transform-modules-amd": "^8.0.1",
- "@babel/plugin-transform-modules-commonjs": "^8.0.1",
- "@babel/plugin-transform-modules-systemjs": "^8.0.1",
- "@babel/plugin-transform-modules-umd": "^8.0.1",
- "@babel/plugin-transform-named-capturing-groups-regex": "^8.0.1",
- "@babel/plugin-transform-new-target": "^8.0.1",
- "@babel/plugin-transform-nullish-coalescing-operator": "^8.0.1",
- "@babel/plugin-transform-numeric-separator": "^8.0.1",
- "@babel/plugin-transform-object-rest-spread": "^8.0.1",
- "@babel/plugin-transform-object-super": "^8.0.1",
- "@babel/plugin-transform-optional-catch-binding": "^8.0.1",
- "@babel/plugin-transform-optional-chaining": "^8.0.1",
- "@babel/plugin-transform-parameters": "^8.0.1",
- "@babel/plugin-transform-private-methods": "^8.0.1",
- "@babel/plugin-transform-private-property-in-object": "^8.0.1",
- "@babel/plugin-transform-property-literals": "^8.0.1",
- "@babel/plugin-transform-regenerator": "^8.0.2",
- "@babel/plugin-transform-regexp-modifiers": "^8.0.1",
- "@babel/plugin-transform-reserved-words": "^8.0.1",
- "@babel/plugin-transform-shorthand-properties": "^8.0.1",
- "@babel/plugin-transform-spread": "^8.0.1",
- "@babel/plugin-transform-sticky-regex": "^8.0.1",
- "@babel/plugin-transform-template-literals": "^8.0.1",
- "@babel/plugin-transform-typeof-symbol": "^8.0.1",
- "@babel/plugin-transform-unicode-escapes": "^8.0.1",
- "@babel/plugin-transform-unicode-property-regex": "^8.0.1",
- "@babel/plugin-transform-unicode-regex": "^8.0.1",
- "@babel/plugin-transform-unicode-sets-regex": "^8.0.1",
- "@babel/preset-modules": "^0.2.0",
- "babel-plugin-polyfill-corejs3": "^1.0.0",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz",
+ "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7",
+ "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.29.7",
+ "@babel/plugin-syntax-import-attributes": "^7.29.7",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.29.7",
+ "@babel/plugin-transform-async-generator-functions": "^7.29.7",
+ "@babel/plugin-transform-async-to-generator": "^7.29.7",
+ "@babel/plugin-transform-block-scoped-functions": "^7.29.7",
+ "@babel/plugin-transform-block-scoping": "^7.29.7",
+ "@babel/plugin-transform-class-properties": "^7.29.7",
+ "@babel/plugin-transform-class-static-block": "^7.29.7",
+ "@babel/plugin-transform-classes": "^7.29.7",
+ "@babel/plugin-transform-computed-properties": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7",
+ "@babel/plugin-transform-dotall-regex": "^7.29.7",
+ "@babel/plugin-transform-duplicate-keys": "^7.29.7",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7",
+ "@babel/plugin-transform-dynamic-import": "^7.29.7",
+ "@babel/plugin-transform-explicit-resource-management": "^7.29.7",
+ "@babel/plugin-transform-exponentiation-operator": "^7.29.7",
+ "@babel/plugin-transform-export-namespace-from": "^7.29.7",
+ "@babel/plugin-transform-for-of": "^7.29.7",
+ "@babel/plugin-transform-function-name": "^7.29.7",
+ "@babel/plugin-transform-json-strings": "^7.29.7",
+ "@babel/plugin-transform-literals": "^7.29.7",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.29.7",
+ "@babel/plugin-transform-member-expression-literals": "^7.29.7",
+ "@babel/plugin-transform-modules-amd": "^7.29.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.29.7",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.7",
+ "@babel/plugin-transform-modules-umd": "^7.29.7",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7",
+ "@babel/plugin-transform-new-target": "^7.29.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7",
+ "@babel/plugin-transform-numeric-separator": "^7.29.7",
+ "@babel/plugin-transform-object-rest-spread": "^7.29.7",
+ "@babel/plugin-transform-object-super": "^7.29.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.29.7",
+ "@babel/plugin-transform-optional-chaining": "^7.29.7",
+ "@babel/plugin-transform-parameters": "^7.29.7",
+ "@babel/plugin-transform-private-methods": "^7.29.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.29.7",
+ "@babel/plugin-transform-property-literals": "^7.29.7",
+ "@babel/plugin-transform-regenerator": "^7.29.7",
+ "@babel/plugin-transform-regexp-modifiers": "^7.29.7",
+ "@babel/plugin-transform-reserved-words": "^7.29.7",
+ "@babel/plugin-transform-shorthand-properties": "^7.29.7",
+ "@babel/plugin-transform-spread": "^7.29.7",
+ "@babel/plugin-transform-sticky-regex": "^7.29.7",
+ "@babel/plugin-transform-template-literals": "^7.29.7",
+ "@babel/plugin-transform-typeof-symbol": "^7.29.7",
+ "@babel/plugin-transform-unicode-escapes": "^7.29.7",
+ "@babel/plugin-transform-unicode-property-regex": "^7.29.7",
+ "@babel/plugin-transform-unicode-regex": "^7.29.7",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.29.7",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.15",
+ "babel-plugin-polyfill-corejs3": "^0.14.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.6",
"core-js-compat": "^3.48.0",
- "semver": "^7.7.3"
+ "semver": "^6.3.1"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
+ "@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/preset-modules": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.2.0.tgz",
- "integrity": "sha512-yz0RBN2fx4fjCeFcTWsWgL7PxSRltvTa0Qg14HkWCU3qS8MO7ZSJlBVbGceynd5C9NsJwwUHNQD3dc6tYO+jqQ==",
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/plugin-transform-dotall-regex": "^8.0.1",
- "@babel/plugin-transform-unicode-property-regex": "^8.0.1",
- "@babel/types": "^8.0.0",
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
"esutils": "^2.0.2"
},
"peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/preset-modules/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/preset-modules/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/preset-modules/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/runtime": {
@@ -2000,167 +1476,37 @@
}
},
"node_modules/@babel/template": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
- "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/parser": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@babel/template/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "license": "MIT"
- },
"node_modules/@babel/traverse": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz",
- "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/generator": "^8.0.0",
- "@babel/helper-globals": "^8.0.0",
- "@babel/parser": "^8.0.4",
- "@babel/template": "^8.0.0",
- "@babel/types": "^8.0.4",
- "obug": "^2.1.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
},
"engines": {
- "node": "^22.18.0 || >=24.11.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@babel/traverse/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "license": "MIT"
- },
"node_modules/@babel/types": {
"version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
@@ -2196,6 +1542,23 @@
"keyv": "^5.6.0"
}
},
+ "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+ "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.4.0",
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "keyv": "^5.6.0"
+ }
+ },
"node_modules/@cacheable/memory/node_modules/keyv": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
@@ -2256,9 +1619,9 @@
}
},
"node_modules/@codemirror/commands": {
- "version": "6.10.4",
- "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
- "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.0.tgz",
+ "integrity": "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -2281,9 +1644,9 @@
}
},
"node_modules/@codemirror/lang-html": {
- "version": "6.4.11",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
- "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
+ "version": "6.4.12",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz",
+ "integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@@ -2382,9 +1745,9 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.43.7",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.7.tgz",
- "integrity": "sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==",
+ "version": "6.43.9",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.9.tgz",
+ "integrity": "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.7.0",
@@ -2394,9 +1757,9 @@
}
},
"node_modules/@conduction/nextcloud-vue": {
- "version": "2.24.3",
- "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.24.3.tgz",
- "integrity": "sha512-Hflys+AxGNNSkDxuMf04RdFzijhWDKtcRJAHtmtYLj6ntwoIT51k0rfRju6S5GuTNF+daJDUv9tHDVVXb9ZIWw==",
+ "version": "2.37.0",
+ "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.37.0.tgz",
+ "integrity": "sha512-3+c+vPHlcswZS0+yEO+HjaFTkgI0ET0Lk33LEmMnZc/E7rDV6lPILq9LNlmxC04WuyQNILx4+zsckkSMYEg7hQ==",
"license": "EUPL-1.2",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
@@ -2460,7 +1823,7 @@
"eslint-plugin-vue": "^9.21.0 || ^10.0.0",
"gridstack": "^12.0.0 || ^13.0.0",
"marked": "^12.0.0",
- "pinia": "^2.0.0 || ^3.0.0",
+ "pinia": "^2.0.0 || ^3.0.0 || ^4.0.0",
"vue": "^3.5.0",
"vue-eslint-parser": "^9.4.0 || ^10.0.0",
"vue-material-design-icons": "^5.0.0"
@@ -2549,9 +1912,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz",
- "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==",
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.10.tgz",
+ "integrity": "sha512-xBja6gaAaH2R2c7eNyl0TY4dhnnZ2uhj+KXpLdEQ6M/wuk9bYFZM8wY0ykw3VO4TgEJ56KGlerXS/9KBKVR/Cg==",
"dev": true,
"funding": [
{
@@ -2673,9 +2036,9 @@
}
},
"node_modules/@cyclonedx/cyclonedx-library": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.1.0.tgz",
- "integrity": "sha512-4yq0KTQIA32UHJwFMDeY5xj2asp3Mk5lzx4JRVXLOb0YE8w1KeR61c1m/gJ4sjMXpiiEDfaITVQu8BLUuy7t3A==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.2.0.tgz",
+ "integrity": "sha512-hGeo1XXM0zuIeTyzJihxPxnEOeNBmgZkuPRmTR28RktlAqF9xLneonHRCzahZHLgCq/LOmtCs5vyojJlnmJ87w==",
"dev": true,
"funding": [
{
@@ -3044,9 +2407,9 @@
}
},
"node_modules/@humanwhocodes/momoa": {
- "version": "3.3.10",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz",
- "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==",
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.12.tgz",
+ "integrity": "sha512-xS9xl4ieqTqhSZfKV3YXj/htwJCUxbgvkTVaK1fkESgrOzvoVSOTRQeQ8tTLOZUS0Csi2xqtJQXgVTRH5OEbHQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -3086,35 +2449,31 @@
"node": ">=12"
}
},
- "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
+ "optional": true
},
- "node_modules/@isaacs/cliui/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==",
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "ansi-regex": "^6.2.2"
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/fs-minipass": {
@@ -3172,36 +2531,19 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@keyv/bigmap": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
- "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hashery": "^1.4.0",
- "hookified": "^1.15.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "peerDependencies": {
- "keyv": "^5.6.0"
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@keyv/serialize": {
@@ -3218,9 +2560,9 @@
"license": "MIT"
},
"node_modules/@lezer/css": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.5.tgz",
- "integrity": "sha512-PrlQ8glBdWS5UOqgnFCmPxAJbhuOhb0WoDnSAXiNQPjivlDujhuUQ1K/fxyk2vFoq4VTBgFtFZOc8sOpiYjz5g==",
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.6.tgz",
+ "integrity": "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -3291,9 +2633,9 @@
}
},
"node_modules/@marijn/find-cluster-break": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
- "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz",
+ "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
"license": "MIT"
},
"node_modules/@mdi/js": {
@@ -3323,14 +2665,13 @@
}
},
"node_modules/@nextcloud/axios": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.5.2.tgz",
- "integrity": "sha512-8frJb77jNMbz00TjsSqs1PymY0nIEbNM4mVmwen2tXY7wNgRai6uXilIlXKOYB9jR/F/HKRj6B4vUwVwZbhdbw==",
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.6.0.tgz",
+ "integrity": "sha512-ehcIgyora8DAJ+STG6iFI4e+ufPVFrIA6o0FgMKeKdfyaxRJ9UM7L+n7V+rc/qv8sDiWC/hWIKwFtLw2W5yE4Q==",
"license": "GPL-3.0-or-later",
"dependencies": {
- "@nextcloud/auth": "^2.5.1",
- "@nextcloud/router": "^3.0.1",
- "axios": "^1.12.2"
+ "@nextcloud/auth": "^2.6.0",
+ "axios": "^1.15.0"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
@@ -3379,7 +2720,7 @@
"dependencies": {
"@mdi/js": "^7.4.47",
"@nextcloud/auth": "^2.6.0",
- "@nextcloud/axios": "^2.6.0",
+ "@nextcloud/axios": "~2.6.0",
"@nextcloud/browser-storage": "^0.5.0",
"@nextcloud/capabilities": "^1.2.1",
"@nextcloud/event-bus": "^3.3.3",
@@ -3459,20 +2800,46 @@
"@nextcloud/files": "^3.12.0"
}
},
- "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/sharing": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz",
- "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==",
- "license": "GPL-3.0-or-later",
+ "node_modules/@nextcloud/dialogs/node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@nextcloud/dialogs/node_modules/@vueuse/core": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz",
+ "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==",
+ "license": "MIT",
"dependencies": {
- "@nextcloud/initial-state": "^3.0.0",
- "is-svg": "^6.1.0"
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "11.3.0",
+ "@vueuse/shared": "11.3.0",
+ "vue-demi": ">=0.14.10"
},
- "engines": {
- "node": "^20.0.0 || ^22.0.0 || ^24.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@nextcloud/dialogs/node_modules/@vueuse/metadata": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz",
+ "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@nextcloud/dialogs/node_modules/@vueuse/shared": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz",
+ "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.10"
},
- "optionalDependencies": {
- "@nextcloud/files": "^3.12.2 || ^4.0.0"
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@nextcloud/eslint-config": {
@@ -3503,92 +2870,17 @@
"eslint": ">=10"
}
},
- "node_modules/@nextcloud/eslint-config/node_modules/fast-xml-parser": {
- "version": "5.10.1",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz",
- "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/NaturalIntelligence"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@nodable/entities": "^3.0.0",
- "fast-xml-builder": "^1.2.0",
- "is-unsafe": "^2.0.0",
- "path-expression-matcher": "^1.6.2",
- "strnum": "^2.4.1",
- "xml-naming": "^0.3.0"
- },
- "bin": {
- "fxparser": "src/cli/cli.js"
- }
- },
- "node_modules/@nextcloud/eslint-config/node_modules/globals": {
- "version": "17.11.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz",
- "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nextcloud/eslint-config/node_modules/is-plain-obj": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nextcloud/eslint-config/node_modules/sort-package-json": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-4.0.0.tgz",
- "integrity": "sha512-6aYOlYI9AWioZ+rzu+4zKLmoFqJP0/fHDxrd7X04yqEibikY+5YVF0EYlyGn4v6X2PJY7yAUWV7oeP+i5rOm/g==",
+ "node_modules/@nextcloud/eslint-config/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": "MIT",
- "dependencies": {
- "detect-indent": "^7.0.2",
- "detect-newline": "^4.0.1",
- "git-hooks-list": "^4.1.1",
- "is-plain-obj": "^4.1.0",
- "semver": "^7.7.3",
- "sort-object-keys": "^2.0.1",
- "tinyglobby": "^0.2.15"
- },
+ "license": "ISC",
"bin": {
- "sort-package-json": "cli.js"
+ "semver": "bin/semver.js"
},
"engines": {
- "node": ">=22"
- }
- },
- "node_modules/@nextcloud/eslint-config/node_modules/strnum": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz",
- "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/NaturalIntelligence"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "anynum": "^1.0.1"
+ "node": ">=10"
}
},
"node_modules/@nextcloud/event-bus": {
@@ -3604,6 +2896,18 @@
"node": "^20 || ^22 || ^24"
}
},
+ "node_modules/@nextcloud/event-bus/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@nextcloud/files": {
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz",
@@ -3626,6 +2930,22 @@
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
}
},
+ "node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz",
+ "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==",
+ "license": "GPL-3.0-or-later",
+ "dependencies": {
+ "@nextcloud/initial-state": "^3.0.0",
+ "is-svg": "^6.1.0"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || ^24.0.0"
+ },
+ "optionalDependencies": {
+ "@nextcloud/files": "^3.12.0"
+ }
+ },
"node_modules/@nextcloud/initial-state": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz",
@@ -3669,7 +2989,7 @@
"integrity": "sha512-07UDgz1xLG9XABP8+mwQ2CsNWZu6lKzz0ErUA2HfE1ZfxXKiwVpo60t30y34UExGB9+Ok1nFaYU8fyJHncz9aQ==",
"license": "AGPL-3.0-or-later",
"dependencies": {
- "@nextcloud/axios": "^2.6.0",
+ "@nextcloud/axios": "~2.6.0",
"@nextcloud/capabilities": "^1.2.1",
"@nextcloud/event-bus": "^3.3.3"
}
@@ -3727,9 +3047,9 @@
}
},
"node_modules/@nextcloud/sharing": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz",
- "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz",
+ "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==",
"license": "GPL-3.0-or-later",
"dependencies": {
"@nextcloud/initial-state": "^3.0.0",
@@ -3739,7 +3059,7 @@
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
},
"optionalDependencies": {
- "@nextcloud/files": "^3.12.0"
+ "@nextcloud/files": "^3.12.2 || ^4.0.0"
}
},
"node_modules/@nextcloud/stylelint-config": {
@@ -3773,15 +3093,15 @@
}
},
"node_modules/@nextcloud/vue": {
- "version": "9.9.0",
- "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.9.0.tgz",
- "integrity": "sha512-TmKnBWp6Aiw+cm+WamwX5cMkhkpJqLSUKrTUB5I5Y9RDt2S0TUlwRaTe9vltBIV3qmKParhpZQ3/ewWHgEQlrQ==",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.11.0.tgz",
+ "integrity": "sha512-LRsyU9Mxs0b2xWqbxygps8O7/i1z4QhNQD9/QDni+VqFY3V0N95mAgmKJm6vfoFuxTAMXERp9bjqFn3UV542gw==",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
"@floating-ui/dom": "^1.8.0",
"@nextcloud/auth": "^2.6.0",
- "@nextcloud/axios": "^2.6.0",
+ "@nextcloud/axios": "~2.6.0",
"@nextcloud/browser-storage": "^0.5.0",
"@nextcloud/capabilities": "^1.2.1",
"@nextcloud/event-bus": "^3.3.3",
@@ -3792,19 +3112,19 @@
"@nextcloud/sharing": "^0.4.0",
"@nextcloud/vue-select": "^4.1.0",
"@vuepic/vue-datepicker": "^11.0.3",
- "@vueuse/components": "^14.3.0",
- "@vueuse/core": "^14.3.0",
+ "@vueuse/components": "^14.4.0",
+ "@vueuse/core": "^14.4.0",
"blurhash": "^2.0.5",
"clone": "^2.1.2",
"debounce": "^3.0.0",
- "dompurify": "^3.4.12",
+ "dompurify": "^3.4.14",
"emoji-mart-vue-fast": "^15.0.5",
"escape-html": "^1.0.3",
"floating-vue": "^5.2.2",
"focus-trap": "^8.2.2",
"linkifyjs": "^4.3.3",
"mdast-util-to-string": "^4.0.0",
- "p-queue": "^9.3.1",
+ "p-queue": "^9.3.3",
"rehype-external-links": "^3.0.0",
"rehype-highlight": "^7.0.2",
"rehype-react": "^8.0.0",
@@ -3821,7 +3141,7 @@
"unist-builder": "^4.0.0",
"unist-util-visit-parents": "^6.0.2",
"vue": "^3.5.18",
- "vue-router": "^5.1.0"
+ "vue-router": "^5.2.0"
},
"engines": {
"node": "^20.11.0 || ^22 || ^24"
@@ -3839,87 +3159,17 @@
"vue": "^3"
}
},
- "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz",
- "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==",
- "license": "GPL-3.0-or-later",
- "dependencies": {
- "@nextcloud/initial-state": "^3.0.0",
- "is-svg": "^6.1.0"
- },
- "engines": {
- "node": "^20.0.0 || ^22.0.0 || ^24.0.0"
- },
- "optionalDependencies": {
- "@nextcloud/files": "^3.12.2 || ^4.0.0"
- }
- },
- "node_modules/@nextcloud/vue/node_modules/@types/web-bluetooth": {
- "version": "0.0.21",
- "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
- "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
- "license": "MIT"
- },
- "node_modules/@nextcloud/vue/node_modules/@vue/devtools-api": {
- "version": "8.2.1",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
- "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
- "license": "MIT",
- "dependencies": {
- "@vue/devtools-kit": "^8.2.1"
- }
- },
- "node_modules/@nextcloud/vue/node_modules/@vueuse/core": {
- "version": "14.4.0",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
- "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
- "license": "MIT",
- "dependencies": {
- "@types/web-bluetooth": "^0.0.21",
- "@vueuse/metadata": "14.4.0",
- "@vueuse/shared": "14.4.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "vue": "^3.5.0"
- }
- },
- "node_modules/@nextcloud/vue/node_modules/@vueuse/metadata": {
- "version": "14.4.0",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
- "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/@nextcloud/vue/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@nextcloud/vue/node_modules/vue-router": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz",
- "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.0.tgz",
+ "integrity": "sha512-a2PBXX9yfkS58JzSJALUffgDa09lEzKmCcEnLaepoIr88L+9hEnsgEQkcadJGID+vtHa0gFpVo064zmylA9fcg==",
"license": "MIT",
"dependencies": {
- "@babel/generator": "^8.0.0",
"@vue-macros/common": "^3.1.3",
"@vue/devtools-api": "^8.1.5",
"ast-walker-scope": "^0.9.0",
"chokidar": "^5.0.0",
- "json5": "^2.2.3",
+ "confbox": "^0.2.4",
"local-pkg": "^1.2.1",
"magic-string": "^0.30.21",
"mlly": "^1.8.2",
@@ -3930,8 +3180,7 @@
"scule": "^1.3.0",
"tinyglobby": "^0.2.17",
"unplugin": "^3.3.0",
- "unplugin-utils": "^0.3.2",
- "yaml": "^2.9.0"
+ "unplugin-utils": "^0.3.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
@@ -4101,6 +3350,20 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/@npmcli/fs/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",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@one-ini/wasm": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.2.1.tgz",
@@ -4460,22 +3723,8 @@
"node": ">= 10.0.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
"node_modules/@pkgjs/parseargs": {
@@ -4565,42 +3814,6 @@
"eslint": "^9.0.0 || ^10.0.0"
}
},
- "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@svgdotjs/svg.draggable.js": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
@@ -4623,9 +3836,9 @@
}
},
"node_modules/@svgdotjs/svg.js": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.7.tgz",
- "integrity": "sha512-pxpRccS8zrBl97JkkTzQNqohJ4c3L+xuWH6ulMW9E6yknZ5lp4z+n6MprHNsykFvFj6O8TQJfpsXaTUAJfcUfw==",
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.8.tgz",
+ "integrity": "sha512-NUdbI/7FDdqGpzKmqr09IQoy9MUcrAC/3bC8gLgXi6OfJuyri450zMLvJekFUhuq3iN5gpdPjoSoStNl9Eijfg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -4716,12 +3929,6 @@
"@types/estree": "*"
}
},
- "node_modules/@types/gensync": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
- "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==",
- "license": "MIT"
- },
"node_modules/@types/hast": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
@@ -4740,12 +3947,6 @@
"@types/sizzle": "*"
}
},
- "node_modules/@types/jsesc": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
- "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
- "license": "MIT"
- },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4769,9 +3970,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "26.1.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
- "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
+ "version": "26.4.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
+ "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4820,22 +4021,61 @@
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
- "version": "0.0.20",
- "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
- "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
- "node_modules/@typescript-eslint/parser": {
+ "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.68.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz",
- "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz",
+ "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.68.0",
- "@typescript-eslint/types": "8.68.0",
- "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/type-utils": "8.68.0",
+ "@typescript-eslint/utils": "8.68.0",
"@typescript-eslint/visitor-keys": "8.68.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.68.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.7",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.7.tgz",
+ "integrity": "sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz",
+ "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.69.0",
+ "@typescript-eslint/types": "8.69.0",
+ "@typescript-eslint/typescript-estree": "8.69.0",
+ "@typescript-eslint/visitor-keys": "8.69.0",
"debug": "^4.4.3"
},
"engines": {
@@ -4850,15 +4090,158 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz",
+ "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.69.0",
+ "@typescript-eslint/types": "^8.69.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz",
+ "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.69.0",
+ "@typescript-eslint/visitor-keys": "8.69.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz",
+ "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz",
+ "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz",
+ "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.69.0",
+ "@typescript-eslint/tsconfig-utils": "8.69.0",
+ "@typescript-eslint/types": "8.69.0",
+ "@typescript-eslint/visitor-keys": "8.69.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.69.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz",
+ "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.69.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/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/@typescript-eslint/project-service": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz",
- "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==",
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz",
+ "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.67.0",
- "@typescript-eslint/types": "^8.67.0",
+ "@typescript-eslint/tsconfig-utils": "^8.68.0",
+ "@typescript-eslint/types": "^8.68.0",
"debug": "^4.4.3"
},
"engines": {
@@ -4891,11 +4274,35 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz",
- "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==",
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz",
+ "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz",
+ "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/utils": "8.68.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -4904,6 +4311,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
@@ -4949,34 +4357,31 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
- "version": "8.68.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz",
- "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==",
+ "node_modules/@typescript-eslint/typescript-estree/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": "MIT",
- "dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.68.0",
- "@typescript-eslint/types": "^8.68.0",
- "debug": "^4.4.3"
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "node": ">=10"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
+ "node_modules/@typescript-eslint/utils": {
"version": "8.68.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz",
- "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz",
+ "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.68.0",
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -4985,6 +4390,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
@@ -5006,6 +4412,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@uiw/codemirror-theme-github": {
"version": "4.25.11",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.11.tgz",
@@ -5038,9 +4457,9 @@
}
},
"node_modules/@ungap/structured-clone": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
- "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
+ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
"license": "ISC"
},
"node_modules/@vue-flow/background": {
@@ -5069,6 +4488,12 @@
"vue": "^3.3.0"
}
},
+ "node_modules/@vue-flow/core/node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
"node_modules/@vue-flow/core/node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
@@ -5211,10 +4636,13 @@
}
},
"node_modules/@vue/devtools-api": {
- "version": "6.6.4",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
- "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
- "license": "MIT"
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
+ "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^8.2.1"
+ }
},
"node_modules/@vue/devtools-kit": {
"version": "8.2.1",
@@ -5331,13 +4759,7 @@
"vue": "^3.5.0"
}
},
- "node_modules/@vueuse/components/node_modules/@types/web-bluetooth": {
- "version": "0.0.21",
- "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
- "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
- "license": "MIT"
- },
- "node_modules/@vueuse/components/node_modules/@vueuse/core": {
+ "node_modules/@vueuse/core": {
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
"integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
@@ -5354,7 +4776,7 @@
"vue": "^3.5.0"
}
},
- "node_modules/@vueuse/components/node_modules/@vueuse/metadata": {
+ "node_modules/@vueuse/metadata": {
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
"integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
@@ -5363,42 +4785,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
- "node_modules/@vueuse/core": {
- "version": "11.3.0",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz",
- "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==",
- "license": "MIT",
- "dependencies": {
- "@types/web-bluetooth": "^0.0.20",
- "@vueuse/metadata": "11.3.0",
- "@vueuse/shared": "11.3.0",
- "vue-demi": ">=0.14.10"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/@vueuse/core/node_modules/@vueuse/shared": {
- "version": "11.3.0",
- "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz",
- "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==",
- "license": "MIT",
- "dependencies": {
- "vue-demi": ">=0.14.10"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/@vueuse/metadata": {
- "version": "11.3.0",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz",
- "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
"node_modules/@vueuse/shared": {
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
@@ -5759,13 +5145,16 @@
}
},
"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==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
+ "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
@@ -5922,9 +5311,9 @@
}
},
"node_modules/axios": {
- "version": "1.19.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
- "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
+ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
@@ -5958,20 +5347,43 @@
}
}
},
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
+ "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
"node_modules/babel-plugin-polyfill-corejs3": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-1.0.0.tgz",
- "integrity": "sha512-yIkslVjbmml2Xjb6XhFW7lISXHsqk6cesxTdDsXoMom4Lnb99DbD3OQbSOoM5Z+ASh8YXYaLAsRQrU2Jeh3Qig==",
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
+ "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
"license": "MIT",
"dependencies": {
- "@babel/helper-define-polyfill-provider": "^1.0.0",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
"core-js-compat": "^3.48.0"
},
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
+ "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8"
},
"peerDependencies": {
- "@babel/core": "^7.4.0 || ^8.0.0"
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/bail": {
@@ -5985,10 +5397,14 @@
}
},
"node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "license": "MIT"
+ "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/base-64": {
"version": "1.0.0",
@@ -6017,9 +5433,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.11.11",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.11.tgz",
- "integrity": "sha512-/yImnXwyTvgMkhgekLHok/Rx5vO6E0BmStWlSqKWMVm2a2ITuZ1Tn+9bgLS+gZRdZmWtd8nxuhHpdmCUOWsTQQ==",
+ "version": "2.11.20",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
+ "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -6061,6 +5477,48 @@
"readable-stream": "^3.4.0"
}
},
+ "node_modules/bl/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/bl/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/blurhash": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
@@ -6093,16 +5551,6 @@
"node": "20 || >=22"
}
},
- "node_modules/brace-expansion/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/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -6245,9 +5693,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.7",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
- "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+ "version": "4.28.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+ "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"funding": [
{
"type": "opencollective",
@@ -6264,11 +5712,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.44",
- "caniuse-lite": "^1.0.30001806",
- "electron-to-chromium": "^1.5.393",
- "node-releases": "^2.0.51",
- "update-browserslist-db": "^1.2.3"
+ "baseline-browser-mapping": "^2.11.12",
+ "caniuse-lite": "^1.0.30001809",
+ "electron-to-chromium": "^1.5.402",
+ "node-releases": "^2.0.53",
+ "update-browserslist-db": "^1.3.0"
},
"bin": {
"browserslist": "cli.js"
@@ -6278,10 +5726,9 @@
}
},
"node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
@@ -6297,10 +5744,9 @@
}
],
"license": "MIT",
- "optional": true,
"dependencies": {
"base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
+ "ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
@@ -6353,6 +5799,48 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/cacache/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/cacache/node_modules/brace-expansion": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/cacache/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -6361,6 +5849,41 @@
"license": "ISC",
"optional": true
},
+ "node_modules/cacache/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "optional": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/cacheable": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
@@ -6449,9 +5972,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001806",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
- "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "version": "1.0.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"funding": [
{
"type": "opencollective",
@@ -6495,6 +6018,19 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chalk/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/character-entities": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
@@ -6667,9 +6203,9 @@
"license": "MIT"
},
"node_modules/colord": {
- "version": "2.9.3",
- "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
- "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz",
+ "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==",
"dev": true,
"license": "MIT"
},
@@ -6750,6 +6286,21 @@
"integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==",
"license": "MIT"
},
+ "node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -6757,11 +6308,14 @@
"license": "MIT"
},
"node_modules/core-js": {
- "version": "3.49.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
- "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+ "version": "3.50.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
+ "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
"hasInstallScript": true,
"license": "MIT",
+ "engines": {
+ "node": "*"
+ },
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -6926,9 +6480,9 @@
}
},
"node_modules/css-loader": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz",
- "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==",
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.5.tgz",
+ "integrity": "sha512-Q7iAfQkU2twNBryKX/vGAlE+GAmkF7quhSzAGNK8fBimxk3+tqg245rH52UPb9dioBolBc8rP0ihmHBaDTJQBA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6961,6 +6515,19 @@
}
}
},
+ "node_modules/css-loader/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/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -7472,10 +7039,23 @@
"node": ">=20"
}
},
+ "node_modules/editorconfig/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/electron-to-chromium": {
- "version": "1.5.399",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz",
- "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==",
+ "version": "1.5.416",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz",
+ "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==",
"license": "ISC"
},
"node_modules/elliptic": {
@@ -7513,21 +7093,11 @@
}
},
"node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "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",
- "optional": true
- },
- "node_modules/empathic": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz",
- "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==",
- "license": "MIT",
- "engines": {
- "node": ">=14"
- }
+ "license": "MIT"
},
"node_modules/encoding": {
"version": "0.1.13",
@@ -7637,9 +7207,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
- "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
"dev": true,
"license": "MIT"
},
@@ -7758,13 +7328,13 @@
}
},
"node_modules/eslint-config-flat-gitignore": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-2.3.0.tgz",
- "integrity": "sha512-bg4ZLGgoARg1naWfsINUUb/52Ksw/K22K+T16D38Y8v+/sGwwIYrGvH/JBjOin+RQtxxC9tzNNiy4shnGtGyyQ==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-2.4.0.tgz",
+ "integrity": "sha512-JhYC+V7qEDiCDsbJcVP8fxNc62fk4+i91YLP/YvmVHlkaQ8Xo99olYN8MO5COdxl7onGlsrfZVONe5NYsZP1UA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint/compat": "^2.0.3"
+ "@eslint/compat": "^2.1.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -7831,6 +7401,19 @@
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
}
},
+ "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/eslint-plugin-jsdoc/node_modules/espree": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
@@ -7849,6 +7432,19 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint-plugin-jsdoc/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/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-5.0.0.tgz",
@@ -7877,108 +7473,6 @@
"eslint": "^8.45.0 || ^9.0.0 || ^10.0.0"
}
},
- "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/scope-manager": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
- "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/types": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
- "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
- "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/project-service": "8.67.0",
- "@typescript-eslint/tsconfig-utils": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
- "debug": "^4.4.3",
- "minimatch": "^10.2.2",
- "semver": "^7.7.3",
- "tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
- "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
- "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "eslint-visitor-keys": "^5.0.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
"node_modules/eslint-plugin-vue": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz",
@@ -8011,14 +7505,46 @@
}
}
},
+ "node_modules/eslint-plugin-vue/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/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/eslint-visitor-keys": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
- "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -8041,18 +7567,12 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/eslint/node_modules/eslint-scope": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
- "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "@types/esrecurse": "^4.3.1",
- "@types/estree": "^1.0.8",
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
+ "license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
@@ -8078,16 +7598,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -8113,19 +7623,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/espree/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -8139,16 +7636,6 @@
"node": ">=0.10"
}
},
- "node_modules/esquery/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -8162,7 +7649,7 @@
"node": ">=4.0"
}
},
- "node_modules/esrecurse/node_modules/estraverse": {
+ "node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
@@ -8313,9 +7800,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz",
+ "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==",
"funding": [
{
"type": "github",
@@ -8329,9 +7816,25 @@
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-builder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz",
- "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz",
+ "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.6.2",
+ "xml-naming": "^0.3.0"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.11.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz",
+ "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==",
"funding": [
{
"type": "github",
@@ -8340,8 +7843,15 @@
],
"license": "MIT",
"dependencies": {
+ "@nodable/entities": "^3.0.0",
+ "fast-xml-builder": "^1.2.0",
+ "is-unsafe": "^2.0.0",
"path-expression-matcher": "^1.6.2",
+ "strnum": "^2.4.2",
"xml-naming": "^0.3.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
}
},
"node_modules/fastest-levenshtein": {
@@ -8355,9 +7865,9 @@
}
},
"node_modules/fastq": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
- "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "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": {
@@ -8407,28 +7917,14 @@
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/file-entry-cache/node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
+ "flat-cache": "^4.0.0"
},
"engines": {
- "node": ">=16"
+ "node": ">=16.0.0"
}
},
"node_modules/file-uri-to-path": {
@@ -8479,15 +7975,17 @@
}
},
"node_modules/flat-cache": {
- "version": "6.1.23",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
- "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "cacheable": "^2.5.0",
- "flatted": "^3.4.2",
- "hookified": "^1.15.0"
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
}
},
"node_modules/flatted": {
@@ -8748,23 +8246,18 @@
"optional": true
},
"node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
- "license": "ISC",
- "optional": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -8783,34 +8276,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
- "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "brace-expansion": "^2.0.2"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/global-modules": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
@@ -8852,10 +8317,23 @@
"which": "bin/which"
}
},
+ "node_modules/globals": {
+ "version": "17.11.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz",
+ "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globby": {
- "version": "16.2.3",
- "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.3.tgz",
- "integrity": "sha512-VZX7TV7jmd/pn71vdnLKtgwy1IWqc3KjI9x1/UtPkwoKk5fKrNLY30ltDe3cAM5xruIN7YuuaulFt133jRrKZg==",
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.4.tgz",
+ "integrity": "sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8863,6 +8341,7 @@
"fast-glob": "^3.3.3",
"ignore": "^7.0.5",
"is-path-inside": "^4.0.0",
+ "micromatch": "^4.0.8",
"slash": "^5.1.0",
"unicorn-magic": "^0.4.0"
},
@@ -8874,9 +8353,9 @@
}
},
"node_modules/globby/node_modules/ignore": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
- "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "version": "7.0.7",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.7.tgz",
+ "integrity": "sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9099,9 +8578,9 @@
}
},
"node_modules/highlight.js": {
- "version": "11.11.1",
- "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
- "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "version": "11.11.2",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.2.tgz",
+ "integrity": "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
@@ -9144,6 +8623,16 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/hot-patcher": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz",
@@ -9371,6 +8860,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
"integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "dev": true,
"license": "MIT",
"funding": {
"type": "github",
@@ -9417,9 +8907,9 @@
}
},
"node_modules/ip-address": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
- "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
+ "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -9508,7 +8998,6 @@
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
@@ -9631,10 +9120,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-plain-object": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
- "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz",
+ "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9690,9 +9191,9 @@
}
},
"node_modules/is-unsafe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz",
- "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz",
+ "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==",
"funding": [
{
"type": "github",
@@ -9756,22 +9257,6 @@
"node": ">= 10.13.0"
}
},
- "node_modules/jest-worker/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
"node_modules/js-beautify": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz",
@@ -9794,41 +9279,6 @@
"node": ">=14"
}
},
- "node_modules/js-beautify/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/js-beautify/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/js-cookie": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
@@ -9840,13 +9290,12 @@
"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.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
- "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+ "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": [
{
@@ -10097,12 +9546,12 @@
}
},
"node_modules/lru-cache": {
- "version": "11.5.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
- "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
}
},
"node_modules/magic-string": {
@@ -10915,6 +10364,19 @@
"node": ">=8.6"
}
},
+ "node_modules/micromatch/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/miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@@ -11300,9 +10762,9 @@
"optional": true
},
"node_modules/nanoid": {
- "version": "3.3.17",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
- "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
@@ -11375,14 +10837,21 @@
"optional": true
},
"node_modules/negotiator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
"dev": true,
"license": "MIT",
"optional": true,
+ "dependencies": {
+ "content-type": "^2.1.0"
+ },
"engines": {
- "node": ">= 0.6"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/neo-async": {
@@ -11399,9 +10868,9 @@
"license": "MIT"
},
"node_modules/node-abi": {
- "version": "3.94.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
- "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
+ "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,
@@ -11412,6 +10881,20 @@
"node": ">=10"
}
},
+ "node_modules/node-abi/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",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -11523,6 +11006,20 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/node-gyp/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",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-gyp/node_modules/which": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
@@ -11572,56 +11069,16 @@
"vm-browserify": "^1.1.2"
},
"engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "webpack": ">=5"
- }
- },
- "node_modules/node-polyfill-webpack-plugin/node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "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": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
- "node_modules/node-polyfill-webpack-plugin/node_modules/readable-stream": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
- "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
- "license": "MIT",
- "dependencies": {
- "abort-controller": "^3.0.0",
- "buffer": "^6.0.3",
- "events": "^3.3.0",
- "process": "^0.11.10",
- "string_decoder": "^1.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "webpack": ">=5"
}
},
"node_modules/node-releases": {
- "version": "2.0.51",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
- "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "version": "2.0.54",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
+ "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -11658,6 +11115,19 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/normalize-package-data/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/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -11751,19 +11221,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/obug": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
- "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -11836,9 +11293,9 @@
}
},
"node_modules/p-map": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz",
- "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==",
+ "version": "7.0.7",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.7.tgz",
+ "integrity": "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -12042,7 +11499,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
"license": "MIT"
},
"node_modules/path-posix": {
@@ -12052,30 +11508,31 @@
"license": "ISC"
},
"node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "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",
- "optional": true,
"dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
},
"engines": {
- "node": ">=16 || 14 >=14.18"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/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==",
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"dev": true,
- "license": "ISC",
- "optional": true
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
},
"node_modules/pathe": {
"version": "2.0.3",
@@ -12113,13 +11570,12 @@
"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,
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -12147,6 +11603,12 @@
}
}
},
+ "node_modules/pinia/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -12269,9 +11731,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.25",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
- "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
@@ -12288,7 +11750,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.16",
+ "nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -12546,6 +12008,41 @@
}
}
},
+ "node_modules/prettier-plugin-packagejson/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/prettier-plugin-packagejson/node_modules/sort-package-json": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.6.0.tgz",
+ "integrity": "sha512-fyJsPLhWvY7u2KsKPZn1PixbXp+1m7V8NWqU8CvgFRbMEX41Ffw1kD8n0CfJiGoaSfoAvbrqRRl/DcHO8omQOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-indent": "^7.0.2",
+ "detect-newline": "^4.0.1",
+ "git-hooks-list": "^4.1.1",
+ "is-plain-obj": "^4.1.0",
+ "semver": "^7.7.3",
+ "sort-object-keys": "^2.0.1",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "sort-package-json": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/proc-log": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
@@ -12598,9 +12095,9 @@
}
},
"node_modules/prosemirror-commands": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
- "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
+ "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
@@ -12670,9 +12167,9 @@
}
},
"node_modules/prosemirror-view": {
- "version": "1.42.2",
- "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz",
- "integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==",
+ "version": "1.42.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz",
+ "integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.25.8",
@@ -12758,9 +12255,9 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.15.3",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
- "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
@@ -12883,35 +12380,26 @@
"rc": "cli.js"
}
},
- "node_modules/rc/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/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
},
"engines": {
- "node": ">= 6"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/readdirp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
- "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
@@ -13128,7 +12616,6 @@
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -13379,9 +12866,9 @@
}
},
"node_modules/sass-loader": {
- "version": "17.0.0",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-17.0.0.tgz",
- "integrity": "sha512-0Ybm8ohBQ9LcrycVrFQp/KQBNX5a3Wda9/smS0mE/xLffzEnwvV8nykOzrbiSWNzTE3IB/jiXx8O4QmDPG2+Gw==",
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-17.0.1.tgz",
+ "integrity": "sha512-pgJMwCuLjVTSIWsv/2luVRXKlCeaViUGcgSe8dx95zMG4hUwqIMFmjbhq29ypFLflJU0GyiJZeLM9iI1n3KYAA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13477,15 +12964,12 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
}
},
"node_modules/set-function-length": {
@@ -13809,9 +13293,9 @@
"license": "MIT"
},
"node_modules/sort-package-json": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.6.0.tgz",
- "integrity": "sha512-fyJsPLhWvY7u2KsKPZn1PixbXp+1m7V8NWqU8CvgFRbMEX41Ffw1kD8n0CfJiGoaSfoAvbrqRRl/DcHO8omQOQ==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-4.0.0.tgz",
+ "integrity": "sha512-6aYOlYI9AWioZ+rzu+4zKLmoFqJP0/fHDxrd7X04yqEibikY+5YVF0EYlyGn4v6X2PJY7yAUWV7oeP+i5rOm/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13827,20 +13311,20 @@
"sort-package-json": "cli.js"
},
"engines": {
- "node": ">=20"
+ "node": ">=22"
}
},
- "node_modules/sort-package-json/node_modules/is-plain-obj": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "node_modules/sort-package-json/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": "MIT",
- "engines": {
- "node": ">=12"
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/sortablejs": {
@@ -13972,6 +13456,20 @@
"readable-stream": "^3.5.0"
}
},
+ "node_modules/stream-browserify/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==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/stream-http": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz",
@@ -13984,6 +13482,20 @@
"xtend": "^4.0.2"
}
},
+ "node_modules/stream-http/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==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -13994,19 +13506,17 @@
}
},
"node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
+ "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true,
"license": "MIT",
- "optional": true,
"dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
+ "get-east-asian-width": "^1.5.0",
+ "strip-ansi": "^7.1.2"
},
"engines": {
- "node": ">=12"
+ "node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -14029,43 +13539,29 @@
"node": ">=8"
}
},
- "node_modules/string-width-cjs/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",
- "optional": true
- },
- "node_modules/string-width/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/string-width-cjs/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",
"optional": true,
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "node": ">=8"
}
},
- "node_modules/string-width/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==",
+ "node_modules/string-width-cjs/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",
"optional": true,
"dependencies": {
- "ansi-regex": "^6.2.2"
+ "ansi-regex": "^5.0.1"
},
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "node": ">=8"
}
},
"node_modules/stringify-entities": {
@@ -14083,16 +13579,19 @@
}
},
"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==",
+ "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": "^5.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
- "node": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
@@ -14107,7 +13606,29 @@
"ansi-regex": "^5.0.1"
},
"engines": {
- "node": ">=8"
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/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",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/striptags": {
@@ -14116,6 +13637,21 @@
"integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==",
"license": "MIT"
},
+ "node_modules/strnum": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz",
+ "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anynum": "^1.0.1"
+ }
+ },
"node_modules/strtok3": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
@@ -14319,6 +13855,19 @@
"stylelint": ">=14.0.0"
}
},
+ "node_modules/stylelint-config-recommended-vue/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/stylelint-scss": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-7.2.0.tgz",
@@ -14358,19 +13907,6 @@
"stylelint": ">= 11 < 18"
}
},
- "node_modules/stylelint/node_modules/ansi-regex": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
- "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
"node_modules/stylelint/node_modules/file-entry-cache": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
@@ -14381,60 +13917,42 @@
"flat-cache": "^6.1.23"
}
},
- "node_modules/stylelint/node_modules/ignore": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
- "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/stylelint/node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
+ "node_modules/stylelint/node_modules/flat-cache": {
+ "version": "6.1.23",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
+ "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "cacheable": "^2.5.0",
+ "flatted": "^3.4.2",
+ "hookified": "^1.15.0"
}
},
- "node_modules/stylelint/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==",
+ "node_modules/stylelint/node_modules/ignore": {
+ "version": "7.0.7",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.7.tgz",
+ "integrity": "sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "node": ">= 4"
}
},
"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==",
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/supports-hyperlinks": {
@@ -14484,7 +14002,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -14522,12 +14039,15 @@
"node": ">=10.0.0"
}
},
- "node_modules/table/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==",
+ "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"
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
},
"node_modules/table/node_modules/string-width": {
"version": "4.2.3",
@@ -14544,6 +14064,19 @@
"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/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
@@ -14616,6 +14149,22 @@
"node": ">=6"
}
},
+ "node_modules/tar-stream/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/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -14742,18 +14291,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -14850,249 +14387,86 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
"integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==",
- "license": "MIT"
- },
- "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-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/type-fest": {
- "version": "4.41.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
- "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/typed-array-buffer": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
- "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "is-typed-array": "^1.1.14"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "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/typescript-eslint": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz",
- "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "8.67.0",
- "@typescript-eslint/parser": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0",
- "@typescript-eslint/utils": "8.67.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz",
- "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/type-utils": "8.67.0",
- "@typescript-eslint/utils": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
- "ignore": "^7.0.5",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.67.0",
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz",
- "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
- "debug": "^4.4.3"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
+ "license": "MIT"
},
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
- "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
+ "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": "MIT",
+ "license": "Apache-2.0",
+ "optional": true,
"dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0"
+ "safe-buffer": "^5.0.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": "*"
}
},
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz",
- "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==",
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0",
- "@typescript-eslint/utils": "8.67.0",
- "debug": "^4.4.3",
- "ts-api-utils": "^2.5.0"
+ "prelude-ls": "^1.2.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "node": ">= 0.8.0"
}
},
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
- "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
- "dev": true,
- "license": "MIT",
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">=16"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
- "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
- "dev": true,
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.67.0",
- "@typescript-eslint/tsconfig-utils": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/visitor-keys": "8.67.0",
- "debug": "^4.4.3",
- "minimatch": "^10.2.2",
- "semver": "^7.7.3",
- "tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.5.0"
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": ">= 0.4"
+ }
+ },
+ "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"
},
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "engines": {
+ "node": ">=14.17"
}
},
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
- "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
+ "node_modules/typescript-eslint": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz",
+ "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.67.0",
- "@typescript-eslint/types": "8.67.0",
- "@typescript-eslint/typescript-estree": "8.67.0"
+ "@typescript-eslint/eslint-plugin": "8.68.0",
+ "@typescript-eslint/parser": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/utils": "8.68.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -15106,15 +14480,18 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.67.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
- "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz",
+ "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.67.0",
- "eslint-visitor-keys": "^5.0.0"
+ "@typescript-eslint/scope-manager": "8.68.0",
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/visitor-keys": "8.68.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -15122,16 +14499,10 @@
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/typescript-eslint/node_modules/ignore": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
- "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typescript-event-target": {
@@ -15225,18 +14596,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unified/node_modules/is-plain-obj": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/unique-filename": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
@@ -15430,34 +14789,10 @@
"url": "https://github.com/sponsors/sxzz"
}
},
- "node_modules/unplugin-utils/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/unplugin/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
+ "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"funding": [
{
"type": "opencollective",
@@ -15629,16 +14964,13 @@
}
},
"node_modules/vue-codemirror6": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.6.1.tgz",
- "integrity": "sha512-0Ue6nWy055UKP/6LkT0I7nW5SRSFJ+3mokwBdHePQsj11T02mrg3A0jtHyBqH23nRHQFxd3NKw63sZJ1ygo5dA==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.6.2.tgz",
+ "integrity": "sha512-U7XdFCKbLDuCGrfH+bCFbWbSPBAhEFrBYLBOrtimB/yvMtmslwabZzO4iLqutA8UBb37SQ6B+Cwj1QmCDuGxng==",
"license": "MIT",
"dependencies": {
"vue-demi": "latest"
},
- "engines": {
- "node": "^22.18.0 || >=24.12.0"
- },
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@@ -15653,9 +14985,9 @@
}
},
"node_modules/vue-component-type-helpers": {
- "version": "3.3.9",
- "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.9.tgz",
- "integrity": "sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz",
+ "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==",
"dev": true,
"license": "MIT"
},
@@ -15709,33 +15041,17 @@
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
}
},
- "node_modules/vue-eslint-parser/node_modules/eslint-scope": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
- "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "node_modules/vue-eslint-parser/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": "BSD-2-Clause",
- "dependencies": {
- "@types/esrecurse": "^4.3.1",
- "@types/estree": "^1.0.8",
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
},
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/vue-eslint-parser/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
"engines": {
- "node": ">=4.0"
+ "node": ">=10"
}
},
"node_modules/vue-loader": {
@@ -15791,6 +15107,12 @@
"vue": "^3.5.0"
}
},
+ "node_modules/vue-router/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
"node_modules/vue3-apexcharts": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.8.0.tgz",
@@ -15866,6 +15188,12 @@
"node": ">=14"
}
},
+ "node_modules/webdav/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
"node_modules/webdav/node_modules/brace-expansion": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
@@ -15887,29 +15215,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/webdav/node_modules/fast-xml-parser": {
- "version": "5.10.1",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz",
- "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/NaturalIntelligence"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@nodable/entities": "^3.0.0",
- "fast-xml-builder": "^1.2.0",
- "is-unsafe": "^2.0.0",
- "path-expression-matcher": "^1.6.2",
- "strnum": "^2.4.1",
- "xml-naming": "^0.3.0"
- },
- "bin": {
- "fxparser": "src/cli/cli.js"
- }
- },
"node_modules/webdav/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -15925,25 +15230,10 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/webdav/node_modules/strnum": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz",
- "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/NaturalIntelligence"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "anynum": "^1.0.1"
- }
- },
"node_modules/webpack": {
- "version": "5.110.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.1.tgz",
- "integrity": "sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==",
+ "version": "5.110.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.2.tgz",
+ "integrity": "sha512-TciLrfM7zgEjqGdY851HkirDsSPQgTFsWQpl9oHqMAMYsHhEC0bKjscvjpnz+pzx10hLC8qISApGrsnrCP4UtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16170,13 +15460,16 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/wrap-ansi-cjs/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==",
+ "node_modules/wrap-ansi-cjs/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",
- "optional": true
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
@@ -16194,18 +15487,18 @@
"node": ">=8"
}
},
- "node_modules/wrap-ansi/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/wrap-ansi-cjs/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",
"optional": true,
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "engines": {
+ "node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
@@ -16222,21 +15515,31 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/wrap-ansi/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==",
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "ansi-regex": "^6.2.2"
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrappy": {
@@ -16310,20 +15613,11 @@
"node": ">=0.4"
}
},
- "node_modules/yaml": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
- "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
diff --git a/package.json b/package.json
index b1e0b5a0..66b7889a 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"build": "NODE_ENV=production webpack --config webpack.config.js --progress",
"dev": "NODE_ENV=development webpack --config webpack.config.js --progress",
"watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch",
- "lint": "eslint src",
+ "lint": "eslint src tests scripts",
"lint-fix": "npm run lint -- --fix",
"test:l10n": "node tests/l10n/check-l10n.js",
"stylelint": "stylelint \"src/**/*.{vue,scss,css}\"",
@@ -37,11 +37,11 @@
"extends @nextcloud/browserslist-config"
],
"dependencies": {
- "@babel/core": "^8.0.1",
- "@babel/preset-env": "^8.0.2",
- "@conduction/nextcloud-vue": "^2.24.3",
+ "@babel/core": "^7.22.9",
+ "@babel/preset-env": "^7.29.7",
+ "@conduction/nextcloud-vue": "^2.37.0",
"@nextcloud/auth": "^2.5.0",
- "@nextcloud/axios": "~2.5.2",
+ "@nextcloud/axios": "~2.6.0",
"@nextcloud/dialogs": "^7.5.0",
"@nextcloud/event-bus": "^3.3.3",
"@nextcloud/initial-state": "^3.0.0",
@@ -63,8 +63,8 @@
"overrides": {
"vue3-apexcharts": "1.8.0",
"libxmljs2": "^0.37.0",
- "@nextcloud/axios": "~2.5.2",
- "@nextcloud/vue": "^9.9.0",
+ "@nextcloud/axios": "$@nextcloud/axios",
+ "@nextcloud/vue": "$@nextcloud/vue",
"@nextcloud/dialogs": {
"@vueuse/core": "11.3.0"
},
@@ -80,12 +80,12 @@
"@nextcloud/stylelint-config": "^3.2.2",
"@nextcloud/webpack-vue-config": "^7.0.4",
"@playwright/test": "^1.61.1",
- "@typescript-eslint/parser": "^8.68.0",
+ "@typescript-eslint/parser": "^8.69.0",
"@vue/compiler-sfc": "^3.5.42",
"@vue/test-utils": "^2.5.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "css-loader": "~7.1.1",
+ "css-loader": "~7.1.5",
"eslint": "^10.9.1",
"eslint-config-prettier": "^10.1.8",
"postcss-html": "^2.0.0",
diff --git a/scripts/build-l10n-js.js b/scripts/build-l10n-js.js
index 176ef519..335b08d2 100644
--- a/scripts/build-l10n-js.js
+++ b/scripts/build-l10n-js.js
@@ -101,6 +101,9 @@ function renderJs(id, translations, pluralForm) {
].join('\n')
}
+/**
+ *
+ */
function main() {
const check = process.argv.includes('--check')
const id = appId()
diff --git a/scripts/check-integration-parity.js b/scripts/check-integration-parity.js
index 0c44a732..9635810a 100755
--- a/scripts/check-integration-parity.js
+++ b/scripts/check-integration-parity.js
@@ -10,11 +10,11 @@
* ---------------------------------------
* A leaf has TWO faces (ADR-019 AD-11/AD-13, ADR-066 decisions 4 and 7):
*
- * * a SERVER face — either a `LeafDescriptor` contributed through
+ * a SERVER face — either a `LeafDescriptor` contributed through
* `RegisterLeafProvidersEvent`, or an `IntegrationProvider` registered on
* OpenRegister's `IntegrationRegistry`. This face is what the
* `openregister.integrations.leaves` capability advertises.
- * * a JS face — a `registerIntegration({ id, … })` call that mounts the
+ * a JS face — a `registerIntegration({ id, … })` call that mounts the
* render pair on `window.OCA.OpenRegister.integrations`.
*
* The two are correlated ONLY by a shared `id`. Nothing at runtime notices when
@@ -132,7 +132,7 @@ function collectFiles(root, test, maxDepth = 10) {
let entries
try {
entries = fs.readdirSync(dir, { withFileTypes: true })
- } catch (e) {
+ } catch {
return
}
for (const ent of entries) {
@@ -289,19 +289,15 @@ function resolvePhp(expr, localConsts, globalConsts) {
}
m = /^(?:self|static)::([A-Z0-9_]+)$/.exec(e)
if (m !== null) {
- return Object.prototype.hasOwnProperty.call(localConsts, m[1])
- ? localConsts[m[1]]
- : null
+ return Object.hasOwn(localConsts, m[1]) ? localConsts[m[1]] : null
}
m = /^([A-Za-z_][A-Za-z0-9_]*)::([A-Z0-9_]+)$/.exec(e)
if (m !== null) {
const key = `${m[1]}::${m[2]}`
- if (Object.prototype.hasOwnProperty.call(FOREIGN_CONSTANTS, key) === true) {
+ if (Object.hasOwn(FOREIGN_CONSTANTS, key) === true) {
return FOREIGN_CONSTANTS[key]
}
- return Object.prototype.hasOwnProperty.call(globalConsts, key)
- ? globalConsts[key]
- : null
+ return Object.hasOwn(globalConsts, key) ? globalConsts[key] : null
}
if (e.startsWith('[') === true) {
const members = splitTopLevel(balanced(e, 0))
@@ -351,8 +347,8 @@ function phpLocalConsts(src) {
*
* Two shapes count as a server face, because both are how a leaf reaches the
* `openregister.integrations.leaves` capability:
- * * `new LeafDescriptor(…)` — the ADR-066 collect-event contribution;
- * * an `IntegrationProvider` (extends `AbstractIntegrationProvider` or
+ * `new LeafDescriptor(…)` — the ADR-066 collect-event contribution;
+ * an `IntegrationProvider` (extends `AbstractIntegrationProvider` or
* implements `IntegrationProviderInterface`) whose `getId()` returns a
* literal — the `IntegrationRegistry::addProvider()` path.
*
@@ -371,7 +367,7 @@ function collectServerFaces() {
let src
try {
src = fs.readFileSync(file, 'utf8')
- } catch (e) {
+ } catch {
continue
}
sources.set(file, src)
@@ -504,7 +500,7 @@ function resolveJs(expr, locals) {
}
if (
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(e) === true
- && Object.prototype.hasOwnProperty.call(locals, e) === true
+ && Object.hasOwn(locals, e) === true
) {
return locals[e]
}
@@ -588,7 +584,7 @@ function collectJsRegistrations() {
let src
try {
src = fs.readFileSync(file, 'utf8')
- } catch (e) {
+ } catch {
continue
}
// BOTH SUPPORTED REGISTRATION APIs, NOT ONE.
@@ -737,7 +733,7 @@ function collectOrSchemas() {
let doc
try {
doc = JSON.parse(fs.readFileSync(file, 'utf8'))
- } catch (e) {
+ } catch {
continue
}
const declared = doc && doc.components && doc.components.schemas
@@ -959,7 +955,7 @@ function main() {
continue
}
counts.R6++
- if (Object.prototype.hasOwnProperty.call(schemas, slug) === false) {
+ if (Object.hasOwn(schemas, slug) === false) {
failures.push(
`✗ [R6 offlineConfig] leaf offlineConfig.${key} = "${slug}" (${r.file}) names a `
+ `schema this repo does not declare in lib/Settings/** — the leaf would query a `
@@ -996,12 +992,10 @@ function main() {
.map(([rule, n]) => `${rule}:${n}`)
.join(' ')
if (failures.length === 0) {
- // eslint-disable-next-line no-console
console.log(
`✓ integration parity: ${scope} — all rules pass (assertions run per rule: ${perRule})`,
)
if (Object.values(counts).every((n) => n === 0) === true) {
- // eslint-disable-next-line no-console
console.error(
'✗ integration parity: every rule had ZERO subject matter, yet gate-24 selected this '
+ 'repo as one that registers leaves. That contradiction means this checker failed to '
@@ -1014,15 +1008,14 @@ function main() {
// The header carries no `✗` on purpose: gate-24 counts violations by
// grepping `^✗` in this log, so every violation — and only a violation —
// starts a line with it.
- // eslint-disable-next-line no-console
+
console.error(
`integration parity gate FAILED — ${failures.length} violation(s) over ${scope}:`,
)
for (const f of failures) {
- // eslint-disable-next-line no-console
console.error(f)
}
- // eslint-disable-next-line no-console
+
console.error(`\nAssertions run per rule: ${perRule}`)
process.exit(1)
}
diff --git a/scripts/check-schema-l10n.js b/scripts/check-schema-l10n.js
index 8a860b33..3c4b2626 100644
--- a/scripts/check-schema-l10n.js
+++ b/scripts/check-schema-l10n.js
@@ -113,6 +113,9 @@ function collect(node, where, sink) {
for (const value of Object.values(node)) collect(value, where, sink)
}
+/**
+ *
+ */
function main() {
const update = process.argv.includes('--update')
const list = process.argv.includes('--list')
diff --git a/scripts/generate-opengemeenten-icons.mjs b/scripts/generate-opengemeenten-icons.mjs
index 45d53def..e43ab555 100644
--- a/scripts/generate-opengemeenten-icons.mjs
+++ b/scripts/generate-opengemeenten-icons.mjs
@@ -35,11 +35,19 @@
*/
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { join, dirname } from 'node:path'
+import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
-const ESM_DIR = join(HERE, '..', 'node_modules', '@opengemeenten', 'iconset-web-component', 'dist', 'esm')
+const ESM_DIR = join(
+ HERE,
+ '..',
+ 'node_modules',
+ '@opengemeenten',
+ 'iconset-web-component',
+ 'dist',
+ 'esm',
+)
const OUT = join(HERE, '..', 'src', 'icons', 'openGemeentenIcons.js')
const ENTRY = /^opengemeenten-icon-(.+)\.entry\.js$/
@@ -100,5 +108,7 @@ writeFileSync(OUT, header + JSON.stringify(icons, null, '\t') + '\n')
process.stdout.write(`Wrote ${icons.length} OpenGemeenten icons to ${OUT}\n`)
if (skipped.length > 0) {
- process.stdout.write(`Skipped ${skipped.length} (no single flat path): ${skipped.join(', ')}\n`)
+ process.stdout.write(
+ `Skipped ${skipped.length} (no single flat path): ${skipped.join(', ')}\n`,
+ )
}
diff --git a/src/icons.js b/src/icons.js
index 762cfc08..90b385f7 100644
--- a/src/icons.js
+++ b/src/icons.js
@@ -17,9 +17,11 @@ import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
import AlertOctagonOutline from 'vue-material-design-icons/AlertOctagonOutline.vue'
import BookOpenVariantOutline from 'vue-material-design-icons/BookOpenVariantOutline.vue'
import Brain from 'vue-material-design-icons/Brain.vue'
+import CalculatorVariantOutline from 'vue-material-design-icons/CalculatorVariantOutline.vue'
import Calendar from 'vue-material-design-icons/Calendar.vue'
import CashMultiple from 'vue-material-design-icons/CashMultiple.vue'
import ChartBar from 'vue-material-design-icons/ChartBar.vue'
+import ChartBoxOutline from 'vue-material-design-icons/ChartBoxOutline.vue'
import ChartLine from 'vue-material-design-icons/ChartLine.vue'
import ChartTimelineVariant from 'vue-material-design-icons/ChartTimelineVariant.vue'
import ChatOutline from 'vue-material-design-icons/ChatOutline.vue'
@@ -81,9 +83,11 @@ export default {
AlertOctagonOutline,
BookOpenVariantOutline,
Brain,
+ CalculatorVariantOutline,
Calendar,
CashMultiple,
ChartBar,
+ ChartBoxOutline,
ChartLine,
ChartTimelineVariant,
ChatOutline,
diff --git a/src/main.js b/src/main.js
index f80070f0..b40721a4 100644
--- a/src/main.js
+++ b/src/main.js
@@ -129,8 +129,35 @@ function routesFromManifest(manifest) {
return routes
}
+/**
+ * The router base for THIS page load.
+ *
+ * ⚠️ `generateUrl('/apps/hermiq')` alone is not enough. Nextcloud serves the
+ * app under BOTH `/apps/hermiq/...` and `/index.php/apps/hermiq/...`, but
+ * `generateUrl()` returns only the form the instance is configured for. A
+ * visitor arriving on the other form — a bookmark, an emailed deep link, an
+ * integration that hardcodes `/index.php` — has a pathname the router cannot
+ * strip its base from. No route matches, the catch-all takes over, and they
+ * land on the dashboard with no error at all: the deep link is silently
+ * swallowed.
+ *
+ * Measured on a live instance for learniq, across all 282 of its routes:
+ * `/apps/learniq/courses` resolved to Courses, `/index.php/apps/learniq/courses`
+ * resolved to the dashboard. Every route behaved the same way, so this is not
+ * one broken page but every deep link in that URL form.
+ *
+ * Deriving the base from the pathname makes both forms resolve, because the
+ * base then always matches the URL the visitor actually arrived on.
+ *
+ * @return {string} The base path vue-router should strip from the URL.
+ */
+function routerBase() {
+ const match = window.location.pathname.match(/^(.*\/apps\/hermiq)(?:\/|$)/)
+ return match ? match[1] : generateUrl('/apps/hermiq')
+}
+
const router = createRouter({
- history: createWebHistory(generateUrl('/apps/hermiq')),
+ history: createWebHistory(routerBase()),
routes: routesFromManifest(bundledManifest),
})
diff --git a/src/manifest.json b/src/manifest.json
index 3b0abf90..ce7a1581 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -31,7 +31,8 @@
"label": "Store",
"icon": "StoreOutline",
"route": "Store",
- "order": 11
+ "order": 92,
+ "section": "footer"
},
{
"id": "AgentMemory",
@@ -111,7 +112,7 @@
{
"id": "Compliance",
"label": "Compliance",
- "icon": "Check",
+ "icon": "ClipboardCheckOutline",
"route": "Compliance",
"section": "settings",
"order": 30
@@ -125,12 +126,12 @@
"order": 96
},
{
- "id": "AiOversightMenu",
- "label": "AI oversight",
- "icon": "ShieldCheckOutline",
- "route": "AiOversight",
- "section": "settings",
- "order": 97
+ "id": "ReportsMenu",
+ "label": "Reports",
+ "icon": "ChartBoxOutline",
+ "route": "Reports",
+ "section": "footer",
+ "order": 95
}
],
"pages": [
@@ -750,11 +751,6 @@
"icon": "Package",
"handler": "navigate",
"route": "Store"
- },
- {
- "id": "dashboard-refresh",
- "type": "refresh",
- "label": "Refresh"
}
],
"widgets": [
@@ -1284,6 +1280,25 @@
"app": "hermiq"
}
},
+ {
+ "id": "Reports",
+ "route": "/reports",
+ "type": "reports",
+ "title": "Reports",
+ "config": {
+ "description": "Pick a report to open it.",
+ "cards": [
+ {
+ "id": "AiOversight",
+ "label": "AI oversight",
+ "description": "What the agents did, and what a human still has to answer for.",
+ "icon": "ShieldCheckOutline",
+ "route": "AiOversight"
+ }
+ ]
+ },
+ "_note": "ADR-112 / ADR-114 Decision 3. Nothing is invented: AI oversight already existed and was an entry in the SETTINGS foldout. That is where configuration lives (ADR-044 Decision 3), and oversight is not configuration — it is a reading of what the agents did, which is what a report is. The page is untouched and stays routable at /ai-oversight, and dossiq deep-links it as an ADR-110 integrations entry, so the route had to survive. Guardrail policy, the algorithm register and Compliance stay in the foldout: those ARE configuration."
+ },
{
"id": "AiOversight",
"route": "/ai-oversight",
@@ -1491,11 +1506,21 @@
},
{
"id": "demo-data",
+ "type": "choice",
+ "display": "cards",
+ "optionsSource": "datasets",
+ "configKey": "demo_dataset",
+ "title": "Load example data?",
+ "required": false,
+ "body": "Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install."
+ },
+ {
+ "id": "load-demo-data",
"type": "run-action",
- "action": "install-demo-data",
- "title": "Demo data (optional)",
+ "action": "load-demo-data",
+ "title": "Load the example data",
"required": false,
- "body": "Load a small example dataset of agents, skills and conversations, so the lists and detail pages show a working product straight away. The data is obviously sample data, it is safe to run more than once, and it can be removed afterwards. Skip this on a production install."
+ "body": "Loads what you picked. The data is obviously sample data, it is safe to run more than once, and you can delete it afterwards."
},
{
"id": "llm-endpoint",
diff --git a/tests/Stubs/Db/Flow.php b/tests/Stubs/Db/Flow.php
index 99383999..32c6addf 100644
--- a/tests/Stubs/Db/Flow.php
+++ b/tests/Stubs/Db/Flow.php
@@ -69,6 +69,8 @@
* @method void setOrganisation(?string $organisation)
* @method string|null getNotes()
* @method void setNotes(?string $notes)
+ * @method string|null getLifecycleStatus()
+ * @method void setLifecycleStatus(?string $lifecycleStatus)
* @method DateTime|null getCreated()
* @method void setCreated(?DateTime $created)
* @method DateTime|null getUpdated()
diff --git a/tests/Stubs/Db/FlowMapper.php b/tests/Stubs/Db/FlowMapper.php
index ff6c89cd..52fbba5d 100644
--- a/tests/Stubs/Db/FlowMapper.php
+++ b/tests/Stubs/Db/FlowMapper.php
@@ -11,8 +11,11 @@
*
* Deliberately NOT a full mirror. Every method declared here is one this stub
* promises hermiq may call, so keeping it to what is actually used keeps the
- * promise small — and the seed step is the only place hermiq touches OR's flow
- * store directly at all.
+ * promise small. The callers are the seed step and, since
+ * schedules-onto-engine-triggers, the ScheduleFlowBridge and the schedule
+ * dispatch node (`findByUuid()` and `delete()`, both with the real
+ * signatures: findByUuid throws DoesNotExistException, delete comes from
+ * QBMapper).
*
* @category Test
* @package OCA\OpenRegister\Db
@@ -74,4 +77,32 @@ public function insert(Flow $entity): Flow {
public function update(Flow $entity): Flow {
return $entity;
}//end update()
+
+ /**
+ * Find a flow by its public uuid.
+ *
+ * Faithful to the real class: an unknown uuid THROWS rather than answers
+ * an empty flow, so a test exercising the missing-mirror path sees the
+ * same shape production does.
+ *
+ * @param string $uuid The flow uuid.
+ *
+ * @return Flow The flow.
+ *
+ * @throws \OCP\AppFramework\Db\DoesNotExistException Always, on the stub.
+ */
+ public function findByUuid(string $uuid): Flow {
+ throw new \OCP\AppFramework\Db\DoesNotExistException('stub: no flow ' . $uuid);
+ }//end findByUuid()
+
+ /**
+ * Delete a flow. Provided by QBMapper on the real class.
+ *
+ * @param Flow $entity The flow to delete.
+ *
+ * @return Flow The deleted flow.
+ */
+ public function delete(Flow $entity): Flow {
+ return $entity;
+ }//end delete()
}//end class
diff --git a/tests/Stubs/Db/FlowRun.php b/tests/Stubs/Db/FlowRun.php
new file mode 100644
index 00000000..286cc0b9
--- /dev/null
+++ b/tests/Stubs/Db/FlowRun.php
@@ -0,0 +1,67 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Db;
+
+/**
+ * Minimal FlowRun stub with round-tripping magic accessors.
+ *
+ * @method string|null getUuid()
+ * @method void setUuid(?string $uuid)
+ * @method string|null getOrganisation()
+ * @method void setOrganisation(?string $organisation)
+ * @method string|null getRunAs()
+ * @method void setRunAs(?string $runAs)
+ * @method string|null getTriggeredBy()
+ * @method void setTriggeredBy(?string $triggeredBy)
+ */
+class FlowRun {
+
+ /**
+ * Values written through the magic setters.
+ *
+ * @var array
+ */
+ private array $magicValues = [];
+
+ /**
+ * Serve the magic accessors the real Entity provides via __call.
+ *
+ * @param string $name The accessor name.
+ * @param array $arguments The accessor arguments.
+ *
+ * @return mixed The property value, or null for a setter.
+ */
+ public function __call(string $name, array $arguments) {
+ if (str_starts_with($name, 'set') === true) {
+ $this->magicValues[lcfirst(substr($name, 3))] = ($arguments[0] ?? null);
+ return null;
+ }
+
+ if (str_starts_with($name, 'get') === true) {
+ return ($this->magicValues[lcfirst(substr($name, 3))] ?? null);
+ }
+
+ return null;
+ }//end __call()
+}//end class
diff --git a/tests/Stubs/Db/FlowRunMapper.php b/tests/Stubs/Db/FlowRunMapper.php
new file mode 100644
index 00000000..8019abc6
--- /dev/null
+++ b/tests/Stubs/Db/FlowRunMapper.php
@@ -0,0 +1,41 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Db;
+
+/**
+ * Minimal FlowRunMapper stub for standalone static analysis.
+ */
+class FlowRunMapper {
+ /**
+ * Find a run by its public uuid.
+ *
+ * @param string $uuid The run uuid.
+ *
+ * @return FlowRun The run.
+ *
+ * @throws \OCP\AppFramework\Db\DoesNotExistException Always, on the stub.
+ */
+ public function findByUuid(string $uuid): FlowRun {
+ throw new \OCP\AppFramework\Db\DoesNotExistException('stub: no run ' . $uuid);
+ }//end findByUuid()
+}//end class
diff --git a/tests/Stubs/Db/FlowVersion.php b/tests/Stubs/Db/FlowVersion.php
new file mode 100644
index 00000000..33a4c515
--- /dev/null
+++ b/tests/Stubs/Db/FlowVersion.php
@@ -0,0 +1,93 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Db;
+
+/**
+ * Minimal FlowVersion stub for standalone static analysis.
+ *
+ * @method string|null getFlowUuid()
+ * @method void setFlowUuid(?string $flowUuid)
+ * @method integer|null getVersion()
+ * @method void setVersion(?int $version)
+ * @method string|null getStatus()
+ * @method void setStatus(?string $status)
+ */
+class FlowVersion {
+
+ /**
+ * Lifecycle state of an editable head, matching the real entity.
+ *
+ * @var string
+ */
+ public const STATUS_DRAFT = 'draft';
+
+ /**
+ * Lifecycle state of the version the engine pins runs to.
+ *
+ * @var string
+ */
+ public const STATUS_PUBLISHED = 'published';
+
+ /**
+ * Lifecycle state of a version that backs no new runs.
+ *
+ * @var string
+ */
+ public const STATUS_DEPRECATED = 'deprecated';
+
+ /**
+ * Values written through the magic setters.
+ *
+ * @var array
+ */
+ private array $magicValues = [];
+
+ /**
+ * Serve the magic accessors the real Entity provides via __call.
+ *
+ * Round-trips every write, the same faithfulness rule as the Flow stub: a
+ * stub that discards writes hides exactly the class of bug these tests
+ * exist to catch.
+ *
+ * @param string $name The accessor name.
+ * @param array $arguments The accessor arguments.
+ *
+ * @return mixed The property value, or null for a setter.
+ */
+ public function __call(string $name, array $arguments) {
+ if (str_starts_with($name, 'set') === true) {
+ $this->magicValues[lcfirst(substr($name, 3))] = ($arguments[0] ?? null);
+ return null;
+ }
+
+ if (str_starts_with($name, 'get') === true) {
+ return ($this->magicValues[lcfirst(substr($name, 3))] ?? null);
+ }
+
+ return null;
+ }//end __call()
+}//end class
diff --git a/tests/Stubs/Db/RegisterMapper.php b/tests/Stubs/Db/RegisterMapper.php
index 540f87ff..0b52f3c6 100644
--- a/tests/Stubs/Db/RegisterMapper.php
+++ b/tests/Stubs/Db/RegisterMapper.php
@@ -29,4 +29,44 @@ class RegisterMapper {
public function find(string|int $id): Register {
return new Register();
}//end find()
+
+ /**
+ * List registers — mirrors the real mapper's signature
+ * (openregister lib/Db/RegisterMapper.php), used by the agentflow
+ * retirement prune to unlink a deleted schema id.
+ *
+ * @param int|null $limit Maximum rows.
+ * @param int|null $offset First row.
+ * @param array|null $filters Column filters.
+ * @param array|null $searchConditions Search conditions.
+ * @param array|null $searchParams Search parameters.
+ * @param bool $_rbac Whether RBAC applies.
+ * @param bool $_multitenancy Whether multi-tenancy applies.
+ *
+ * @return Register[] The registers.
+ */
+ public function findAll(
+ ?int $limit = null,
+ ?int $offset = null,
+ ?array $filters = [],
+ ?array $searchConditions = [],
+ ?array $searchParams = [],
+ bool $_rbac = true,
+ bool $_multitenancy = true,
+ ): array {
+ return [];
+ }//end findAll()
+
+ /**
+ * Persist a changed register — the real mapper inherits
+ * `QBMapper::update(Entity)`; the stub narrows to the one entity hermiq
+ * ever passes.
+ *
+ * @param Register $register The register to update.
+ *
+ * @return Register The updated register.
+ */
+ public function update(Register $register): Register {
+ return $register;
+ }//end update()
}//end class
diff --git a/tests/Stubs/Db/SchemaMapper.php b/tests/Stubs/Db/SchemaMapper.php
index 1f273d71..98dc0644 100644
--- a/tests/Stubs/Db/SchemaMapper.php
+++ b/tests/Stubs/Db/SchemaMapper.php
@@ -45,4 +45,18 @@ public function find(
): Schema {
return new Schema();
}//end find()
+
+ /**
+ * Resolve a schema by slug within one app's ownership — mirrors the real
+ * mapper's signature (openregister lib/Db/SchemaMapper.php), used by the
+ * agentflow retirement prune.
+ *
+ * @param string $slug The schema slug.
+ * @param string $application The owning app id.
+ *
+ * @return Schema|null The schema, or null when the app owns no such slug.
+ */
+ public function findByApplicationAndSlug(string $slug, string $application): ?Schema {
+ return null;
+ }//end findByApplicationAndSlug()
}//end class
diff --git a/tests/Stubs/Db/Task.php b/tests/Stubs/Db/Task.php
new file mode 100644
index 00000000..85c1d0ad
--- /dev/null
+++ b/tests/Stubs/Db/Task.php
@@ -0,0 +1,256 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Db;
+
+/**
+ * Minimal Task stub for standalone unit runs.
+ *
+ * @SuppressWarnings(PHPMD.TooManyPublicMethods) A value stub is accessors by definition.
+ */
+class Task {
+
+ /**
+ * The task uuid.
+ *
+ * @var string|null
+ */
+ private ?string $uuid = null;
+
+ /**
+ * The lifecycle state.
+ *
+ * @var string|null
+ */
+ private ?string $state = null;
+
+ /**
+ * The terminal outcome.
+ *
+ * @var string|null
+ */
+ private ?string $outcome = null;
+
+ /**
+ * The metadata payload.
+ *
+ * @var array|null
+ */
+ private ?array $metadata = null;
+
+ /**
+ * The completing identity.
+ *
+ * @var string|null
+ */
+ private ?string $completedBy = null;
+
+ /**
+ * The assigned identity.
+ *
+ * @var string|null
+ */
+ private ?string $assignee = null;
+
+ /**
+ * The completion comment.
+ *
+ * @var string|null
+ */
+ private ?string $comment = null;
+
+ /**
+ * The completion result text.
+ *
+ * @var string|null
+ */
+ private ?string $resultText = null;
+
+ /**
+ * Get the task uuid.
+ *
+ * @return string|null The uuid.
+ */
+ public function getUuid(): ?string {
+ return $this->uuid;
+ }//end getUuid()
+
+ /**
+ * Set the task uuid.
+ *
+ * @param string|null $uuid The uuid.
+ *
+ * @return void
+ */
+ public function setUuid(?string $uuid): void {
+ $this->uuid = $uuid;
+ }//end setUuid()
+
+ /**
+ * Get the lifecycle state.
+ *
+ * @return string|null The state.
+ */
+ public function getState(): ?string {
+ return $this->state;
+ }//end getState()
+
+ /**
+ * Set the lifecycle state.
+ *
+ * @param string|null $state The state.
+ *
+ * @return void
+ */
+ public function setState(?string $state): void {
+ $this->state = $state;
+ }//end setState()
+
+ /**
+ * Get the terminal outcome.
+ *
+ * @return string|null The outcome.
+ */
+ public function getOutcome(): ?string {
+ return $this->outcome;
+ }//end getOutcome()
+
+ /**
+ * Set the terminal outcome.
+ *
+ * @param string|null $outcome The outcome.
+ *
+ * @return void
+ */
+ public function setOutcome(?string $outcome): void {
+ $this->outcome = $outcome;
+ }//end setOutcome()
+
+ /**
+ * Get the metadata payload.
+ *
+ * @return array|null The metadata.
+ */
+ public function getMetadata(): ?array {
+ return $this->metadata;
+ }//end getMetadata()
+
+ /**
+ * Set the metadata payload.
+ *
+ * @param array|null $metadata The metadata.
+ *
+ * @return void
+ */
+ public function setMetadata(?array $metadata): void {
+ $this->metadata = $metadata;
+ }//end setMetadata()
+
+ /**
+ * Get the completing identity.
+ *
+ * @return string|null The completer uid.
+ */
+ public function getCompletedBy(): ?string {
+ return $this->completedBy;
+ }//end getCompletedBy()
+
+ /**
+ * Set the completing identity.
+ *
+ * @param string|null $completedBy The completer uid.
+ *
+ * @return void
+ */
+ public function setCompletedBy(?string $completedBy): void {
+ $this->completedBy = $completedBy;
+ }//end setCompletedBy()
+
+ /**
+ * Get the assigned identity.
+ *
+ * @return string|null The assignee uid.
+ */
+ public function getAssignee(): ?string {
+ return $this->assignee;
+ }//end getAssignee()
+
+ /**
+ * Set the assigned identity.
+ *
+ * @param string|null $assignee The assignee uid.
+ *
+ * @return void
+ */
+ public function setAssignee(?string $assignee): void {
+ $this->assignee = $assignee;
+ }//end setAssignee()
+
+ /**
+ * Get the completion comment.
+ *
+ * @return string|null The comment.
+ */
+ public function getComment(): ?string {
+ return $this->comment;
+ }//end getComment()
+
+ /**
+ * Set the completion comment.
+ *
+ * @param string|null $comment The comment.
+ *
+ * @return void
+ */
+ public function setComment(?string $comment): void {
+ $this->comment = $comment;
+ }//end setComment()
+
+ /**
+ * Get the completion result text.
+ *
+ * @return string|null The result text.
+ */
+ public function getResultText(): ?string {
+ return $this->resultText;
+ }//end getResultText()
+
+ /**
+ * Set the completion result text.
+ *
+ * @param string|null $resultText The result text.
+ *
+ * @return void
+ */
+ public function setResultText(?string $resultText): void {
+ $this->resultText = $resultText;
+ }//end setResultText()
+}//end class
diff --git a/tests/Stubs/Event/TaskTerminalEvent.php b/tests/Stubs/Event/TaskTerminalEvent.php
new file mode 100644
index 00000000..d0ffca22
--- /dev/null
+++ b/tests/Stubs/Event/TaskTerminalEvent.php
@@ -0,0 +1,75 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Event;
+
+use OCA\OpenRegister\Db\Task;
+use OCP\EventDispatcher\Event;
+
+/**
+ * Minimal TaskTerminalEvent stub for standalone unit runs.
+ *
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The real event's contract:
+ * one class serves the in-transaction and after-commit dispatch points.
+ */
+class TaskTerminalEvent extends Event {
+
+ /**
+ * Constructor — same signature as the real event.
+ *
+ * @param Task $task The task, already persisted in a terminal state.
+ * @param bool $committed TRUE when the terminal write's transaction has
+ * closed; FALSE when dispatched inside it.
+ */
+ public function __construct(
+ private readonly Task $task,
+ private readonly bool $committed = true,
+ ) {
+ parent::__construct();
+ }//end __construct()
+
+ /**
+ * The terminal task.
+ *
+ * @return Task The task as persisted.
+ */
+ public function getTask(): Task {
+ return $this->task;
+ }//end getTask()
+
+ /**
+ * Whether the terminal write's transaction has closed.
+ *
+ * @return bool TRUE for the after-commit dispatch.
+ */
+ public function isCommitted(): bool {
+ return $this->committed;
+ }//end isCommitted()
+}//end class
diff --git a/tests/Stubs/Service/Flow/FlowOversightRegistry.php b/tests/Stubs/Service/Flow/FlowOversightRegistry.php
new file mode 100644
index 00000000..a415fe1a
--- /dev/null
+++ b/tests/Stubs/Service/Flow/FlowOversightRegistry.php
@@ -0,0 +1,74 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Service\Flow;
+
+use OCP\EventDispatcher\IEventDispatcher;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Minimal FlowOversightRegistry stub.
+ */
+class FlowOversightRegistry {
+
+ /**
+ * The registered checks, keyed by id.
+ *
+ * @var array
+ */
+ private array $checks = [];
+
+ /**
+ * Constructor, mirroring the REAL signature.
+ *
+ * The real class requires the logger and lazily collects contributions
+ * through the optional dispatcher. A zero-argument stub constructor
+ * already cost one CI round: a test built against it fataled the moment
+ * the real OpenRegister was installed (ArgumentCountError), which is the
+ * exact drift these stubs promise not to have.
+ *
+ * @param LoggerInterface $logger Unused by the stub; required by the real class.
+ * @param IEventDispatcher|null $dispatcher Unused by the stub; the real
+ * class dispatches discovery on it.
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter) Signature fidelity is the point.
+ */
+ public function __construct(
+ LoggerInterface $logger,
+ ?IEventDispatcher $dispatcher = null,
+ ) {
+
+ }//end __construct()
+
+ /**
+ * Register an oversight check.
+ *
+ * @param IFlowOversightCheck $check The check to add.
+ *
+ * @return void
+ */
+ public function register(IFlowOversightCheck $check): void {
+ $this->checks[$check->getId()] = $check;
+ }//end register()
+
+ /**
+ * The registered checks, keyed by id.
+ *
+ * @return array The checks.
+ */
+ public function all(): array {
+ return $this->checks;
+ }//end all()
+}//end class
diff --git a/tests/Stubs/Service/Flow/FlowVersionService.php b/tests/Stubs/Service/Flow/FlowVersionService.php
new file mode 100644
index 00000000..9920d7d2
--- /dev/null
+++ b/tests/Stubs/Service/Flow/FlowVersionService.php
@@ -0,0 +1,77 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Service\Flow;
+
+use OCA\OpenRegister\Db\Flow;
+use OCA\OpenRegister\Db\FlowVersion;
+
+/**
+ * Minimal FlowVersionService stub for standalone static analysis.
+ */
+class FlowVersionService {
+
+ /**
+ * Publish the flow's head, as the real service does.
+ *
+ * @param Flow $flow The flow whose head is being published.
+ * @param string|null $publishedBy The acting user.
+ *
+ * @return FlowVersion The now-published version.
+ */
+ public function publish(Flow $flow, ?string $publishedBy = null): FlowVersion {
+ $flow->setLifecycleStatus(FlowVersion::STATUS_PUBLISHED);
+
+ $version = new FlowVersion();
+ $version->setFlowUuid((string)$flow->getUuid());
+ $version->setStatus(FlowVersion::STATUS_PUBLISHED);
+
+ return $version;
+ }//end publish()
+
+ /**
+ * Create a draft head, as the real service does.
+ *
+ * @param Flow $flow The flow to draft from.
+ *
+ * @return FlowVersion The new draft version row.
+ */
+ public function createDraft(Flow $flow): FlowVersion {
+ $flow->setLifecycleStatus(FlowVersion::STATUS_DRAFT);
+
+ $version = new FlowVersion();
+ $version->setFlowUuid((string)$flow->getUuid());
+ $version->setStatus(FlowVersion::STATUS_DRAFT);
+
+ return $version;
+ }//end createDraft()
+}//end class
diff --git a/tests/Stubs/Service/Flow/IFlowOversightCheck.php b/tests/Stubs/Service/Flow/IFlowOversightCheck.php
new file mode 100644
index 00000000..cac66088
--- /dev/null
+++ b/tests/Stubs/Service/Flow/IFlowOversightCheck.php
@@ -0,0 +1,39 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Service\Flow;
+
+/**
+ * Minimal IFlowOversightCheck stub.
+ */
+interface IFlowOversightCheck {
+ /**
+ * Stable id, namespaced by the contributing app (`{app}.{check}`).
+ *
+ * @return string The check's id.
+ */
+ public function getId(): string;
+
+ /**
+ * Decide whether the next hop may execute.
+ *
+ * Returning a reason string is a veto; returning null is consent.
+ *
+ * @param array $context The run context.
+ *
+ * @return string|null The reason for refusing, or null to allow the hop.
+ */
+ public function veto(array $context): ?string;
+}//end interface
diff --git a/tests/Stubs/Service/Flow/RegisterFlowOversightEvent.php b/tests/Stubs/Service/Flow/RegisterFlowOversightEvent.php
new file mode 100644
index 00000000..27d54755
--- /dev/null
+++ b/tests/Stubs/Service/Flow/RegisterFlowOversightEvent.php
@@ -0,0 +1,47 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Service\Flow;
+
+use OCP\EventDispatcher\Event;
+
+/**
+ * Minimal RegisterFlowOversightEvent stub.
+ */
+class RegisterFlowOversightEvent extends Event {
+ /**
+ * Constructor.
+ *
+ * @param FlowOversightRegistry $registry The registry to contribute to.
+ */
+ public function __construct(
+ private readonly FlowOversightRegistry $registry,
+ ) {
+ parent::__construct();
+
+ }//end __construct()
+
+ /**
+ * Contribute an oversight check.
+ *
+ * @param IFlowOversightCheck $check The check.
+ *
+ * @return void
+ */
+ public function registerCheck(IFlowOversightCheck $check): void {
+ $this->registry->register(check: $check);
+
+ }//end registerCheck()
+}//end class
diff --git a/tests/Stubs/Service/SchemaDeletionService.php b/tests/Stubs/Service/SchemaDeletionService.php
new file mode 100644
index 00000000..191f90b6
--- /dev/null
+++ b/tests/Stubs/Service/SchemaDeletionService.php
@@ -0,0 +1,47 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Service;
+
+use OCA\OpenRegister\Db\Schema;
+
+/**
+ * Minimal SchemaDeletionService stub for standalone unit runs.
+ */
+class SchemaDeletionService {
+
+ /**
+ * Cascade-delete a schema: its objects, its magic tables, and the schema
+ * itself — mirrors the real service's signature and result shape.
+ *
+ * @param Schema $schema The schema to tear down.
+ *
+ * @return array{deletedCount: int, deletedUuids: array, tableDropped: bool} The cascade result.
+ */
+ public function cascadeDeleteSchema(Schema $schema): array {
+ return [
+ 'deletedCount' => 0,
+ 'deletedUuids' => [],
+ 'tableDropped' => false,
+ ];
+ }//end cascadeDeleteSchema()
+}//end class
diff --git a/tests/Stubs/Service/Task/TaskService.php b/tests/Stubs/Service/Task/TaskService.php
new file mode 100644
index 00000000..e538e2ba
--- /dev/null
+++ b/tests/Stubs/Service/Task/TaskService.php
@@ -0,0 +1,82 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\OpenRegister\Service\Task;
+
+use LogicException;
+use OCA\OpenRegister\Db\Task;
+
+/**
+ * Analysis-and-mocking stand-in. Never executed; every body throws.
+ */
+class TaskService {
+
+ /**
+ * Refuses: this stub exists for static analysis and mocking only.
+ *
+ * @return never
+ *
+ * @throws LogicException Always.
+ */
+ private static function refuse(): never {
+ throw new LogicException(
+ 'OCA\OpenRegister\Service\Task\TaskService was resolved from hermiq\'s ANALYSIS STUB '
+ . 'rather than from OpenRegister. The real service ships with OpenRegister.'
+ );
+ }//end refuse()
+
+ /**
+ * Create a task on the TRUSTED path (real signature; stub throws).
+ *
+ * @param array $data The task fields, canonical or legacy.
+ * @param string|null $actor The creating identity.
+ *
+ * @return Task The created task.
+ */
+ public function import(array $data, ?string $actor): Task {
+ unset($data, $actor);
+ self::refuse();
+ }//end import()
+
+ /**
+ * Terminate a task made moot (real signature; stub throws).
+ *
+ * @param string $uuid The task uuid.
+ * @param string $reason What made it moot.
+ * @param string $source The propagation source recorded as actor.
+ *
+ * @return Task The terminated task, or the task untouched when already terminal.
+ */
+ public function terminateAsMoot(string $uuid, string $reason, string $source): Task {
+ unset($uuid, $reason, $source);
+ self::refuse();
+ }//end terminateAsMoot()
+}//end class
diff --git a/tests/Unit/BackgroundJob/ScheduleTaskTest.php b/tests/Unit/BackgroundJob/ScheduleTaskTest.php
new file mode 100644
index 00000000..b90f5aba
--- /dev/null
+++ b/tests/Unit/BackgroundJob/ScheduleTaskTest.php
@@ -0,0 +1,102 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Test
+ * @package OCA\Hermiq\Tests\Unit\BackgroundJob
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\BackgroundJob;
+
+use OCA\Hermiq\BackgroundJob\ScheduleTask;
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
+use OCA\Hermiq\Service\ScheduleService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * The tick arms first and never lets the bridge block the dispatch.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+class ScheduleTaskTest extends TestCase {
+
+ /**
+ * The tick syncs the engine delegation BEFORE dispatching, so a schedule
+ * created since the last tick delegates within one poll interval.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testArmsBeforeDispatching(): void {
+ $order = [];
+
+ $bridge = $this->createMock(ScheduleFlowBridge::class);
+ $bridge->expects($this->once())->method('syncAll')->willReturnCallback(
+ function () use (&$order): array {
+ $order[] = 'sync';
+ return ['mirrored' => 0, 'refreshed' => 0, 'retired' => 0, 'skipped' => 0, 'failed' => 0];
+ }
+ );
+
+ $scheduleService = $this->createMock(ScheduleService::class);
+ $scheduleService->expects($this->once())->method('run')->willReturnCallback(
+ function () use (&$order): void {
+ $order[] = 'dispatch';
+ }
+ );
+
+ $task = new ScheduleTask(
+ time: $this->createMock(ITimeFactory::class),
+ scheduleService: $scheduleService,
+ flowBridge: $bridge,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ $task->run(argument: null);
+
+ $this->assertSame(['sync', 'dispatch'], $order);
+
+ }//end testArmsBeforeDispatching()
+
+ /**
+ * A bridge failure is isolated: the tick still dispatches locally, so an
+ * engine-side outage never silences every schedule.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testBridgeFailureNeverBlocksTheTick(): void {
+ $bridge = $this->createMock(ScheduleFlowBridge::class);
+ $bridge->method('syncAll')->willThrowException(new RuntimeException('flow store down'));
+
+ $scheduleService = $this->createMock(ScheduleService::class);
+ $scheduleService->expects($this->once())->method('run');
+
+ $task = new ScheduleTask(
+ time: $this->createMock(ITimeFactory::class),
+ scheduleService: $scheduleService,
+ flowBridge: $bridge,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ $task->run(argument: null);
+
+ }//end testBridgeFailureNeverBlocksTheTick()
+}//end class
diff --git a/tests/Unit/Command/RollbackApprovalTaskMirrorTest.php b/tests/Unit/Command/RollbackApprovalTaskMirrorTest.php
new file mode 100644
index 00000000..41150b92
--- /dev/null
+++ b/tests/Unit/Command/RollbackApprovalTaskMirrorTest.php
@@ -0,0 +1,157 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Command;
+
+use OCA\Hermiq\Command\RollbackApprovalTaskMirror;
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Tester\CommandTester;
+
+/**
+ * Tests the mirror rollback.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+class RollbackApprovalTaskMirrorTest extends TestCase {
+
+ /**
+ * Mock ObjectService.
+ *
+ * @var ObjectService&MockObject
+ */
+ private ObjectService $objectService;
+
+ /**
+ * Mock bridge.
+ *
+ * @var ApprovalTaskBridge&MockObject
+ */
+ private ApprovalTaskBridge $taskBridge;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->objectService = $this->createMock(ObjectService::class);
+ $this->objectService->method('setRegister')->willReturnSelf();
+ $this->objectService->method('setSchema')->willReturnSelf();
+ $this->taskBridge = $this->createMock(ApprovalTaskBridge::class);
+ }//end setUp()
+
+ /**
+ * Build a tester around the command with the current mocks.
+ *
+ * @return CommandTester
+ */
+ private function tester(): CommandTester {
+ return new CommandTester(
+ new RollbackApprovalTaskMirror(
+ $this->objectService,
+ $this->taskBridge,
+ $this->createMock(LoggerInterface::class),
+ )
+ );
+ }//end tester()
+
+ /**
+ * Build an approval entity.
+ *
+ * @param string $uuid The uuid.
+ * @param array $payload The payload.
+ *
+ * @return ObjectEntity
+ */
+ private function approval(string $uuid, array $payload): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid($uuid);
+ $entity->setObject($payload);
+
+ return $entity;
+ }//end approval()
+
+ /**
+ * A mirrored pending approval is released and its backlink cleared; an
+ * unmirrored pending one and a decided one are untouched.
+ *
+ * @return void
+ */
+ public function testRollsBackOnlyMirroredPendingApprovals(): void {
+ $mirrored = $this->approval('appr-1', ['status' => 'pending', 'taskUuid' => 'task-1']);
+ $bare = $this->approval('appr-2', ['status' => 'pending']);
+ $decided = $this->approval('appr-3', ['status' => 'approved', 'taskUuid' => 'task-3']);
+
+ $this->objectService->method('findAll')->willReturn([$mirrored, $bare, $decided]);
+
+ $this->taskBridge->expects($this->once())
+ ->method('releaseFor')
+ ->with($mirrored, 'rolled-back', 'occ:rollback-task-mirror');
+
+ $saved = [];
+ $this->objectService->method('saveObject')->willReturnCallback(
+ function (array $object, ...$rest) use (&$saved): ObjectEntity {
+ unset($rest);
+ $saved[] = $object;
+ return new ObjectEntity();
+ }
+ );
+
+ $tester = $this->tester();
+ $this->assertSame(0, $tester->execute([]));
+
+ $this->assertCount(1, $saved, 'Only the mirrored pending approval is rewritten.');
+ $this->assertNull($saved[0]['taskUuid'], 'The backlink must be cleared.');
+ $this->assertStringContainsString('1 approvals rolled back', $tester->getDisplay());
+
+ }//end testRollsBackOnlyMirroredPendingApprovals()
+
+ /**
+ * A dry run reports without releasing or writing anything.
+ *
+ * @return void
+ */
+ public function testDryRunWritesNothing(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [$this->approval('appr-1', ['status' => 'pending', 'taskUuid' => 'task-1'])]
+ );
+
+ $this->taskBridge->expects($this->never())->method('releaseFor');
+ $this->objectService->expects($this->never())->method('saveObject');
+
+ $tester = $this->tester();
+ $this->assertSame(0, $tester->execute(['--dry-run' => true]));
+ $this->assertStringContainsString('Would roll back approval appr-1', $tester->getDisplay());
+
+ }//end testDryRunWritesNothing()
+}//end class
diff --git a/tests/Unit/Command/RollbackScheduleFlowMirrorTest.php b/tests/Unit/Command/RollbackScheduleFlowMirrorTest.php
new file mode 100644
index 00000000..0a44c8ae
--- /dev/null
+++ b/tests/Unit/Command/RollbackScheduleFlowMirrorTest.php
@@ -0,0 +1,199 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Test
+ * @package OCA\Hermiq\Tests\Unit\Command
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Command;
+
+use OCA\Hermiq\Command\RollbackScheduleFlowMirror;
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
+use OCA\Hermiq\Service\ScheduleService;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Tester\CommandTester;
+
+/**
+ * Rollback clears markers first, deletes flows second, and dry-run touches
+ * nothing.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+class RollbackScheduleFlowMirrorTest extends TestCase {
+
+ /**
+ * Mock ObjectService.
+ *
+ * @var ObjectService&MockObject
+ */
+ private ObjectService $objectService;
+
+ /**
+ * Mock ScheduleService.
+ *
+ * @var ScheduleService&MockObject
+ */
+ private ScheduleService $scheduleService;
+
+ /**
+ * Mock bridge.
+ *
+ * @var ScheduleFlowBridge&MockObject
+ */
+ private ScheduleFlowBridge $flowBridge;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->objectService = $this->createMock(ObjectService::class);
+ $this->objectService->method('setRegister')->willReturnSelf();
+ $this->objectService->method('setSchema')->willReturnSelf();
+ $this->scheduleService = $this->createMock(ScheduleService::class);
+ $this->flowBridge = $this->createMock(ScheduleFlowBridge::class);
+ }//end setUp()
+
+ /**
+ * Build a tester around the command with the current mocks.
+ *
+ * @return CommandTester
+ */
+ private function tester(): CommandTester {
+ return new CommandTester(
+ new RollbackScheduleFlowMirror(
+ objectService: $this->objectService,
+ scheduleService: $this->scheduleService,
+ flowBridge: $this->flowBridge,
+ logger: $this->createMock(LoggerInterface::class),
+ )
+ );
+ }//end tester()
+
+ /**
+ * Build a schedule entity.
+ *
+ * @param string $uuid The uuid.
+ * @param array $payload The payload.
+ *
+ * @return ObjectEntity
+ */
+ private function schedule(string $uuid, array $payload): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid($uuid);
+ $entity->setObject($payload);
+
+ return $entity;
+ }//end schedule()
+
+ /**
+ * Only delegated schedules are rolled back: marker cleared, flow deleted.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testRollsBackOnlyDelegatedSchedules(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [
+ $this->schedule('sched-1', ['engineFlowId' => 'flow-1']),
+ $this->schedule('sched-2', ['engineFlowId' => '']),
+ $this->schedule('sched-3', []),
+ ]
+ );
+
+ $this->scheduleService->expects($this->once())->method('clearEngineDelegation');
+ $deleted = [];
+ $this->flowBridge->method('deleteFlow')->willReturnCallback(
+ function (string $flowId) use (&$deleted): void {
+ $deleted[] = $flowId;
+ }
+ );
+
+ $tester = $this->tester();
+ $this->assertSame(0, $tester->execute([]));
+ $this->assertSame(['flow-1'], $deleted);
+ $this->assertStringContainsString('1 schedules rolled back', $tester->getDisplay());
+
+ }//end testRollsBackOnlyDelegatedSchedules()
+
+ /**
+ * Dry-run reports without clearing or deleting anything.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testDryRunTouchesNothing(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [$this->schedule('sched-1', ['engineFlowId' => 'flow-1'])]
+ );
+
+ $this->scheduleService->expects($this->never())->method('clearEngineDelegation');
+ $this->flowBridge->expects($this->never())->method('deleteFlow');
+
+ $tester = $this->tester();
+ $this->assertSame(0, $tester->execute(['--dry-run' => true]));
+ $this->assertStringContainsString('Would roll back schedule sched-1', $tester->getDisplay());
+
+ }//end testDryRunTouchesNothing()
+
+ /**
+ * An unreadable schedule store exits non-zero instead of reporting a
+ * rollback that never ran.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testUnreadableStoreExitsNonZero(): void {
+ $this->objectService->method('findAll')->willThrowException(new \RuntimeException('db gone'));
+
+ $tester = $this->tester();
+ $this->assertSame(1, $tester->execute([]));
+ $this->assertStringContainsString('Could not read schedules', $tester->getDisplay());
+
+ }//end testUnreadableStoreExitsNonZero()
+
+ /**
+ * A schedule whose marker cannot be cleared is counted as failed and the
+ * command exits non-zero, so a partial rollback never reads as complete.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testFailedClearCountsAndExitsNonZero(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [$this->schedule('sched-1', ['engineFlowId' => 'flow-1'])]
+ );
+ $this->scheduleService->method('clearEngineDelegation')
+ ->willThrowException(new \RuntimeException('save refused'));
+
+ $tester = $this->tester();
+ $this->assertSame(1, $tester->execute([]));
+ $this->assertStringContainsString('1 failed', $tester->getDisplay());
+
+ }//end testFailedClearCountsAndExitsNonZero()
+}//end class
diff --git a/tests/Unit/Controller/ChatHealthControllerTest.php b/tests/Unit/Controller/ChatHealthControllerTest.php
index 85adba9c..3d0b08e7 100644
--- a/tests/Unit/Controller/ChatHealthControllerTest.php
+++ b/tests/Unit/Controller/ChatHealthControllerTest.php
@@ -3,10 +3,11 @@
/**
* Unit tests for ChatHealthController (agent-engine-port).
*
- * The probe reports 200 {status:ok} only when a chat provider is configured in
- * `hermiq.llm`; a missing provider is 503 {status:no_provider} and a failing
- * config read is 503 {status:config_error} (distinct so operators can tell a
- * fresh instance from a broken config service).
+ * The probe reports 200 {status:ok} when a chat provider is configured in
+ * `hermiq.llm` and 200 {status:unconfigured, capabilities:[]} when none is: an
+ * unconfigured app is healthy, and a 5xx would trip every co-installed app's
+ * no-5xx e2e guard. Only a failing config read, the app itself being broken,
+ * is 503 {status:config_error}.
*
* @category Test
* @package OCA\Hermiq\Tests\Unit\Controller
@@ -74,22 +75,26 @@ public function testHealthOkWhenProviderConfigured(): void {
}//end testHealthOkWhenProviderConfigured()
/**
- * No configured chat provider yields 503 {status:no_provider}.
+ * No configured chat provider yields 200 {status:unconfigured}: healthy,
+ * not broken. The empty capability list is what tells a consumer there is
+ * no chat to offer; the HTTP code no longer carries that decision.
*
* @return void
*
* @spec openspec/changes/agent-engine-port/tasks.md#task-4-1
*/
- public function testHealthNoProviderWhenUnconfigured(): void {
+ public function testHealthUnconfiguredIsStillHealthy(): void {
$llmSettings = $this->createMock(LlmSettingsHandler::class);
$llmSettings->method('getLLMSettingsOnly')->willReturn(['chatProvider' => null]);
$response = $this->controller($llmSettings)->health();
- $this->assertSame(503, $response->getStatus());
- $this->assertSame('no_provider', $response->getData()['status']);
+ $this->assertSame(200, $response->getStatus());
+ $this->assertSame('unconfigured', $response->getData()['status']);
+ $this->assertFalse($response->getData()['configured']);
+ $this->assertSame([], $response->getData()['capabilities']);
- }//end testHealthNoProviderWhenUnconfigured()
+ }//end testHealthUnconfiguredIsStillHealthy()
/**
* A failing config read yields 503 {status:config_error}, not no_provider.
diff --git a/tests/Unit/Controller/SetupControllerDemoDataTest.php b/tests/Unit/Controller/SetupControllerDemoDataTest.php
new file mode 100644
index 00000000..4936e663
--- /dev/null
+++ b/tests/Unit/Controller/SetupControllerDemoDataTest.php
@@ -0,0 +1,189 @@
+written = [];
+ $this->config = [];
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $this->appConfig->method('getValueString')
+ ->willReturnCallback(function (string $app, string $key, string $default = ''): string {
+ return ($this->config[$key] ?? $default);
+ });
+ $this->appConfig->method('setValueString')
+ ->willReturnCallback(function (string $app, string $key, string $value): bool {
+ $this->written[$key] = $value;
+
+ return true;
+ });
+ $this->demoData = $this->createMock(DemoDataService::class);
+ }
+
+ private function controller(array $params = []): SetupController {
+ $request = $this->createMock(IRequest::class);
+ $request->method('getParams')->willReturn($params);
+ // 🔴 BOTH READERS, OR THE VALIDATION IS INVISIBLE TO THE TEST.
+ // saveConfig() reads the dataset with getParam() and the rest of the
+ // body with getParams(); a fake that answers only the second makes
+ // every assertion about the first pass for the wrong reason.
+ $request->method('getParam')
+ ->willReturnCallback(static function (string $key, $default = null) use ($params) {
+ return ($params[$key] ?? $default);
+ });
+
+ return new SetupController(
+ $request,
+ $this->createMock(IClientService::class),
+ $this->appConfig,
+ $this->createMock(LoggerInterface::class),
+ $this->demoData
+ );
+ }
+
+ public function testStatusReportsBothExampleDataSteps(): void {
+ $this->demoData->method('listChoices')->willReturn([]);
+
+ $steps = $this->controller()->status()->getData()['steps'];
+
+ // Absence is the defect this guards: a step the wizard is never told
+ // about cannot be offered and cannot be completed.
+ $this->assertArrayHasKey('demo-data', $steps);
+ $this->assertArrayHasKey('load-demo-data', $steps);
+ $this->assertFalse($steps['demo-data']['done']);
+ $this->assertFalse($steps['load-demo-data']['done']);
+ }
+
+ public function testStatusCarriesTheOptionListTheChoiceStepReads(): void {
+ // 🔴 THIS RESPONSE *IS* THE OPTION LIST. The step declares
+ // `optionsSource: datasets` and carries no options of its own, so a
+ // dataset missing here is a dataset nobody can pick.
+ $this->demoData->method('listChoices')->willReturn([
+ ['id' => 'none', 'label' => 'None', 'description' => 'Nothing.', 'objectCount' => 0, 'icon' => 'CloseCircleOutline'],
+ ['id' => 'demo', 'label' => 'Example data', 'description' => 'Sample values.', 'objectCount' => 12, 'icon' => 'DatabaseOutline'],
+ ]);
+
+ $data = $this->controller()->status()->getData();
+
+ $this->assertSame(['none', 'demo'], array_column($data['datasets'], 'id'));
+ $this->assertSame(12, $data['datasets'][1]['objectCount']);
+ }
+
+ public function testChoosingNoneClosesBothStepsWithoutRunningAnything(): void {
+ $this->demoData->method('listChoices')->willReturn([]);
+ $this->config['demo_dataset'] = 'none';
+
+ $steps = $this->controller()->status()->getData()['steps'];
+
+ $this->assertTrue($steps['demo-data']['done']);
+ $this->assertTrue($steps['load-demo-data']['done']);
+ }
+
+ public function testTheChoiceIsPersisted(): void {
+ $this->demoData->method('listChoices')->willReturn([
+ ['id' => 'none', 'label' => 'None', 'description' => '', 'objectCount' => 0, 'icon' => ''],
+ ['id' => 'demo', 'label' => 'Example data', 'description' => '', 'objectCount' => 1, 'icon' => ''],
+ ]);
+
+ $data = $this->controller(['demo_dataset' => 'demo'])->saveConfig()->getData();
+
+ $this->assertTrue($data['success']);
+ $this->assertSame('demo', $this->written['demo_dataset'] ?? null);
+ }
+
+ public function testAnUnknownDatasetIsRefusedRatherThanStored(): void {
+ // Storing it would leave the load step pointing at nothing, so the
+ // failure would surface one step later with no clue why.
+ $this->demoData->method('listChoices')->willReturn([
+ ['id' => 'none', 'label' => 'None', 'description' => '', 'objectCount' => 0, 'icon' => ''],
+ ]);
+
+ $response = $this->controller(['demo_dataset' => 'atlantis'])->saveConfig();
+
+ $this->assertSame(400, $response->getStatus());
+ $this->assertSame([], $this->written);
+ }
+
+ public function testSkippingClosesBOTHStepsOrTheWizardNeverCloses(): void {
+ $response = $this->controller()->runAction('skip-demo-data');
+
+ $this->assertTrue($response->getData()['success']);
+ $this->assertSame('skipped', $this->written['demo_data_decided'] ?? null);
+ $this->assertSame('none', $this->written['demo_dataset'] ?? null, 'skipping IS choosing none');
+ }
+
+ public function testLoadingWithoutAChoiceRefusesRatherThanGuessing(): void {
+ // 🔴 NO SILENT DEFAULT. Importing because the operator clicked Run one
+ // step early would plant example objects nobody asked for.
+ $this->demoData->expects($this->never())->method('install');
+
+ $response = $this->controller()->runAction('load-demo-data');
+
+ $this->assertSame(400, $response->getStatus());
+ $this->assertFalse($response->getData()['success']);
+ }
+
+ public function testChoosingNoneAndThenRunningImportsNothing(): void {
+ $this->config['demo_dataset'] = 'none';
+ $this->demoData->expects($this->never())->method('install');
+
+ $data = $this->controller()->runAction('load-demo-data')->getData();
+
+ $this->assertTrue($data['success']);
+ $this->assertStringContainsString('No example data', $data['message']);
+ }
+
+ public function testTheLegacyActionStillImportsTheShippedDataset(): void {
+ // `install-demo-data` was the id before the step asked WHICH dataset. A
+ // runbook or script that still posts it must keep working, and it names
+ // the shipped set by naming itself.
+ $this->demoData->method('install')->willReturn(['objects' => 7, 'registers' => 1, 'schemas' => 2]);
+
+ $data = $this->controller()->runAction('install-demo-data')->getData();
+
+ $this->assertTrue($data['success']);
+ $this->assertStringContainsString('7', $data['message']);
+ }
+
+ public function testAFailedLoadIsReportedAndLeavesTheStepUNDECIDED(): void {
+ // 🔴 Recording the decision here would close the step for an operator
+ // who asked for example data and received none: the wizard would never
+ // offer it again, and nothing would have been imported.
+ $this->config['demo_dataset'] = 'demo';
+ $this->demoData->method('install')->willThrowException(new RuntimeException('OpenRegister is not installed.'));
+
+ $response = $this->controller()->runAction('load-demo-data');
+
+ $this->assertFalse($response->getData()['success']);
+ $this->assertArrayNotHasKey('demo_data_decided', $this->written);
+ }
+}
diff --git a/tests/Unit/Flow/HermiqScheduleDispatchNodeTest.php b/tests/Unit/Flow/HermiqScheduleDispatchNodeTest.php
new file mode 100644
index 00000000..64974131
--- /dev/null
+++ b/tests/Unit/Flow/HermiqScheduleDispatchNodeTest.php
@@ -0,0 +1,339 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Test
+ * @package OCA\Hermiq\Tests\Unit\Flow
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Flow;
+
+use OCA\Hermiq\Flow\HermiqScheduleDispatchNode;
+use OCA\Hermiq\Service\ScheduleService;
+use OCA\OpenRegister\Db\Flow;
+use OCA\OpenRegister\Db\FlowMapper;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use UnexpectedValueException;
+
+/**
+ * Delegation guard, gate pass-through and item annotation.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+class HermiqScheduleDispatchNodeTest extends TestCase {
+
+ /**
+ * Mock ScheduleService.
+ *
+ * @var ScheduleService&MockObject
+ */
+ private ScheduleService $scheduleService;
+
+ /**
+ * Mock ObjectService.
+ *
+ * @var ObjectService&MockObject
+ */
+ private ObjectService $objectService;
+
+ /**
+ * Mock FlowMapper handed out by the container.
+ *
+ * @var FlowMapper&MockObject
+ */
+ private FlowMapper $flowMapper;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->scheduleService = $this->createMock(ScheduleService::class);
+ $this->objectService = $this->createMock(ObjectService::class);
+ $this->flowMapper = $this->createMock(FlowMapper::class);
+ }//end setUp()
+
+ /**
+ * Build the node with the current mocks.
+ *
+ * @return HermiqScheduleDispatchNode
+ */
+ private function node(): HermiqScheduleDispatchNode {
+ $l10n = $this->createMock(IL10N::class);
+ $l10n->method('t')->willReturnCallback(
+ static function (string $text, array $params = []): string {
+ return vsprintf(str_replace('%s', '%s', $text), $params);
+ }
+ );
+
+ $container = $this->createMock(ContainerInterface::class);
+ $container->method('get')->willReturn($this->flowMapper);
+
+ return new HermiqScheduleDispatchNode(
+ scheduleService: $this->scheduleService,
+ objectService: $this->objectService,
+ container: $container,
+ logger: $this->createMock(LoggerInterface::class),
+ l10n: $l10n,
+ urls: $this->createMock(IURLGenerator::class),
+ );
+ }//end node()
+
+ /**
+ * Build a schedule entity.
+ *
+ * @param array $payload The schedule body.
+ *
+ * @return ObjectEntity
+ */
+ private function schedule(array $payload): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid('sched-1');
+ $entity->setOwner('alice');
+ $entity->setObject($payload);
+
+ return $entity;
+ }//end schedule()
+
+ /**
+ * A nameless dispatch step refuses at execute time too: a seeded or
+ * imported flow never went through validateConfig().
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testExecuteRefusesWithoutScheduleId(): void {
+ $this->expectException(UnexpectedValueException::class);
+ $this->node()->execute(items: [['json' => []]], config: [], context: []);
+
+ }//end testExecuteRefusesWithoutScheduleId()
+
+ /**
+ * A flow the schedule no longer names is stale: the step refuses, never
+ * runs the agent, and switches the leftover flow off.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testStaleDelegationRefusesAndDisablesTheFlow(): void {
+ $this->objectService->method('find')->willReturn(
+ $this->schedule(['enabled' => true, 'engineFlowId' => 'the-real-clock'])
+ );
+ $this->scheduleService->expects($this->never())->method('runNow');
+
+ $staleFlow = new Flow();
+ $staleFlow->setUuid('stale-flow');
+ $staleFlow->setEnabled(true);
+ $this->flowMapper->method('findByUuid')->willReturn($staleFlow);
+
+ $updated = null;
+ $this->flowMapper->method('update')->willReturnCallback(
+ function (Flow $f) use (&$updated): Flow {
+ $updated = $f;
+ return $f;
+ }
+ );
+
+ try {
+ $this->node()->execute(
+ items: [['json' => []]],
+ config: ['scheduleId' => 'sched-1'],
+ context: ['payload' => ['flowId' => 'stale-flow']]
+ );
+ $this->fail('A stale delegation must refuse.');
+ } catch (UnexpectedValueException $e) {
+ $this->assertStringContainsString('not delegated', $e->getMessage());
+ }
+
+ $this->assertNotNull($updated, 'The stale flow must be switched off.');
+ $this->assertFalse($updated->getEnabled());
+
+ }//end testStaleDelegationRefusesAndDisablesTheFlow()
+
+ /**
+ * A disabled schedule must simply not run, loudly.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testDisabledScheduleRefuses(): void {
+ $this->objectService->method('find')->willReturn(
+ $this->schedule(['enabled' => false, 'engineFlowId' => 'flow-1'])
+ );
+ $this->scheduleService->expects($this->never())->method('runNow');
+
+ $this->expectException(UnexpectedValueException::class);
+ $this->node()->execute(
+ items: [['json' => []]],
+ config: ['scheduleId' => 'sched-1'],
+ context: ['payload' => ['flowId' => 'flow-1']]
+ );
+
+ }//end testDisabledScheduleRefuses()
+
+ /**
+ * The happy path re-enters runNow() once (all gates included) and puts
+ * the occurrence's status on each item.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testDispatchRunsOnceAndAnnotatesItems(): void {
+ $this->objectService->method('find')->willReturn(
+ $this->schedule(['enabled' => true, 'engineFlowId' => 'flow-1', 'lastStatus' => 'ok'])
+ );
+ $this->scheduleService->expects($this->once())->method('runNow');
+
+ $out = $this->node()->execute(
+ items: [['json' => ['a' => 1]]],
+ config: ['scheduleId' => 'sched-1'],
+ context: ['payload' => ['flowId' => 'flow-1']]
+ );
+
+ $this->assertCount(1, $out);
+ $this->assertSame('sched-1', $out[0]['json']['scheduleId']);
+ $this->assertSame('ok', $out[0]['json']['scheduleStatus']);
+ $this->assertSame(1, $out[0]['json']['a'], 'Existing item fields survive.');
+
+ }//end testDispatchRunsOnceAndAnnotatesItems()
+
+ /**
+ * The palette surface answers, and validateConfig refuses a nameless
+ * dispatch step at save time.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testPaletteSurfaceAndConfigValidation(): void {
+ $node = $this->node();
+
+ $this->assertSame('hermiq.schedule-dispatch', $node->getId());
+ $this->assertNotSame('', $node->getDisplayName());
+ $this->assertNotSame('', $node->getDescription());
+ $node->getIcon();
+ $this->assertTrue($node->isAvailableForScope(scope: 0));
+
+ $node->validateConfig(config: ['scheduleId' => 'sched-1']);
+
+ $this->expectException(UnexpectedValueException::class);
+ $node->validateConfig(config: []);
+
+ }//end testPaletteSurfaceAndConfigValidation()
+
+ /**
+ * A missing schedule refuses AND switches the firing flow off, so an
+ * orphaned mirror (schedule deleted in the UI) self-heals loudly.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testMissingScheduleRefusesAndDisablesTheFlow(): void {
+ $this->objectService->method('find')->willReturn(null);
+ $this->scheduleService->expects($this->never())->method('runNow');
+
+ $orphanFlow = new Flow();
+ $orphanFlow->setUuid('orphan-flow');
+ $orphanFlow->setEnabled(true);
+ $this->flowMapper->method('findByUuid')->willReturn($orphanFlow);
+
+ $updated = null;
+ $this->flowMapper->method('update')->willReturnCallback(
+ function (Flow $f) use (&$updated): Flow {
+ $updated = $f;
+ return $f;
+ }
+ );
+
+ try {
+ $this->node()->execute(
+ items: [['json' => []]],
+ config: ['scheduleId' => 'gone'],
+ context: ['payload' => ['flowId' => 'orphan-flow']]
+ );
+ $this->fail('A missing schedule must refuse.');
+ } catch (UnexpectedValueException $e) {
+ $this->assertStringContainsString('no longer exists', $e->getMessage());
+ }
+
+ $this->assertNotNull($updated);
+ $this->assertFalse($updated->getEnabled());
+
+ }//end testMissingScheduleRefusesAndDisablesTheFlow()
+
+ /**
+ * When the post-run re-read fails, the item carries 'unknown' rather
+ * than a stale or invented status.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testUnreadableRefreshAnswersUnknown(): void {
+ $schedule = $this->schedule(['enabled' => true, 'engineFlowId' => 'flow-1']);
+ $this->objectService->method('find')->willReturnOnConsecutiveCalls($schedule, null);
+ $this->scheduleService->expects($this->once())->method('runNow');
+
+ $out = $this->node()->execute(
+ items: [['json' => []]],
+ config: ['scheduleId' => 'sched-1'],
+ context: ['payload' => ['flowId' => 'flow-1']]
+ );
+
+ $this->assertSame('unknown', $out[0]['json']['scheduleStatus']);
+
+ }//end testUnreadableRefreshAnswersUnknown()
+
+ /**
+ * A gate skip is a step success: runNow() records the gate status on the
+ * schedule and returns normally, and the item carries that status.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testGateSkipIsAStepSuccessWithTheGateStatus(): void {
+ $schedule = $this->schedule(
+ ['enabled' => true, 'engineFlowId' => 'flow-1', 'requiresApproval' => true, 'lastStatus' => 'awaiting_approval']
+ );
+ $this->objectService->method('find')->willReturn($schedule);
+ $this->scheduleService->expects($this->once())->method('runNow');
+
+ $out = $this->node()->execute(
+ items: [['json' => []]],
+ config: ['scheduleId' => 'sched-1'],
+ context: ['payload' => ['flowId' => 'flow-1']]
+ );
+
+ $this->assertSame('awaiting_approval', $out[0]['json']['scheduleStatus']);
+
+ }//end testGateSkipIsAStepSuccessWithTheGateStatus()
+}//end class
diff --git a/tests/Unit/Flow/TenantKillSwitchCheckTest.php b/tests/Unit/Flow/TenantKillSwitchCheckTest.php
new file mode 100644
index 00000000..33f80fd1
--- /dev/null
+++ b/tests/Unit/Flow/TenantKillSwitchCheckTest.php
@@ -0,0 +1,261 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Test
+ * @package OCA\Hermiq\Tests\Unit\Flow
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Flow;
+
+use OCA\Hermiq\Flow\TenantKillSwitchCheck;
+use OCA\Hermiq\Listener\FlowOversightRegistrationListener;
+use OCA\OpenRegister\Db\FlowRun;
+use OCA\OpenRegister\Db\FlowRunMapper;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\Flow\FlowOversightRegistry;
+use OCA\OpenRegister\Service\Flow\RegisterFlowOversightEvent;
+use OCA\OpenRegister\Service\ObjectService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * Veto semantics: scoped to hermiq hops, fail-closed on attribution.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+class TenantKillSwitchCheckTest extends TestCase {
+
+ /**
+ * Mock ObjectService.
+ *
+ * @var ObjectService&MockObject
+ */
+ private ObjectService $objectService;
+
+ /**
+ * Mock FlowRunMapper handed out by the container.
+ *
+ * @var FlowRunMapper&MockObject
+ */
+ private FlowRunMapper $runMapper;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->objectService = $this->createMock(ObjectService::class);
+ $this->objectService->method('setRegister')->willReturnSelf();
+ $this->objectService->method('setSchema')->willReturnSelf();
+ $this->runMapper = $this->createMock(FlowRunMapper::class);
+ }//end setUp()
+
+ /**
+ * Build the check with the current mocks.
+ *
+ * @return TenantKillSwitchCheck
+ */
+ private function check(): TenantKillSwitchCheck {
+ $container = $this->createMock(ContainerInterface::class);
+ $container->method('get')->willReturn($this->runMapper);
+
+ return new TenantKillSwitchCheck(
+ objectService: $this->objectService,
+ container: $container,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ }//end check()
+
+ /**
+ * Declare one engaged TenantControl for the organisation.
+ *
+ * @param string $organisation The engaged organisation.
+ *
+ * @return void
+ */
+ private function engage(string $organisation): void {
+ $control = new ObjectEntity();
+ $control->setOrganisation($organisation);
+ $control->setObject(['engaged' => true]);
+ $this->objectService->method('findAll')->willReturn([$control]);
+ }//end engage()
+
+ /**
+ * Declare a run attributed to the organisation.
+ *
+ * @param string $organisation The run's organisation.
+ *
+ * @return void
+ */
+ private function runBelongsTo(string $organisation): void {
+ $run = new FlowRun();
+ $run->setUuid('run-1');
+ $run->setOrganisation($organisation);
+ $this->runMapper->method('findByUuid')->willReturn($run);
+ }//end runBelongsTo()
+
+ /**
+ * Other apps' hops are not hermiq's to veto, engaged or not.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testConsentsToNonHermiqHops(): void {
+ $this->engage('org-1');
+
+ $this->assertNull(
+ $this->check()->veto(context: ['nodeType' => 'openregister.object-write', 'runUuid' => 'run-1'])
+ );
+
+ }//end testConsentsToNonHermiqHops()
+
+ /**
+ * No engaged switch means consent on the cheap path.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testConsentsWhenNothingIsEngaged(): void {
+ $this->objectService->method('findAll')->willReturn([]);
+
+ $this->assertNull(
+ $this->check()->veto(context: ['nodeType' => 'hermiq.agent-step', 'runUuid' => 'run-1'])
+ );
+
+ }//end testConsentsWhenNothingIsEngaged()
+
+ /**
+ * An engaged organisation's hermiq hop is vetoed with a reason naming the
+ * kill switch.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testVetoesAnEngagedOrganisationsHop(): void {
+ $this->engage('org-1');
+ $this->runBelongsTo('org-1');
+
+ $reason = $this->check()->veto(context: ['nodeType' => 'hermiq.agent-step', 'runUuid' => 'run-1']);
+
+ $this->assertNotNull($reason);
+ $this->assertStringContainsString('kill switch', $reason);
+
+ }//end testVetoesAnEngagedOrganisationsHop()
+
+ /**
+ * Another organisation's hop keeps running: the veto is per tenant.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testConsentsToAnotherOrganisationsHop(): void {
+ $this->engage('org-1');
+ $this->runBelongsTo('org-2');
+
+ $this->assertNull(
+ $this->check()->veto(context: ['nodeType' => 'hermiq.agent-step', 'runUuid' => 'run-1'])
+ );
+
+ }//end testConsentsToAnotherOrganisationsHop()
+
+ /**
+ * While any switch is engaged, an unattributable hermiq hop is vetoed: a
+ * kill switch that cannot tell whose run it sees must stop it.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testVetoesAnUnattributableHopWhileEngaged(): void {
+ $this->engage('org-1');
+
+ $reason = $this->check()->veto(context: ['nodeType' => 'hermiq.agent-step']);
+
+ $this->assertNotNull($reason);
+ $this->assertStringContainsString('could not be established', $reason);
+
+ }//end testVetoesAnUnattributableHopWhileEngaged()
+
+ /**
+ * A TenantControl read failure consents (logged): the dispatcher's own
+ * documented fail-open-on-read choice, so a transient outage never halts
+ * every tenant.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testReadFailureConsents(): void {
+ $this->objectService->method('findAll')->willThrowException(new RuntimeException('db gone'));
+
+ $this->assertNull(
+ $this->check()->veto(context: ['nodeType' => 'hermiq.agent-step', 'runUuid' => 'run-1'])
+ );
+
+ }//end testReadFailureConsents()
+
+ /**
+ * A run row without an organisation is unattributable, and so vetoed
+ * while any switch is engaged.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testOrganisationlessRunIsUnattributable(): void {
+ $this->engage('org-1');
+ $this->runBelongsTo('');
+
+ $reason = $this->check()->veto(context: ['nodeType' => 'hermiq.agent-step', 'runUuid' => 'run-1']);
+
+ $this->assertNotNull($reason);
+ $this->assertStringContainsString('could not be established', $reason);
+ $this->assertSame('hermiq.tenant-killswitch', $this->check()->getId());
+
+ }//end testOrganisationlessRunIsUnattributable()
+
+ /**
+ * The registration listener contributes the check under its stable id.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-the-tenant-kill-switch-reaches-every-hermiq-flow-hop
+ */
+ public function testListenerRegistersTheCheck(): void {
+ // The real registry (present when CI installs OpenRegister) requires
+ // the logger; the stub mirrors that signature, so this construction
+ // works identically against either.
+ $registry = new FlowOversightRegistry($this->createMock(LoggerInterface::class));
+ $event = new RegisterFlowOversightEvent(registry: $registry);
+
+ $listener = new FlowOversightRegistrationListener(killSwitchCheck: $this->check());
+ $listener->handle(event: $event);
+
+ $this->assertArrayHasKey('hermiq.tenant-killswitch', $registry->all());
+
+ }//end testListenerRegistersTheCheck()
+}//end class
diff --git a/tests/Unit/Listener/TaskTerminalListenerTest.php b/tests/Unit/Listener/TaskTerminalListenerTest.php
new file mode 100644
index 00000000..1041034d
--- /dev/null
+++ b/tests/Unit/Listener/TaskTerminalListenerTest.php
@@ -0,0 +1,221 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Listener;
+
+use OCA\Hermiq\Listener\TaskTerminalListener;
+use OCA\Hermiq\Service\ApprovalService;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Db\Task;
+use OCA\OpenRegister\Event\TaskTerminalEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Tests the terminal-task-to-approval translation.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-task-decision-is-an-approval-decision
+ */
+class TaskTerminalListenerTest extends TestCase {
+
+ /**
+ * Mock approval service.
+ *
+ * @var ApprovalService&MockObject
+ */
+ private ApprovalService $approvalService;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->approvalService = $this->createMock(ApprovalService::class);
+ }//end setUp()
+
+ /**
+ * Build the listener with the current mocks.
+ *
+ * @return TaskTerminalListener
+ */
+ private function listener(): TaskTerminalListener {
+ return new TaskTerminalListener(
+ $this->approvalService,
+ $this->createMock(LoggerInterface::class),
+ );
+ }//end listener()
+
+ /**
+ * Build a completed hermiq mirror task.
+ *
+ * @param string $outcome The terminal outcome.
+ * @param string|null $completedBy The completing identity.
+ *
+ * @return Task
+ */
+ private function completedTask(string $outcome = 'approved', ?string $completedBy = 'carol'): Task {
+ $task = new Task();
+ $task->setUuid('task-1');
+ $task->setState('completed');
+ $task->setOutcome($outcome);
+ $task->setCompletedBy($completedBy);
+ $task->setComment('Because I said so');
+ $task->setMetadata(['hermiq' => ['approvalUuid' => 'appr-1', 'sourceType' => 'schedule']]);
+
+ return $task;
+ }//end completedTask()
+
+ /**
+ * Build the pending Approval the mirror resolves to.
+ *
+ * @return ObjectEntity
+ */
+ private function pendingApproval(): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid('appr-1');
+ $entity->setObject(['status' => 'pending', 'sourceType' => 'schedule']);
+
+ return $entity;
+ }//end pendingApproval()
+
+ /**
+ * A committed completion with an approving outcome applies approve(),
+ * attributed to the task's completer.
+ *
+ * @return void
+ */
+ public function testApprovingOutcomeApproves(): void {
+ $approval = $this->pendingApproval();
+ $this->approvalService->method('loadApproval')->with('appr-1')->willReturn($approval);
+ $this->approvalService->expects($this->once())
+ ->method('approve')
+ ->with($approval, 'carol')
+ ->willReturn(['status' => 'approved', 'ran' => true]);
+ $this->approvalService->expects($this->never())->method('deny');
+
+ $this->listener()->handle(new TaskTerminalEvent(task: $this->completedTask(), committed: true));
+ }//end testApprovingOutcomeApproves()
+
+ /**
+ * A rejecting outcome applies deny() with the task's comment as reason.
+ *
+ * @return void
+ */
+ public function testRejectingOutcomeDeniesWithTheComment(): void {
+ $approval = $this->pendingApproval();
+ $this->approvalService->method('loadApproval')->with('appr-1')->willReturn($approval);
+ $this->approvalService->expects($this->once())
+ ->method('deny')
+ ->with($approval, 'carol', 'Because I said so');
+ $this->approvalService->expects($this->never())->method('approve');
+
+ $this->listener()->handle(
+ new TaskTerminalEvent(task: $this->completedTask(outcome: 'rejected'), committed: true)
+ );
+ }//end testRejectingOutcomeDeniesWithTheComment()
+
+ /**
+ * The in-transaction dispatch (committed=false) never decides: only the
+ * after-commit dispatch may resume work.
+ *
+ * @return void
+ */
+ public function testUncommittedDispatchIsIgnored(): void {
+ $this->approvalService->expects($this->never())->method('loadApproval');
+
+ $this->listener()->handle(new TaskTerminalEvent(task: $this->completedTask(), committed: false));
+ }//end testUncommittedDispatchIsIgnored()
+
+ /**
+ * A terminated (cancelled/moot) task is not a decision: the Approval
+ * stays pending and decidable in hermiq.
+ *
+ * @return void
+ */
+ public function testTerminatedStateIsIgnored(): void {
+ $task = $this->completedTask(outcome: 'terminated');
+ $task->setState('terminated');
+ $this->approvalService->expects($this->never())->method('loadApproval');
+
+ $this->listener()->handle(new TaskTerminalEvent(task: $task, committed: true));
+ }//end testTerminatedStateIsIgnored()
+
+ /**
+ * A task without the hermiq metadata backlink is foreign and skipped.
+ *
+ * @return void
+ */
+ public function testForeignTaskIsIgnored(): void {
+ $task = $this->completedTask();
+ $task->setMetadata(['someoneElse' => true]);
+ $this->approvalService->expects($this->never())->method('loadApproval');
+
+ $this->listener()->handle(new TaskTerminalEvent(task: $task, committed: true));
+ }//end testForeignTaskIsIgnored()
+
+ /**
+ * Without a completing identity the assignee decides; without either the
+ * event is skipped: never decide as nobody.
+ *
+ * @return void
+ */
+ public function testAssigneeFallsBackAndNobodyNeverDecides(): void {
+ $approval = $this->pendingApproval();
+ $this->approvalService->method('loadApproval')->willReturn($approval);
+
+ $task = $this->completedTask(completedBy: null);
+ $task->setAssignee('dave');
+ $this->approvalService->expects($this->once())
+ ->method('approve')
+ ->with($approval, 'dave')
+ ->willReturn(['status' => 'approved', 'ran' => false]);
+ $this->listener()->handle(new TaskTerminalEvent(task: $task, committed: true));
+
+ $anonymous = $this->completedTask(completedBy: null);
+ $anonymous->setAssignee(null);
+ $this->approvalService->expects($this->never())->method('deny');
+ $this->listener()->handle(new TaskTerminalEvent(task: $anonymous, committed: true));
+ }//end testAssigneeFallsBackAndNobodyNeverDecides()
+
+ /**
+ * A mirror naming an approval that no longer resolves is logged and
+ * skipped, never fatal.
+ *
+ * @return void
+ */
+ public function testUnresolvableApprovalIsSkipped(): void {
+ $this->approvalService->method('loadApproval')->willReturn(null);
+ $this->approvalService->expects($this->never())->method('approve');
+ $this->approvalService->expects($this->never())->method('deny');
+
+ $this->listener()->handle(new TaskTerminalEvent(task: $this->completedTask(), committed: true));
+ }//end testUnresolvableApprovalIsSkipped()
+}//end class
diff --git a/tests/Unit/Repair/MirrorPendingApprovalsToTasksTest.php b/tests/Unit/Repair/MirrorPendingApprovalsToTasksTest.php
new file mode 100644
index 00000000..cf7fb784
--- /dev/null
+++ b/tests/Unit/Repair/MirrorPendingApprovalsToTasksTest.php
@@ -0,0 +1,174 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Repair;
+
+use OCA\Hermiq\Repair\MirrorPendingApprovalsToTasks;
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\ObjectService;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * Tests the in-flight approval mirror pass.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-in-flight-approvals-migrate-and-roll-back
+ */
+class MirrorPendingApprovalsToTasksTest extends TestCase {
+
+ /**
+ * Mock ObjectService.
+ *
+ * @var ObjectService&MockObject
+ */
+ private ObjectService $objectService;
+
+ /**
+ * Mock bridge.
+ *
+ * @var ApprovalTaskBridge&MockObject
+ */
+ private ApprovalTaskBridge $taskBridge;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->objectService = $this->createMock(ObjectService::class);
+ $this->objectService->method('setRegister')->willReturnSelf();
+ $this->objectService->method('setSchema')->willReturnSelf();
+ $this->taskBridge = $this->createMock(ApprovalTaskBridge::class);
+ }//end setUp()
+
+ /**
+ * Build the step with the current mocks.
+ *
+ * @return MirrorPendingApprovalsToTasks
+ */
+ private function step(): MirrorPendingApprovalsToTasks {
+ return new MirrorPendingApprovalsToTasks(
+ $this->objectService,
+ $this->taskBridge,
+ $this->createMock(LoggerInterface::class),
+ );
+ }//end step()
+
+ /**
+ * Build an approval entity.
+ *
+ * @param string $uuid The uuid.
+ * @param array $payload The payload.
+ *
+ * @return ObjectEntity
+ */
+ private function approval(string $uuid, array $payload): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid($uuid);
+ $entity->setObject($payload);
+
+ return $entity;
+ }//end approval()
+
+ /**
+ * Only unmirrored pending approvals are mirrored and backlinked; already
+ * mirrored ones are skipped, which is what makes a second run a no-op.
+ *
+ * @return void
+ */
+ public function testMirrorsOnlyUnmirroredPendingApprovals(): void {
+ $fresh = $this->approval('appr-1', ['status' => 'pending']);
+ $mirrored = $this->approval('appr-2', ['status' => 'pending', 'taskUuid' => 'task-old']);
+
+ $this->objectService->method('findAll')->willReturn([$fresh, $mirrored]);
+
+ $this->taskBridge->expects($this->once())
+ ->method('ensureTaskFor')
+ ->with($fresh)
+ ->willReturn('task-new');
+
+ $saved = [];
+ $this->objectService->method('saveObject')->willReturnCallback(
+ function (array $object, ...$rest) use (&$saved): ObjectEntity {
+ unset($rest);
+ $saved[] = $object;
+ return new ObjectEntity();
+ }
+ );
+
+ $this->step()->run($this->createMock(IOutput::class));
+
+ $this->assertCount(1, $saved, 'Only the unmirrored approval gets a backlink write.');
+ $this->assertSame('task-new', $saved[0]['taskUuid']);
+
+ }//end testMirrorsOnlyUnmirroredPendingApprovals()
+
+ /**
+ * A bridge that cannot mirror (degraded surface) is a warned count, never
+ * an exception: the upgrade must not abort over an unmirrorable approval.
+ *
+ * @return void
+ */
+ public function testBridgeFailureIsCountedNotRaised(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [$this->approval('appr-1', ['status' => 'pending'])]
+ );
+ $this->taskBridge->method('ensureTaskFor')->willReturn(null);
+ $this->objectService->expects($this->never())->method('saveObject');
+
+ $output = $this->createMock(IOutput::class);
+ $output->expects($this->once())->method('warning')
+ ->with($this->stringContains('appr-1'));
+
+ $this->step()->run($output);
+
+ }//end testBridgeFailureIsCountedNotRaised()
+
+ /**
+ * An unreadable register (fresh install, descriptor not yet imported) is
+ * an informational skip, never an aborted upgrade.
+ *
+ * @return void
+ */
+ public function testUnreadableRegisterSkips(): void {
+ $this->objectService->method('findAll')
+ ->willThrowException(new RuntimeException('no register'));
+ $this->taskBridge->expects($this->never())->method('ensureTaskFor');
+
+ $this->step()->run($this->createMock(IOutput::class));
+ // Reaching this line IS the assertion: nothing escaped.
+ $this->addToAssertionCount(1);
+
+ }//end testUnreadableRegisterSkips()
+}//end class
diff --git a/tests/Unit/Repair/MirrorSchedulesToEngineFlowsTest.php b/tests/Unit/Repair/MirrorSchedulesToEngineFlowsTest.php
new file mode 100644
index 00000000..e1eb299b
--- /dev/null
+++ b/tests/Unit/Repair/MirrorSchedulesToEngineFlowsTest.php
@@ -0,0 +1,102 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Test
+ * @package OCA\Hermiq\Tests\Unit\Repair
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Repair;
+
+use OCA\Hermiq\Repair\MirrorSchedulesToEngineFlows;
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * The step is one sync pass, and it never aborts an upgrade.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+class MirrorSchedulesToEngineFlowsTest extends TestCase {
+
+ /**
+ * The step delegates to syncAll() and reports its counts.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testRunsOneSyncPassAndReportsCounts(): void {
+ $bridge = $this->createMock(ScheduleFlowBridge::class);
+ $bridge->expects($this->once())->method('syncAll')->willReturn(
+ ['mirrored' => 2, 'refreshed' => 1, 'retired' => 0, 'skipped' => 3, 'failed' => 0]
+ );
+
+ $messages = [];
+ $output = $this->createMock(IOutput::class);
+ $output->method('info')->willReturnCallback(
+ function (string $message) use (&$messages): void {
+ $messages[] = $message;
+ }
+ );
+
+ $step = new MirrorSchedulesToEngineFlows(
+ flowBridge: $bridge,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ $step->run(output: $output);
+
+ $this->assertCount(1, $messages);
+ $this->assertStringContainsString('2 mirrored', $messages[0]);
+ $this->assertStringContainsString('3 stayed local', $messages[0]);
+
+ }//end testRunsOneSyncPassAndReportsCounts()
+
+ /**
+ * A sync failure is reported, never thrown: a repair step that aborts the
+ * upgrade over one unmappable schedule would cost the whole instance.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testSyncFailureNeverAbortsTheUpgrade(): void {
+ $bridge = $this->createMock(ScheduleFlowBridge::class);
+ $bridge->method('syncAll')->willThrowException(new RuntimeException('no register yet'));
+
+ $messages = [];
+ $output = $this->createMock(IOutput::class);
+ $output->method('info')->willReturnCallback(
+ function (string $message) use (&$messages): void {
+ $messages[] = $message;
+ }
+ );
+
+ $step = new MirrorSchedulesToEngineFlows(
+ flowBridge: $bridge,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ $step->run(output: $output);
+
+ $this->assertCount(1, $messages);
+ $this->assertStringContainsString('skipped', $messages[0]);
+
+ }//end testSyncFailureNeverAbortsTheUpgrade()
+}//end class
diff --git a/tests/Unit/Repair/PruneRetiredAgentFlowSchemasTest.php b/tests/Unit/Repair/PruneRetiredAgentFlowSchemasTest.php
new file mode 100644
index 00000000..2cc84af6
--- /dev/null
+++ b/tests/Unit/Repair/PruneRetiredAgentFlowSchemasTest.php
@@ -0,0 +1,278 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Repair;
+
+use OCA\Hermiq\Repair\PruneRetiredAgentFlowSchemas;
+use OCA\OpenRegister\Db\Register;
+use OCA\OpenRegister\Db\RegisterMapper;
+use OCA\OpenRegister\Db\Schema;
+use OCA\OpenRegister\Db\SchemaMapper;
+use OCA\OpenRegister\Service\SchemaDeletionService;
+use OCP\Migration\IOutput;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * @covers \OCA\Hermiq\Repair\PruneRetiredAgentFlowSchemas
+ *
+ * @spec openspec/specs/flow-authoring/spec.md#requirement-a-flow-is-stored-once-by-openregister-req-fa-002
+ */
+final class PruneRetiredAgentFlowSchemasTest extends TestCase {
+
+ /**
+ * Build the step around a container serving the given collaborators.
+ *
+ * @param SchemaMapper $schemaMapper The schema store.
+ * @param RegisterMapper $registerMapper The register store.
+ * @param SchemaDeletionService $deletionService The cascade teardown.
+ *
+ * @return PruneRetiredAgentFlowSchemas The step under test.
+ */
+ private function step(
+ SchemaMapper $schemaMapper,
+ RegisterMapper $registerMapper,
+ SchemaDeletionService $deletionService,
+ ): PruneRetiredAgentFlowSchemas {
+ $container = $this->createMock(ContainerInterface::class);
+ $container->method('get')->willReturnCallback(
+ static function (string $id) use ($schemaMapper, $registerMapper, $deletionService) {
+ return match ($id) {
+ SchemaMapper::class => $schemaMapper,
+ RegisterMapper::class => $registerMapper,
+ SchemaDeletionService::class => $deletionService,
+ default => throw new RuntimeException('not available in this test: ' . $id),
+ };
+ }
+ );
+
+ return new PruneRetiredAgentFlowSchemas(
+ container: $container,
+ logger: $this->createMock(LoggerInterface::class)
+ );
+ }//end step()
+
+ /**
+ * A schema entity with the given id and slug (the PSR-4 test stub, which
+ * carries real state — see tests/bootstrap.php).
+ *
+ * @param int $id The schema id.
+ * @param string $slug The schema slug.
+ *
+ * @return Schema The schema.
+ */
+ private function schema(int $id, string $slug): Schema {
+ $schema = new Schema();
+ $schema->setId($id);
+ $schema->setSlug($slug);
+
+ return $schema;
+ }//end schema()
+
+ /**
+ * Both retired slugs are resolved APP-SCOPED and cascade-deleted.
+ *
+ * Asserts the ARGUMENTS, not a call count: an app filter other than
+ * `hermiq` would reach a same-slug schema another app owns, which is the
+ * failure the scoping exists to prevent.
+ *
+ * @return void
+ */
+ public function testPrunesBothRetiredSlugsAppScoped(): void {
+ $flowSchema = $this->schema(id: 74, slug: 'agentflow');
+ $runSchema = $this->schema(id: 75, slug: 'agentflowrun');
+
+ $lookups = [];
+ $schemaMapper = $this->createMock(SchemaMapper::class);
+ $schemaMapper->method('findByApplicationAndSlug')->willReturnCallback(
+ static function (string $slug, string $application) use ($flowSchema, $runSchema, &$lookups): ?Schema {
+ $lookups[] = [$slug, $application];
+ return match ($slug) {
+ 'agentflow' => $flowSchema,
+ 'agentflowrun' => $runSchema,
+ default => null,
+ };
+ }
+ );
+
+ $registerMapper = $this->createMock(RegisterMapper::class);
+ $registerMapper->method('findAll')->willReturn([]);
+
+ $deleted = [];
+ $deletionService = $this->createMock(SchemaDeletionService::class);
+ $deletionService->expects($this->exactly(2))->method('cascadeDeleteSchema')->willReturnCallback(
+ static function (Schema $schema) use (&$deleted): array {
+ $deleted[] = $schema->getSlug();
+ return ['deletedCount' => 3, 'deletedUuids' => [], 'tableDropped' => true];
+ }
+ );
+
+ $this->step($schemaMapper, $registerMapper, $deletionService)
+ ->run($this->createMock(IOutput::class));
+
+ $this->assertSame(
+ [['agentflow', 'hermiq'], ['agentflowrun', 'hermiq']],
+ $lookups,
+ 'Both retired slugs must be resolved, and only within hermiq\'s ownership.'
+ );
+ $this->assertSame(['agentflow', 'agentflowrun'], $deleted);
+ }//end testPrunesBothRetiredSlugsAppScoped()
+
+ /**
+ * The schema id is unlinked from a referencing register BEFORE the delete,
+ * in every stored form — the list holds ids as ints or strings depending
+ * on which import era wrote them — while non-numeric entries survive.
+ *
+ * @return void
+ */
+ public function testUnlinksEveryStoredFormOfTheSchemaId(): void {
+ $flowSchema = $this->schema(id: 74, slug: 'agentflow');
+
+ $schemaMapper = $this->createMock(SchemaMapper::class);
+ $schemaMapper->method('findByApplicationAndSlug')->willReturnCallback(
+ static fn (string $slug, string $application): ?Schema => ($slug === 'agentflow') ? $flowSchema : null
+ );
+
+ $register = new Register();
+ $register->setId(1);
+ $register->setSlug('hermiq');
+ $register->setSchemas([12, '74', 74, 'not-a-number', 15]);
+
+ $updated = [];
+ $registerMapper = $this->createMock(RegisterMapper::class);
+ $registerMapper->method('findAll')->willReturn([$register]);
+ $registerMapper->expects($this->once())->method('update')->willReturnCallback(
+ static function (Register $entity) use (&$updated): Register {
+ $updated = $entity->getSchemas();
+ return $entity;
+ }
+ );
+
+ $deletionService = $this->createMock(SchemaDeletionService::class);
+ $deletionService->method('cascadeDeleteSchema')
+ ->willReturn(['deletedCount' => 0, 'deletedUuids' => [], 'tableDropped' => true]);
+
+ $this->step($schemaMapper, $registerMapper, $deletionService)
+ ->run($this->createMock(IOutput::class));
+
+ $this->assertSame(
+ [12, 'not-a-number', 15],
+ $updated,
+ 'Both the int and the string form of the id must go; unrelated entries must survive.'
+ );
+ }//end testUnlinksEveryStoredFormOfTheSchemaId()
+
+ /**
+ * 🔑 Idempotency, proven rather than claimed: after a prune the lookup
+ * finds nothing, so a second run performs ZERO deletes and ZERO register
+ * writes. A step that deleted again on re-run would be reaching a schema
+ * some later change legitimately re-created under the same slug.
+ *
+ * @return void
+ */
+ public function testSecondRunAfterPruneIsANoOp(): void {
+ $schemaMapper = $this->createMock(SchemaMapper::class);
+ $schemaMapper->method('findByApplicationAndSlug')->willReturn(null);
+
+ $registerMapper = $this->createMock(RegisterMapper::class);
+ $registerMapper->expects($this->never())->method('findAll');
+ $registerMapper->expects($this->never())->method('update');
+
+ $deletionService = $this->createMock(SchemaDeletionService::class);
+ $deletionService->expects($this->never())->method('cascadeDeleteSchema');
+
+ $step = $this->step($schemaMapper, $registerMapper, $deletionService);
+ $step->run($this->createMock(IOutput::class));
+ $step->run($this->createMock(IOutput::class));
+
+ $this->addToAssertionCount(1);
+ }//end testSecondRunAfterPruneIsANoOp()
+
+ /**
+ * A container that cannot serve OpenRegister's deletion service produces a
+ * SKIP, not an aborted upgrade — and nothing is deleted.
+ *
+ * @return void
+ */
+ public function testSkipsWhenOpenRegisterIsUnavailable(): void {
+ $container = $this->createMock(ContainerInterface::class);
+ $container->method('get')->willThrowException(new RuntimeException('OpenRegister is not installed'));
+
+ $output = $this->createMock(IOutput::class);
+ $output->expects($this->once())->method('info');
+
+ $step = new PruneRetiredAgentFlowSchemas(
+ container: $container,
+ logger: $this->createMock(LoggerInterface::class)
+ );
+ $step->run($output);
+ }//end testSkipsWhenOpenRegisterIsUnavailable()
+
+ /**
+ * A delete that throws is reported and the step continues with the next
+ * slug: an upgrade must not abort over stale rows, and one broken slug
+ * must not shield the other from its prune.
+ *
+ * @return void
+ */
+ public function testAFailedDeleteIsReportedAndTheNextSlugStillPrunes(): void {
+ $schemaMapper = $this->createMock(SchemaMapper::class);
+ $schemaMapper->method('findByApplicationAndSlug')->willReturnCallback(
+ fn (string $slug, string $application): ?Schema => $this->schema(
+ id: ($slug === 'agentflow') ? 74 : 75,
+ slug: $slug
+ )
+ );
+
+ $registerMapper = $this->createMock(RegisterMapper::class);
+ $registerMapper->method('findAll')->willReturn([]);
+
+ $deleted = [];
+ $deletionService = $this->createMock(SchemaDeletionService::class);
+ $deletionService->method('cascadeDeleteSchema')->willReturnCallback(
+ static function (Schema $schema) use (&$deleted): array {
+ if ($schema->getSlug() === 'agentflow') {
+ throw new RuntimeException('phase 1 rollback');
+ }
+
+ $deleted[] = $schema->getSlug();
+ return ['deletedCount' => 0, 'deletedUuids' => [], 'tableDropped' => true];
+ }
+ );
+
+ $output = $this->createMock(IOutput::class);
+ $output->expects($this->once())->method('warning');
+
+ $this->step($schemaMapper, $registerMapper, $deletionService)->run($output);
+
+ $this->assertSame(['agentflowrun'], $deleted, 'The second slug must still be pruned.');
+ }//end testAFailedDeleteIsReportedAndTheNextSlugStillPrunes()
+}//end class
diff --git a/tests/Unit/Service/Approval/ApprovalTaskBridgeTest.php b/tests/Unit/Service/Approval/ApprovalTaskBridgeTest.php
new file mode 100644
index 00000000..21d09707
--- /dev/null
+++ b/tests/Unit/Service/Approval/ApprovalTaskBridgeTest.php
@@ -0,0 +1,274 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Service\Approval;
+
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Db\Task;
+use OCA\OpenRegister\Service\Task\TaskService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * Tests the mirror creation and release paths.
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+class ApprovalTaskBridgeTest extends TestCase {
+
+ /**
+ * Mock container.
+ *
+ * @var ContainerInterface&MockObject
+ */
+ private ContainerInterface $container;
+
+ /**
+ * Mock task service (resolved from the container by FQN string).
+ *
+ * @var TaskService&MockObject
+ */
+ private TaskService $taskService;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->taskService = $this->createMock(TaskService::class);
+ $this->container = $this->createMock(ContainerInterface::class);
+ $this->container->method('get')
+ ->with(ApprovalTaskBridge::TASK_SERVICE_CLASS)
+ ->willReturn($this->taskService);
+ }//end setUp()
+
+ /**
+ * Build the bridge with the current mocks.
+ *
+ * @return ApprovalTaskBridge
+ */
+ private function bridge(): ApprovalTaskBridge {
+ return new ApprovalTaskBridge(
+ $this->container,
+ $this->createMock(LoggerInterface::class),
+ );
+ }//end bridge()
+
+ /**
+ * Build a pending Approval entity.
+ *
+ * @param array $payload Payload overrides.
+ *
+ * @return ObjectEntity
+ */
+ private function approval(array $payload = []): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid('appr-1');
+ $entity->setOwner('alice');
+ $entity->setObject(
+ array_merge(
+ [
+ 'status' => 'pending',
+ 'sourceType' => 'schedule',
+ 'reviewer' => 'bob',
+ 'reviewerType' => 'user',
+ 'agentId' => 'agent-1',
+ 'prompt' => 'Summarise the week',
+ ],
+ $payload
+ )
+ );
+
+ return $entity;
+ }//end approval()
+
+ /**
+ * A user-reviewer approval imports an ACTIVE, assigned mirror task with
+ * the hermiq backlink in its metadata and the system seat as requester.
+ *
+ * @return void
+ */
+ public function testEnsureTaskForImportsAssignedTaskForUserReviewer(): void {
+ $task = new Task();
+ $task->setUuid('task-1');
+
+ $this->taskService->expects($this->once())
+ ->method('import')
+ ->with(
+ $this->callback(
+ function (array $data): bool {
+ $this->assertSame('active', $data['state']);
+ $this->assertSame('bob', $data['assignee']);
+ $this->assertNull($data['candidateGroups']);
+ $this->assertSame('user', $data['performerType']);
+ $this->assertSame(ApprovalTaskBridge::REQUESTER, $data['requester']);
+ $this->assertSame('appr-1', $data['objectUuid']);
+ $this->assertSame('appr-1', $data['metadata']['hermiq']['approvalUuid']);
+ $this->assertSame('schedule', $data['metadata']['hermiq']['sourceType']);
+ $this->assertStringContainsString('agent-1', (string)$data['title']);
+ $this->assertStringContainsString('Summarise the week', (string)$data['description']);
+
+ return true;
+ }
+ ),
+ ApprovalTaskBridge::REQUESTER
+ )
+ ->willReturn($task);
+
+ $this->assertSame('task-1', $this->bridge()->ensureTaskFor(approval: $this->approval()));
+ }//end testEnsureTaskForImportsAssignedTaskForUserReviewer()
+
+ /**
+ * A group-reviewer approval imports an ENABLED task offered to the group.
+ *
+ * @return void
+ */
+ public function testEnsureTaskForOffersEnabledTaskToGroupReviewer(): void {
+ $task = new Task();
+ $task->setUuid('task-2');
+
+ $this->taskService->expects($this->once())
+ ->method('import')
+ ->with(
+ $this->callback(
+ function (array $data): bool {
+ $this->assertSame('enabled', $data['state']);
+ $this->assertNull($data['assignee']);
+ $this->assertSame(['reviewers'], $data['candidateGroups']);
+ $this->assertSame('group', $data['performerType']);
+
+ return true;
+ }
+ ),
+ ApprovalTaskBridge::REQUESTER
+ )
+ ->willReturn($task);
+
+ $uuid = $this->bridge()->ensureTaskFor(
+ approval: $this->approval(payload: ['reviewer' => 'reviewers', 'reviewerType' => 'group'])
+ );
+ $this->assertSame('task-2', $uuid);
+ }//end testEnsureTaskForOffersEnabledTaskToGroupReviewer()
+
+ /**
+ * An Approval already carrying a taskUuid is returned as-is: the task
+ * surface is never touched (the idempotency guard).
+ *
+ * @return void
+ */
+ public function testEnsureTaskForShortCircuitsOnExistingBacklink(): void {
+ $this->taskService->expects($this->never())->method('import');
+
+ $uuid = $this->bridge()->ensureTaskFor(
+ approval: $this->approval(payload: ['taskUuid' => 'task-existing'])
+ );
+ $this->assertSame('task-existing', $uuid);
+ }//end testEnsureTaskForShortCircuitsOnExistingBacklink()
+
+ /**
+ * A non-pending Approval is never mirrored.
+ *
+ * @return void
+ */
+ public function testEnsureTaskForRefusesNonPendingApproval(): void {
+ $this->taskService->expects($this->never())->method('import');
+
+ $this->assertNull(
+ $this->bridge()->ensureTaskFor(approval: $this->approval(payload: ['status' => 'approved']))
+ );
+ }//end testEnsureTaskForRefusesNonPendingApproval()
+
+ /**
+ * A throwing task surface degrades to null (logged), never to an
+ * exception on the approval write-path.
+ *
+ * @return void
+ */
+ public function testEnsureTaskForDegradesToNullWhenTheSurfaceThrows(): void {
+ $this->taskService->method('import')
+ ->willThrowException(new RuntimeException('no task tables yet'));
+
+ $this->assertNull($this->bridge()->ensureTaskFor(approval: $this->approval()));
+ }//end testEnsureTaskForDegradesToNullWhenTheSurfaceThrows()
+
+ /**
+ * A hermiq decision terminates the mirror as moot, naming the decision.
+ *
+ * @return void
+ */
+ public function testReleaseForTerminatesTheMirrorAsMoot(): void {
+ $this->taskService->expects($this->once())
+ ->method('terminateAsMoot')
+ ->with(
+ 'task-1',
+ $this->stringContains('approved by carol'),
+ ApprovalTaskBridge::RELEASE_SOURCE
+ )
+ ->willReturn(new Task());
+
+ $this->bridge()->releaseFor(
+ approval: $this->approval(payload: ['taskUuid' => 'task-1']),
+ decision: 'approved',
+ deciderUid: 'carol'
+ );
+ }//end testReleaseForTerminatesTheMirrorAsMoot()
+
+ /**
+ * An unmirrored Approval releases nothing.
+ *
+ * @return void
+ */
+ public function testReleaseForIsANoOpWithoutABacklink(): void {
+ $this->taskService->expects($this->never())->method('terminateAsMoot');
+
+ $this->bridge()->releaseFor(approval: $this->approval(), decision: 'denied', deciderUid: 'carol');
+ }//end testReleaseForIsANoOpWithoutABacklink()
+
+ /**
+ * A throwing release is logged, never raised: the hermiq decision stands.
+ *
+ * @return void
+ */
+ public function testReleaseForSwallowsAThrowingSurface(): void {
+ $this->taskService->method('terminateAsMoot')
+ ->willThrowException(new RuntimeException('gone'));
+
+ $this->bridge()->releaseFor(
+ approval: $this->approval(payload: ['taskUuid' => 'task-1']),
+ decision: 'approved',
+ deciderUid: 'carol'
+ );
+ // Reaching this line IS the assertion: nothing escaped.
+ $this->addToAssertionCount(1);
+ }//end testReleaseForSwallowsAThrowingSurface()
+}//end class
diff --git a/tests/Unit/Service/ApprovalServiceTest.php b/tests/Unit/Service/ApprovalServiceTest.php
index db255606..5d8d376b 100644
--- a/tests/Unit/Service/ApprovalServiceTest.php
+++ b/tests/Unit/Service/ApprovalServiceTest.php
@@ -28,6 +28,7 @@
namespace OCA\Hermiq\Tests\Unit\Service;
+use OCA\Hermiq\Service\Approval\ApprovalTaskBridge;
use OCA\Hermiq\Service\ApprovalService;
use OCA\Hermiq\Service\DeliveryResult;
use OCA\Hermiq\Service\DeliveryService;
@@ -83,6 +84,13 @@ class ApprovalServiceTest extends TestCase {
*/
private ContainerInterface $container;
+ /**
+ * Mock ApprovalTaskBridge (approval-task convergence seam).
+ *
+ * @var ApprovalTaskBridge&MockObject
+ */
+ private ApprovalTaskBridge $taskBridge;
+
/**
* Wire fresh mocks before each test.
*
@@ -104,6 +112,13 @@ protected function setUp(): void {
);
$this->container = $this->createMock(ContainerInterface::class);
+ // Approval-task convergence: by default the bridge reports "unmirrored"
+ // (null task uuid), which is exactly the degraded-install branch every
+ // pre-convergence expectation in this file describes: no second save,
+ // no release write.
+ $this->taskBridge = $this->createMock(ApprovalTaskBridge::class);
+ $this->taskBridge->method('ensureTaskFor')->willReturn(null);
+
}//end setUp()
/**
@@ -126,6 +141,7 @@ private function service(): ApprovalService {
new RedactionService($this->createMock(IConfig::class)),
$this->container,
$this->createMock(LoggerInterface::class),
+ $this->taskBridge,
);
}//end service()
@@ -1163,4 +1179,121 @@ public function testDenySkillDraftSourceTypeReconcilesTheDraft(): void {
$this->service()->deny($approval, 'bob', 'not convincing');
}//end testDenySkillDraftSourceTypeReconcilesTheDraft()
+
+ /**
+ * Approval-task convergence: creating a pending Approval mirrors it as an
+ * OpenRegister task and writes the mirror's uuid back as `taskUuid` in a
+ * second save (design.md Decision 2).
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+ public function testPendingApprovalCreationMirrorsAsTask(): void {
+ $this->objectService->method('findAll')->willReturn([]);
+
+ $saved = [];
+ $this->objectService->method('saveObject')->willReturnCallback(
+ function (array $object) use (&$saved): ObjectEntity {
+ $saved[] = $object;
+ $entity = new ObjectEntity();
+ $entity->setUuid('appr-new');
+ $entity->setObject($object);
+ return $entity;
+ }
+ );
+
+ $this->taskBridge = $this->createMock(ApprovalTaskBridge::class);
+ $this->taskBridge->expects($this->once())
+ ->method('ensureTaskFor')
+ ->willReturn('task-9');
+
+ $this->service()->ensurePendingApproval(
+ $this->schedule(['requiresApproval' => true, 'agentId' => 'agent-1', 'prompt' => 'p', 'reviewer' => 'bob', 'reviewerType' => 'user'])
+ );
+
+ $this->assertCount(2, $saved, 'The mirror uuid must be written back in a second save.');
+ $this->assertArrayNotHasKey('taskUuid', $saved[0], 'The first save predates the mirror.');
+ $this->assertSame('task-9', $saved[1]['taskUuid']);
+
+ }//end testPendingApprovalCreationMirrorsAsTask()
+
+ /**
+ * Approval-task convergence: an unmirrored creation (bridge answers null,
+ * the degraded-install branch) stays a single save with no taskUuid.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-every-pending-approval-is-mirrored-as-one-openregister-task
+ */
+ public function testUnmirroredCreationStaysSingleSave(): void {
+ $this->objectService->method('findAll')->willReturn([]);
+
+ $saved = [];
+ $this->objectService->method('saveObject')->willReturnCallback(
+ function (array $object) use (&$saved): ObjectEntity {
+ $saved[] = $object;
+ $entity = new ObjectEntity();
+ $entity->setUuid('appr-new');
+ $entity->setObject($object);
+ return $entity;
+ }
+ );
+
+ $this->service()->ensurePendingApproval(
+ $this->schedule(['requiresApproval' => true, 'agentId' => 'agent-1', 'prompt' => 'p', 'reviewer' => 'bob', 'reviewerType' => 'user'])
+ );
+
+ $this->assertCount(1, $saved);
+ $this->assertArrayNotHasKey('taskUuid', $saved[0]);
+
+ }//end testUnmirroredCreationStaysSingleSave()
+
+ /**
+ * Approval-task convergence: approve() releases the mirror BEFORE the
+ * resume, so a resume crash cannot leave a decided approval in an inbox.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-hermiq-decision-releases-the-mirror
+ */
+ public function testApproveReleasesTheMirror(): void {
+ $approval = $this->approval(['status' => 'pending', 'scheduleId' => 'sched-1', 'taskUuid' => 'task-9']);
+
+ $this->objectService->method('saveObject')->willReturn(new ObjectEntity());
+ $this->objectService->method('find')->willReturn($this->schedule(['agentId' => 'agent-1']));
+
+ $scheduleService = $this->createMock(ScheduleService::class);
+ $scheduleService->method('runNow');
+ $this->container->method('get')->willReturn($scheduleService);
+
+ $this->taskBridge = $this->createMock(ApprovalTaskBridge::class);
+ $this->taskBridge->expects($this->once())
+ ->method('releaseFor')
+ ->with($approval, 'approved', 'bob');
+
+ $this->service()->approve($approval, 'bob');
+
+ }//end testApproveReleasesTheMirror()
+
+ /**
+ * Approval-task convergence: deny() releases the mirror too.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/approval-task-convergence/specs/approval-task-convergence/spec.md#requirement-a-hermiq-decision-releases-the-mirror
+ */
+ public function testDenyReleasesTheMirror(): void {
+ $approval = $this->approval(['status' => 'pending', 'scheduleId' => 'sched-1', 'taskUuid' => 'task-9']);
+
+ $this->objectService->method('saveObject')->willReturn(new ObjectEntity());
+
+ $this->taskBridge = $this->createMock(ApprovalTaskBridge::class);
+ $this->taskBridge->expects($this->once())
+ ->method('releaseFor')
+ ->with($approval, 'denied', 'bob');
+
+ $this->service()->deny($approval, 'bob', 'no');
+
+ }//end testDenyReleasesTheMirror()
}//end class
diff --git a/tests/Unit/Service/Schedule/ScheduleFlowBridgeTest.php b/tests/Unit/Service/Schedule/ScheduleFlowBridgeTest.php
new file mode 100644
index 00000000..c5867074
--- /dev/null
+++ b/tests/Unit/Service/Schedule/ScheduleFlowBridgeTest.php
@@ -0,0 +1,836 @@
+
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @category Test
+ * @package OCA\Hermiq\Tests\Unit\Service\Schedule
+ *
+ * @author Conduction Development Team
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Hermiq\Tests\Unit\Service\Schedule;
+
+use OCA\Hermiq\Service\Schedule\ScheduleCadenceMapper;
+use OCA\Hermiq\Service\Schedule\ScheduleFlowBridge;
+use OCA\Hermiq\Service\Schedule\ScheduleFlowPublisher;
+use OCA\Hermiq\Service\Schedule\ScheduleMirrorDefinition;
+use OCA\Hermiq\Service\ScheduleService;
+use OCA\OpenRegister\Db\Flow;
+use OCA\OpenRegister\Db\FlowMapper;
+use OCA\OpenRegister\Db\FlowVersion;
+use OCA\OpenRegister\Db\ObjectEntity;
+use OCA\OpenRegister\Service\Flow\FlowVersionService;
+use OCA\OpenRegister\Service\ObjectService;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Bridge behaviour: mapping, identity, sync outcomes.
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+class ScheduleFlowBridgeTest extends TestCase {
+
+ /**
+ * Mock ObjectService.
+ *
+ * @var ObjectService&MockObject
+ */
+ private ObjectService $objectService;
+
+ /**
+ * Mock ScheduleService.
+ *
+ * @var ScheduleService&MockObject
+ */
+ private ScheduleService $scheduleService;
+
+ /**
+ * Mock FlowMapper handed out by the container.
+ *
+ * @var FlowMapper&MockObject
+ */
+ private FlowMapper $flowMapper;
+
+ /**
+ * Mock version service handed out by the container.
+ *
+ * Its publish() moves the flow to the published lifecycle, exactly as the
+ * real service and the stub do. That is the pin's contract: OpenRegister's
+ * `FlowRunVersionPin` refuses every scheduled dispatch of an unpublished
+ * flow, so the tests assert the flow's lifecycle, and only a real publish
+ * call can put it there. A fake whose publish did nothing would run
+ * unpublished flows and could never fail.
+ *
+ * @var FlowVersionService&MockObject
+ */
+ private FlowVersionService $flowVersions;
+
+ /**
+ * The version-service calls made, in order.
+ *
+ * @var array
+ */
+ private array $versionCalls = [];
+
+ /**
+ * When set, the version service's publish() throws it.
+ *
+ * @var \RuntimeException|null
+ */
+ private ?\RuntimeException $publishFailure = null;
+
+ /**
+ * Mock user manager.
+ *
+ * @var IUserManager&MockObject
+ */
+ private IUserManager $userManager;
+
+ /**
+ * Mock config.
+ *
+ * @var IConfig&MockObject
+ */
+ private IConfig $config;
+
+ /**
+ * Wire fresh mocks before each test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->objectService = $this->createMock(ObjectService::class);
+ $this->objectService->method('setRegister')->willReturnSelf();
+ $this->objectService->method('setSchema')->willReturnSelf();
+ $this->scheduleService = $this->createMock(ScheduleService::class);
+ $this->flowMapper = $this->createMock(FlowMapper::class);
+ $this->versionCalls = [];
+ $this->publishFailure = null;
+ $this->flowVersions = $this->createMock(FlowVersionService::class);
+ $this->flowVersions->method('publish')->willReturnCallback(
+ function (Flow $flow, ?string $publishedBy = null): FlowVersion {
+ $this->versionCalls[] = 'publish';
+ if ($this->publishFailure !== null) {
+ throw $this->publishFailure;
+ }
+
+ // The real service's observable effect: the head is published.
+ $flow->setLifecycleStatus(FlowVersion::STATUS_PUBLISHED);
+ $version = new FlowVersion();
+ $version->setStatus(FlowVersion::STATUS_PUBLISHED);
+
+ return $version;
+ }
+ );
+ $this->flowVersions->method('createDraft')->willReturnCallback(
+ function (Flow $flow): FlowVersion {
+ $this->versionCalls[] = 'createDraft';
+ $flow->setLifecycleStatus(FlowVersion::STATUS_DRAFT);
+ $version = new FlowVersion();
+ $version->setStatus(FlowVersion::STATUS_DRAFT);
+
+ return $version;
+ }
+ );
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ // Default: every uid is a live, enabled user.
+ $user = $this->createMock(IUser::class);
+ $user->method('isEnabled')->willReturn(true);
+ $this->userManager->method('get')->willReturn($user);
+
+ // Default: the owner's timezone equals the server default, so plain
+ // crons are timezone-safe unless a test says otherwise.
+ $this->config->method('getUserValue')->willReturn(date_default_timezone_get());
+ $this->config->method('getSystemValueString')->willReturn('UTC');
+ }//end setUp()
+
+ /**
+ * Build the bridge with the current mocks.
+ *
+ * @return ScheduleFlowBridge
+ */
+ private function bridge(): ScheduleFlowBridge {
+ $container = $this->createMock(ContainerInterface::class);
+ $container->method('get')->willReturnCallback(
+ function (string $id) {
+ if ($id === FlowVersionService::class) {
+ return $this->flowVersions;
+ }
+
+ return $this->flowMapper;
+ }
+ );
+
+ return new ScheduleFlowBridge(
+ objectService: $this->objectService,
+ scheduleService: $this->scheduleService,
+ cadenceMapper: new ScheduleCadenceMapper(config: $this->config),
+ container: $container,
+ publisher: new ScheduleFlowPublisher(container: $container),
+ definition: new ScheduleMirrorDefinition(),
+ userManager: $this->userManager,
+ logger: $this->createMock(LoggerInterface::class),
+ );
+ }//end bridge()
+
+ /**
+ * Build a schedule entity.
+ *
+ * @param array $payload The schedule body.
+ * @param string $uuid The uuid.
+ * @param string $owner The owner uid.
+ * @param string $organisation The organisation.
+ *
+ * @return ObjectEntity
+ */
+ private function schedule(
+ array $payload,
+ string $uuid = 'sched-1',
+ string $owner = 'alice',
+ string $organisation = 'org-1',
+ ): ObjectEntity {
+ $entity = new ObjectEntity();
+ $entity->setUuid($uuid);
+ $entity->setOwner($owner);
+ $entity->setOrganisation($organisation);
+ $entity->setObject($payload);
+
+ return $entity;
+ }//end schedule()
+
+ /**
+ * The shape map: crons pass through, intervals derive a cadence, once and
+ * inexpressible intervals answer null.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testMappedCronPerShape(): void {
+ $bridge = $this->bridge();
+
+ $this->assertSame(
+ '0 8 * * *',
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'cron', 'cronExpr' => '0 8 * * *']))
+ );
+ $this->assertNull(
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'cron', 'cronExpr' => '0 0 8 * * *'])),
+ 'A 6-field cron has no engine form.'
+ );
+ $this->assertSame(
+ '*/15 * * * *',
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'interval', 'intervalMinutes' => 15]))
+ );
+ $this->assertSame(
+ '0 * * * *',
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'interval', 'intervalMinutes' => 60]))
+ );
+ $this->assertSame(
+ '0 */2 * * *',
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'interval', 'intervalMinutes' => 120]))
+ );
+ $this->assertNull(
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'interval', 'intervalMinutes' => 90])),
+ '90 minutes has no 5-field cron form.'
+ );
+ $this->assertNull(
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'once', 'runAt' => '2030-01-01T00:00:00+00:00'])),
+ 'A deadline is FlowTimerService territory, staged.'
+ );
+
+ }//end testMappedCronPerShape()
+
+ /**
+ * An hour-anchored cron for an owner in another timezone stays local; a
+ * pure minute cadence migrates regardless of the timezone difference.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testTimezoneSensitiveCronStaysLocal(): void {
+ // Pick an owner timezone that cannot equal the server default.
+ $other = 'Pacific/Chatham';
+ if (date_default_timezone_get() === $other) {
+ $other = 'Europe/Amsterdam';
+ }
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->config->method('getUserValue')->willReturn($other);
+ $this->config->method('getSystemValueString')->willReturn('UTC');
+
+ $bridge = $this->bridge();
+
+ $this->assertNull(
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'cron', 'cronExpr' => '0 9 * * *'])),
+ 'An hour-anchored cron would silently shift; it stays local.'
+ );
+ $this->assertSame(
+ '*/5 * * * *',
+ $bridge->mappedCron(schedule: $this->schedule(['kind' => 'cron', 'cronExpr' => '*/5 * * * *'])),
+ 'A pure cadence survives any fixed offset.'
+ );
+
+ }//end testTimezoneSensitiveCronStaysLocal()
+
+ /**
+ * runAs is the resolved acting identity: the agent's live actingUser
+ * first, the owner otherwise, and null when nobody live resolves.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testResolveRunAsPrefersLiveActingUser(): void {
+ $agent = new ObjectEntity();
+ $agent->setUuid('agent-1');
+ $agent->setObject(['actingUser' => 'svc-runner']);
+ $this->objectService->method('find')->willReturn($agent);
+
+ $bridge = $this->bridge();
+
+ $this->assertSame(
+ 'svc-runner',
+ $bridge->resolveRunAs(schedule: $this->schedule(['agentId' => 'agent-1']))
+ );
+
+ }//end testResolveRunAsPrefersLiveActingUser()
+
+ /**
+ * When nobody live resolves, the schedule is ineligible instead of being
+ * mirrored with an identity the engine would refuse.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-a-mirrored-run-declares-and-re-enters-its-governed-identity
+ */
+ public function testNoLiveIdentityIsIneligible(): void {
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userManager->method('get')->willReturn(null);
+
+ $bridge = $this->bridge();
+ $schedule = $this->schedule(['kind' => 'interval', 'intervalMinutes' => 15, 'agentId' => '']);
+
+ $this->assertNull($bridge->resolveRunAs(schedule: $schedule));
+ $this->assertNotNull($bridge->ineligibilityReason(schedule: $schedule));
+
+ }//end testNoLiveIdentityIsIneligible()
+
+ /**
+ * An organisation-less schedule is never mirrored: the flow row would be
+ * invisible to every tenant forever (hermiq#140).
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testOrganisationLessScheduleIsIneligible(): void {
+ $bridge = $this->bridge();
+ $schedule = $this->schedule(
+ ['kind' => 'interval', 'intervalMinutes' => 15],
+ 'sched-1',
+ 'alice',
+ ''
+ );
+
+ $this->assertStringContainsString(
+ 'organisation',
+ (string)$bridge->ineligibilityReason(schedule: $schedule)
+ );
+
+ }//end testOrganisationLessScheduleIsIneligible()
+
+ /**
+ * The sync mirrors an eligible, unmirrored schedule: one flow row with the
+ * trigger's cron and runAs, the dispatch node naming the schedule, and the
+ * delegation marker written back.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testSyncMirrorsAnEligibleSchedule(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'cron',
+ 'cronExpr' => '0 8 * * *',
+ 'name' => 'Daily brief',
+ 'agentId' => '',
+ 'enabled' => true,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $inserted = null;
+ $this->flowMapper->method('insert')->willReturnCallback(
+ function (Flow $flow) use (&$inserted): Flow {
+ $inserted = $flow;
+ return $flow;
+ }
+ );
+
+ $markedFlowId = null;
+ $this->scheduleService->expects($this->once())
+ ->method('markEngineDelegation')
+ ->willReturnCallback(
+ function (ObjectEntity $s, string $flowId) use (&$markedFlowId): void {
+ $markedFlowId = $flowId;
+ }
+ );
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['mirrored']);
+ $this->assertNotNull($inserted, 'A flow row must be inserted.');
+ $this->assertSame('schedule', $inserted->getTrigger());
+ $this->assertSame('0 8 * * *', $inserted->getCron());
+ $this->assertSame('org-1', $inserted->getOrganisation(), 'An org-less row would be unreachable (hermiq#140).');
+ $this->assertSame($inserted->getUuid(), $markedFlowId, 'The marker must name the inserted flow.');
+
+ $nodes = $inserted->getNodes();
+ $this->assertSame('openregister.trigger-schedule', $nodes[0]['type']);
+ $this->assertSame('alice', $nodes[0]['config']['runAs'], 'The trigger declares the resolved identity.');
+ $this->assertSame('hermiq.schedule-dispatch', $nodes[1]['type']);
+ $this->assertSame('sched-1', $nodes[1]['config']['scheduleId']);
+ $this->assertSame('openregister.end', $nodes[2]['type']);
+
+ }//end testSyncMirrorsAnEligibleSchedule()
+
+ /**
+ * The mirror publishes its flow, and only then writes the marker.
+ *
+ * OpenRegister's FlowRunVersionPin refuses every scheduled dispatch of a
+ * flow with no published version, so a mirror that is merely inserted is
+ * a clock that never ticks. The lifecycle assertion is the pin's
+ * acceptance test: only a publish call can put the flow there.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testMirrorPublishesBeforeTheMarkerIsWritten(): void {
+ $schedule = $this->schedule(
+ ['kind' => 'cron', 'cronExpr' => '0 8 * * *', 'agentId' => '', 'enabled' => true]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $inserted = null;
+ $this->flowMapper->method('insert')->willReturnCallback(
+ function (Flow $flow) use (&$inserted): Flow {
+ $inserted = $flow;
+ return $flow;
+ }
+ );
+
+ $this->scheduleService->expects($this->once())
+ ->method('markEngineDelegation')
+ ->willReturnCallback(
+ function (): void {
+ $this->versionCalls[] = 'mark';
+ }
+ );
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['mirrored']);
+ $this->assertNotNull($inserted);
+ $this->assertSame(
+ FlowVersion::STATUS_PUBLISHED,
+ $inserted->getLifecycleStatus(),
+ 'The pin refuses an unpublished mirror; the bridge must publish what it creates.'
+ );
+ // Whether a createDraft precedes the publish depends on the Flow
+ // entity's birth state: the real entity is born draft (no createDraft
+ // needed), the standalone stub is born blank (drafted first). Both
+ // are correct; what this test pins is publish, then the marker.
+ $publishAndMark = array_values(
+ array_filter(
+ $this->versionCalls,
+ static fn (string $call): bool => in_array($call, ['publish', 'mark'], true)
+ )
+ );
+ $this->assertSame(
+ ['publish', 'mark'],
+ $publishAndMark,
+ 'Publish before the marker: a publish failure must leave the schedule on its local clock.'
+ );
+
+ }//end testMirrorPublishesBeforeTheMarkerIsWritten()
+
+ /**
+ * A publish failure unmirrors: no marker, the flow is taken back out,
+ * and the miss is counted. The schedule keeps its local clock instead of
+ * being delegated to a flow the pin refuses on every tick.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testMirrorPublishFailureLeavesTheScheduleLocal(): void {
+ $schedule = $this->schedule(
+ ['kind' => 'cron', 'cronExpr' => '0 8 * * *', 'agentId' => '', 'enabled' => true]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+ $this->publishFailure = new \RuntimeException('publish refused');
+
+ $this->scheduleService->expects($this->never())->method('markEngineDelegation');
+ $this->flowMapper->expects($this->once())->method('delete');
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['failed']);
+ $this->assertSame(0, $stats['mirrored']);
+
+ }//end testMirrorPublishFailureLeavesTheScheduleLocal()
+
+ /**
+ * A drifted mirror republishes: the changed cadence must not keep firing
+ * the old pinned version. Draft first, update second, publish last, so a
+ * crash in between is visible as an unpublished head the next pass heals.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testRefreshRepublishesAChangedCadence(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'cron',
+ 'cronExpr' => '0 9 * * *',
+ 'agentId' => '',
+ 'engineFlowId' => 'flow-1',
+ 'enabled' => true,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $flow = new Flow();
+ $flow->setUuid('flow-1');
+ $flow->setCron('0 8 * * *');
+ $flow->setEnabled(true);
+ $flow->setLifecycleStatus(FlowVersion::STATUS_PUBLISHED);
+ $flow->setNodes(
+ [
+ [
+ 'id' => 'trigger',
+ 'type' => 'openregister.trigger-schedule',
+ 'config' => ['cron' => '0 8 * * *', 'runAs' => 'alice'],
+ ],
+ ]
+ );
+ $this->flowMapper->method('findByUuid')->willReturn($flow);
+
+ $updateCalls = [];
+ $this->flowMapper->method('update')->willReturnCallback(
+ function (Flow $f) use (&$updateCalls): Flow {
+ $updateCalls[] = 'update';
+ return $f;
+ }
+ );
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['refreshed']);
+ $this->assertSame('0 9 * * *', $flow->getCron());
+ $this->assertSame(
+ ['createDraft', 'publish'],
+ $this->versionCalls,
+ 'The new definition must land as a NEW published version, drafted before the row update.'
+ );
+ $this->assertSame(
+ FlowVersion::STATUS_PUBLISHED,
+ $flow->getLifecycleStatus(),
+ 'After a refresh the head is published again; the engine fires the new cadence.'
+ );
+
+ }//end testRefreshRepublishesAChangedCadence()
+
+ /**
+ * An undrifted mirror whose head is not published gets published: this
+ * heals mirrors created before the bridge published (dead until a manual
+ * publish) and any crash between an update and its republish.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testUnpublishedUndriftedMirrorIsPublished(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'cron',
+ 'cronExpr' => '0 8 * * *',
+ 'agentId' => '',
+ 'engineFlowId' => 'flow-1',
+ 'enabled' => true,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $flow = new Flow();
+ $flow->setUuid('flow-1');
+ $flow->setCron('0 8 * * *');
+ $flow->setEnabled(true);
+ $flow->setLifecycleStatus(FlowVersion::STATUS_DRAFT);
+ $flow->setNodes(
+ [
+ [
+ 'id' => 'trigger',
+ 'type' => 'openregister.trigger-schedule',
+ 'config' => ['cron' => '0 8 * * *', 'runAs' => 'alice'],
+ ],
+ ]
+ );
+ $this->flowMapper->method('findByUuid')->willReturn($flow);
+ $this->flowMapper->expects($this->never())->method('update');
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['refreshed']);
+ $this->assertSame(
+ FlowVersion::STATUS_PUBLISHED,
+ $flow->getLifecycleStatus(),
+ 'A pre-fix mirror must be healed by the next sync pass, not left for a manual publish.'
+ );
+
+ }//end testUnpublishedUndriftedMirrorIsPublished()
+
+ /**
+ * A mirrored schedule edited into an inexpressible shape is retired:
+ * marker cleared first, flow deleted second.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testSyncRetiresAnIneligibleMirroredSchedule(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'once',
+ 'runAt' => '2030-01-01T00:00:00+00:00',
+ 'engineFlowId' => 'flow-1',
+ 'enabled' => true,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $order = [];
+ $this->scheduleService->expects($this->once())
+ ->method('clearEngineDelegation')
+ ->willReturnCallback(
+ function () use (&$order): void {
+ $order[] = 'clear';
+ }
+ );
+
+ $flow = new Flow();
+ $flow->setUuid('flow-1');
+ $this->flowMapper->method('findByUuid')->willReturn($flow);
+ $this->flowMapper->method('delete')->willReturnCallback(
+ function (Flow $f) use (&$order): Flow {
+ $order[] = 'delete';
+ return $f;
+ }
+ );
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['retired']);
+ $this->assertSame(['clear', 'delete'], $order, 'Marker first, flow second: no double clock on a crash.');
+
+ }//end testSyncRetiresAnIneligibleMirroredSchedule()
+
+ /**
+ * A mirrored, undrifted schedule is a no-op, which is what makes the
+ * repair step idempotent.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testSyncSkipsAnUndriftedMirror(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'cron',
+ 'cronExpr' => '0 8 * * *',
+ 'agentId' => '',
+ 'engineFlowId' => 'flow-1',
+ 'enabled' => true,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $flow = new Flow();
+ $flow->setUuid('flow-1');
+ $flow->setCron('0 8 * * *');
+ $flow->setEnabled(true);
+ $flow->setLifecycleStatus(FlowVersion::STATUS_PUBLISHED);
+ $flow->setNodes(
+ [
+ [
+ 'id' => 'trigger',
+ 'type' => 'openregister.trigger-schedule',
+ 'config' => ['cron' => '0 8 * * *', 'runAs' => 'alice'],
+ ],
+ ]
+ );
+ $this->flowMapper->method('findByUuid')->willReturn($flow);
+ $this->flowMapper->expects($this->never())->method('update');
+ $this->flowMapper->expects($this->never())->method('insert');
+ $this->scheduleService->expects($this->never())->method('markEngineDelegation');
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['skipped']);
+ $this->assertSame([], $this->versionCalls, 'A published, undrifted mirror needs no lifecycle calls.');
+
+ }//end testSyncSkipsAnUndriftedMirror()
+
+ /**
+ * A drifted mirror (schedule disabled) is refreshed onto the flow.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testSyncRefreshesADriftedMirror(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'cron',
+ 'cronExpr' => '0 8 * * *',
+ 'agentId' => '',
+ 'engineFlowId' => 'flow-1',
+ 'enabled' => false,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+
+ $flow = new Flow();
+ $flow->setUuid('flow-1');
+ $flow->setCron('0 8 * * *');
+ $flow->setEnabled(true);
+ $flow->setLifecycleStatus(FlowVersion::STATUS_PUBLISHED);
+ $flow->setNodes(
+ [
+ [
+ 'id' => 'trigger',
+ 'type' => 'openregister.trigger-schedule',
+ 'config' => ['cron' => '0 8 * * *', 'runAs' => 'alice'],
+ ],
+ ]
+ );
+ $this->flowMapper->method('findByUuid')->willReturn($flow);
+
+ $updated = null;
+ $this->flowMapper->method('update')->willReturnCallback(
+ function (Flow $f) use (&$updated): Flow {
+ $updated = $f;
+ return $f;
+ }
+ );
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['refreshed']);
+ $this->assertNotNull($updated);
+ $this->assertFalse($updated->getEnabled(), 'A paused schedule pauses its mirror.');
+ $this->assertSame(
+ FlowVersion::STATUS_PUBLISHED,
+ $flow->getLifecycleStatus(),
+ 'A refreshed mirror must end republished, or the engine keeps running the old pinned version.'
+ );
+
+ }//end testSyncRefreshesADriftedMirror()
+
+ /**
+ * A delegated schedule whose mirror flow row is GONE gets its clock back
+ * (marker cleared), so the local dispatcher covers it until the next
+ * pass re-mirrors.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testMissingMirrorFlowHandsTheClockBack(): void {
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'cron',
+ 'cronExpr' => '0 8 * * *',
+ 'agentId' => '',
+ 'engineFlowId' => 'flow-gone',
+ 'enabled' => true,
+ ]
+ );
+ $this->objectService->method('findAll')->willReturn([$schedule]);
+ $this->flowMapper->method('findByUuid')->willThrowException(
+ new \OCP\AppFramework\Db\DoesNotExistException('gone')
+ );
+ $this->scheduleService->expects($this->once())->method('clearEngineDelegation');
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['refreshed']);
+
+ }//end testMissingMirrorFlowHandsTheClockBack()
+
+ /**
+ * Deleting a flow that is already gone is the end state a delete wants:
+ * no error, no fallback disable.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testDeleteFlowTreatsAlreadyGoneAsDone(): void {
+ $this->flowMapper->method('findByUuid')->willThrowException(
+ new \OCP\AppFramework\Db\DoesNotExistException('gone')
+ );
+ $this->flowMapper->expects($this->never())->method('delete');
+ $this->flowMapper->expects($this->never())->method('update');
+
+ $this->bridge()->deleteFlow(flowId: 'flow-gone');
+
+ }//end testDeleteFlowTreatsAlreadyGoneAsDone()
+
+ /**
+ * A schedule-store read failure is counted and never thrown: the tick
+ * that called the sync must still dispatch locally.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testScheduleReadFailureIsCountedNotThrown(): void {
+ $this->objectService->method('findAll')->willThrowException(new \RuntimeException('db gone'));
+
+ $stats = $this->bridge()->syncAll();
+
+ $this->assertSame(1, $stats['failed']);
+ $this->assertSame(0, $stats['mirrored']);
+
+ }//end testScheduleReadFailureIsCountedNotThrown()
+}//end class
diff --git a/tests/Unit/Service/ScheduleServiceTest.php b/tests/Unit/Service/ScheduleServiceTest.php
index 45b4b328..12758b32 100644
--- a/tests/Unit/Service/ScheduleServiceTest.php
+++ b/tests/Unit/Service/ScheduleServiceTest.php
@@ -3443,4 +3443,118 @@ function (array $object, ?array $extend = [], mixed $register = null, mixed $sch
$this->assertSame('go do the thing', $this->auditCalls[0]['context']['prompt']);
}//end testRealRunAuditEntryPersistsPromptAndDryRunFalse()
+
+ /**
+ * A schedule whose clock is delegated to the engine (engineFlowId set) is
+ * never selected as due by nextRun: the tick dispatches nothing for it.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testDelegatedScheduleIsNotDueByNextRun(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [
+ $this->schedule(
+ [
+ 'kind' => 'interval',
+ 'intervalMinutes' => 15,
+ 'agentId' => 'agent-uuid',
+ 'prompt' => 'go',
+ 'deliver' => 'none',
+ 'enabled' => true,
+ 'nextRun' => '2000-01-01T00:00:00+00:00',
+ 'engineFlowId' => 'flow-owns-this-clock',
+ 'repeat' => ['times' => 0, 'completed' => 0],
+ ]
+ ),
+ ]
+ );
+
+ $this->objectService->expects($this->never())->method('saveObject');
+ $this->chatService->expects($this->never())->method('processMessage');
+
+ $this->service->run();
+
+ }//end testDelegatedScheduleIsNotDueByNextRun()
+
+ /**
+ * Delegation never drops an open retry sequence: a delegated schedule with
+ * a due retryState still fires from the local tick.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-eligible-schedules-delegate-their-clock-to-the-engine
+ */
+ public function testDelegatedScheduleWithDueRetryStillFires(): void {
+ $this->objectService->method('findAll')->willReturn(
+ [
+ $this->schedule(
+ [
+ 'kind' => 'interval',
+ 'intervalMinutes' => 15,
+ 'agentId' => 'agent-uuid',
+ 'prompt' => 'go',
+ 'deliver' => 'none',
+ 'enabled' => true,
+ 'nextRun' => '2999-01-01T00:00:00+00:00',
+ 'engineFlowId' => 'flow-owns-this-clock',
+ 'retryEnabled' => true,
+ 'retryState' => ['attempt' => 1, 'nextAttemptAt' => '2000-01-01T00:00:00+00:00'],
+ 'repeat' => ['times' => 0, 'completed' => 0],
+ ]
+ ),
+ ]
+ );
+
+ $saves = 0;
+ $this->objectService->method('saveObject')->willReturnCallback(
+ function () use (&$saves): ObjectEntity {
+ $saves++;
+ return new ObjectEntity();
+ }
+ );
+
+ $this->service->run();
+
+ $this->assertGreaterThan(0, $saves, 'A due retry on a delegated schedule must still dispatch.');
+
+ }//end testDelegatedScheduleWithDueRetryStillFires()
+
+ /**
+ * The delegation marker round-trips through the sanitised persist:
+ * markEngineDelegation() stores the flow uuid, clearEngineDelegation()
+ * empties it, and both go through saveObject.
+ *
+ * @return void
+ *
+ * @spec openspec/changes/schedules-onto-engine-triggers/specs/schedule-engine-delegation/spec.md#requirement-in-flight-schedules-migrate-and-roll-back
+ */
+ public function testMarkAndClearEngineDelegationPersistTheMarker(): void {
+ $saved = [];
+ $this->objectService->method('saveObject')->willReturnCallback(
+ function (array $object) use (&$saved): ObjectEntity {
+ $saved[] = $object;
+ return new ObjectEntity();
+ }
+ );
+
+ $schedule = $this->schedule(
+ [
+ 'kind' => 'interval',
+ 'intervalMinutes' => 15,
+ 'agentId' => 'agent-uuid',
+ 'deliver' => 'none',
+ 'enabled' => true,
+ ]
+ );
+
+ $this->service->markEngineDelegation(schedule: $schedule, flowId: ' flow-1 ');
+ $this->service->clearEngineDelegation(schedule: $schedule);
+
+ $this->assertCount(2, $saved);
+ $this->assertSame('flow-1', $saved[0]['engineFlowId'], 'The marker must store the trimmed flow uuid.');
+ $this->assertSame('', $saved[1]['engineFlowId'], 'Clearing must empty the marker.');
+
+ }//end testMarkAndClearEngineDelegationPersistTheMarker()
}//end class
diff --git a/tests/Unit/Settings/AgentFlowRetirementTest.php b/tests/Unit/Settings/AgentFlowRetirementTest.php
new file mode 100644
index 00000000..7ccad931
--- /dev/null
+++ b/tests/Unit/Settings/AgentFlowRetirementTest.php
@@ -0,0 +1,143 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * @link https://conduction.nl
+ */
+
+namespace OCA\Hermiq\Tests\Unit\Settings;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversNothing Guards declarative register files and info.xml, not a PHP class.
+ *
+ * @spec openspec/specs/flow-authoring/spec.md#requirement-a-flow-is-stored-once-by-openregister-req-fa-002
+ */
+class AgentFlowRetirementTest extends TestCase {
+
+ /**
+ * The retired slugs, compared case-insensitively: a slug rename hides in
+ * casing, so `AgentFlow` and `agentflow` are the same claim.
+ *
+ * @var string[]
+ */
+ private const RETIRED_SLUGS = [
+ 'agentflow',
+ 'agentflowrun',
+ ];
+
+ /**
+ * A parsed JSON file from lib/Settings.
+ *
+ * @param string $name The file name.
+ *
+ * @return array The parsed document.
+ */
+ private function settingsJson(string $name): array {
+ $path = __DIR__ . '/../../../lib/Settings/' . $name;
+ $this->assertFileExists($path);
+
+ $document = json_decode((string)file_get_contents($path), true);
+ $this->assertIsArray($document, $name . ' must be valid JSON.');
+
+ return $document;
+ }//end settingsJson()
+
+ /**
+ * The register descriptor declares neither retired schema: not as a
+ * component and not in the register's schema list.
+ *
+ * @return void
+ */
+ public function testRegisterDescriptorCarriesNoAgentFlowSchema(): void {
+ $register = $this->settingsJson('hermiq_register.json');
+
+ foreach (array_keys((array)($register['components']['schemas'] ?? [])) as $componentName) {
+ $this->assertNotContains(
+ strtolower((string)$componentName),
+ self::RETIRED_SLUGS,
+ 'The retired ' . $componentName . ' schema must stay out of the descriptor: '
+ . 'the next version bump would import it and re-open the retired store.'
+ );
+ }
+
+ $slugs = array_map(
+ static fn ($slug): string => strtolower((string)$slug),
+ (array)($register['components']['registers']['hermiq']['schemas'] ?? [])
+ );
+ foreach (self::RETIRED_SLUGS as $retired) {
+ $this->assertNotContains($retired, $slugs, 'The register list must not claim "' . $retired . '".');
+ }
+ }//end testRegisterDescriptorCarriesNoAgentFlowSchema()
+
+ /**
+ * The demo data seeds no objects into the retired schemas: a row written
+ * there is unreadable — the store has no runner, resolver or frontend.
+ *
+ * @return void
+ */
+ public function testMockRegisterSeedsNoAgentFlowObjects(): void {
+ $mock = $this->settingsJson('hermiq_mock_register.json');
+
+ $objects = (array)($mock['components']['objects'] ?? []);
+ $this->assertNotEmpty($objects, 'The demo data must still seed objects for the live schemas.');
+
+ foreach ($objects as $object) {
+ $schema = strtolower((string)($object['@self']['schema'] ?? ''));
+ $this->assertNotContains(
+ $schema,
+ self::RETIRED_SLUGS,
+ 'Demo object "' . (string)($object['@self']['slug'] ?? '?') . '" seeds the retired "'
+ . $schema . '" schema, which no code can read.'
+ );
+ }
+ }//end testMockRegisterSeedsNoAgentFlowObjects()
+
+ /**
+ * The prune runs on upgrade. Without this registration the descriptor
+ * edit is only half the retirement: the import unions schema ids and
+ * never removes one, so existing installs keep both schemas forever.
+ *
+ * @return void
+ */
+ public function testThePruneStepIsRegisteredForUpgrades(): void {
+ $path = __DIR__ . '/../../../appinfo/info.xml';
+ $this->assertFileExists($path);
+
+ $info = (string)file_get_contents($path);
+ $this->assertStringContainsString(
+ 'OCA\Hermiq\Repair\PruneRetiredAgentFlowSchemas',
+ $info,
+ 'PruneRetiredAgentFlowSchemas must be registered as a repair step.'
+ );
+ }//end testThePruneStepIsRegisteredForUpgrades()
+}//end class
diff --git a/tests/e2e/agent-detail-and-chrome.spec.ts b/tests/e2e/agent-detail-and-chrome.spec.ts
index 576291f6..ff3bfb77 100644
--- a/tests/e2e/agent-detail-and-chrome.spec.ts
+++ b/tests/e2e/agent-detail-and-chrome.spec.ts
@@ -34,7 +34,9 @@
* @spec openspec/specs/manifest-driven-pages/spec.md
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/app-chrome.spec.ts b/tests/e2e/app-chrome.spec.ts
new file mode 100644
index 00000000..167aa253
--- /dev/null
+++ b/tests/e2e/app-chrome.spec.ts
@@ -0,0 +1,165 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ *
+ * The bottom-left app chrome, in a browser (ADR-114).
+ *
+ * gate-107 reads the manifest and can prove the entries are DECLARED. It
+ * cannot prove they RENDER, and this programme has produced three defects of
+ * exactly that shape: an unregistered icon name renders NO glyph (no fallback,
+ * no console error — ChartBoxOutline had to be added to src/icons.js for this
+ * very entry), an entry whose `route` names a page the app does not host
+ * renders a row that goes nowhere, and `nav.includePersonalSettings: false`
+ * silently removed the entry reaching the user's notification preferences in
+ * two apps.
+ *
+ * ⚠️ SCOPE EVERY SELECTOR TO `[data-testid="cn-nav"]`. An unscoped selector
+ * also matches Nextcloud's own user menu, which is attached-but-hidden:
+ * `waitFor({state:'attached'})` passes on it and the click never becomes
+ * actionable, so the spec fails with "Target page has been closed" — a timeout
+ * wearing a crash's clothes.
+ *
+ * ⚠️ SETTINGS ENTRIES ARE ATTACHED, NOT VISIBLE, inside a collapsed foldout.
+ *
+ * The config supplies `use.storageState`, so specs start signed in.
+ */
+
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
+
+const APP_BASE = '/index.php/apps/hermiq'
+
+/**
+ * Dismiss the first-run setup wizard if it is open.
+ *
+ * ⚠️ On a FRESH instance CnSetupWizard opens over the app and its modal
+ * intercepts pointer events, so every nav click resolves its locator and then
+ * times out after 30s — a failure that reads like the navigation is broken.
+ * Tests that navigate by URL pass, which is what makes this so easy to miss:
+ * only the click-through tests fail, and only on a clean install.
+ *
+ * @param page The page.
+ */
+async function dismissSetupWizard(page: Page): Promise {
+ const modal = page.locator('[data-testid="cn-modal"]')
+ if ((await modal.count()) === 0) {
+ return
+ }
+ await modal.first().getByRole('button', { name: 'Close' }).click()
+ await expect(modal).toHaveCount(0, { timeout: 15_000 })
+}
+
+test.describe('app chrome (ADR-114)', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`${APP_BASE}/`, { waitUntil: 'domcontentloaded' })
+ await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible({
+ timeout: 30_000,
+ })
+ await dismissSetupWizard(page)
+ })
+
+ test('the footer reads Documentation, Store, Reports, Features & roadmap, each with a glyph', async ({
+ page,
+ }) => {
+ const footer = page.locator(
+ '[data-testid="cn-nav"] .cn-app-nav__footer-list',
+ )
+ await expect(footer).toBeAttached({ timeout: 15_000 })
+
+ const rows = footer.locator('li')
+ const texts = (await rows.allInnerTexts())
+ .map((t) => t.trim())
+ .filter(Boolean)
+
+ // Hermiq is one of only four apps that had a Store before this
+ // programme, so its footer carries all four declared chrome items.
+ // ORDER is the rule, not the numbers.
+ const seen = texts.filter((t) =>
+ /Documentation|Store|Reports|roadmap/i.test(t),
+ )
+ expect(seen.length).toBe(4)
+ expect(seen[0]).toMatch(/Documentation/i)
+ expect(seen[1]).toMatch(/Store/i)
+ expect(seen[2]).toMatch(/Reports/i)
+ expect(seen[3]).toMatch(/roadmap/i)
+
+ for (const row of await rows.all()) {
+ await expect(
+ row.locator('svg, .material-design-icon').first(),
+ ).toBeAttached()
+ }
+ })
+
+ test('AI oversight is a card on Reports, not a settings entry', async ({
+ page,
+ }) => {
+ const nav = page.locator('[data-testid="cn-nav"]')
+
+ // It sat in the SETTINGS foldout, which is where configuration belongs
+ // (ADR-044 Decision 3). Oversight is not configuration — it is a reading
+ // of what the agents did.
+ await expect(
+ nav.locator('[data-testid="cn-nav-entry-AiOversightMenu"]'),
+ ).toHaveCount(0)
+
+ await nav
+ .locator('[data-testid="cn-nav-entry-ReportsMenu"] a')
+ .first()
+ .click()
+ await expect(page).toHaveURL(/\/apps\/hermiq\/reports(\?|$)/, {
+ timeout: 15_000,
+ })
+ await expect(
+ page.getByText('AI oversight', { exact: false }).first(),
+ ).toBeVisible({ timeout: 15_000 })
+ })
+
+ test('the AI oversight page is still routable, because another app deep-links it', async ({
+ page,
+ }) => {
+ // dossiq declares /apps/hermiq/ai-oversight as an ADR-110 integrations
+ // entry. Retiring the menu entry must not take the ROUTE with it, or
+ // that link 404s from another app (ADR-044 Decision 5).
+ await page.goto(`${APP_BASE}/ai-oversight`)
+ await expect(page).toHaveURL(/\/ai-oversight(\?|$)/, { timeout: 15_000 })
+ await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible()
+ })
+
+ test('guardrails, the algorithm register and compliance stay in the foldout', async ({
+ page,
+ }) => {
+ // Deliberate asymmetry with AI oversight: these ARE configuration, so
+ // ADR-044 Decision 3 keeps them where they are. A later sweep that cards
+ // them fails here rather than passing review.
+ const nav = page.locator('[data-testid="cn-nav"]')
+ for (const id of ['GuardrailPolicy', 'AlgorithmRegister', 'Compliance']) {
+ await expect(
+ nav.locator(`[data-testid="cn-nav-entry-${id}"]`),
+ ).toBeAttached({ timeout: 15_000 })
+ }
+ })
+
+ test('the settings foldout carries Personal settings, Admin settings and Flows', async ({
+ page,
+ }) => {
+ const nav = page.locator('[data-testid="cn-nav"]')
+
+ await expect(nav.locator('[data-testid="cn-nav-settings"]')).toBeAttached({
+ timeout: 15_000,
+ })
+ await expect(
+ nav.locator('[data-testid="cn-nav-personal-settings"]'),
+ ).toBeAttached()
+ await expect(
+ nav.locator('[data-testid="cn-nav-entry-FlowIndex"]'),
+ ).toBeAttached()
+
+ const admin = nav.locator('[data-testid="cn-nav-admin-settings"]')
+ await expect(admin).toBeAttached()
+ await expect(admin.locator('a').first()).toHaveAttribute(
+ 'href',
+ /\/settings\/admin\/hermiq$/,
+ )
+ })
+})
diff --git a/tests/e2e/companion-everywhere.spec.ts b/tests/e2e/companion-everywhere.spec.ts
index 17aa8573..f9d1ac45 100644
--- a/tests/e2e/companion-everywhere.spec.ts
+++ b/tests/e2e/companion-everywhere.spec.ts
@@ -20,7 +20,9 @@
* @spec openspec/changes/companion-everywhere/specs/companion-everywhere/spec.md
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/dashboard-and-agents.spec.ts b/tests/e2e/dashboard-and-agents.spec.ts
index ff1c827e..c4948cc5 100644
--- a/tests/e2e/dashboard-and-agents.spec.ts
+++ b/tests/e2e/dashboard-and-agents.spec.ts
@@ -21,7 +21,9 @@
* openspec/specs/agent-management-ui/spec.md (Agents list renders).
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
@@ -74,13 +76,11 @@ function collectConsoleErrors(page: Page): string[] {
) {
return
}
- // The chat-health probe answers a DESIGNED 503 ({"status":"no_provider"})
- // on instances without an LLM provider configured (e.g. disposable e2e
- // instances); the browser logs that as a resource error, but it is an
- // expected response, not an app failure.
- if ((msg.location()?.url || '').includes('/api/chat/health')) {
- return
- }
+ // The chat-health probe used to answer a designed 503 on instances
+ // without an LLM provider, which forced a carve-out here. It answers
+ // 200 {status:"unconfigured"} now, so a 503 from it means the app is
+ // broken (config_error) and rightly fails this sweep.
+ //
// 🔴 Only hermiq's own failures may fail a hermiq regression test.
//
// Nextcloud's Dashboard hosts every installed app's widgets, so a shared
diff --git a/tests/e2e/docs-screenshots.spec.ts b/tests/e2e/docs-screenshots.spec.ts
index 0be072db..8c52d47f 100644
--- a/tests/e2e/docs-screenshots.spec.ts
+++ b/tests/e2e/docs-screenshots.spec.ts
@@ -27,9 +27,11 @@
* Pattern reference: ADR-030 (hydra/openspec/architecture/).
*/
-import { test, type Page } from '@playwright/test'
-import * as path from 'path'
+import type { Page } from '@playwright/test'
+
+import { test } from '@playwright/test'
import * as fs from 'fs'
+import * as path from 'path'
const SHOT_ROOT = path.resolve(
__dirname,
diff --git a/tests/e2e/flow-execution.spec.ts b/tests/e2e/flow-execution.spec.ts
index af44da54..bfd4b3b3 100644
--- a/tests/e2e/flow-execution.spec.ts
+++ b/tests/e2e/flow-execution.spec.ts
@@ -38,7 +38,9 @@
* @spec openspec/specs/flow-canvas/spec.md
*/
-import { test, expect, type APIRequestContext, type Page } from '@playwright/test'
+import type { APIRequestContext, Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
@@ -337,7 +339,7 @@ test.describe('hermiq regression: the engine runs a flow from the browser', () =
// reads the row mid-flight and reports `running` as a failure — against
// an engine that is working.
const terminal = ['stopped', 'completed', 'failed']
- let runs: Record | null = null
+ let runs: Record | null
let list: Array> = []
let latest: Record | undefined
diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts
index 66f7d724..12ba0707 100644
--- a/tests/e2e/global-setup.ts
+++ b/tests/e2e/global-setup.ts
@@ -12,10 +12,12 @@
* status.php health poll). Pattern reference: ADR-030.
*/
-import { chromium, request, type FullConfig } from '@playwright/test'
+import type { FullConfig } from '@playwright/test'
+
+import { chromium, request } from '@playwright/test'
import { execSync } from 'child_process'
-import * as path from 'path'
import * as fs from 'fs'
+import * as path from 'path'
const AUTH_DIR = path.resolve(__dirname, '.auth')
const STORAGE_STATE = path.join(AUTH_DIR, 'admin.json')
@@ -38,7 +40,7 @@ function ensureBundleBuilt(): void {
if (fs.existsSync(BUNDLE_PATH)) {
return
}
- // eslint-disable-next-line no-console
+
console.log(
`[playwright globalSetup] bundle missing at ${BUNDLE_PATH}; running 'npm run build' once…`,
)
@@ -89,7 +91,7 @@ async function ensureNextcloudReachable(baseURL: string): Promise {
} catch (err) {
last = `request failed: ${(err as Error).message}`
}
- // eslint-disable-next-line no-await-in-loop
+
await new Promise((resolve) => setTimeout(resolve, 5_000))
}
throw new Error(
@@ -218,7 +220,7 @@ export default async function globalSetup(config: FullConfig): Promise {
await page.evaluate(() => {
try {
window.localStorage.setItem('cn-walkthrough-seen:hermiq', '999.0.0')
- } catch (e) {
+ } catch {
// localStorage unavailable — specs fall back to dismissing by hand.
}
})
diff --git a/tests/e2e/l10n-browser-catalogue.spec.ts b/tests/e2e/l10n-browser-catalogue.spec.ts
index 047d6072..1197e6aa 100644
--- a/tests/e2e/l10n-browser-catalogue.spec.ts
+++ b/tests/e2e/l10n-browser-catalogue.spec.ts
@@ -39,11 +39,10 @@
* nothing about apps whose translations differ.
*/
+import { expect, test } from '@playwright/test'
import { readFileSync } from 'node:fs'
import path from 'node:path'
-import { expect, test } from '@playwright/test'
-
/**
* The app id this repo declares. Resolved from the repo root by walking up
* from this file, so it does not depend on the working directory playwright
diff --git a/tests/e2e/skill-bundle.spec.ts b/tests/e2e/skill-bundle.spec.ts
index 2b2afdd9..cc45e117 100644
--- a/tests/e2e/skill-bundle.spec.ts
+++ b/tests/e2e/skill-bundle.spec.ts
@@ -26,7 +26,9 @@
* @e2e skills-marketplace::a-crafted-skill-name-cannot-escape-the-bundle
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/skill-evals.spec.ts b/tests/e2e/skill-evals.spec.ts
index ac0b49b6..97207839 100644
--- a/tests/e2e/skill-evals.spec.ts
+++ b/tests/e2e/skill-evals.spec.ts
@@ -37,7 +37,9 @@
* state that deliberately never changes.
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/skill-learnings.spec.ts b/tests/e2e/skill-learnings.spec.ts
index 47540ac1..96387749 100644
--- a/tests/e2e/skill-learnings.spec.ts
+++ b/tests/e2e/skill-learnings.spec.ts
@@ -22,7 +22,9 @@
* @e2e skill-learnings::promotion-alone-does-not-grant-l6
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/skill-maturity.spec.ts b/tests/e2e/skill-maturity.spec.ts
index bf14994c..26273a69 100644
--- a/tests/e2e/skill-maturity.spec.ts
+++ b/tests/e2e/skill-maturity.spec.ts
@@ -19,7 +19,9 @@
* (catalog dots, Qualify row action, SkillDetail scorecard, fresh-install maturity spread).
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/skill-multifile-install.spec.ts b/tests/e2e/skill-multifile-install.spec.ts
index dec81103..13157931 100644
--- a/tests/e2e/skill-multifile-install.spec.ts
+++ b/tests/e2e/skill-multifile-install.spec.ts
@@ -27,7 +27,9 @@
* @e2e skills-marketplace::a-dangerous-payload-hidden-in-an-auxiliary-file-is-caught
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/skill-provenance.spec.ts b/tests/e2e/skill-provenance.spec.ts
index 836af0d9..b0e90343 100644
--- a/tests/e2e/skill-provenance.spec.ts
+++ b/tests/e2e/skill-provenance.spec.ts
@@ -31,7 +31,9 @@
* @e2e skills-marketplace::the-learnings-notice-appears-only-when-learnings-are-ahead-of-the-source
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/skill-self-improvement.spec.ts b/tests/e2e/skill-self-improvement.spec.ts
index c9091b6d..782072a9 100644
--- a/tests/e2e/skill-self-improvement.spec.ts
+++ b/tests/e2e/skill-self-improvement.spec.ts
@@ -60,7 +60,9 @@
* @e2e openspec/specs/skills-marketplace/spec.md#publish-refuses-to-overwrite-an-existing-repository
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/spec-coverage/_fixtures.ts b/tests/e2e/spec-coverage/_fixtures.ts
index 2d34fef4..6896ca84 100644
--- a/tests/e2e/spec-coverage/_fixtures.ts
+++ b/tests/e2e/spec-coverage/_fixtures.ts
@@ -21,7 +21,9 @@
* never collide.
*/
-import { type APIRequestContext, type Page, expect } from '@playwright/test'
+import type { APIRequestContext, Page } from '@playwright/test'
+
+import { expect } from '@playwright/test'
/** Shared family prefix for ALL spec-coverage artefacts (every run). */
export const TEST_FAMILY = 'e2espec-'
@@ -76,7 +78,6 @@ export async function appRoot(page: Page): Promise {
await page.goto('/index.php/apps/hermiq/', { waitUntil: 'domcontentloaded' })
const base = await page.evaluate(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const oc = (window as any).OC
if (typeof oc?.generateUrl === 'function') {
return String(oc.generateUrl('/apps/hermiq'))
@@ -165,7 +166,6 @@ export async function harvestToken(page: Page): Promise {
// and this only needs a hermiq page to read the token off — not the router.
await page.goto('/index.php/apps/hermiq/', { waitUntil: 'domcontentloaded' })
const token = await page.evaluate(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
() =>
(window as any).OC?.requestToken
|| document.head
@@ -356,12 +356,10 @@ export async function cleanupFamily(
Array.isArray(body) ? body : (body.results ?? body.data ?? [])
) as Array>
for (const obj of list) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const name = String(obj.name ?? (obj as any)['@self']?.name ?? '')
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
const id = String(obj.id ?? (obj as any)['@self']?.id ?? '')
if (name.startsWith(TEST_FAMILY) && id !== '') {
- // eslint-disable-next-line no-await-in-loop
await deleteObject(req, token, schema, id)
}
}
@@ -442,9 +440,9 @@ export function collectHermiqConsoleErrors(page: Page): string[] {
) {
return
}
- if ((msg.location()?.url || '').includes('/api/chat/health')) {
- return
- }
+ // No chat/health carve-out any more: the probe answers 200
+ // {status:"unconfigured"} on a providerless instance, so a console
+ // error from it means the app is broken and belongs in the sweep.
const source = `${msg.location()?.url || ''} ${text}`
const foreignApp =
source.match(/\/custom_apps\/([^/]+)\//)?.[1]
diff --git a/tests/e2e/spec-coverage/agents-approvals.spec.ts b/tests/e2e/spec-coverage/agents-approvals.spec.ts
index ad1de256..e07c0272 100644
--- a/tests/e2e/spec-coverage/agents-approvals.spec.ts
+++ b/tests/e2e/spec-coverage/agents-approvals.spec.ts
@@ -29,16 +29,16 @@
* Seeding: OpenRegister objects API via _fixtures (register 'hermiq').
*/
-import { test, expect } from '@playwright/test'
+import { expect, test } from '@playwright/test'
import {
- TEST_PREFIX,
appRoot,
cleanupFamily,
dismissTour,
harvestToken,
resolveRegisterSchema,
seedAgent,
-} from './_fixtures'
+ TEST_PREFIX,
+} from './_fixtures.ts'
/*
* 🔴 Every navigation below goes through `appRoot(page)` rather than a literal
diff --git a/tests/e2e/spec-coverage/chat.spec.ts b/tests/e2e/spec-coverage/chat.spec.ts
index c0bd667d..b0bc4054 100644
--- a/tests/e2e/spec-coverage/chat.spec.ts
+++ b/tests/e2e/spec-coverage/chat.spec.ts
@@ -27,9 +27,10 @@
* Auth: shared storageState session (tests/e2e/global-setup.ts).
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
import {
- TEST_PREFIX,
appRoot,
cleanupFamily,
dismissTour,
@@ -37,7 +38,8 @@ import {
jsonHeaders,
resolveRegisterSchema,
seedAgent,
-} from './_fixtures'
+ TEST_PREFIX,
+} from './_fixtures.ts'
/**
* Collect app-level console errors, filtering known benign noise.
diff --git a/tests/e2e/spec-coverage/flow-seed.spec.ts b/tests/e2e/spec-coverage/flow-seed.spec.ts
index 188608c8..81c7263e 100644
--- a/tests/e2e/spec-coverage/flow-seed.spec.ts
+++ b/tests/e2e/spec-coverage/flow-seed.spec.ts
@@ -21,8 +21,8 @@
* agentflow.
*/
-import { test, expect } from '@playwright/test'
-import { harvestToken, jsonHeaders } from './_fixtures'
+import { expect, test } from '@playwright/test'
+import { harvestToken, jsonHeaders } from './_fixtures.ts'
/** Where the repair step records how it ended. */
const OUTCOME_KEY = 'hydra_triage_flow_seed'
@@ -36,12 +36,7 @@ const DETAIL_KEY = 'hydra_triage_flow_seed_detail'
* @param key The app-config key.
* @return The stored string, or '' when unset.
*/
-async function appConfig(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- request: any,
- token: string,
- key: string,
-): Promise {
+async function appConfig(request: any, token: string, key: string): Promise {
const res = await request.get(
`/ocs/v2.php/apps/provisioning_api/api/v1/config/apps/hermiq/${key}?format=json`,
{ headers: jsonHeaders(token) },
diff --git a/tests/e2e/spec-coverage/hydra-console-agent-leaves.spec.ts b/tests/e2e/spec-coverage/hydra-console-agent-leaves.spec.ts
index 3f5eb60e..eeb70dce 100644
--- a/tests/e2e/spec-coverage/hydra-console-agent-leaves.spec.ts
+++ b/tests/e2e/spec-coverage/hydra-console-agent-leaves.spec.ts
@@ -36,8 +36,10 @@
* npx playwright test --project chromium hydra-console-agent-leaves
*/
-import { test, expect, type APIRequestContext } from '@playwright/test'
-import { harvestToken, jsonHeaders, OR_API } from './_fixtures'
+import type { APIRequestContext } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
+import { harvestToken, jsonHeaders, OR_API } from './_fixtures.ts'
/** The seeded triage agent's name — also its idempotency key (SeedHydraTriageAgent). */
const AGENT_NAME = 'Hydra Triage'
@@ -66,7 +68,6 @@ async function objectsNamed(
token: string,
schema: string,
name: string,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise {
// `_limit`, underscore-prefixed: OpenRegister control params require the
// prefix — a bare `limit` is treated as a PROPERTY filter and silently
@@ -79,7 +80,7 @@ async function objectsNamed(
const rows = Array.isArray(body) ? body : (body.results ?? body.data ?? [])
// OR returns an ENVELOPE ({results,…}) on most surfaces, a bare array on some —
// normalise rather than assuming, then match the seeded name exactly.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
return rows.filter((row: any) => (row?.name ?? row?.object?.name) === name)
}
@@ -363,7 +364,7 @@ test.describe('hydra-console-agent-leaves', () => {
})
const body = registers.ok() ? await registers.json() : {}
const rows = Array.isArray(body) ? body : (body.results ?? [])
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
const hydra = rows.find((row: any) => row?.slug === HYDRA_REGISTER)
test.skip(
diff --git a/tests/e2e/spec-coverage/manifest-pages.spec.ts b/tests/e2e/spec-coverage/manifest-pages.spec.ts
index 536e62b6..9a89a82b 100644
--- a/tests/e2e/spec-coverage/manifest-pages.spec.ts
+++ b/tests/e2e/spec-coverage/manifest-pages.spec.ts
@@ -34,10 +34,12 @@
* Auth: shared storageState session (tests/e2e/global-setup.ts).
*/
-import { test, expect, type Page, type ConsoleMessage } from '@playwright/test'
+import type { ConsoleMessage, Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
-import { appRoot } from './_fixtures'
+import { appRoot } from './_fixtures.ts'
/* --------------------------------------------------------------------- *
* Manifest loading (+ optional manifest.d fragments)
@@ -169,19 +171,12 @@ const IGNORED_CONSOLE_PATTERNS: RegExp[] = [
// mid-run it serves the 404 HTML page, tripping a MIME-type refusal.
/Refused to apply style/i,
/is not a supported stylesheet MIME type/i,
- // 🔴 `GET /apps/hermiq/api/chat/health` answering 503 is hermiq's SPECIFIED
- // behaviour, not a fault: ChatHealthController's contract is
- // `200 {status:"ok"}` when an LLM provider is configured and
- // `503 {status:"no_provider"}` when none is. The nextcloud-vue AI companion
- // widget probes it once at mount on every page precisely to decide whether
- // to render itself, and CI configures no provider by design — this whole
- // suite is "UI mechanics, no LLM required".
- //
- // Measured: 30 such responses across one run (30878205902), and it was the
- // ONLY 503 on the instance. Scoped to this exact route, NOT to the 503
- // status: a 503 on any other hermiq endpoint is still a failure, which is
- // what this assertion is for.
- /\/apps\/hermiq\/api\/chat\/health/,
+ // `GET /apps/hermiq/api/chat/health` no longer needs a carve-out here.
+ // It used to answer a designed 503 ({status:"no_provider"}) on the
+ // providerless CI instance, and this list excused it; the contract is now
+ // 200 {status:"unconfigured"} for a healthy app without a provider, so a
+ // 503 from it means config_error, the app itself broken, and must fail
+ // this assertion like any other 5xx.
]
/**
diff --git a/tests/e2e/spec-coverage/speech-services.spec.ts b/tests/e2e/spec-coverage/speech-services.spec.ts
index 3ccbd698..daf6bc38 100644
--- a/tests/e2e/spec-coverage/speech-services.spec.ts
+++ b/tests/e2e/spec-coverage/speech-services.spec.ts
@@ -46,16 +46,18 @@
* Seeding: OpenRegister objects API via _fixtures (register 'hermiq').
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
import {
- TEST_PREFIX,
appRoot,
deleteObject,
dismissTour,
harvestToken,
resolveRegisterSchema,
seedAgent,
-} from './_fixtures'
+ TEST_PREFIX,
+} from './_fixtures.ts'
/** The companion launcher, present on every Nextcloud page. */
const FAB = '[data-testid="cn-ai-fab"]'
@@ -221,7 +223,7 @@ async function openCompanion(page: Page): Promise {
const fab = page.locator(FAB).first()
try {
await expect(fab).toBeVisible({ timeout: 20_000 })
- } catch (e) {
+ } catch {
await page.reload({ waitUntil: 'domcontentloaded' })
await dismissTour(page)
await expect(fab).toBeVisible({ timeout: 30_000 })
@@ -433,10 +435,51 @@ test.describe('speech-services: the engine is the agent’s choice', () => {
// natively `disabled`: a disabled button suppresses hover, so its tooltip
// can never appear and it would refuse without ever saying why. A real
// user's click lands; only the test tool's actionability check does not.
- await mic.click({ force: true })
+ // 🔴 THE BANNER OUTLIVES NOTHING: IT WIPES ITSELF AFTER 6s.
+ //
+ // CnAiInput's `showDictationError()` arms
+ // `setTimeout(..., DICTATION_ERROR_TIMEOUT)` with DICTATION_ERROR_TIMEOUT
+ // = 6000, deliberately — a banner with no lifetime "becomes furniture",
+ // as the component says. This assertion used to allow 15s to observe a
+ // thing that exists for 6, so if the runner stalled between the click and
+ // the first poll the banner had already cleared, and Playwright then
+ // waited out the remaining timeout for an element that was gone:
+ //
+ // Error: expect(locator).toContainText(expected) failed
+ // Locator: locator('[data-testid="cn-ai-input-dictation-error"]')
+ // Expected pattern: /private/i Timeout: 15000ms
+ // Error: element(s) not found
+ //
+ // Measured on development 2026-08-31: 2 failures in 8 runs, and a PUSH
+ // and PULL_REQUEST run of the SAME commit disagreed — the signature of a
+ // missed window, not of an app that failed to refuse.
+ //
+ // So observe inside the banner's own lifetime, and re-trigger if the
+ // window was missed. Clicking the mic again is safe and changes nothing
+ // the test is about: the control is `aria-disabled`, the click is
+ // refused by the app either way, and the assertion that matters — that
+ // NO browser recogniser was constructed — is checked below over the
+ // whole test, not per click.
+ //
+ // ⚠️ Deliberately NOT a longer timeout and NOT a weakened matcher. The
+ // refusal message is the point of the test; it still must appear, still
+ // must say "private", and a genuine failure to show it still fails here
+ // after three honest attempts.
+ const dictationError = page.locator(
+ '[data-testid="cn-ai-input-dictation-error"]',
+ )
+ let banner = false
+ for (let attempt = 0; attempt < 3 && !banner; attempt++) {
+ await mic.click({ force: true })
+ banner = await dictationError
+ .waitFor({ state: 'visible', timeout: 4_000 })
+ .then(() => true)
+ .catch(() => false)
+ }
await expect(
- page.locator('[data-testid="cn-ai-input-dictation-error"]'),
- ).toContainText(/private/i)
+ dictationError,
+ 'the refusal must be shown and must name the private service',
+ ).toContainText(/private/i, { timeout: 4_000 })
// The assertion that matters: nothing reached for the cloud engine.
const constructed = await page.evaluate(
diff --git a/tests/e2e/spec-coverage/talk-agent-sessions.spec.ts b/tests/e2e/spec-coverage/talk-agent-sessions.spec.ts
index c7666d27..18bb7b7d 100644
--- a/tests/e2e/spec-coverage/talk-agent-sessions.spec.ts
+++ b/tests/e2e/spec-coverage/talk-agent-sessions.spec.ts
@@ -26,19 +26,19 @@
* NEXTCLOUD_URL=http://localhost:8080 npx playwright test tests/e2e/spec-coverage/talk-agent-sessions.spec.ts
*/
-import { test, expect } from '@playwright/test'
+import { expect, test } from '@playwright/test'
import {
- OR_API,
- TEST_PREFIX,
appEnabled,
appRoot,
cleanupFamily,
dismissTour,
harvestToken,
jsonHeaders,
+ OR_API,
resolveRegisterSchema,
seedAgent,
-} from './_fixtures'
+ TEST_PREFIX,
+} from './_fixtures.ts'
const HERMIQ_API = '/index.php/apps/hermiq/api'
diff --git a/tests/e2e/spec-coverage/tool-grant-reach.spec.ts b/tests/e2e/spec-coverage/tool-grant-reach.spec.ts
index fb756b5b..dcddd822 100644
--- a/tests/e2e/spec-coverage/tool-grant-reach.spec.ts
+++ b/tests/e2e/spec-coverage/tool-grant-reach.spec.ts
@@ -31,24 +31,21 @@
* Covers openspec/changes/agent-capability-reach/specs/agent-capability-reach/spec.md
*/
+import type { APIRequestContext } from '@playwright/test'
+import type { SecondUser } from './_fixtures.ts'
+
+import { expect, request as playwrightRequest, test } from '@playwright/test'
import {
- test,
- expect,
- request as playwrightRequest,
- type APIRequestContext,
-} from '@playwright/test'
-import {
- OR_API,
- TEST_PREFIX,
assertSecondUserAuthenticates,
cleanupFamily,
createSecondUser,
deleteSecondUser,
harvestToken,
jsonHeaders,
+ OR_API,
seedAgent,
- type SecondUser,
-} from './_fixtures'
+ TEST_PREFIX,
+} from './_fixtures.ts'
/** The closed reach vocabulary, ordered least to most far-reaching. */
const REACHES = ['self', 'user', 'instance', 'external']
@@ -149,20 +146,19 @@ test.describe('agent-capability-reach: catalogue reach, waiver round-trip, owner
'The catalogue must not be empty with both apps installed',
).toBeGreaterThan(0)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const missing = tools.filter(
(t: any) => typeof t?.reach !== 'string' || t.reach === '',
)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
expect(
missing.map((t: any) => t.id),
'Every entry must carry a reach',
).toEqual([])
const outOfVocabulary = tools
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
.filter((t: any) => !REACHES.includes(t.reach))
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
.map((t: any) => `${t.id}=${t.reach}`)
expect(
outOfVocabulary,
@@ -172,12 +168,12 @@ test.describe('agent-capability-reach: catalogue reach, waiver round-trip, owner
// 🔴 THE CROSS-APP ASSERTION. The two checks above would both PASS if the
// bridge were dropping `reach` — every entry would still carry one, and
// `external` is in the vocabulary. Only the distribution gives it away.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
const byId = new Map(tools.map((t: any) => [t.id, t]))
for (const id of NATIVE_READS) {
const tool = byId.get(id)
expect(tool, `${id} must be present in the catalogue`).toBeTruthy()
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
expect(
(tool as any).reach,
`${id} must resolve to 'user'. Getting 'external' means the reach `
@@ -187,7 +183,7 @@ test.describe('agent-capability-reach: catalogue reach, waiver round-trip, owner
}
// Same `read` scope, different reach — the axis earning its existence.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
const webFetch = byId.get('hermiq.webFetch') as any
expect(webFetch, 'hermiq.webFetch must be present').toBeTruthy()
expect(webFetch.reach).toBe('external')
diff --git a/tests/e2e/sync-conversion-probe.spec.ts b/tests/e2e/sync-conversion-probe.spec.ts
index 9f8ae7d5..d8405744 100644
--- a/tests/e2e/sync-conversion-probe.spec.ts
+++ b/tests/e2e/sync-conversion-probe.spec.ts
@@ -20,7 +20,9 @@
* Run: NEXTCLOUD_URL=http://localhost:8091 npx playwright test tests/e2e/sync-conversion-probe.spec.ts
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/tool-grant-matrix.spec.ts b/tests/e2e/tool-grant-matrix.spec.ts
index f78f32d0..cf53f989 100644
--- a/tests/e2e/tool-grant-matrix.spec.ts
+++ b/tests/e2e/tool-grant-matrix.spec.ts
@@ -18,7 +18,9 @@
* SPDX-FileCopyrightText: 2026 Conduction B.V.
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/e2e/tutorial-captures.spec.ts b/tests/e2e/tutorial-captures.spec.ts
index 744d0fe0..5cd8e817 100644
--- a/tests/e2e/tutorial-captures.spec.ts
+++ b/tests/e2e/tutorial-captures.spec.ts
@@ -17,7 +17,9 @@
* npx playwright test tests/e2e/tutorial-captures.spec.ts --project chromium
*/
-import { test, expect, type Page, type Locator } from '@playwright/test'
+import type { Locator, Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
diff --git a/tests/e2e/wave2-surfaces.spec.ts b/tests/e2e/wave2-surfaces.spec.ts
index b7005afd..e267bb62 100644
--- a/tests/e2e/wave2-surfaces.spec.ts
+++ b/tests/e2e/wave2-surfaces.spec.ts
@@ -28,7 +28,9 @@
* wave-2 siblings (agent-evals / compliance-control-packs / agent-template-gallery, archived).
*/
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import { expect, test } from '@playwright/test'
const NC_USER = process.env.NC_USER || 'admin'
const NC_PASS = process.env.NC_PASS || 'admin'
diff --git a/tests/grant-flatten.spec.js b/tests/grant-flatten.spec.js
index 0746163a..bd95b599 100644
--- a/tests/grant-flatten.spec.js
+++ b/tests/grant-flatten.spec.js
@@ -65,7 +65,7 @@ function check(label, stored, want) {
try {
assert.deepStrictEqual(got.slice().sort(), want.slice().sort())
console.log(` ok ${label}`)
- } catch (error) {
+ } catch {
failures++
console.log(` FAIL ${label}`)
console.log(` want ${JSON.stringify(want)}`)
diff --git a/tests/l10n/check-l10n.js b/tests/l10n/check-l10n.js
index 70c2c26c..922da5d3 100644
--- a/tests/l10n/check-l10n.js
+++ b/tests/l10n/check-l10n.js
@@ -128,7 +128,7 @@ function unescape (s) {
const used = new Map()
function record (key, file, idx, content) {
- if (key == null) {
+ if ((key === null || key === undefined)) {
return
}
const k = unescape(key)
@@ -154,7 +154,7 @@ for (const file of files) {
const missing = []
for (const [key, locations] of used) {
- if (!Object.prototype.hasOwnProperty.call(translations, key)) {
+ if (!Object.hasOwn(translations, key)) {
missing.push({ key, locations: [...locations] })
}
}
diff --git a/tests/manifest-v2.spec.js b/tests/manifest-v2.spec.js
index b3348026..1aec0e74 100644
--- a/tests/manifest-v2.spec.js
+++ b/tests/manifest-v2.spec.js
@@ -76,8 +76,8 @@ function loadJson(file) {
}
function loadAjv() {
- let Ajv2020 = null
- let addFormats = null
+ let Ajv2020
+ let addFormats
try {
Ajv2020 = require('ajv/dist/2020').default || require('ajv/dist/2020')
} catch (_) {
@@ -128,7 +128,7 @@ function structuralLintV2(manifest) {
if (!page || typeof page !== 'object') continue
// 3. No top-level layout[] (v1 remnant)
- if (Object.prototype.hasOwnProperty.call(page, 'layout')) {
+ if (Object.hasOwn(page, 'layout')) {
errors.push(
`pages[${i}] (id="${page.id}"): top-level layout[] is not allowed in v2 — use widgets[] with gridX/gridY`,
)
diff --git a/tests/registry.spec.js b/tests/registry.spec.js
index 68901a88..ec488be2 100644
--- a/tests/registry.spec.js
+++ b/tests/registry.spec.js
@@ -126,7 +126,9 @@ function parseRegistry(srcPath) {
try {
vm.runInNewContext(src, sandbox)
} catch (err) {
- throw new Error(`Failed to evaluate registry.js: ${err.message}`)
+ throw new Error(`Failed to evaluate registry.js: ${err.message}`, {
+ cause: err,
+ })
}
return sandbox.module.exports
}
diff --git a/tests/tool-taxonomy.spec.js b/tests/tool-taxonomy.spec.js
index bf1c34d1..426c800b 100644
--- a/tests/tool-taxonomy.spec.js
+++ b/tests/tool-taxonomy.spec.js
@@ -60,7 +60,7 @@ function check(id, taxonomy, want) {
console.log(
` ok ${id} -> ${got.subject} / ${got.specialLabel ?? got.verb}`,
)
- } catch (error) {
+ } catch {
failures++
console.log(` FAIL ${id}`)
console.log(` want ${JSON.stringify(want)}`)
diff --git a/tests/validate-json-strict.js b/tests/validate-json-strict.js
index 8a27cecf..cd1982f6 100644
--- a/tests/validate-json-strict.js
+++ b/tests/validate-json-strict.js
@@ -45,7 +45,6 @@ function targetFiles() {
// `pathPrefix` is the JSON-pointer-ish path used in the error message.
function parseStrict(text, label) {
const dupErrors = []
- const reviverPathStack = []
// JSON.parse's reviver can't see duplicates (the object is already
// collapsed). So we re-implement just enough: tokenise object keys.
// Simpler robust approach: walk the raw text with a tiny tokenizer.
@@ -101,7 +100,7 @@ function parseStrict(text, label) {
return
}
let idx = 0
- // eslint-disable-next-line no-constant-condition
+
while (true) {
readValue(`${pathStr}/${idx}`)
idx++
@@ -126,7 +125,7 @@ function parseStrict(text, label) {
i++
return
}
- // eslint-disable-next-line no-constant-condition
+
while (true) {
skipWs()
if (text[i] !== '"') err('expected string key in object')
diff --git a/tests/validate-manifest.js b/tests/validate-manifest.js
index 3ce26d1f..6b3c7cc3 100644
--- a/tests/validate-manifest.js
+++ b/tests/validate-manifest.js
@@ -108,8 +108,8 @@ function loadAjv() {
// "https://json-schema.org/draft/2020-12/schema"). Standard Ajv (v7+)
// does not auto-load the 2020 meta-schema; we need the `ajv/dist/2020`
// entry point.
- let Ajv2020 = null
- let addFormats = null
+ let Ajv2020
+ let addFormats
try {
// Ajv 8+ ships the 2020 draft entry point.
Ajv2020 = require('ajv/dist/2020').default || require('ajv/dist/2020')