From 592b72202f4e2077be4a66387680d2055e1c0421 Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Tue, 15 Sep 2026 10:18:13 +0200 Subject: [PATCH] feat: parse the minimal project intent in both implementations and match its oracle --- .github/workflows/ci.yml | 16 + .gitignore | 4 + .prettierignore | 2 + README.md | 2 +- docs/architecture-rules.md | 3 +- docs/requirements.md | 4 +- emf/cli/META-INF/MANIFEST.MF | 12 + emf/cli/build.properties | 3 + emf/cli/pom.xml | 16 + .../deploykit/emf/cli/Diagnostic.java | 12 + .../deploykit/emf/cli/IntentJson.java | 73 + .../deploykit/emf/cli/Parsed.java | 28 + .../deploykit/emf/cli/Pipeline.java | 41 + .../deploykit/emf/cli/DiagnosticTest.java | 30 + .../deploykit/emf/cli/IntentJsonTest.java | 78 + .../deploykit/emf/cli/PipelineTest.java | 71 + emf/docs/architecture.md | 14 +- emf/docs/witnesses.md | 4 +- emf/metamodel/META-INF/MANIFEST.MF | 11 +- emf/metamodel/build.properties | 3 + emf/metamodel/model/project-intent.ecore | 109 + emf/metamodel/model/project-intent.genmodel | 9 + emf/metamodel/plugin.properties | 4 + emf/metamodel/plugin.xml | 11 + emf/metamodel/pom.xml | 76 + .../emf/metamodel/GenerateProjectIntent.mwe2 | 21 + .../deploykit/emf/metamodel/SkeletonTest.java | 18 +- emf/parity/META-INF/MANIFEST.MF | 9 + emf/parity/build.properties | 3 + emf/parity/pom.xml | 13 +- .../deploykit/emf/parity/ParityTest.java | 75 + emf/pom.xml | 1 + emf/scripts/summary.sh | 2 +- emf/syntax/META-INF/MANIFEST.MF | 7 +- emf/syntax/pom.xml | 74 +- ...eleton.mwe2 => GenerateProjectIntent.mwe2} | 29 +- .../deploykit/emf/syntax/ProjectIntent.xtext | 135 + .../syntax/ProjectIntentRuntimeModule.java | 9 + .../syntax/ProjectIntentStandaloneSetup.java | 14 + .../deploykit/emf/syntax/Skeleton.xtext | 9 - .../emf/syntax/blocks/BlockTokenSource.java | 126 + .../emf/syntax/blocks/BlockTokens.java | 38 + .../antlr/ProjectIntentTokenSource.java | 28 + .../deploykit/emf/syntax/SkeletonTest.java | 46 - .../syntax/blocks/BlockTokenSourceTest.java | 121 + .../emf/syntax/blocks/BlockTokensTest.java | 41 + eslint.config.js | 1 + package-lock.json | 2677 ++++++++++++++++- package.json | 4 + src/application/parse-project-intent.ts | 13 + src/domain/diagnostic.ts | 10 + src/domain/project-intent/model.ts | 62 + src/domain/project-intent/vocabularies.ts | 23 + src/index.ts | 3 + src/wire/project-intent/map.ts | 64 + src/wire/project-intent/read.ts | 37 + src/wire/project-intent/schema.ts | 71 + stryker.config.json | 19 + test/model/project-intent.test.ts | 242 ++ test/mutation-contract.test.ts | 64 + tsconfig.json | 8 +- vitest.config.ts | 15 +- vitest.mutation.config.ts | 13 + 63 files changed, 4592 insertions(+), 189 deletions(-) create mode 100644 emf/cli/META-INF/MANIFEST.MF create mode 100644 emf/cli/build.properties create mode 100644 emf/cli/pom.xml create mode 100644 emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java create mode 100644 emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java create mode 100644 emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Parsed.java create mode 100644 emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Pipeline.java create mode 100644 emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/DiagnosticTest.java create mode 100644 emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/IntentJsonTest.java create mode 100644 emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/PipelineTest.java create mode 100644 emf/metamodel/model/project-intent.ecore create mode 100644 emf/metamodel/model/project-intent.genmodel create mode 100644 emf/metamodel/plugin.properties create mode 100644 emf/metamodel/plugin.xml create mode 100644 emf/metamodel/src/main/java/dev/jorisjonkers/deploykit/emf/metamodel/GenerateProjectIntent.mwe2 create mode 100644 emf/parity/META-INF/MANIFEST.MF create mode 100644 emf/parity/build.properties create mode 100644 emf/parity/src/test/java/dev/jorisjonkers/deploykit/emf/parity/ParityTest.java rename emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/{GenerateSkeleton.mwe2 => GenerateProjectIntent.mwe2} (50%) create mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent.xtext create mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentRuntimeModule.java create mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentStandaloneSetup.java delete mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/Skeleton.xtext create mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSource.java create mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokens.java create mode 100644 emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/parser/antlr/ProjectIntentTokenSource.java delete mode 100644 emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/SkeletonTest.java create mode 100644 emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSourceTest.java create mode 100644 emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokensTest.java create mode 100644 src/application/parse-project-intent.ts create mode 100644 src/domain/diagnostic.ts create mode 100644 src/domain/project-intent/model.ts create mode 100644 src/domain/project-intent/vocabularies.ts create mode 100644 src/wire/project-intent/map.ts create mode 100644 src/wire/project-intent/read.ts create mode 100644 src/wire/project-intent/schema.ts create mode 100644 stryker.config.json create mode 100644 test/model/project-intent.test.ts create mode 100644 test/mutation-contract.test.ts create mode 100644 vitest.mutation.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2352ab6..d6303d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,6 +169,21 @@ - 'name': 'Test' 'run': 'npm run test:coverage' + # Every module under src/, mutated; a surviving mutant below the break + # threshold in stryker.config.json fails the job. The threshold is the + # measured score and only rises, like the coverage ratchet. + 'mutation': + 'name': 'Mutation' + 'runs-on': 'ubuntu-latest' + 'timeout-minutes': 20 + 'permissions': + 'contents': 'read' + 'steps': + - 'uses': 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' # v7.0.1 + - 'uses': './.github/actions/setup' + - 'name': 'Mutation' + 'run': 'npm run test:mutation' + 'package-contents': 'name': 'Package contents' 'runs-on': 'ubuntu-latest' @@ -290,6 +305,7 @@ - 'architecture' - 'contracts' - 'tests' + - 'mutation' - 'package-contents' - 'actionlint' - 'secret-scan' diff --git a/.gitignore b/.gitignore index 287a053..a47dc2d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,10 @@ __pycache__/ # these; the template did not ignore them. coverage/ .nyc_output/ + +# Mutation testing: Stryker's report and its sandbox copies of the tree. +reports/ +.stryker-tmp/ *.lcov # Build and tool caches diff --git a/.prettierignore b/.prettierignore index 00e9dbc..8a3b739 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,8 @@ # Generated CHANGELOG.md coverage/ +reports/ +.stryker-tmp/ **/target/ dist/ node_modules/ diff --git a/README.md b/README.md index 65f0b24..29a16c1 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ npm run verify # lint, format, typecheck, ADR contract, tests + coverage `npm run lint:adrs` alone runs the decision-record contract, and `npm test` runs the suite without enforcing coverage. `npm run test:coverage` (part of `npm run verify`) enforces the ratchet in `vitest.config.ts`: statements -98.37%, branches 93.1%, functions 100%, lines 98.25%. +98.46%, branches 93.44%, functions 100%, lines 98.35%. ## Conventions diff --git a/docs/architecture-rules.md b/docs/architecture-rules.md index dc143ba..26cb8a5 100644 --- a/docs/architecture-rules.md +++ b/docs/architecture-rules.md @@ -57,7 +57,7 @@ fails the gate, so the taxonomy cannot grow entries nothing stands behind. ## Rules -This ledger holds **63** rules, **12** of them pending. +This ledger holds **64** rules, **12** of them pending. A row is enforced or pending, never both. An enforced row names its enforcer as `kind:value`: `depcruise:` a rule in @@ -139,6 +139,7 @@ moving a live rule to pending fails the gate rather than quietly retiring it. | RULE-061 | gates | A workflow step that runs the Maven wrapper runs it in a directory holding a POM and the wrapper, and the model-driven reactor names only modules on disk | `file:test/emf-wiring.test.ts` | [test/emf-wiring.test.ts](../test/emf-wiring.test.ts) `which names a POM that does not exist` | | RULE-062 | gates | CodeQL analyses the model-driven implementation's Java without a build, ignoring build output and generated sources | `file:.github/codeql/codeql-config.yml` | [test/emf-wiring.test.ts](../test/emf-wiring.test.ts) `'language': 'java-kotlin'` | | RULE-063 | gates | A CodeQL finding of any severity fails `Pipeline Complete`, unless the finding is filtered in the CodeQL configuration | `file:.github/workflows/codeql.yml` | [test/pipeline-wiring.test.ts](../test/pipeline-wiring.test.ts) `'name': 'Fail on any finding'` | +| RULE-064 | gates | Every module under `src/` is mutated, and a mutation score below the measured break threshold fails the build | `file:stryker.config.json` | [test/mutation-contract.test.ts](../test/mutation-contract.test.ts) `"break": 100` | ## Considered and rejected diff --git a/docs/requirements.md b/docs/requirements.md index 212fff9..fb591ca 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -19,7 +19,7 @@ test file and holds at least one test; ids are unique; the count this document states matches the number of rows it holds; and every id cited anywhere in the tracked tree resolves to a row here. -This ledger holds **20** rows. The compiler's behaviours join it as they land. +This ledger holds **22** rows. The compiler's behaviours join it as they land. | id | a contributor or a consumer can rely on | proved by | |---|---|---| @@ -43,3 +43,5 @@ This ledger holds **20** rows. The compiler's behaviours join it as they land. | REQ-018 | The compiler's inner rings cannot read the environment, the clock, randomness, a child process or the filesystem synchronously, and only `src/cli/boundary.ts` exits the process or writes output | [test/seams.test.ts](../test/seams.test.ts) | | REQ-019 | Every committed oracle file is byte-identical to its own RFC 8785 canonicalisation, so a hand edit cannot leave one in a form the other implementation would not produce | [test/oracles.test.ts](../test/oracles.test.ts) | | REQ-020 | The production implementation's canonical JSON writer sorts keys by UTF-16 code units, formats numbers as ECMAScript does, and refuses null, non-finite numbers, non-JSON values and lone surrogates, on the same cases as the model-driven writer | [test/canonical-json.test.ts](../test/canonical-json.test.ts) | +| REQ-021 | An authored Project Intent file parses to its committed intent oracle byte for byte, and YAML outside the one-document, anchor-free subset or a field outside the language is refused with a diagnostic rather than guessed at | [test/model/project-intent.test.ts](../test/model/project-intent.test.ts) | +| REQ-022 | Every module under `src/` is mutation-tested, and a surviving mutant that takes the score below the measured threshold fails the build | [test/mutation-contract.test.ts](../test/mutation-contract.test.ts) | diff --git a/emf/cli/META-INF/MANIFEST.MF b/emf/cli/META-INF/MANIFEST.MF new file mode 100644 index 0000000..ee459e6 --- /dev/null +++ b/emf/cli/META-INF/MANIFEST.MF @@ -0,0 +1,12 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: deploy-kit model-driven pipeline +Bundle-SymbolicName: dev.jorisjonkers.deploykit.emf.cli;singleton:=true +Bundle-Version: 0.1.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Automatic-Module-Name: dev.jorisjonkers.deploykit.emf.cli +Export-Package: dev.jorisjonkers.deploykit.emf.cli +Require-Bundle: dev.jorisjonkers.deploykit.emf.metamodel, + dev.jorisjonkers.deploykit.emf.syntax, + org.eclipse.emf.ecore, + org.eclipse.xtext diff --git a/emf/cli/build.properties b/emf/cli/build.properties new file mode 100644 index 0000000..a1ec8c4 --- /dev/null +++ b/emf/cli/build.properties @@ -0,0 +1,3 @@ +source.. = src/main/java/ +bin.includes = META-INF/,\ + . diff --git a/emf/cli/pom.xml b/emf/cli/pom.xml new file mode 100644 index 0000000..3697356 --- /dev/null +++ b/emf/cli/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + + dev.jorisjonkers.deploykit.emf + emf-parent + 0.1.0-SNAPSHOT + + + dev.jorisjonkers.deploykit.emf.cli + eclipse-plugin + deploy-kit model-driven cli + diff --git a/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java new file mode 100644 index 0000000..214750a --- /dev/null +++ b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java @@ -0,0 +1,12 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +/** + * A refusal: the code the specification gives it, the JSON Pointer of the authored value it + * concerns, and a message for a human. The code and the path are the parity contract's; the message + * is this implementation's own. + */ +public record Diagnostic(String code, String path, String message) { + + /** The code every refusal of a document's shape carries, until a rule gives it its own. */ + public static final String SCHEMA = "schema"; +} diff --git a/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java new file mode 100644 index 0000000..caec009 --- /dev/null +++ b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java @@ -0,0 +1,73 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.emf.common.util.Enumerator; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; + +/** + * Reads a parsed model as the JSON value the parity contract compares: every feature named as the + * authored key it holds, a map entry as an object, an enumeration as its literal, and an optional + * feature absent when the document left it out. The metamodel is walked reflectively, so a feature + * added to the {@code .ecore} reaches the intent without a line here. + */ +public final class IntentJson { + + private IntentJson() {} + + /** The JSON value of {@code root}, as a map of authored key to value. */ + public static Map of(EObject root) { + Map json = new LinkedHashMap<>(); + for (EStructuralFeature feature : root.eClass().getEAllStructuralFeatures()) { + if (isSet(root, feature)) { + json.put(feature.getName(), value(root, feature)); + } + } + return json; + } + + /** Whether the document carries {@code feature}: a required feature always, an optional one when set. */ + private static boolean isSet(EObject owner, EStructuralFeature feature) { + return feature.isRequired() || owner.eIsSet(feature); + } + + private static Object value(EObject owner, EStructuralFeature feature) { + Object value = owner.eGet(feature); + if (feature.isMany()) { + return many(feature, (List) value); + } + return single(value); + } + + private static Object many(EStructuralFeature feature, List values) { + if (isMapEntry(feature)) { + Map entries = new LinkedHashMap<>(); + for (Object value : values) { + Map.Entry entry = (Map.Entry) value; + entries.put(String.valueOf(entry.getKey()), single(entry.getValue())); + } + return entries; + } + List items = new ArrayList<>(); + for (Object value : values) { + items.add(single(value)); + } + return items; + } + + /** Whether {@code feature} holds map entries, which are written as one object rather than a list. */ + private static boolean isMapEntry(EStructuralFeature feature) { + return Map.Entry.class.getName().equals(feature.getEType().getInstanceClassName()); + } + + private static Object single(Object value) { + return switch (value) { + case EObject child -> of(child); + case Enumerator literal -> literal.getLiteral(); + default -> value; + }; + } +} diff --git a/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Parsed.java b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Parsed.java new file mode 100644 index 0000000..b7a9174 --- /dev/null +++ b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Parsed.java @@ -0,0 +1,28 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +import java.util.List; +import java.util.Map; + +/** + * The outcome of reading one authored document: the parsed intent as a JSON value, or the + * diagnostics that refused it. One of the two is always empty. + */ +public record Parsed(Map intent, List diagnostics) { + + public Parsed { + intent = Map.copyOf(intent); + diagnostics = List.copyOf(diagnostics); + } + + public static Parsed of(Map intent) { + return new Parsed(intent, List.of()); + } + + public static Parsed refused(List diagnostics) { + return new Parsed(Map.of(), diagnostics); + } + + public boolean ok() { + return diagnostics.isEmpty(); + } +} diff --git a/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Pipeline.java b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Pipeline.java new file mode 100644 index 0000000..adc854c --- /dev/null +++ b/emf/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Pipeline.java @@ -0,0 +1,41 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.ProjectIntentPackage; +import dev.jorisjonkers.deploykit.emf.syntax.ProjectIntentStandaloneSetup; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.resource.XtextResourceSet; + +/** + * The pipeline entry: an authored project file in, its parsed intent or the diagnostics that refused + * it out. It is the seam the parity suite runs every case through. + */ +public final class Pipeline { + + private Pipeline() {} + + /** The parsed intent of the project file at {@code path}, or the diagnostics refusing it. */ + public static Parsed intent(Path path) { + // Outside OSGi nothing registers the metamodel, and the grammar's rules return its classes. + EPackage.Registry.INSTANCE.putIfAbsent(ProjectIntentPackage.eNS_URI, ProjectIntentPackage.eINSTANCE); + XtextResourceSet resources = new ProjectIntentStandaloneSetup() + .createInjectorAndDoEMFRegistration() + .getInstance(XtextResourceSet.class); + Resource resource = + resources.getResource(URI.createFileURI(path.toAbsolutePath().toString()), true); + List refusals = new ArrayList<>(); + for (Resource.Diagnostic error : resource.getErrors()) { + refusals.add(new Diagnostic(Diagnostic.SCHEMA, "", "line " + error.getLine() + ": " + error.getMessage())); + } + if (resource.getContents().isEmpty()) { + refusals.add(new Diagnostic(Diagnostic.SCHEMA, "", path.getFileName() + " holds no document")); + } + return refusals.isEmpty() + ? Parsed.of(IntentJson.of(resource.getContents().get(0))) + : Parsed.refused(refusals); + } +} diff --git a/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/DiagnosticTest.java b/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/DiagnosticTest.java new file mode 100644 index 0000000..5173d98 --- /dev/null +++ b/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/DiagnosticTest.java @@ -0,0 +1,30 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +/** What a refusal carries: the code and the path the parity contract compares, and a message. */ +class DiagnosticTest { + + @Test + void aDiagnosticCarriesItsCodePathAndMessage() { + Diagnostic diagnostic = new Diagnostic(Diagnostic.SCHEMA, "/applications/0/processes/0/runtime", "refused"); + + assertThat(diagnostic.code()).isEqualTo("schema"); + assertThat(diagnostic.path()).isEqualTo("/applications/0/processes/0/runtime"); + assertThat(diagnostic.message()).isEqualTo("refused"); + } + + @Test + void aParsedDocumentIsEitherAnIntentOrRefusals() { + Diagnostic diagnostic = new Diagnostic(Diagnostic.SCHEMA, "", "refused"); + + assertThat(Parsed.refused(List.of(diagnostic)).ok()).isFalse(); + assertThat(Parsed.refused(List.of(diagnostic)).intent()).isEmpty(); + assertThat(Parsed.of(java.util.Map.of("project", "notes")).ok()).isTrue(); + assertThat(Parsed.of(java.util.Map.of("project", "notes")).diagnostics()) + .isEmpty(); + } +} diff --git a/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/IntentJsonTest.java b/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/IntentJsonTest.java new file mode 100644 index 0000000..07eebb6 --- /dev/null +++ b/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/IntentJsonTest.java @@ -0,0 +1,78 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Lifecycle; +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Placement; +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Probe; +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Probes; +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Process; +import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.ProjectIntentFactory; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** What the intent of a parsed model holds: the authored keys, and nothing the document left out. */ +class IntentJsonTest { + + private static final ProjectIntentFactory MODEL = ProjectIntentFactory.eINSTANCE; + + private static Process process() { + Process process = MODEL.createProcess(); + process.setName("notes-api"); + process.setLifecycle(Lifecycle.APPLICATION); + process.setImage("notes-api"); + Placement placement = MODEL.createPlacement(); + placement.setMemory("256Mi"); + placement.setCpu("50m"); + process.setPlacement(placement); + return process; + } + + @Test + void aRequiredFeatureIsWrittenEvenWhenItHoldsItsDefault() { + Map intent = IntentJson.of(process()); + + assertThat(intent).contains(entry("name", "notes-api"), entry("image", "notes-api")); + assertThat(intent).containsKey("cutover"); + } + + @Test + void anEnumerationIsWrittenAsItsLiteral() { + assertThat(IntentJson.of(process())).contains(entry("lifecycle", "application")); + } + + @Test + void anOptionalFeatureTheDocumentLeftOutIsAbsent() { + assertThat(IntentJson.of(process())).doesNotContainKeys("startupBudget", "probes", "provides"); + } + + @Test + void anOptionalFeatureTheDocumentSetIsWritten() { + Process process = process(); + process.setStartupBudget("20s"); + Probes probes = MODEL.createProbes(); + Probe readiness = MODEL.createProbe(); + readiness.setPath("/healthz/ready"); + readiness.setPort(8080); + probes.setReadiness(readiness); + process.setProbes(probes); + + assertThat(IntentJson.of(process())) + .doesNotContainKey("startupBudget"); // the sample above is a different object + assertThat(IntentJson.of(process)) + .contains( + entry("startupBudget", "20s"), + entry("probes", Map.of("readiness", Map.of("path", "/healthz/ready", "port", 8080)))); + } + + @Test + void aMapEntryIsWrittenAsAnObjectAndAListAsAList() { + Process process = process(); + process.getProvides().put("http", 8080); + + assertThat(IntentJson.of(process())).doesNotContainKey("provides"); + assertThat(IntentJson.of(process())).doesNotContainKey("applications"); + assertThat(IntentJson.of(process)).contains(entry("provides", Map.of("http", 8080))); + } +} diff --git a/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/PipelineTest.java b/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/PipelineTest.java new file mode 100644 index 0000000..bb42b59 --- /dev/null +++ b/emf/cli/src/test/java/dev/jorisjonkers/deploykit/emf/cli/PipelineTest.java @@ -0,0 +1,71 @@ +package dev.jorisjonkers.deploykit.emf.cli; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** The pipeline entry: an authored file in, its intent or the diagnostics that refused it out. */ +class PipelineTest { + + private static final String MINIMAL = """ + apiVersion: intent.jorisjonkers.dev/v1 + kind: Project + schemaVersion: 1.0.0 + project: notes + owner: joris + applications: + - id: notes + processes: + - name: notes-api + lifecycle: application + image: notes-api + runtime: node + placement: + memory: 256Mi + cpu: 50m + cutover: rolling + """; + + private static Path file(Path directory, String text) throws IOException { + Path file = directory.resolve("notes.project.yml"); + Files.writeString(file, text); + return file; + } + + @Test + void anAuthoredDocumentParsesToItsIntent(@TempDir Path directory) throws IOException { + Parsed parsed = Pipeline.intent(file(directory, MINIMAL)); + + assertThat(parsed.ok()).isTrue(); + assertThat(parsed.diagnostics()).isEmpty(); + assertThat(parsed.intent()).containsEntry("project", "notes").containsEntry("owner", "joris"); + } + + @Test + void yamlOutsideTheGrammarIsRefusedWithADiagnostic(@TempDir Path directory) throws IOException { + Parsed parsed = Pipeline.intent(file(directory, MINIMAL.replace("runtime: node", "runtime: rust"))); + + assertThat(parsed.ok()).isFalse(); + assertThat(parsed.intent()).isEmpty(); + assertThat(parsed.diagnostics()) + .allSatisfy(diagnostic -> { + assertThat(diagnostic.code()).isEqualTo(Diagnostic.SCHEMA); + assertThat(diagnostic.path()).isEmpty(); + assertThat(diagnostic.message()).startsWith("line "); + }) + .isNotEmpty(); + } + + @Test + void anEmptyDocumentIsRefused(@TempDir Path directory) throws IOException { + Parsed parsed = Pipeline.intent(file(directory, "")); + + assertThat(parsed.diagnostics()) + .extracting(Diagnostic::message) + .contains("notes.project.yml holds no document"); + } +} diff --git a/emf/docs/architecture.md b/emf/docs/architecture.md index f6966c0..d07fbc4 100644 --- a/emf/docs/architecture.md +++ b/emf/docs/architecture.md @@ -83,7 +83,7 @@ Tycho configuration and the target platform. | `syntax/` | the Xtext grammar for the authored YAML subset, and the generated editor bundles that run the OCL validators | Task 1 | | `resolve/` | the QVTo transformation from Project Intent and Platform Intent to the Resolved Deployment | Task 2 | | `render/` | the Acceleo 4 templates from a Resolved Deployment model to the Deliverable Set's files | Task 3 | -| `cli/` | the pipeline entry point: files in, canonical JSON, diagnostics and rendered files out | Task 1 onward | +| `cli/` | the pipeline entry point: files in, the parsed intent, diagnostics and rendered files out | Task 1 onward | | `parity/` | JUnit suites asserting each stage against the committed oracles, and the witness ledger check | Task 1 onward | A module may depend on the modules above it in this table and on nothing @@ -112,7 +112,9 @@ Cross-document references, including those from a Project into the Platform document, are Ecore references, not strings. Typed Java for each metamodel is generated from its `.genmodel` during the -Maven build into `target/`, and never committed. +Maven build into `target/`, and never committed: an MWE2 workflow runs EMF's +`EcoreGenerator` in `generate-sources`, and the generated packages are left out +of formatting, coverage and mutation. The descriptor exporter walks the source `EPackage` reflectively and writes the descriptor the parity contract fixes. It is the only place the Ecore structure @@ -147,6 +149,14 @@ The grammar imports the hand-written source metamodel, so the parser produces instances of the graded metamodel directly. There is no inferred syntax metamodel and no mapping step between parsing and validation. +Indentation is not the grammar's concern: a token source turns the block +structure into the synthetic `BEGIN` and `END` tokens the rules read. A line +indented further than the one before it opens a block, a dash opens one around +the item that follows it, and a flow collection opens and closes one on a single +line, so `{ path: /, match: prefix }` and the same keys written as an indented +block parse through one rule. The rules themselves are unordered groups, because +the order of keys in a mapping is not meaning. + The generated editor is configured to run the OCL validators and to mark each constraint violation on the source line it concerns, with the diagnostic code as its message. It is built by the same Maven and Tycho build as an Eclipse plugin; diff --git a/emf/docs/witnesses.md b/emf/docs/witnesses.md index 2751646..8c1330e 100644 --- a/emf/docs/witnesses.md +++ b/emf/docs/witnesses.md @@ -10,8 +10,8 @@ names the JUnit test that proves the same behaviour here has no witness here, when a witness names an id that is not a model row, or when it names a test method that does not exist. -This list holds **0** witnesses. No model behaviour row exists yet; the first -lands with #38. +This list holds **1** witness. | id | JUnit test | |---|---| +| REQ-021 | `ParityTest#theParsedIntentEqualsTheCommittedOracle` | diff --git a/emf/metamodel/META-INF/MANIFEST.MF b/emf/metamodel/META-INF/MANIFEST.MF index 5984db5..0ae5c8d 100644 --- a/emf/metamodel/META-INF/MANIFEST.MF +++ b/emf/metamodel/META-INF/MANIFEST.MF @@ -5,7 +5,14 @@ Bundle-SymbolicName: dev.jorisjonkers.deploykit.emf.metamodel;singleton:=true Bundle-Version: 0.1.0.qualifier Bundle-RequiredExecutionEnvironment: JavaSE-21 Automatic-Module-Name: dev.jorisjonkers.deploykit.emf.metamodel -Require-Bundle: org.eclipse.emf.ecore, +Export-Package: dev.jorisjonkers.deploykit.emf.metamodel.projectintent, + dev.jorisjonkers.deploykit.emf.metamodel.projectintent.impl, + dev.jorisjonkers.deploykit.emf.metamodel.projectintent.util +Require-Bundle: org.eclipse.emf.ecore;visibility:=reexport, org.eclipse.emf.ecore.xmi, org.eclipse.ocl.pivot, - org.eclipse.ocl.xtext.completeocl + org.eclipse.ocl.xtext.completeocl, + org.eclipse.emf.mwe2.launch, + org.eclipse.emf.mwe.utils, + org.eclipse.emf.mwe2.lib, + org.eclipse.emf.codegen.ecore diff --git a/emf/metamodel/build.properties b/emf/metamodel/build.properties index b5582e2..2c524df 100644 --- a/emf/metamodel/build.properties +++ b/emf/metamodel/build.properties @@ -1,2 +1,5 @@ +source.. = target/generated-sources/emf/ bin.includes = META-INF/,\ + .,\ + plugin.xml,\ model/ diff --git a/emf/metamodel/model/project-intent.ecore b/emf/metamodel/model/project-intent.ecore new file mode 100644 index 0000000..70ab7da --- /dev/null +++ b/emf/metamodel/model/project-intent.ecore @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/emf/metamodel/model/project-intent.genmodel b/emf/metamodel/model/project-intent.genmodel new file mode 100644 index 0000000..011a67b --- /dev/null +++ b/emf/metamodel/model/project-intent.genmodel @@ -0,0 +1,9 @@ + + + project-intent.ecore + + diff --git a/emf/metamodel/plugin.properties b/emf/metamodel/plugin.properties new file mode 100644 index 0000000..111e9b1 --- /dev/null +++ b/emf/metamodel/plugin.properties @@ -0,0 +1,4 @@ +# + +pluginName = deploy-kit model-driven metamodel +providerName = www.example.org diff --git a/emf/metamodel/plugin.xml b/emf/metamodel/plugin.xml new file mode 100644 index 0000000..2fbc601 --- /dev/null +++ b/emf/metamodel/plugin.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/emf/metamodel/pom.xml b/emf/metamodel/pom.xml index 48e6ce9..a69de53 100644 --- a/emf/metamodel/pom.xml +++ b/emf/metamodel/pom.xml @@ -13,4 +13,80 @@ dev.jorisjonkers.deploykit.emf.metamodel eclipse-plugin deploy-kit model-driven metamodel + + + + + + org.codehaus.mojo + exec-maven-plugin + + + generate-metamodel + generate-sources + java + + + + org.eclipse.emf.mwe2.launch.runtime.Mwe2Launcher + + /${project.basedir}/src/main/java/dev/jorisjonkers/deploykit/emf/metamodel/GenerateProjectIntent.mwe2 + -p + rootPath=/${project.basedir}/.. + + compile + false + + ${project.basedir}/src/main/java + + + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + + dev.jorisjonkers.deploykit.emf.metamodel.projectintent.* + dev.jorisjonkers.deploykit.emf.metamodel.projectintent.impl.* + dev.jorisjonkers.deploykit.emf.metamodel.projectintent.util.* + + + + + report + + + dev/jorisjonkers/deploykit/emf/metamodel/projectintent/** + + + + + coverage-floor + + + dev/jorisjonkers/deploykit/emf/metamodel/projectintent/** + + + + + + + org.pitest + pitest-maven + + dev.jorisjonkers.deploykit.emf.metamodel.projectintent.* + false + + + + diff --git a/emf/metamodel/src/main/java/dev/jorisjonkers/deploykit/emf/metamodel/GenerateProjectIntent.mwe2 b/emf/metamodel/src/main/java/dev/jorisjonkers/deploykit/emf/metamodel/GenerateProjectIntent.mwe2 new file mode 100644 index 0000000..bfa1347 --- /dev/null +++ b/emf/metamodel/src/main/java/dev/jorisjonkers/deploykit/emf/metamodel/GenerateProjectIntent.mwe2 @@ -0,0 +1,21 @@ +module dev.jorisjonkers.deploykit.emf.metamodel.GenerateProjectIntent + +import org.eclipse.emf.mwe.utils.* +import org.eclipse.emf.mwe2.ecore.* + +var rootPath = ".." +var project = "dev.jorisjonkers.deploykit.emf.metamodel" + +// The genmodel's Java, written under target/ and never committed +// (emf/docs/architecture.md#metamodels). +Workflow { + bean = StandaloneSetup { + scanClassPath = true + projectMapping = { projectName = project path = "${rootPath}/metamodel" } + } + component = EcoreGenerator { + genModel = "platform:/resource/${project}/model/project-intent.genmodel" + srcPath = "platform:/resource/${project}/target/generated-sources/emf" + lineDelimiter = "\n" + } +} diff --git a/emf/metamodel/src/test/java/dev/jorisjonkers/deploykit/emf/metamodel/SkeletonTest.java b/emf/metamodel/src/test/java/dev/jorisjonkers/deploykit/emf/metamodel/SkeletonTest.java index c27ec92..2e8273d 100644 --- a/emf/metamodel/src/test/java/dev/jorisjonkers/deploykit/emf/metamodel/SkeletonTest.java +++ b/emf/metamodel/src/test/java/dev/jorisjonkers/deploykit/emf/metamodel/SkeletonTest.java @@ -17,8 +17,9 @@ import org.eclipse.ocl.xtext.completeocl.validation.CompleteOCLEObjectValidator; import org.junit.jupiter.api.Test; -// Walking skeleton (#81): Ecore and Complete OCL run headless. Deleted by the -// Task 1 metamodel ticket, whose suite covers both. +// Walking skeleton (#81): Complete OCL runs headless. The Ecore half went with +// the Project Intent metamodel (#83); this half is deleted by the ticket that +// gives the metamodel its constraints (#85). class SkeletonTest { private static final Path MODEL = Path.of("model").toAbsolutePath(); @@ -47,19 +48,6 @@ private static EObject load(ResourceSet resources, String file) { return resources.getResource(uri(file), true).getContents().get(0); } - @Test - void anEcoreMetamodelLoadsAndAnInstanceValidates() { - OCL ocl = ocl(); - EPackage skeleton = register(ocl.getResourceSet()); - - assertThat(skeleton.getEClassifiers()).extracting("name").containsExactly("Project", "Application"); - assertThat(Diagnostician.INSTANCE - .validate(load(ocl.getResourceSet(), "notes.xmi")) - .getSeverity()) - .isEqualTo(Diagnostic.OK); - ocl.dispose(); - } - @Test void aCompleteOclInvariantFiresOnAnInstance() { OCL ocl = ocl(); diff --git a/emf/parity/META-INF/MANIFEST.MF b/emf/parity/META-INF/MANIFEST.MF new file mode 100644 index 0000000..d35b51c --- /dev/null +++ b/emf/parity/META-INF/MANIFEST.MF @@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: deploy-kit model-driven parity +Bundle-SymbolicName: dev.jorisjonkers.deploykit.emf.parity;singleton:=true +Bundle-Version: 0.1.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Automatic-Module-Name: dev.jorisjonkers.deploykit.emf.parity +Export-Package: dev.jorisjonkers.deploykit.emf.parity +Require-Bundle: dev.jorisjonkers.deploykit.emf.cli diff --git a/emf/parity/build.properties b/emf/parity/build.properties new file mode 100644 index 0000000..a1ec8c4 --- /dev/null +++ b/emf/parity/build.properties @@ -0,0 +1,3 @@ +source.. = src/main/java/ +bin.includes = META-INF/,\ + . diff --git a/emf/parity/pom.xml b/emf/parity/pom.xml index 689cd29..fadf308 100644 --- a/emf/parity/pom.xml +++ b/emf/parity/pom.xml @@ -10,20 +10,11 @@ 0.1.0-SNAPSHOT - emf-parity + dev.jorisjonkers.deploykit.emf.parity + eclipse-plugin deploy-kit model-driven parity - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - test - com.tngtech.archunit archunit-junit5 diff --git a/emf/parity/src/test/java/dev/jorisjonkers/deploykit/emf/parity/ParityTest.java b/emf/parity/src/test/java/dev/jorisjonkers/deploykit/emf/parity/ParityTest.java new file mode 100644 index 0000000..0134f67 --- /dev/null +++ b/emf/parity/src/test/java/dev/jorisjonkers/deploykit/emf/parity/ParityTest.java @@ -0,0 +1,75 @@ +package dev.jorisjonkers.deploykit.emf.parity; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.jorisjonkers.deploykit.emf.cli.Parsed; +import dev.jorisjonkers.deploykit.emf.cli.Pipeline; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Every case under {@code spec/v1/examples/} that carries an intent oracle, run through the pipeline + * entry and compared with the committed file byte for byte (docs/architecture.md#the-parity-contract). + */ +class ParityTest { + + private static List casesWithAnIntentOracle() { + Path examples = repository().resolve("spec/v1/examples"); + try (Stream tree = Files.walk(examples)) { + return tree.filter(path -> path.endsWith("expected/intent.json")) + .map(path -> path.getParent().getParent()) + .sorted() + .toList(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("casesWithAnIntentOracle") + void theParsedIntentEqualsTheCommittedOracle(Path directory) throws IOException { + Parsed parsed = Pipeline.intent(projectFile(directory)); + + assertThat(parsed.diagnostics()).isEmpty(); + assertThat(CanonicalJson.write(parsed.intent())).isEqualTo(read(directory.resolve("expected/intent.json"))); + } + + @Test + void oneChangedFieldNoLongerMatchesTheOracle() throws IOException { + Path directory = casesWithAnIntentOracle().get(0); + Path project = projectFile(directory); + Path changed = Files.createTempDirectory("parity").resolve(project.getFileName()); + Files.writeString(changed, read(project).replace("cpu: 50m", "cpu: 60m")); + + assertThat(CanonicalJson.write(Pipeline.intent(changed).intent())) + .isNotEqualTo(read(directory.resolve("expected/intent.json"))); + } + + private static Path projectFile(Path directory) throws IOException { + try (Stream files = Files.list(directory)) { + return files.filter(path -> path.getFileName().toString().endsWith(".project.yml")) + .findFirst() + .orElseThrow(); + } + } + + private static String read(Path path) throws IOException { + return Files.readString(path, StandardCharsets.UTF_8); + } + + private static Path repository() { + Path dir = Path.of("").toAbsolutePath(); + while (!Files.isRegularFile(dir.resolve("emf/pom.xml"))) { + dir = dir.getParent(); + } + return dir; + } +} diff --git a/emf/pom.xml b/emf/pom.xml index 9b3bde8..15f5ea9 100644 --- a/emf/pom.xml +++ b/emf/pom.xml @@ -18,6 +18,7 @@ metamodel syntax + cli resolve render parity diff --git a/emf/scripts/summary.sh b/emf/scripts/summary.sh index 123fcbe..ff38fa5 100755 --- a/emf/scripts/summary.sh +++ b/emf/scripts/summary.sh @@ -15,7 +15,7 @@ coverage=$(cat ./*/target/site/jacoco/jacoco.csv 2>/dev/null | awk -F, ' END {if (missed + covered) printf "%.1f%%", 100 * covered / (missed + covered); else print "n/a"}') mutation=$(cat ./*/target/pit-reports/mutations.xml 2>/dev/null | awk ' - {total += gsub(/ org.jacoco jacoco-maven-plugin - - - dev/jorisjonkers/deploykit/emf/syntax/*Skeleton* - dev/jorisjonkers/deploykit/emf/syntax/skeleton/** + + + prepare-agent + + + dev.jorisjonkers.deploykit.emf.syntax.ProjectIntent* + dev.jorisjonkers.deploykit.emf.syntax.AbstractProjectIntent* + dev.jorisjonkers.deploykit.emf.syntax.parser.* + dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.* + dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.internal.* + dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.lexer.* + dev.jorisjonkers.deploykit.emf.syntax.serializer.* + dev.jorisjonkers.deploykit.emf.syntax.services.* + dev.jorisjonkers.deploykit.emf.syntax.scoping.* + dev.jorisjonkers.deploykit.emf.syntax.validation.* + + + + + report + + + dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent*.class + dev/jorisjonkers/deploykit/emf/syntax/AbstractProjectIntent*.class dev/jorisjonkers/deploykit/emf/syntax/parser/** dev/jorisjonkers/deploykit/emf/syntax/serializer/** dev/jorisjonkers/deploykit/emf/syntax/services/** dev/jorisjonkers/deploykit/emf/syntax/scoping/** dev/jorisjonkers/deploykit/emf/syntax/validation/** - - + + + + + coverage-floor + + + dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent*.class + dev/jorisjonkers/deploykit/emf/syntax/AbstractProjectIntent*.class + dev/jorisjonkers/deploykit/emf/syntax/parser/** + dev/jorisjonkers/deploykit/emf/syntax/serializer/** + dev/jorisjonkers/deploykit/emf/syntax/services/** + dev/jorisjonkers/deploykit/emf/syntax/scoping/** + dev/jorisjonkers/deploykit/emf/syntax/validation/** + + + + org.pitest pitest-maven - dev.jorisjonkers.deploykit.emf.syntax.*Skeleton* - dev.jorisjonkers.deploykit.emf.syntax.skeleton.* - dev.jorisjonkers.deploykit.emf.syntax.parser.* - dev.jorisjonkers.deploykit.emf.syntax.serializer.* - dev.jorisjonkers.deploykit.emf.syntax.services.* - dev.jorisjonkers.deploykit.emf.syntax.scoping.* - dev.jorisjonkers.deploykit.emf.syntax.validation.* + dev.jorisjonkers.deploykit.emf.syntax.*ProjectIntent* + dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.lexer.* + dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.internal.* + dev.jorisjonkers.deploykit.emf.syntax.parser.* + dev.jorisjonkers.deploykit.emf.syntax.serializer.* + dev.jorisjonkers.deploykit.emf.syntax.services.* + dev.jorisjonkers.deploykit.emf.syntax.scoping.* + dev.jorisjonkers.deploykit.emf.syntax.validation.* - - false @@ -67,7 +103,7 @@ org.eclipse.emf.mwe2.launch.runtime.Mwe2Launcher - /${project.basedir}/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateSkeleton.mwe2 + /${project.basedir}/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateProjectIntent.mwe2 -p rootPath=/${project.basedir}/.. diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateSkeleton.mwe2 b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateProjectIntent.mwe2 similarity index 50% rename from emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateSkeleton.mwe2 rename to emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateProjectIntent.mwe2 index 6f320cb..6ccb718 100644 --- a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateSkeleton.mwe2 +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/GenerateProjectIntent.mwe2 @@ -1,24 +1,34 @@ -module dev.jorisjonkers.deploykit.emf.syntax.GenerateSkeleton +module dev.jorisjonkers.deploykit.emf.syntax.GenerateProjectIntent +import org.eclipse.emf.mwe.utils.* import org.eclipse.xtext.xtext.generator.* import org.eclipse.xtext.xtext.generator.model.project.* var rootPath = ".." var generated = "${rootPath}/syntax/target/generated-sources/xtext" -// Every Java file this writes, stubs included, lands under target/ and is -// never committed (emf/docs/architecture.md#metamodels). +// Every generated file lands under target/ except the two stubs the generator +// writes once and this tree then owns: the runtime module and the standalone +// setup, in src/main/java (emf/docs/architecture.md#concrete-syntax). Workflow { + bean = StandaloneSetup { + scanClassPath = true + projectMapping = { + projectName = "dev.jorisjonkers.deploykit.emf.metamodel" + path = "${rootPath}/metamodel" + } + projectMapping = { projectName = "syntax" path = "${rootPath}/syntax" } + } component = XtextGenerator { configuration = { project = StandardProjectConfig { baseName = "syntax" rootPath = rootPath runtime = { - src = generated - srcGen = generated - ecoreModel = "${rootPath}/syntax/target/generated-sources/xtext-model" root = "${rootPath}/syntax" + src = "${rootPath}/syntax/src/main/java" + srcGen = generated + ecoreModel = "${generated}-model" } runtimeTest = { enabled = false } eclipsePlugin = { enabled = false } @@ -31,9 +41,10 @@ Workflow { } } language = StandardLanguage { - name = "dev.jorisjonkers.deploykit.emf.syntax.Skeleton" - grammarUri = "classpath:/dev/jorisjonkers/deploykit/emf/syntax/Skeleton.xtext" - fileExtensions = "skeleton" + name = "dev.jorisjonkers.deploykit.emf.syntax.ProjectIntent" + grammarUri = "classpath:/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent.xtext" + fileExtensions = "yml" + referencedResource = "platform:/resource/dev.jorisjonkers.deploykit.emf.metamodel/model/project-intent.genmodel" serializer = { generateStub = false } validator = { generateStub = false } scopeProvider = { generateStub = false } diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent.xtext b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent.xtext new file mode 100644 index 0000000..439e89e --- /dev/null +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntent.xtext @@ -0,0 +1,135 @@ +grammar dev.jorisjonkers.deploykit.emf.syntax.ProjectIntent hidden(WS, SL_COMMENT) + +import "https://jorisjonkers.dev/deploy-kit/project-intent/1" +import "http://www.eclipse.org/emf/2002/Ecore" as ecore + +// The YAML subset the authored project files use. Block structure arrives as +// the synthetic BEGIN and END tokens the blocks package produces, so an +// indented block and a flow mapping parse through one rule. + +Project returns Project: + (('apiVersion' ':' apiVersion=Text) + & ('kind' ':' kind=Text) + & ('schemaVersion' ':' schemaVersion=Text) + & ('project' ':' project=Text) + & ('owner' ':' owner=Text) + & ('applications' ':' BEGIN (DASH BEGIN applications+=Application END)+ END)); + +Application returns Application: + (('id' ':' id=Text) + & ('observability' ':' BEGIN observability=Observability END)? + & ('exposure' ':' BEGIN (DASH BEGIN exposure+=Exposure END)+ END)? + & ('processes' ':' BEGIN (DASH BEGIN processes+=Process END)+ END)); + +Observability returns Observability: + (('alertClass' ':' alertClass=AlertClass) + & ('scrape' ':' BEGIN scrape=Scrape END)); + +Scrape returns Scrape: + (('process' ':' process=Text) + & ('surface' ':' surface=Text) + & ('path' ':' path=Text)); + +Exposure returns Exposure: + (('name' ':' name=Text) + & ('host' ':' host=Text) + & ('audience' ':' audience=Audience) + & ('contentPolicy' ':' contentPolicy=ContentPolicy) + & ('routes' ':' BEGIN (DASH BEGIN routes+=Route END)+ END)); + +Route returns Route: + (('path' ':' path=Text) + & ('match' ':' match=Match) + & ('process' ':' process=Text) + & ('surface' ':' surface=Text)); + +Process returns Process: + (('name' ':' name=Text) + & ('lifecycle' ':' lifecycle=Lifecycle) + & ('image' ':' image=Text) + & ('runtime' ':' runtime=Runtime) + & ('provides' ':' BEGIN provides+=SurfacePort+ END)? + & ('placement' ':' BEGIN placement=Placement END) + & ('probes' ':' BEGIN probes=Probes END)? + & ('startupBudget' ':' startupBudget=Text)? + & ('cutover' ':' cutover=Cutover)); + +SurfacePort returns SurfacePort: + key=Text ':' value=Port; + +Placement returns Placement: + (('memory' ':' memory=Text) + & ('cpu' ':' cpu=Text)); + +Probes returns Probes: + (('readiness' ':' BEGIN readiness=Probe END)? + & ('liveness' ':' BEGIN liveness=Probe END)?); + +Probe returns Probe: + (('path' ':' path=Text) + & ('port' ':' port=Port)); + +Port returns ecore::EInt: + INT; + +// A scalar, quoted or not. Every keyword above is also a scalar, so a value +// that happens to read like a key is still a value. +Text returns ecore::EString: + SCALAR | STRING | INT | Keyword; + +Keyword: + 'apiVersion' | 'kind' | 'schemaVersion' | 'project' | 'owner' | 'applications' | 'id' | 'observability' | + 'exposure' | 'processes' | 'alertClass' | 'scrape' | 'name' | 'host' | 'audience' | 'contentPolicy' | + 'routes' | 'match' | 'lifecycle' | 'image' | 'runtime' | 'provides' | 'placement' | 'probes' | + 'startupBudget' | 'cutover' | 'memory' | 'cpu' | 'readiness' | 'liveness' | 'path' | 'port' | + 'process' | 'surface' | + 'application' | 'job' | 'jvm' | 'python' | 'node' | 'static' | 'none' | 'rolling' | 'recreate' | + 'business-hours' | 'urgent' | 'page' | 'anonymous' | 'authenticated' | 'internal' | 'lan' | + 'strict' | 'admin' | 'workflow' | 'prefix' | 'exact'; + +enum Lifecycle returns Lifecycle: + application='application' | job='job'; + +enum Runtime returns Runtime: + jvm='jvm' | python='python' | node='node' | static='static' | none='none'; + +enum Cutover returns Cutover: + rolling='rolling' | recreate='recreate'; + +enum AlertClass returns AlertClass: + businessHours='business-hours' | urgent='urgent' | page='page'; + +enum Audience returns Audience: + anonymous='anonymous' | authenticated='authenticated' | internal='internal' | lan='lan'; + +enum ContentPolicy returns ContentPolicy: + strict='strict' | admin='admin' | workflow='workflow'; + +enum Match returns Match: + prefix='prefix' | exact='exact'; + +terminal BEGIN: 'synthetic:BEGIN'; +terminal END: 'synthetic:END'; + +// The block markers the token source rewrites: a dash opens a sequence item, +// and a flow collection opens and closes a block on one line. The parser never +// sees FLOW_ or LIST_ tokens; they arrive as BEGIN and END. +terminal DASH: '-'; +terminal FLOW_BEGIN: '{'; +terminal FLOW_END: '}'; +terminal FLOW_SEP: ','; +terminal LIST_BEGIN: '['; +terminal LIST_END: ']'; + +terminal INT returns ecore::EInt: ('0'..'9')+; + +terminal SCALAR: + !(' ' | '\t' | '\r' | '\n' | ':' | ',' | '{' | '}' | '[' | ']' | '#' | '"' | "'" | '-') + (!(' ' | '\t' | '\r' | '\n' | ':' | ',' | '{' | '}' | '[' | ']' | '#'))*; + +terminal STRING: + '"' !('"')* '"' | "'" !("'")* "'"; + +terminal SL_COMMENT: '#' !('\n' | '\r')*; + +terminal WS: (' ' | '\t' | '\r' | '\n')+; diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentRuntimeModule.java b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentRuntimeModule.java new file mode 100644 index 0000000..5f3fe65 --- /dev/null +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentRuntimeModule.java @@ -0,0 +1,9 @@ +/* + * generated by Xtext + */ +package dev.jorisjonkers.deploykit.emf.syntax; + +/** + * Use this class to register components to be used at runtime / without the Equinox extension registry. + */ +public class ProjectIntentRuntimeModule extends AbstractProjectIntentRuntimeModule {} diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentStandaloneSetup.java b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentStandaloneSetup.java new file mode 100644 index 0000000..2023b8e --- /dev/null +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/ProjectIntentStandaloneSetup.java @@ -0,0 +1,14 @@ +/* + * generated by Xtext + */ +package dev.jorisjonkers.deploykit.emf.syntax; + +/** + * Initialization support for running Xtext languages without Equinox extension registry. + */ +public class ProjectIntentStandaloneSetup extends ProjectIntentStandaloneSetupGenerated { + + public static void doSetup() { + new ProjectIntentStandaloneSetup().createInjectorAndDoEMFRegistration(); + } +} diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/Skeleton.xtext b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/Skeleton.xtext deleted file mode 100644 index dda8d26..0000000 --- a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/Skeleton.xtext +++ /dev/null @@ -1,9 +0,0 @@ -grammar dev.jorisjonkers.deploykit.emf.syntax.Skeleton with org.eclipse.xtext.common.Terminals - -generate skeleton "https://jorisjonkers.dev/deploy-kit/emf/syntax/skeleton" - -Document: - entries+=Entry*; - -Entry: - key=ID ':' value=STRING; diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSource.java b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSource.java new file mode 100644 index 0000000..7867a89 --- /dev/null +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSource.java @@ -0,0 +1,126 @@ +package dev.jorisjonkers.deploykit.emf.syntax.blocks; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Queue; +import org.antlr.runtime.CommonToken; +import org.antlr.runtime.Token; +import org.antlr.runtime.TokenSource; + +/** + * Turns the block structure of the authored YAML into the synthetic BEGIN and END tokens the grammar + * reads, so the grammar states the model's shape rather than the file's layout + * (emf/docs/architecture.md#concrete-syntax). + * + *

