Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.46%, branches 93.44%, functions 100%, lines 98.35%.
98.6%, branches 93.8%, functions 100%, lines 98.51%.

## Conventions

Expand Down
4 changes: 2 additions & 2 deletions docs/architecture-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fails the gate, so the taxonomy cannot grow entries nothing stands behind.

## Rules

This ledger holds **64** rules, **12** of them pending.
This ledger holds **64** rules, **11** of them pending.

A row is enforced or pending, never both. An enforced row names its enforcer as
`kind:value`: `depcruise:` a rule in
Expand Down Expand Up @@ -118,7 +118,7 @@ moving a live rule to pending fails the gate rather than quietly retiring it.
| RULE-040 | toolchain | Coverage is a ratchet over an explicit include list, and no ignore comment exempts a line from it | `file:vitest.config.ts` | [test/harness.test.ts](../test/harness.test.ts) `an ignore is slack nobody decided` |
| RULE-041 | toolchain | No default export outside a tool configuration file | `eslint:no-restricted-exports` | [test/seams.test.ts](../test/seams.test.ts) `RULE-041 refuses a default export` |
| RULE-042 | toolchain | Shipped code is ESM, and the one CommonJS file is the dependency-cruiser configuration that cannot be anything else | pending (#105): stated by `type: module`; no configured rule refuses a second CommonJS file | pending |
| RULE-043 | toolchain | Generated artifacts are committed, and CI fails when regenerating one produces a diff | pending (#38): the first generator is the JSON Schema #38 derives from the Project Intent metamodel, and its diff check lands with it | pending |
| RULE-043 | toolchain | Generated artifacts are committed, and CI fails when regenerating one produces a diff | `file:src/wire/project-intent/json-schema.ts` | [test/model/descriptor.test.ts](../test/model/descriptor.test.ts) `regenerates without a diff` |
| RULE-044 | cli | The CLI prints help on `--help` and `-h`, data on stdout and diagnostics on stderr, emits only data under `--json`, maps failures through one exit-code enum, honours `NO_COLOR`, and never prompts | pending (#105): each clause needs a process-level test, and the CLI ring does not exist yet | pending |
| RULE-045 | registry | Every registered adapter satisfies the adapter port, attributes every Deliverable to itself, and renders deterministically | pending (#97): the adapter contract suite arrives with the first adapter | pending |
| RULE-046 | registry | Every estate-wide invariant is registered with its code, its spec anchor and its test, so an unregistered one is detectable rather than merely absent | pending (#44): the invariant registry is that ticket's deliverable | pending |
Expand Down
15 changes: 14 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,20 @@ feature's type and multiplicity, and every closed vocabulary with its literals.
Target structures are compared through what they generate, not structurally.
The TypeScript side builds it from the Zod schemas with Zod's native
`z.toJSONSchema()` and a normaliser; the descriptor's shape is fixed here, not
by either source format.
by either source format:

- `classes`, sorted by name. Each carries its `name` and its `features`, sorted
by name; a class the language writes as one word carries that word as
`scalar`.
- A **feature** carries its `name`, the `types` it admits sorted by name,
whether it is `required`, whether it holds `many` values, and whether it is a
`map` keyed by string. A type is a class name, a vocabulary name, or one of
`string`, `int` and `boolean`.
- A union is not a class: a feature whose value may be one of several classes
names them all, so an abstract class on one side and a union on the other
describe the same model.
- `vocabularies`, sorted by name, each with its `literals` in the order the
model declares them.

**The constraint ledger** gives every model constraint a `CONS-NNN` id, the
diagnostic code it emits, the check that enforces it in `src/`, and a refused
Expand Down
3 changes: 2 additions & 1 deletion docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 **22** rows. The compiler's behaviours join it as they land.
This ledger holds **23** rows. The compiler's behaviours join it as they land.

| id | a contributor or a consumer can rely on | proved by |
|---|---|---|
Expand All @@ -45,3 +45,4 @@ This ledger holds **22** rows. The compiler's behaviours join it as they land.
| 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) |
| REQ-023 | The Project Intent metamodel's structure is committed as a descriptor both implementations are held to, and the JSON Schema an editor completes a project file against regenerates from the metamodel without a diff | [test/model/descriptor.test.ts](../test/model/descriptor.test.ts) |
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
import java.util.List;
import java.util.Map;
import org.eclipse.emf.common.util.Enumerator;
import org.eclipse.emf.ecore.EAnnotation;
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.
* feature absent when the document left it out. A class the metamodel annotates as a scalar is
* written as that scalar, which is how {@code probes: none} stays the word it was authored as. 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 annotation a class carries when the language writes it as one word rather than a block. */
private static final String JSON = "https://jorisjonkers.dev/deploy-kit/json";

/** The JSON value of {@code root}, as a map of authored key to value. */
public static Map<String, Object> of(EObject root) {
Map<String, Object> json = new LinkedHashMap<>();
Expand Down Expand Up @@ -58,14 +64,20 @@ private static Object many(EStructuralFeature feature, List<?> values) {
return items;
}

/** The word a class is written as, or {@code null} when it is written as a block. */
private static String scalar(EObject object) {
EAnnotation annotation = object.eClass().getEAnnotation(JSON);
return annotation == null ? null : annotation.getDetails().get("scalar");
}

/** 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 EObject child -> scalar(child) == null ? of(child) : scalar(child);
case Enumerator literal -> literal.getLiteral();
default -> value;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.HttpProbe;
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;
Expand Down Expand Up @@ -52,7 +52,7 @@ void anOptionalFeatureTheDocumentSetIsWritten() {
Process process = process();
process.setStartupBudget("20s");
Probes probes = MODEL.createProbes();
Probe readiness = MODEL.createProbe();
HttpProbe readiness = MODEL.createHttpProbe();
readiness.setPath("/healthz/ready");
readiness.setPort(8080);
probes.setReadiness(readiness);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dev.jorisjonkers.deploykit.emf.cli;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

/** Scratch: every worked example parses. */
class WorkedExamplesTest {

private static List<Path> projectFiles() {
Path examples = repository().resolve("spec/v1/examples");
try (Stream<Path> tree = Files.walk(examples)) {
return tree.filter(path -> path.getFileName().toString().endsWith(".project.yml"))
.filter(path -> !path.toString().contains("refusals"))
.sorted()
.toList();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@ParameterizedTest(name = "{0}")
@MethodSource("projectFiles")
void everyWorkedExampleParses(Path file) {
Parsed parsed = Pipeline.intent(file);

assertThat(parsed.diagnostics()).isEmpty();
}

private static Path repository() {
Path dir = Path.of("").toAbsolutePath();
while (!Files.isRegularFile(dir.resolve("emf/pom.xml"))) {
dir = dir.getParent();
}
return dir;
}
}
15 changes: 12 additions & 3 deletions emf/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ 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
is compared with anything.
is compared with anything. Two shapes it normalises: an abstract class is a
union, so it is not listed and a feature that points at one names its concrete
classes; and a map entry is not a class, so the feature that holds the entries
is a map.

## Constraints

Expand Down Expand Up @@ -146,11 +149,17 @@ subset those files use, with indentation handled by synthetic block tokens, and
refuses anything outside the subset with a diagnostic rather than a guess.

The grammar imports the hand-written source metamodel, so the parser produces
instances of the graded metamodel directly. There is no inferred syntax
instances of the graded metamodel directly. A union in the model is a union in
the grammar: a probe is HTTP or TCP, a probe policy is a block or the word
`none`, and a grant is keyed by its engine, which a grant that has one writes
first. The language binds the `.yml` extension, because EMF resolves a resource
factory by the last extension alone; which document a file holds is the file
name's to say, and that lands with the Platform document. 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
structure into the synthetic `BEGIN` and `END` tokens the rules read, and folds
a scalar written over several lines into one token. 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
Expand Down
3 changes: 2 additions & 1 deletion emf/docs/witnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ 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 **1** witness.
This list holds **2** witnesses.

| id | JUnit test |
|---|---|
| REQ-021 | `ParityTest#theParsedIntentEqualsTheCommittedOracle` |
| REQ-023 | `ParityTest#theMetamodelsStructureEqualsTheCommittedDescriptor` |
3 changes: 2 additions & 1 deletion emf/metamodel/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ 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
Export-Package: dev.jorisjonkers.deploykit.emf.metamodel.projectintent,
Export-Package: dev.jorisjonkers.deploykit.emf.metamodel.descriptor,
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,
Expand Down
3 changes: 2 additions & 1 deletion emf/metamodel/build.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
source.. = target/generated-sources/emf/
source.. = src/main/java/,\
target/generated-sources/emf/
bin.includes = META-INF/,\
.,\
plugin.xml,\
Expand Down
Loading
Loading