A line indented further than the line before it opens a block, and every level it falls back + * closes one. A dash opens a block of its own around the item that follows it, so the first key of + * an item sits in the same block as the keys under it. A flow collection opens and closes a block on + * one line, and the comma between its entries becomes whitespace. Inside a flow collection + * indentation says nothing, so it is not read. + */ +public class BlockTokenSource implements TokenSource { + + private final TokenSource delegate; + private final BlockTokens types; + private final Deque blocks = new ArrayDeque<>(); + private final Queue pending = new ArrayDeque<>(); + private boolean lineStart = true; + private boolean afterDash = false; + private int flowDepth = 0; + + public BlockTokenSource(TokenSource delegate, BlockTokens types) { + this.delegate = delegate; + this.types = types; + blocks.push(0); + } + + @Override + public Token nextToken() { + while (pending.isEmpty()) { + read(delegate.nextToken()); + } + return pending.remove(); + } + + @Override + public String getSourceName() { + return delegate.getSourceName(); + } + + private void read(Token token) { + if (token.getType() == Token.EOF) { + closeTo(0, token); + pending.add(token); + } else if (token.getType() == types.whitespace() || token.getType() == types.comment()) { + lineStart |= token.getText().indexOf('\n') >= 0; + pending.add(token); + } else if (token.getType() == types.separator()) { + pending.add(hidden(token)); + } else { + significant(token); + } + } + + private void significant(Token token) { + if (lineStart && flowDepth == 0) { + indent(token); + } + lineStart = false; + if (types.opensFlow(token.getType())) { + afterDash = false; + flowDepth++; + pending.add(marker(types.begin(), token)); + } else if (types.closesFlow(token.getType())) { + flowDepth--; + pending.add(marker(types.end(), token)); + } else { + if (afterDash) { + afterDash = false; + blocks.push(token.getCharPositionInLine()); + pending.add(marker(types.begin(), token)); + } + pending.add(token); + afterDash = token.getType() == types.dash(); + } + } + + /** Opens or closes the blocks the column of the line's first token asks for. */ + private void indent(Token token) { + int column = token.getCharPositionInLine(); + if (column > current()) { + blocks.push(column); + pending.add(marker(types.begin(), token)); + } else { + closeTo(column, token); + } + } + + private void closeTo(int column, Token token) { + while (current() > column) { + blocks.pop(); + pending.add(marker(types.end(), token)); + } + } + + private int current() { + return blocks.element(); + } + + /** A zero-width token at {@code at}, so the node model's text stays the file's. The lexer's tokens are + * {@link CommonToken}s, the end of file included. */ + private Token marker(int type, Token at) { + CommonToken marker = new CommonToken(type, ""); + marker.setLine(at.getLine()); + marker.setCharPositionInLine(at.getCharPositionInLine()); + int start = ((CommonToken) at).getStartIndex(); + marker.setStartIndex(start); + marker.setStopIndex(start - 1); + return marker; + } + + private Token hidden(Token token) { + CommonToken copy = new CommonToken(token); + copy.setType(types.whitespace()); + return copy; + } +} diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokens.java b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokens.java new file mode 100644 index 0000000..1543b23 --- /dev/null +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokens.java @@ -0,0 +1,38 @@ +package dev.jorisjonkers.deploykit.emf.syntax.blocks; + +/** + * The token types {@link BlockTokenSource} rewrites, as the generated parser numbers them. + * + * @param begin the synthetic token that opens a block + * @param end the synthetic token that closes one + * @param whitespace whitespace, which carries the line breaks and the indentation + * @param comment a comment, which is hidden and never opens a block + * @param dash the marker of a block sequence item + * @param flowBegin an opening brace + * @param flowEnd a closing brace + * @param listBegin an opening bracket + * @param listEnd a closing bracket + * @param separator the comma between flow entries + */ +public record BlockTokens( + int begin, + int end, + int whitespace, + int comment, + int dash, + int flowBegin, + int flowEnd, + int listBegin, + int listEnd, + int separator) { + + /** Whether {@code type} opens a flow collection, which is a block on one line. */ + public boolean opensFlow(int type) { + return type == flowBegin || type == listBegin; + } + + /** Whether {@code type} closes one. */ + public boolean closesFlow(int type) { + return type == flowEnd || type == listEnd; + } +} diff --git a/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/parser/antlr/ProjectIntentTokenSource.java b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/parser/antlr/ProjectIntentTokenSource.java new file mode 100644 index 0000000..e13916e --- /dev/null +++ b/emf/syntax/src/main/java/dev/jorisjonkers/deploykit/emf/syntax/parser/antlr/ProjectIntentTokenSource.java @@ -0,0 +1,28 @@ +/* + * generated by Xtext, then owned here: the block rules are this language's, not the generator's. + */ +package dev.jorisjonkers.deploykit.emf.syntax.parser.antlr; + +import dev.jorisjonkers.deploykit.emf.syntax.blocks.BlockTokenSource; +import dev.jorisjonkers.deploykit.emf.syntax.blocks.BlockTokens; +import dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.internal.InternalProjectIntentParser; +import org.antlr.runtime.TokenSource; + +public class ProjectIntentTokenSource extends BlockTokenSource { + + public static final BlockTokens TYPES = new BlockTokens( + InternalProjectIntentParser.RULE_BEGIN, + InternalProjectIntentParser.RULE_END, + InternalProjectIntentParser.RULE_WS, + InternalProjectIntentParser.RULE_SL_COMMENT, + InternalProjectIntentParser.RULE_DASH, + InternalProjectIntentParser.RULE_FLOW_BEGIN, + InternalProjectIntentParser.RULE_FLOW_END, + InternalProjectIntentParser.RULE_LIST_BEGIN, + InternalProjectIntentParser.RULE_LIST_END, + InternalProjectIntentParser.RULE_FLOW_SEP); + + public ProjectIntentTokenSource(TokenSource delegate) { + super(delegate, TYPES); + } +} diff --git a/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/SkeletonTest.java b/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/SkeletonTest.java deleted file mode 100644 index e86f319..0000000 --- a/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/SkeletonTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.jorisjonkers.deploykit.emf.syntax; - -import static org.assertj.core.api.Assertions.assertThat; - -import dev.jorisjonkers.deploykit.emf.syntax.skeleton.Document; -import dev.jorisjonkers.deploykit.emf.syntax.skeleton.Entry; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.xtext.resource.XtextResourceSet; -import org.junit.jupiter.api.Test; - -// Walking skeleton (#81): a generated Xtext parser reads a document headless. -// Deleted by the Task 1 syntax ticket, whose suite covers Xtext. -class SkeletonTest { - - private static Resource parse(String text) throws IOException { - XtextResourceSet resources = new SkeletonStandaloneSetup() - .createInjectorAndDoEMFRegistration() - .getInstance(XtextResourceSet.class); - Resource resource = resources.createResource(URI.createURI("memory:/notes.skeleton")); - resource.load(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), Map.of()); - return resource; - } - - @Test - void anXtextGrammarParsesAThreeLineDocument() throws IOException { - Resource resource = parse("project: 'notes'\napplication: 'notes'\nprocess: 'web'\n"); - - assertThat(resource.getErrors()).isEmpty(); - assertThat(((Document) resource.getContents().get(0)).getEntries()) - .extracting(Entry::getKey, Entry::getValue) - .containsExactly( - org.assertj.core.groups.Tuple.tuple("project", "notes"), - org.assertj.core.groups.Tuple.tuple("application", "notes"), - org.assertj.core.groups.Tuple.tuple("process", "web")); - } - - @Test - void aDocumentOutsideTheGrammarIsRefused() throws IOException { - assertThat(parse("project 'notes'\n").getErrors()).isNotEmpty(); - } -} diff --git a/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSourceTest.java b/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSourceTest.java new file mode 100644 index 0000000..42dc8fd --- /dev/null +++ b/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokenSourceTest.java @@ -0,0 +1,121 @@ +package dev.jorisjonkers.deploykit.emf.syntax.blocks; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.ProjectIntentTokenSource; +import dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.internal.InternalProjectIntentParser; +import dev.jorisjonkers.deploykit.emf.syntax.parser.antlr.lexer.InternalProjectIntentLexer; +import java.util.ArrayList; +import java.util.List; +import org.antlr.runtime.ANTLRStringStream; +import org.antlr.runtime.CommonToken; +import org.antlr.runtime.Token; +import org.junit.jupiter.api.Test; + +/** The block structure the grammar reads, as the tokens this source produces for authored YAML. */ +class BlockTokenSourceTest { + + /** Every significant token of {@code text}, as "BEGIN", "END" or its own text. */ + private static List blocks(String text) { + ProjectIntentTokenSource source = + new ProjectIntentTokenSource(new InternalProjectIntentLexer(new ANTLRStringStream(text))); + List tokens = new ArrayList<>(); + for (Token token = source.nextToken(); token.getType() != Token.EOF; token = source.nextToken()) { + if (token.getType() == InternalProjectIntentParser.RULE_BEGIN) { + tokens.add("BEGIN"); + } else if (token.getType() == InternalProjectIntentParser.RULE_END) { + tokens.add("END"); + } else if (token.getType() != InternalProjectIntentParser.RULE_WS + && token.getType() != InternalProjectIntentParser.RULE_SL_COMMENT) { + tokens.add(token.getText()); + } + } + return tokens; + } + + @Test + void theSourceNameIsTheOneTheLexerGives() { + assertThat(new ProjectIntentTokenSource(new InternalProjectIntentLexer(new ANTLRStringStream("a: 1"))) + .getSourceName()) + .isEqualTo(new InternalProjectIntentLexer(new ANTLRStringStream("a: 1")).getSourceName()); + } + + @Test + void anIndentedLineOpensABlockAndFallingBackClosesIt() { + assertThat(blocks("a:\n b:\n c: 1\nd: 2\n")) + .containsExactly("a", ":", "BEGIN", "b", ":", "BEGIN", "c", ":", "1", "END", "END", "d", ":", "2"); + } + + @Test + void everyBlockStillOpenAtTheEndOfTheFileIsClosed() { + assertThat(blocks("a:\n b:\n c: 1")).endsWith("1", "END", "END"); + } + + @Test + void aDashOpensABlockAroundTheItemThatFollowsIt() { + assertThat(blocks("a:\n - b: 1\n c: 2\n - b: 3\n")) + .containsExactly( + "a", ":", "BEGIN", "-", "BEGIN", "b", ":", "1", "c", ":", "2", "END", "-", "BEGIN", "b", ":", + "3", "END", "END"); + } + + @Test + void aFlowMappingIsABlockOnOneLine() { + assertThat(blocks("a: { b: 1, c: 2 }\n")) + .containsExactly("a", ":", "BEGIN", "b", ":", "1", "c", ":", "2", "END"); + } + + @Test + void aDashFollowedByAFlowMappingOpensOneBlock() { + assertThat(blocks("a:\n - { b: 1 }\n - { b: 2 }\n")) + .containsExactly( + "a", ":", "BEGIN", "-", "BEGIN", "b", ":", "1", "END", "-", "BEGIN", "b", ":", "2", "END", + "END"); + } + + @Test + void aBlockOpenedByAFlowMappingIsClosedWithIt() { + assertThat(blocks("a: { b: 1 }\nc:\n d: 2\n")) + .containsExactly("a", ":", "BEGIN", "b", ":", "1", "END", "c", ":", "BEGIN", "d", ":", "2", "END"); + } + + @Test + void aSyntheticTokenSitsWhereTheTokenThatAskedForItSits() { + ProjectIntentTokenSource source = + new ProjectIntentTokenSource(new InternalProjectIntentLexer(new ANTLRStringStream("a:\n b: 1\n"))); + List tokens = new ArrayList<>(); + for (Token token = source.nextToken(); token.getType() != Token.EOF; token = source.nextToken()) { + tokens.add(token); + } + Token begin = tokens.stream() + .filter(token -> token.getType() == InternalProjectIntentParser.RULE_BEGIN) + .findFirst() + .orElseThrow(); + CommonToken key = (CommonToken) tokens.stream() + .filter(token -> "b".equals(token.getText())) + .findFirst() + .orElseThrow(); + + assertThat(begin.getText()).isEmpty(); + assertThat(begin.getLine()).isEqualTo(key.getLine()); + assertThat(begin.getCharPositionInLine()).isEqualTo(key.getCharPositionInLine()); + assertThat(((CommonToken) begin).getStartIndex()).isEqualTo(key.getStartIndex()); + assertThat(((CommonToken) begin).getStopIndex()).isEqualTo(key.getStartIndex() - 1); + } + + @Test + void indentationInsideAFlowMappingIsNotRead() { + assertThat(blocks("a: { b: 1,\n c: 2 }\nd: 3\n")) + .containsExactly("a", ":", "BEGIN", "b", ":", "1", "c", ":", "2", "END", "d", ":", "3"); + } + + @Test + void aCommentLineOpensNoBlock() { + assertThat(blocks("a: 1\n # a comment, indented\nb: 2\n")).containsExactly("a", ":", "1", "b", ":", "2"); + } + + @Test + void aFlowSequenceIsAnEmptyBlockRatherThanAnItem() { + assertThat(blocks("a: []\n")).containsExactly("a", ":", "BEGIN", "END"); + } +} diff --git a/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokensTest.java b/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokensTest.java new file mode 100644 index 0000000..85f891a --- /dev/null +++ b/emf/syntax/src/test/java/dev/jorisjonkers/deploykit/emf/syntax/blocks/BlockTokensTest.java @@ -0,0 +1,41 @@ +package dev.jorisjonkers.deploykit.emf.syntax.blocks; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** Which token types open and close a flow collection. */ +class BlockTokensTest { + + private static final BlockTokens TYPES = new BlockTokens(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + @Test + void aBraceAndABracketOpenAndCloseAFlowCollection() { + assertThat(TYPES.opensFlow(TYPES.flowBegin())).isTrue(); + assertThat(TYPES.opensFlow(TYPES.listBegin())).isTrue(); + assertThat(TYPES.closesFlow(TYPES.flowEnd())).isTrue(); + assertThat(TYPES.closesFlow(TYPES.listEnd())).isTrue(); + } + + @Test + void nothingElseOpensOrClosesOne() { + assertThat(TYPES.opensFlow(TYPES.flowEnd())).isFalse(); + assertThat(TYPES.opensFlow(TYPES.dash())).isFalse(); + assertThat(TYPES.closesFlow(TYPES.flowBegin())).isFalse(); + assertThat(TYPES.closesFlow(TYPES.separator())).isFalse(); + } + + @Test + void theTypesAreTheOnesItWasGiven() { + assertThat(TYPES.begin()).isEqualTo(1); + assertThat(TYPES.end()).isEqualTo(2); + assertThat(TYPES.whitespace()).isEqualTo(3); + assertThat(TYPES.comment()).isEqualTo(4); + assertThat(TYPES.dash()).isEqualTo(5); + assertThat(TYPES.flowBegin()).isEqualTo(6); + assertThat(TYPES.flowEnd()).isEqualTo(7); + assertThat(TYPES.listBegin()).isEqualTo(8); + assertThat(TYPES.listEnd()).isEqualTo(9); + assertThat(TYPES.separator()).isEqualTo(10); + } +} diff --git a/eslint.config.js b/eslint.config.js index c0f8ac8..3dce10c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,7 @@ export default defineConfig( "dist/**", "node_modules/**", "reports/**", + ".stryker-tmp/**", // Maven build output from the model-driven implementation under emf/. "**/target/**", ], diff --git a/package-lock.json b/package-lock.json index f552b5f..3d6398d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,13 @@ "version": "0.2.0", "license": "LicenseRef-JorisJonkers-Proprietary-1.0", "dependencies": { + "yaml": "2.9.1", "zod": "^4.5.4" }, "devDependencies": { "@eslint/js": "10.0.1", + "@stryker-mutator/core": "10.0.0", + "@stryker-mutator/vitest-runner": "10.0.0", "@types/node": "24.13.3", "@vitest/coverage-v8": "4.1.11", "@vitest/eslint-plugin": "1.6.27", @@ -28,6 +31,536 @@ "node": ">=24" } }, + "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==", + "dev": true, + "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/code-frame/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/core": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.5.tgz", + "integrity": "sha512-2/oWkgTbBYoqioCWAE4XJobOrzwxTDa5/XjDP3tJ1BhDr/owcd9qnXBp4xc3/2G5X4bvXM04nrxbRJxFcXAxFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.5", + "@babel/helper-compilation-targets": "^8.0.5", + "@babel/helpers": "^8.0.5", + "@babel/parser": "^8.0.5", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.5", + "@babel/types": "^8.0.5", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/parser": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.5.tgz", + "integrity": "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.5" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/generator": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.5.tgz", + "integrity": "sha512-f/TuhuMAxJqhwxEGNsJrswuG9VHmh0oNFoQoo6TbpgtFAz9wYZXcTAcWZMHfp7ljesr0RG04bp3Aos9GI59L7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.5", + "@babel/types": "^8.0.5", + "@jridgewell/gen-mapping": "0.4.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.31", + "@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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.5.tgz", + "integrity": "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.5" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/helper-compilation-targets": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.5.tgz", + "integrity": "sha512-Qk8ahMGooH5mz6uuhoDvfZGkUf/Mf3RTBucVVl4MKx4LKMTv872TeW8O92h15iVtlN8wAROBIpI1aV6x1z0LCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^8.0.5", + "@babel/helper-validator-option": "^8.0.0", + "browserslist": "^4.24.0", + "lru-cache": "^11.0.0", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-8.0.5.tgz", + "integrity": "sha512-ckUE7tmolBbW4QV02lD874sWSPy8da2A09i/h0qS+IWolR+WiNhpq7C/rHZm00uOIkUb0Cp6+0Cy1ku/wj5uQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^8.0.0", + "@babel/helper-member-expression-to-functions": "^8.0.5", + "@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.5", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-8.0.5.tgz", + "integrity": "sha512-GLe05QD98BkNFTkjaqeqF9QSRoHLKrrB4tpoplyQuuPrQJ72rtmkM4wvqJLX/sjZPqkbK5MUYSsuUTqdJoOVjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^8.0.5", + "@babel/types": "^8.0.5" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/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==", + "dev": true, + "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-module-imports/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-module-imports/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/helper-module-transforms": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.5.tgz", + "integrity": "sha512-lUsSqMD0l5cJKl+3vlTV/hPDA/IvNkcHWVI3zFud2Vur+C9bCKdpqbpkN9eABembedbdWwF0qow6nbdIpNlpcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4", + "@babel/traverse": "^8.0.5" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.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==", + "dev": true, + "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" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -35,33 +568,504 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helpers": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.5.tgz", + "integrity": "sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.5" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-8.0.2.tgz", + "integrity": "sha512-+C6O6KKXU7BBq1GNaIkFJxrALUVGRcr+WeWm4OcuRl3h+l/CmNfcTLMrT2Lm3uvGBimBH/8pEBRrXJFLoO67Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^8.0.1", + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/plugin-syntax-decorators": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-8.0.1.tgz", + "integrity": "sha512-NI+0S/6MvR6GlcQFwjDZ+WIc2qvG6TXN534lYs9llNldwW4b7Dh6KTtk030FA0xWdYGs4t1lWo+OEWN8wGB+Nw==", + "dev": true, + "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-syntax-jsx": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-8.0.1.tgz", + "integrity": "sha512-n0jtCOxEovhU7METqSQjcZO9pX53nu9uNIjMS+hEt+Nt9jA7oOZoBIgbCxhhASmF6T6rPDGge5UAvh6Z4eFz/g==", + "dev": true, + "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-syntax-typescript": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-8.0.3.tgz", + "integrity": "sha512-jmTPwps7oSQSZaV1SxkQ3C12UWyufGysGc5OzDpZzvPAIX4mO7dJT3hoqkWVrSImvkcMiknir1iLN1SNV/CZzg==", + "dev": true, + "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-destructuring": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-8.0.5.tgz", + "integrity": "sha512-evjxc5fGvpXG2WMGSalpX8IJZVumLIDgk5r5eRhvXRDqFX/GPWq1BeRfJalPCcwJfrR5A/wvegUhx9W5Nr7/mg==", + "dev": true, + "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-explicit-resource-management": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-8.0.5.tgz", + "integrity": "sha512-Vr20jk/ZxRGu30o0jYvcKl2Pf0tiyr8H9q2ZGxYD6yjOpv9DdP6//I85q0cSOdADZhsu/ZZoApjGVCfl4xnVJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/plugin-transform-destructuring": "^8.0.5" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.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==", + "dev": true, + "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-react-display-name": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-8.0.1.tgz", + "integrity": "sha512-soLishXlkyu6jcICPyO3HEP7A3GCzKEnn7XfvYrImuWEOwFAz93qShmWSYPf5ww0ZkO4By0zsN2bVIDF54fSdA==", + "dev": true, + "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-react-jsx": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-8.0.1.tgz", + "integrity": "sha512-NgkoF7Uq+30TmOPDdNUimT0Nta02uVjqJRFNlVWKrbOCu/CkzfHa4aMnIs0lMpkMmZmWA1e42Va+F04i/pY1zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^8.0.0", + "@babel/helper-module-imports": "^8.0.0", + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/plugin-syntax-jsx": "^8.0.1", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-8.0.1.tgz", + "integrity": "sha512-Hb+HUZpV9KFHjm+F+P3aLDMi8QXU9l3ROCQv20z18Me2sGyW5nNNR5YTevNlgHvCpFek3BnAwhDGq/BRndXViw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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-react-pure-annotations": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-8.0.1.tgz", + "integrity": "sha512-7/8UwU8hoPBurXa9tUiTTC8aACTRy5tCqLUtqikHp2eGiWoEB57AduOdbQ71OOMTEvawKrGhv3WfzkDpI+/oSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^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-typescript": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-8.0.5.tgz", + "integrity": "sha512-o6XW6OngFfpEQat3J1MxUDEd94Rjh80BZQqWmOXa9YTtoyDnk8Zim3NXcx3RTgGiOgkoayCMYp+Xk5NxEmzoMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^8.0.0", + "@babel/helper-create-class-features-plugin": "^8.0.5", + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0", + "@babel/plugin-syntax-typescript": "^8.0.3" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-8.0.1.tgz", + "integrity": "sha512-jrFuPp/pTddFZbtmWhdLNAYc6UMcpboeUPnw0BBrm4nOmcAko/1TRcFi1PzWCeOFRU+VaSiKmat87W1HvR7mIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/helper-validator-option": "^8.0.0", + "@babel/plugin-transform-react-display-name": "^8.0.1", + "@babel/plugin-transform-react-jsx": "^8.0.1", + "@babel/plugin-transform-react-jsx-development": "^8.0.1", + "@babel/plugin-transform-react-pure-annotations": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-8.0.1.tgz", + "integrity": "sha512-qrPhQIN1NLrPmzgazF9XKQqXrOcp/WJly+K+6ReFonn24FZqRJO7clxOJo6Ni75L+2vAqI3cHVU2OJLBxoPp5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^8.0.1", + "@babel/helper-validator-option": "^8.0.0", + "@babel/plugin-transform-modules-commonjs": "^8.0.1", + "@babel/plugin-transform-typescript": "^8.0.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "peerDependencies": { + "@babel/core": "^8.0.0" + } + }, + "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==", + "dev": true, + "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/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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.5.tgz", + "integrity": "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.8" + "@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/traverse": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.5.tgz", + "integrity": "sha512-XFfnuvapSc/vJOcUO7kwORSvpBIvraofKEZ2dhT0PjiF21BRCD7YbAFC8UEeDJNeLoQz82/gVqzgX5hCzkCbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.5", + "@babel/helper-globals": "^8.0.0", + "@babel/parser": "^8.0.5", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.5", + "obug": "^2.1.1" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.5.tgz", + "integrity": "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.5" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=6.0.0" + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/types": { @@ -263,23 +1267,378 @@ "engines": { "node": ">=12.22" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.5.tgz", + "integrity": "sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.3.tgz", + "integrity": "sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/external-editor": "^3.0.5", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.5.tgz", + "integrity": "sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.5.tgz", + "integrity": "sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.6.tgz", + "integrity": "sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.3.tgz", + "integrity": "sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.2.tgz", + "integrity": "sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.2.tgz", + "integrity": "sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.5", + "@inquirer/confirm": "^6.3.2", + "@inquirer/editor": "^5.3.3", + "@inquirer/expand": "^5.1.5", + "@inquirer/input": "^5.1.6", + "@inquirer/number": "^4.2.3", + "@inquirer/password": "^5.2.2", + "@inquirer/rawlist": "^5.3.5", + "@inquirer/search": "^4.3.3", + "@inquirer/select": "^5.2.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.5.tgz", + "integrity": "sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.3.tgz", + "integrity": "sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.5.tgz", + "integrity": "sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=18.18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -430,9 +1789,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -450,9 +1806,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -470,9 +1823,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -490,9 +1840,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -510,9 +1857,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -530,9 +1874,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -600,6 +1941,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -607,6 +1968,213 @@ "dev": true, "license": "MIT" }, + "node_modules/@stryker-mutator/api": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-10.0.0.tgz", + "integrity": "sha512-ZtAJ0ZT3MVRCWJTBE2h90XB/6E+4lifHYtcTyNG6nU2nLekPgTo4gD5esjX6Okxo1b/JB4jJzyxYB54fwKAoJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.8.4", + "mutation-testing-report-schema": "3.8.4", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-10.0.0.tgz", + "integrity": "sha512-ZvMsRyaXQQ5e6Thcid9pkuODv6Fn9E3nrBQJUap+hcJuGJ4unm26afo3m6YKSjn8kinyxJ/3TXf0cTWRDaTxVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@stryker-mutator/api": "10.0.0", + "@stryker-mutator/instrumenter": "10.0.0", + "@stryker-mutator/util": "10.0.0", + "ajv": "~8.20.0", + "chalk": "~5.6.0", + "commander": "~14.0.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.6.0", + "execa": "~9.6.0", + "json-rpc-2.0": "^1.7.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~10.2.4", + "mutation-server-protocol": "~0.4.0", + "mutation-testing-elements": "3.8.4", + "mutation-testing-metrics": "3.8.4", + "mutation-testing-report-schema": "3.8.4", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.3.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-10.0.0.tgz", + "integrity": "sha512-B7Wmn1KlEWyFeOz6D6oGvQGRfi5Xw3VemG6dEKvFQp4qLvxD9Mf4kcZghfxffgnYwXd3bFgqXsJ+ZGlhdfIOrQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~8.0.0", + "@babel/generator": "~8.0.0", + "@babel/parser": "~8.0.0", + "@babel/plugin-proposal-decorators": "~8.0.0", + "@babel/plugin-transform-explicit-resource-management": "^8.0.0", + "@babel/preset-react": "~8.0.0", + "@babel/preset-typescript": "~8.0.0", + "@babel/traverse": "~8.0.4", + "@stryker-mutator/api": "10.0.0", + "@stryker-mutator/util": "10.0.0", + "angular-html-parser": "~10.11.0", + "semver": "~7.8.0", + "tslib": "2.8.1", + "weapon-regex": "~2.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/parser": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.5.tgz", + "integrity": "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/types": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", + "dev": true, + "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/@stryker-mutator/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-10.0.0.tgz", + "integrity": "sha512-LzOpHiJaCp2ABQgnPMlrQQcsK43bd5Vo/2FGL78aN62yDoeRQ+4j3tzeuXxK5OAHdC3fUz6TDoy4IsoAuLAd3w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stryker-mutator/vitest-runner": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-10.0.0.tgz", + "integrity": "sha512-SHK2/vfvRUpiz7jXPnQMBnr6zLdm69DK03Mo5mPhaZWcRSygrKUqYsPqWsXsK+5ySHzlMTfCyFK5NQ/X9sJFFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "10.0.0", + "@stryker-mutator/util": "10.0.0", + "semver": "^7.7.4", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "10.0.0", + "vitest": ">=2.0.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -639,6 +2207,20 @@ "dev": true, "license": "MIT" }, + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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", @@ -1134,6 +2716,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/angular-html-parser": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.11.0.tgz", + "integrity": "sha512-3vERzJ65UFDr3C7uozLJwsNcQS3FS784dSh583oDgDTTZMgXe3/pdyXgKndxiP5R2lvYRfW6gSl145Hnyf2OFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1182,6 +2774,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz", + "integrity": "sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -1195,6 +2800,92 @@ "node": "20 || >=22" } }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1222,6 +2913,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1360,14 +3068,71 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.428", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.428.tgz", + "integrity": "sha512-1JxbaFJj1bRKurj1uY3l4xxpU9kOUAUjcIgApj0qu1Pao5GhoIWI8iL0BeMYJ2njig1hBx0A7eKD9VGjH9wlHw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14" } }, "node_modules/enhanced-resolve": { @@ -1384,6 +3149,16 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -1401,6 +3176,29 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1579,6 +3377,33 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1610,6 +3435,50 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1628,6 +3497,22 @@ } } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1704,6 +3589,72 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1746,6 +3697,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1763,6 +3727,19 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1783,6 +3760,33 @@ "dev": true, "license": "MIT" }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1793,6 +3797,17 @@ "node": ">= 4" } }, + "node_modules/import-meta-resolve": { + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1803,6 +3818,13 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", @@ -1892,6 +3914,45 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1938,6 +3999,13 @@ "node": ">=8" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -1945,6 +4013,19 @@ "dev": true, "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -1952,6 +4033,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-rpc-2.0": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.8.0.tgz", + "integrity": "sha512-4nw+XlJbk5XokA7BtqHGu+0PEiUPfAkRzXXd1Cgjy1c3F2UI/3Gy7yr76wyJEv3X1F1SRGGF8xPAPPhelBiGmA==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2156,9 +4244,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2180,9 +4265,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2204,9 +4286,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2228,9 +4307,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2302,6 +4378,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, + "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/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2340,6 +4433,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -2373,6 +4483,53 @@ "dev": true, "license": "MIT" }, + "node_modules/mutation-server-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", + "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mutation-testing-elements": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.8.4.tgz", + "integrity": "sha512-5CF1SNa7at5ZH33vEr+21wNebTSrtNIVvnzaUlxortHajOrIPaSLczIWvg6sI/fsExekQ7jookwf2cHftkckqQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.8.4.tgz", + "integrity": "sha512-DZcmndJBH6nrNs3tpiB3OcMVq9KkG2cHCpJSnDxSxPwi9qrafRmoec40xjhkbzOoX1n7/4UkDqg5tIj4A6nvCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.8.4" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.8.4.tgz", + "integrity": "sha512-s4G71R6Lt/PpZ0cqeglIcgyBdzLM8E+SeCHZAPg1wkSsPtRBa4XfPzAozYKdiJk/TLbNEEb7En9t0/bveuPuxA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nanoid": { "version": "3.3.19", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", @@ -2399,6 +4556,59 @@ "dev": true, "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", @@ -2463,6 +4673,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2572,6 +4795,32 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -2596,6 +4845,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -2619,6 +4884,16 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -2675,6 +4950,16 @@ "@rolldown/binding-win32-x64-msvc": "1.2.8" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", @@ -2685,6 +4970,13 @@ "regexp-tree": "~0.1.1" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2721,6 +5013,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2728,6 +5096,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -2735,6 +5116,16 @@ "dev": true, "license": "MIT" }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2769,6 +5160,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2853,6 +5257,16 @@ "node": ">=14.0.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -2897,6 +5311,23 @@ "node": ">=10.13.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2910,6 +5341,33 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -2948,6 +5406,13 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -2955,6 +5420,50 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2965,6 +5474,19 @@ "punycode": "^2.1.0" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/vite": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", @@ -3146,6 +5668,13 @@ "node": "^22.13||^24||>=26" } }, + "node_modules/weapon-regex": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-2.0.5.tgz", + "integrity": "sha512-BJZkSdtcae9Wb8+hKNtAqR+Q61EMOAuQnpCCHKwT50K7fIWg7/2/RnbKR27YB+sMcYxYLUVj+Asm5Y2XhD8mZg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3189,6 +5718,21 @@ "node": ">=0.10.0" } }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3202,6 +5746,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", diff --git a/package.json b/package.json index fce2781..9b171f0 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,13 @@ "typecheck": "tsc --noEmit", "test": "vitest run", "test:coverage": "vitest run --coverage", + "test:mutation": "stryker run", "verify": "npm run lint && npm run format:check && npm run typecheck && npm run lint:adrs && npm run lint:links && npm run lint:manifests && npm run lint:requirements && npm run lint:rules && npm run lint:codes && npm run lint:docs && npm run lint:secrets && npm run lint:boundaries && npm run test:coverage" }, "devDependencies": { "@eslint/js": "10.0.1", + "@stryker-mutator/core": "10.0.0", + "@stryker-mutator/vitest-runner": "10.0.0", "@types/node": "24.13.3", "@vitest/coverage-v8": "4.1.11", "@vitest/eslint-plugin": "1.6.27", @@ -60,6 +63,7 @@ "gitops" ], "dependencies": { + "yaml": "2.9.1", "zod": "^4.5.4" } } diff --git a/src/application/parse-project-intent.ts b/src/application/parse-project-intent.ts new file mode 100644 index 0000000..c35535d --- /dev/null +++ b/src/application/parse-project-intent.ts @@ -0,0 +1,13 @@ +import type { Result } from "../domain/diagnostic.ts"; +import { + validateProjectIntent, + type ValidatedProjectIntent, +} from "../wire/project-intent/map.ts"; +import { readYaml } from "../wire/project-intent/read.ts"; + +export function parseProjectIntent( + text: string, +): Result { + const read = readYaml(text); + return read.ok ? validateProjectIntent(read.value) : read; +} diff --git a/src/domain/diagnostic.ts b/src/domain/diagnostic.ts new file mode 100644 index 0000000..0c2f072 --- /dev/null +++ b/src/domain/diagnostic.ts @@ -0,0 +1,10 @@ +export interface Diagnostic { + readonly code: string; + readonly path: string; + readonly message: string; + readonly hint: string; +} + +export type Result = + | { readonly ok: true; readonly value: T } + | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] }; diff --git a/src/domain/project-intent/model.ts b/src/domain/project-intent/model.ts new file mode 100644 index 0000000..3b4e38e --- /dev/null +++ b/src/domain/project-intent/model.ts @@ -0,0 +1,62 @@ +import type { + AlertClass, + Audience, + ContentPolicy, + Cutover, + Lifecycle, + Match, + Runtime, +} from "./vocabularies.ts"; + +export interface Project { + readonly name: string; + readonly owner: string; + readonly applications: readonly Application[]; +} + +export interface Application { + readonly id: string; + readonly observability?: Observability; + readonly exposures: readonly Exposure[]; + readonly processes: readonly Process[]; +} + +export interface SurfaceRef { + readonly process: string; + readonly surface: string; +} + +export interface Observability { + readonly alertClass: AlertClass; + readonly scrape: SurfaceRef & { readonly path: string }; +} + +export interface Exposure { + readonly name: string; + readonly host: string; + readonly audience: Audience; + readonly contentPolicy: ContentPolicy; + readonly routes: readonly Route[]; +} + +export interface Route extends SurfaceRef { + readonly path: string; + readonly match: Match; +} + +export interface Probe { + readonly path: string; + readonly port: number; +} + +export interface Process { + readonly name: string; + readonly lifecycle: Lifecycle; + readonly image: string; + readonly runtime: Runtime; + readonly provides: ReadonlyMap; + readonly placement: { readonly memory: string; readonly cpu: string }; + readonly probes: { readonly readiness?: Probe; readonly liveness?: Probe }; + readonly startupBudget?: string; + readonly cutover: Cutover; +} diff --git a/src/domain/project-intent/vocabularies.ts b/src/domain/project-intent/vocabularies.ts new file mode 100644 index 0000000..836cb5c --- /dev/null +++ b/src/domain/project-intent/vocabularies.ts @@ -0,0 +1,23 @@ +// The closed vocabularies of spec/v1/10-project-intent.md#the-closed-vocabularies. +// Each is declared here once; the wire schema enumerates from these lists. + +export const LIFECYCLES = ["application", "job"] as const; +export const RUNTIMES = ["jvm", "python", "node", "static", "none"] as const; +export const CUTOVERS = ["rolling", "recreate"] as const; +export const ALERT_CLASSES = ["business-hours", "urgent", "page"] as const; +export const AUDIENCES = [ + "anonymous", + "authenticated", + "internal", + "lan", +] as const; +export const CONTENT_POLICIES = ["strict", "admin", "workflow"] as const; +export const MATCHES = ["prefix", "exact"] as const; + +export type Lifecycle = (typeof LIFECYCLES)[number]; +export type Runtime = (typeof RUNTIMES)[number]; +export type Cutover = (typeof CUTOVERS)[number]; +export type AlertClass = (typeof ALERT_CLASSES)[number]; +export type Audience = (typeof AUDIENCES)[number]; +export type ContentPolicy = (typeof CONTENT_POLICIES)[number]; +export type Match = (typeof MATCHES)[number]; diff --git a/src/index.ts b/src/index.ts index bc859cc..ac53d04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,4 @@ +export { parseProjectIntent } from "./application/parse-project-intent.ts"; +export type { Diagnostic, Result } from "./domain/diagnostic.ts"; +export type * from "./domain/project-intent/model.ts"; export { canonicalJson } from "./infrastructure/canonical-json.ts"; diff --git a/src/wire/project-intent/map.ts b/src/wire/project-intent/map.ts new file mode 100644 index 0000000..b087120 --- /dev/null +++ b/src/wire/project-intent/map.ts @@ -0,0 +1,64 @@ +import type { Diagnostic, Result } from "../../domain/diagnostic.ts"; +import type { + Application, + Process, + Project, +} from "../../domain/project-intent/model.ts"; +import { projectIntent, type ProjectIntentDocument } from "./schema.ts"; + +type WireApplication = ProjectIntentDocument["applications"][number]; +type WireProcess = WireApplication["processes"][number]; + +const escape = (segment: PropertyKey): string => + String(segment).replaceAll("~", "~0").replaceAll("/", "~1"); + +function toProcess(process: WireProcess): Process { + const { provides, probes, ...rest } = process; + return { + ...rest, + provides: new Map(Object.entries(provides ?? {})), + probes: probes ?? {}, + }; +} + +function toApplication(application: WireApplication): Application { + const { exposure, processes, ...rest } = application; + return { + ...rest, + exposures: exposure ?? [], + processes: processes.map(toProcess), + }; +} + +export interface ValidatedProjectIntent { + readonly document: ProjectIntentDocument; + readonly project: Project; +} + +export function validateProjectIntent( + value: unknown, +): Result { + const parsed = projectIntent.safeParse(value); + if (!parsed.success) + return { + ok: false, + diagnostics: parsed.error.issues.map((issue): Diagnostic => ({ + code: "schema", + path: issue.path.map((segment) => `/${escape(segment)}`).join(""), + message: issue.message, + hint: "Correct the field against spec/v1/10-project-intent.md.", + })), + }; + const { project, owner, applications } = parsed.data; + return { + ok: true, + value: { + document: parsed.data, + project: { + name: project, + owner, + applications: applications.map(toApplication), + }, + }, + }; +} diff --git a/src/wire/project-intent/read.ts b/src/wire/project-intent/read.ts new file mode 100644 index 0000000..5c9398e --- /dev/null +++ b/src/wire/project-intent/read.ts @@ -0,0 +1,37 @@ +// Authored YAML in, a plain value out. Only the subset the model's files use is +// read: one document, no anchors, aliases or explicit tags. Anything else is +// refused rather than interpreted. +import { isAlias, isNode, parseAllDocuments, visit } from "yaml"; +import type { Diagnostic, Result } from "../../domain/diagnostic.ts"; + +const HINT = + "Write plain block or flow YAML: one document, no anchors, aliases or tags."; + +function refusal(message: string): Diagnostic { + return { code: "schema", path: "", message, hint: HINT }; +} + +export function readYaml(text: string): Result { + const documents = parseAllDocuments(text); + if (documents.length !== 1) + return { + ok: false, + diagnostics: [ + refusal(`expected one YAML document, found ${documents.length}`), + ], + }; + const [document] = documents as [(typeof documents)[number]]; + const diagnostics = [...document.errors, ...document.warnings].map((error) => + refusal(error.message), + ); + visit(document, (_key, node) => { + if (isAlias(node)) diagnostics.push(refusal("an alias is not read")); + else if (isNode(node) && node.anchor !== undefined) + diagnostics.push(refusal("an anchor is not read")); + else if (isNode(node) && node.tag !== undefined) + diagnostics.push(refusal("an explicit tag is not read")); + }); + return diagnostics.length > 0 + ? { ok: false, diagnostics } + : { ok: true, value: document.toJS() as unknown }; +} diff --git a/src/wire/project-intent/schema.ts b/src/wire/project-intent/schema.ts new file mode 100644 index 0000000..64cc2bb --- /dev/null +++ b/src/wire/project-intent/schema.ts @@ -0,0 +1,71 @@ +// The authoring shape of a Project Intent document, schemaVersion 1. It covers +// the fields the minimal example uses; the rest of chapter 10 lands with #84. +import { z } from "zod"; +import { + ALERT_CLASSES, + AUDIENCES, + CONTENT_POLICIES, + CUTOVERS, + LIFECYCLES, + MATCHES, + RUNTIMES, +} from "../../domain/project-intent/vocabularies.ts"; + +const text = z.string().min(1); +const port = z.int().min(1).max(65535); + +const surfaceRef = { process: text, surface: text }; + +const probe = z.strictObject({ path: text, port }); + +const process = z.strictObject({ + name: text, + lifecycle: z.enum(LIFECYCLES), + image: text, + runtime: z.enum(RUNTIMES), + provides: z.record(text, port).exactOptional(), + placement: z.strictObject({ memory: text, cpu: text }), + probes: z + .strictObject({ + readiness: probe.exactOptional(), + liveness: probe.exactOptional(), + }) + .exactOptional(), + startupBudget: text.exactOptional(), + cutover: z.enum(CUTOVERS), +}); + +const exposure = z.strictObject({ + name: text, + host: text, + audience: z.enum(AUDIENCES), + contentPolicy: z.enum(CONTENT_POLICIES), + routes: z + .array( + z.strictObject({ path: text, match: z.enum(MATCHES), ...surfaceRef }), + ) + .min(1), +}); + +const application = z.strictObject({ + id: text, + observability: z + .strictObject({ + alertClass: z.enum(ALERT_CLASSES), + scrape: z.strictObject({ ...surfaceRef, path: text }), + }) + .exactOptional(), + exposure: z.array(exposure).exactOptional(), + processes: z.array(process).min(1), +}); + +export const projectIntent = z.strictObject({ + apiVersion: z.literal("intent.jorisjonkers.dev/v1"), + kind: z.literal("Project"), + schemaVersion: z.string().regex(/^\d+\.\d+\.\d+$/), + project: text, + owner: text, + applications: z.array(application).min(1), +}); + +export type ProjectIntentDocument = z.output; diff --git a/stryker.config.json b/stryker.config.json new file mode 100644 index 0000000..94f1194 --- /dev/null +++ b/stryker.config.json @@ -0,0 +1,19 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "testRunner": "vitest", + "vitest": { + "configFile": "vitest.mutation.config.ts" + }, + "mutate": ["src/**/*.ts", "!src/cli/boundary.ts"], + "coverageAnalysis": "perTest", + "reporters": ["clear-text", "progress", "json"], + "jsonReporter": { + "fileName": "reports/mutation/mutation.json" + }, + "tempDirName": ".stryker-tmp", + "thresholds": { + "high": 100, + "low": 100, + "break": 100 + } +} diff --git a/test/model/project-intent.test.ts b/test/model/project-intent.test.ts new file mode 100644 index 0000000..a4de65d --- /dev/null +++ b/test/model/project-intent.test.ts @@ -0,0 +1,242 @@ +// REQ-021 (docs/requirements.md): an authored Project Intent file parses to its +// committed intent oracle, and YAML or fields outside the language are refused. +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { canonicalJson, parseProjectIntent } from "../../src/index.ts"; + +const EXAMPLES = join( + import.meta.dirname, + "..", + "..", + "spec", + "v1", + "examples", +); +const MINIMAL = readFileSync( + join(EXAMPLES, "minimal", "notes.project.yml"), + "utf8", +); +const ORACLE = readFileSync( + join(EXAMPLES, "minimal", "expected", "intent.json"), + "utf8", +); + +const HEADER = `apiVersion: intent.jorisjonkers.dev/v1 +kind: Project +schemaVersion: 1.0.0 +project: p +owner: o +`; + +const PROCESS = ` - name: worker + lifecycle: job + image: worker + runtime: none + placement: { memory: 64Mi, cpu: 10m } + cutover: recreate +`; + +const withApplications = (applications: string): string => + `${HEADER}applications:\n${applications}`; + +function refused(text: string) { + const result = parseProjectIntent(text); + if (result.ok) throw new Error("expected a refusal"); + return result.diagnostics.map(({ code, path, message }) => ({ + code, + path, + message, + })); +} + +describe("parseProjectIntent", () => { + it("parses the minimal case to its committed intent oracle, byte for byte", () => { + const result = parseProjectIntent(MINIMAL); + + expect(result.ok && canonicalJson(result.value.document)).toBe(ORACLE); + }); + + it("differs from the oracle when one authored field changes", () => { + const result = parseProjectIntent(MINIMAL.replace("cpu: 50m", "cpu: 60m")); + + expect(result.ok && canonicalJson(result.value.document)).not.toBe(ORACLE); + }); + + it("maps the minimal case into the domain model", () => { + const result = parseProjectIntent(MINIMAL); + + expect(result.ok && result.value.project).toStrictEqual({ + name: "notes", + owner: "joris", + applications: [ + { + id: "notes", + observability: { + alertClass: "business-hours", + scrape: { process: "notes-api", surface: "http", path: "/metrics" }, + }, + exposures: [ + { + name: "public", + host: "notes.jorisjonkers.dev", + audience: "anonymous", + contentPolicy: "strict", + routes: [ + { + path: "/", + match: "prefix", + process: "notes-api", + surface: "http", + }, + ], + }, + ], + processes: [ + { + name: "notes-api", + lifecycle: "application", + image: "notes-api", + runtime: "node", + provides: new Map([["http", 8080]]), + placement: { memory: "256Mi", cpu: "50m" }, + probes: { + readiness: { path: "/healthz/ready", port: 8080 }, + liveness: { path: "/healthz/live", port: 8080 }, + }, + startupBudget: "20s", + cutover: "rolling", + }, + ], + }, + ], + }); + }); + + it("maps an absent block to an empty one, and leaves an absent optional field absent", () => { + const result = parseProjectIntent( + withApplications(` - id: batch\n processes:\n${PROCESS}`), + ); + + expect(result.ok && result.value.project.applications).toStrictEqual([ + { + id: "batch", + exposures: [], + processes: [ + { + name: "worker", + lifecycle: "job", + image: "worker", + runtime: "none", + provides: new Map(), + placement: { memory: "64Mi", cpu: "10m" }, + probes: {}, + cutover: "recreate", + }, + ], + }, + ]); + expect(result.ok && canonicalJson(result.value.document)).not.toContain( + "startupBudget", + ); + }); + + it.each([ + ["no document", "", "expected one YAML document, found 0"], + [ + "two documents", + `${HEADER}---\n${HEADER}`, + "expected one YAML document, found 2", + ], + ["an anchor", `${HEADER}applications: &a []\n`, "an anchor is not read"], + ["an alias", `x: &a 1\ny: *a\n`, "an alias is not read"], + [ + "an explicit tag", + `${HEADER}applications: !!seq []\n`, + "an explicit tag is not read", + ], + ])("refuses %s rather than interpreting it", (_name, text, message) => { + expect(refused(text)).toContainEqual({ code: "schema", path: "", message }); + }); + + it("refuses malformed YAML and a duplicated key at the document, before the schema runs", () => { + const batch = ` - id: batch\n processes:\n${PROCESS}`; + + expect( + refused(withApplications(`${batch} - [\n`)).map(({ path }) => path), + ).toStrictEqual([""]); + expect(refused(`owner: again\n${withApplications(batch)}`)).toStrictEqual([ + expect.objectContaining({ code: "schema", path: "" }), + ]); + }); + + it.each([ + ["10.20.30", true], + ["v1.0.0", false], + ["1.0.0-rc.1", false], + ["1.0", false], + ])("reads schemaVersion %s as valid: %s", (version, valid) => { + const text = withApplications( + ` - id: batch\n processes:\n${PROCESS}`, + ).replace("schemaVersion: 1.0.0", `schemaVersion: "${version}"`); + + expect(parseProjectIntent(text).ok).toBe(valid); + }); + + it("accepts several processes and refuses an application with none or an exposure with no route", () => { + expect( + parseProjectIntent( + withApplications( + ` - id: batch\n processes:\n${PROCESS}${PROCESS.replace("name: worker", "name: second")}`, + ), + ).ok, + ).toBe(true); + expect( + refused( + withApplications( + ` - id: batch\n processes: []\n - id: web\n exposure:\n - { name: e, host: h, audience: lan, contentPolicy: admin, routes: [] }\n processes:\n${PROCESS}`, + ), + ).map(({ path }) => path), + ).toStrictEqual([ + "/applications/0/processes", + "/applications/1/exposure/0/routes", + ]); + }); + + it("refuses a field outside the language at its JSON Pointer", () => { + const diagnostics = refused( + withApplications( + ` - id: batch\n processes:\n${PROCESS.replace("runtime: none", "runtime: rust")} stateful: true\n`, + ), + ); + + expect(diagnostics.map(({ code, path }) => ({ code, path }))).toStrictEqual( + [ + { code: "schema", path: "/applications/0/processes/0/runtime" }, + { code: "schema", path: "/applications/0/processes/0" }, + ], + ); + }); + + it("escapes a pointer segment and refuses a port that is not an integer", () => { + const diagnostics = refused( + withApplications( + ` - id: batch\n processes:\n${PROCESS} provides: { "a/b~c": "8080" }\n`, + ), + ); + + expect(diagnostics.map(({ path }) => path)).toStrictEqual([ + "/applications/0/processes/0/provides/a~1b~0c", + ]); + }); + + it("gives every diagnostic a hint", () => { + const diagnostics = ["", `${HEADER}applications: []\n`].flatMap((text) => { + const result = parseProjectIntent(text); + return result.ok ? [] : result.diagnostics; + }); + + expect(diagnostics).toHaveLength(2); + expect(diagnostics.every(({ hint }) => hint.length > 0)).toBe(true); + }); +}); diff --git a/test/mutation-contract.test.ts b/test/mutation-contract.test.ts new file mode 100644 index 0000000..0bd1109 --- /dev/null +++ b/test/mutation-contract.test.ts @@ -0,0 +1,64 @@ +// REQ-022 (docs/requirements.md): the mutation gate over src/. +import { readFileSync, readdirSync } from "node:fs"; +import { join, relative } from "node:path"; +import { describe, expect, it } from "vitest"; + +const REPOSITORY = join(import.meta.dirname, ".."); + +interface StrykerConfig { + readonly mutate: readonly string[]; + readonly thresholds: { readonly break: number }; + readonly vitest: { readonly configFile: string }; +} + +const config = JSON.parse( + readFileSync(join(REPOSITORY, "stryker.config.json"), "utf8"), +) as StrykerConfig; + +// A suite that imports from src/ but cannot run in Stryker's sandbox, which +// holds no git index, with the suite that kills its mutants instead. +const OUTSIDE_THE_SANDBOX: Readonly> = { + "test/oracles.test.ts": + "lists oracle files from the git index; test/canonical-json.test.ts covers the writer", +}; + +function testFiles(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name); + if (entry.isDirectory()) return testFiles(path); + return entry.name.endsWith(".test.ts") ? [relative(REPOSITORY, path)] : []; + }); +} + +describe("the mutation gate", () => { + it('mutates every module under src/ and breaks below the measured score, "break": 100', () => { + const text = readFileSync(join(REPOSITORY, "stryker.config.json"), "utf8"); + + expect(text).toContain('"break": 100'); + expect(config.thresholds.break).toBe(100); + expect(config.mutate).toContain("src/**/*.ts"); + }); + + it("runs every test file that imports from src/", () => { + const runner = readFileSync( + join(REPOSITORY, config.vitest.configFile), + "utf8", + ); + const importing = testFiles(join(REPOSITORY, "test")).filter( + (file) => + !(file in OUTSIDE_THE_SANDBOX) && + /from "(\.\.\/)+src\//.test( + readFileSync(join(REPOSITORY, file), "utf8"), + ), + ); + + expect(importing.length).toBeGreaterThan(0); + for (const file of importing) + expect( + runner.includes(`"${file}"`) || + (file.startsWith("test/model/") && + runner.includes('"test/model/**/*.test.ts"')), + file, + ).toBe(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 89bcbc3..b1a26f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,12 @@ "noEmit": true, "skipLibCheck": true }, - "include": ["src/**/*", "test/**/*", "scripts/**/*", "vitest.config.ts"], + "include": [ + "src/**/*", + "test/**/*", + "scripts/**/*", + "vitest.config.ts", + "vitest.mutation.config.ts" + ], "exclude": ["dist", "coverage", "node_modules"] } diff --git a/vitest.config.ts b/vitest.config.ts index 30034bf..a1a1e08 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -23,16 +23,15 @@ export default defineConfig({ // A ratchet, per docs/adr/architecture/0101-coverage-is-a-ratchet.md: // set from what the suite reaches, and only ever raised. // - // Measured 2026-09-14, after the first module under src/ (the canonical - // JSON writer, at 100%): statements 786/799, branches 432/464, - // functions 100%, lines 731/744. What is left uncovered is the one-line - // command guard at the bottom of each gate and the branches for a tool - // that cannot be started at all. + // Measured 2026-09-15, with the Project Intent parser under src/ at 100%: + // statements 832/845, branches 456/488, functions 100%, lines 776/789. + // What is left uncovered is the one-line command guard at the bottom of + // each gate and the branches for a tool that cannot be started at all. thresholds: { - statements: 98.37, - branches: 93.1, + statements: 98.46, + branches: 93.44, functions: 100, - lines: 98.25, + lines: 98.35, }, }, }, diff --git a/vitest.mutation.config.ts b/vitest.mutation.config.ts new file mode 100644 index 0000000..d5d2c15 --- /dev/null +++ b/vitest.mutation.config.ts @@ -0,0 +1,13 @@ +// The suites that exercise src/, which is all Stryker mutates. The gate suites +// read the git index and the tool binaries, neither of which exists in +// Stryker's sandbox, and none of them reaches src/. +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/model/**/*.test.ts", "test/canonical-json.test.ts"], + setupFiles: ["./test/setup.ts"], + restoreMocks: true, + unstubEnvs: true, + }, +});