@effected/schemastore 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ import { DocumentLint } from "./DocumentLint.js";
2
+ import { SchemaFile } from "./SchemaFile.js";
3
+ import { SchemaValidator } from "./SchemaValidator.js";
4
+ import { StoreDocument } from "./StoreDocument.js";
5
+ import { Effect, Schema } from "effect";
6
+
7
+ //#region src/SchemaPipeline.ts
8
+ /**
9
+ * One problem found while emitting a target, from either gate, normalized
10
+ * so a single policy predicate can judge both.
11
+ *
12
+ * `DocumentLint` findings keep their own severity; engine findings are
13
+ * `"warning"` — a document the engine rejects is not advisory.
14
+ *
15
+ * @public
16
+ */
17
+ var PipelineFinding = class extends Schema.Class("PipelineFinding")({
18
+ /** Which gate produced it. */
19
+ source: Schema.Literals(["lint", "validator"]),
20
+ /** `"warning"` blocks under the default policy; `"advisory"` does not. */
21
+ severity: Schema.Literals(["warning", "advisory"]),
22
+ /** The lint check's name, or the engine keyword, when one is named. */
23
+ check: Schema.optionalKey(Schema.String),
24
+ /** JSON pointer into the flat document (`""` is the root). */
25
+ path: Schema.String,
26
+ /** Human-readable explanation. */
27
+ message: Schema.String
28
+ }) {
29
+ /**
30
+ * What to call this finding when rendering it: the check name when the
31
+ * gate named one, the gate itself otherwise.
32
+ *
33
+ * Engine findings do not always carry a keyword, so without this every
34
+ * consumer that logs findings writes the same `finding.check ?? …`
35
+ * fallback.
36
+ */
37
+ get label() {
38
+ return this.check ?? this.source;
39
+ }
40
+ };
41
+ /**
42
+ * Indicates that at least one target's findings blocked under the active
43
+ * gating policy. Carries every blocking finding, so a caller renders one
44
+ * report instead of discovering problems one run at a time.
45
+ *
46
+ * @public
47
+ */
48
+ var SchemaGateError = class extends Schema.TaggedErrorClass()("SchemaGateError", {
49
+ /** The `$id` of the target that failed the gate. */
50
+ $id: Schema.String,
51
+ /** Every finding that blocked, in discovery order. */
52
+ findings: Schema.Array(PipelineFinding)
53
+ }) {
54
+ get message() {
55
+ return `Schema "${this.$id}" failed its gate with ${this.findings.length} blocking finding(s)`;
56
+ }
57
+ };
58
+ const defaultBlocking = (finding) => finding.severity === "warning";
59
+ const gather = (document, options) => Effect.gen(function* () {
60
+ const validator = yield* SchemaValidator;
61
+ const lint = DocumentLint.lint(document).map((finding) => PipelineFinding.make({
62
+ source: "lint",
63
+ severity: finding.severity === "warning" ? "warning" : "advisory",
64
+ check: finding.check,
65
+ path: finding.path,
66
+ message: finding.message
67
+ }));
68
+ const validation = (yield* validator.validate(document.toJson(), options?.validator)).map((finding) => PipelineFinding.make({
69
+ source: "validator",
70
+ severity: "warning",
71
+ path: finding.path,
72
+ message: finding.message,
73
+ ...finding.keyword !== void 0 ? { check: finding.keyword } : {}
74
+ }));
75
+ return [...lint, ...validation];
76
+ });
77
+ const blockingFindings = (findings, options) => findings.filter(options?.blocking ?? defaultBlocking);
78
+ const gate = (target, findings, options) => {
79
+ const blocking = blockingFindings(findings, options);
80
+ return blocking.length > 0 ? Effect.fail(SchemaGateError.make({
81
+ $id: target.$id,
82
+ findings: blocking
83
+ })) : Effect.void;
84
+ };
85
+ /**
86
+ * The emit pipeline over a target manifest: generate, lint, validate, gate,
87
+ * write — the loop every consumer of this package was writing by hand.
88
+ *
89
+ * Requires `SchemaFile` and `SchemaValidator` in `R`; provide
90
+ * `SchemaFile.layer` and `SchemaValidator.layer` (plus a platform
91
+ * `FileSystem` / `Path`) at the edge. Findings come back as **values**, so
92
+ * the package never chooses your log wording — but the gating decision,
93
+ * which is the part that must not silently differ between consumers, has
94
+ * one default and one override point.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import { SchemaFile, SchemaPipeline, SchemaTarget, SchemaValidator } from "@effected/schemastore";
99
+ * import { NodeServices } from "@effect/platform-node";
100
+ * import { Effect, Layer, Schema } from "effect";
101
+ *
102
+ * const targets = [
103
+ * SchemaTarget.make({
104
+ * schema: Schema.Struct({ name: Schema.String }),
105
+ * $id: "https://example.com/config.schema.json",
106
+ * path: "schemas/config.schema.json",
107
+ * }),
108
+ * ];
109
+ *
110
+ * const program = SchemaPipeline.run(targets).pipe(
111
+ * Effect.provide(Layer.mergeAll(SchemaFile.layer, SchemaValidator.layer)),
112
+ * Effect.provide(NodeServices.layer),
113
+ * );
114
+ * ```
115
+ *
116
+ * @public
117
+ */
118
+ var SchemaPipeline = class SchemaPipeline {
119
+ constructor() {}
120
+ /**
121
+ * Run every target: build its document, gather both gates' findings,
122
+ * fail with a {@link SchemaGateError} if any block, and write otherwise.
123
+ *
124
+ * Targets are processed in order and the run stops at the first gate
125
+ * failure — a document that fails its gate is not written, and neither
126
+ * are the targets after it.
127
+ */
128
+ static run(targets, options) {
129
+ return Effect.gen(function* () {
130
+ const files = yield* SchemaFile;
131
+ const results = [];
132
+ for (const target of targets) {
133
+ const document = yield* StoreDocument.fromSchema(target.schema, { $id: target.$id });
134
+ const findings = yield* gather(document, options);
135
+ yield* gate(target, findings, options);
136
+ const { outcome, change } = yield* files.write(target.path, document, options?.write);
137
+ results.push({
138
+ $id: target.$id,
139
+ path: target.path,
140
+ outcome,
141
+ change,
142
+ findings
143
+ });
144
+ }
145
+ return results;
146
+ });
147
+ }
148
+ /**
149
+ * The same walk with **no writes** — the drift-check counterpart, for a
150
+ * CI job asserting the committed schemas are current.
151
+ *
152
+ * Unlike {@link SchemaPipeline.run} this is **total over the targets**:
153
+ * it never stops at a failing gate, and reports `blocked` per target
154
+ * instead of failing. Reporting is the job here, and a repo with three
155
+ * broken documents should learn that in one run rather than fixing them
156
+ * one run at a time. A blocked target is still never mistaken for clean
157
+ * drift — `blocked` says so explicitly.
158
+ *
159
+ * The error channel is left to the mechanisms that genuinely cannot
160
+ * produce a report (generation, serialization, the engine, the read).
161
+ */
162
+ static check(targets, options) {
163
+ return Effect.gen(function* () {
164
+ const files = yield* SchemaFile;
165
+ const results = [];
166
+ for (const target of targets) {
167
+ const document = yield* StoreDocument.fromSchema(target.schema, { $id: target.$id });
168
+ const findings = yield* gather(document, options);
169
+ const { wouldWrite, change } = yield* files.check(target.path, document, options?.write);
170
+ results.push({
171
+ $id: target.$id,
172
+ path: target.path,
173
+ wouldWrite,
174
+ blocked: blockingFindings(findings, options).length > 0,
175
+ change,
176
+ findings
177
+ });
178
+ }
179
+ return results;
180
+ });
181
+ }
182
+ /**
183
+ * {@link SchemaPipeline.run} for a single target, answering its one
184
+ * result directly — so a caller with one target does not index into an
185
+ * array and prove to the type system that element zero exists.
186
+ */
187
+ static runOne(target, options) {
188
+ return SchemaPipeline.run([target], options).pipe(Effect.map((results) => results[0]));
189
+ }
190
+ /**
191
+ * {@link SchemaPipeline.check} for a single target, answering its one
192
+ * result directly.
193
+ */
194
+ static checkOne(target, options) {
195
+ return SchemaPipeline.check([target], options).pipe(Effect.map((results) => results[0]));
196
+ }
197
+ };
198
+
199
+ //#endregion
200
+ export { PipelineFinding, SchemaGateError, SchemaPipeline };
package/SchemaTarget.js CHANGED
@@ -7,20 +7,20 @@
7
7
  var SchemaTarget = class {
8
8
  constructor() {}
9
9
  /**
10
- * Builds a target. `$id`, `name` and `path` must be non-empty — an
11
- * empty identity is a wiring mistake and throws.
10
+ * Builds a target. `$id` and `path` must be non-empty — an empty
11
+ * identity is a wiring mistake and throws, as does an empty `name` when
12
+ * one is given. The `name`-with-`version` invariant is enforced by the
13
+ * overloads above; the runtime check remains for untyped callers.
12
14
  */
13
15
  static make(options) {
14
- for (const key of [
15
- "$id",
16
- "name",
17
- "path"
18
- ]) if (options[key].length === 0) throw new Error(`SchemaTarget.make requires a non-empty "${key}"`);
16
+ for (const key of ["$id", "path"]) if (options[key].length === 0) throw new Error(`SchemaTarget.make requires a non-empty "${key}"`);
17
+ if (options.name !== void 0 && options.name.length === 0) throw new Error("SchemaTarget.make requires a non-empty \"name\" when one is given");
18
+ if (options.version !== void 0 && options.name === void 0) throw new Error("SchemaTarget.make requires a \"name\" when \"version\" is given (catalog naming is name-<version>.json)");
19
19
  return {
20
20
  schema: options.schema,
21
21
  $id: options.$id,
22
- name: options.name,
23
22
  path: options.path,
23
+ ...options.name !== void 0 ? { name: options.name } : {},
24
24
  ...options.version !== void 0 ? { version: options.version } : {}
25
25
  };
26
26
  }
@@ -1,4 +1,6 @@
1
+ import { KeywordFamilies } from "./KeywordFamilies.js";
1
2
  import { Context, Effect, Layer, Schema } from "effect";
3
+ import { Ajv } from "ajv";
2
4
 
3
5
  //#region src/SchemaValidator.ts
4
6
  /**
@@ -34,20 +36,41 @@ var ValidationFinding = class extends Schema.Class("ValidationFinding")({
34
36
  /** The JSON Schema keyword the finding is about, when the engine names one. */
35
37
  keyword: Schema.optionalKey(Schema.String)
36
38
  }) {};
39
+ const collectDeclaredKeywords = (node, into, depth) => {
40
+ if (depth >= 256 || typeof node !== "object" || node === null) return;
41
+ if (Array.isArray(node)) {
42
+ for (const element of node) collectDeclaredKeywords(element, into, depth + 1);
43
+ return;
44
+ }
45
+ for (const [key, value] of Object.entries(node)) {
46
+ if (KeywordFamilies.isDeclared(key)) into.add(key);
47
+ collectDeclaredKeywords(value, into, depth + 1);
48
+ }
49
+ };
50
+ const findingFromAjvError = (error) => ValidationFinding.make({
51
+ path: error.instancePath,
52
+ message: error.message ?? "schema is not valid",
53
+ keyword: error.keyword
54
+ });
37
55
  /** The default for an unstubbed {@link SchemaValidator.makeTest} member. */
38
56
  const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`SchemaValidator.makeTest: ${method}() was called but not stubbed — no honest default exists for a test double; pass a \`${method}\` override.`));
39
57
  /**
40
- * Contract for real-engine JSON Schema document validation the seam
41
- * through which SchemaStore's own gate (ajv strict mode) reaches this
42
- * package without ajv ever entering its dependency graph.
58
+ * Real-engine JSON Schema document validation, closed by default over ajv —
59
+ * the engine SchemaStore's own gate is defined in terms of.
43
60
  *
44
- * This is a contract-only service: {@link SchemaValidator.noop} is the sole
45
- * implementation this package ships, and it validates nothing. The consumer
46
- * closes the seam with a real engine at the application edge e.g. an ajv
47
- * adapter whose `validate` compiles the document with
48
- * `new Ajv({ strict: true, allErrors: true })` and answers compile failures
49
- * as findings. `DocumentLint` remains the owned, always-available
50
- * structural half of the validation story.
61
+ * {@link SchemaValidator.layer} is the shipped implementation: provide it and
62
+ * validation works, with no adapter to write. The service stays an interface
63
+ * so a test can swap it ({@link SchemaValidator.layerTest}) or skip it
64
+ * ({@link SchemaValidator.noop}), and so a consumer standardized on a
65
+ * different engine can substitute one but writing an adapter is no longer
66
+ * the price of admission. `DocumentLint` remains the owned, engine-free
67
+ * structural half of the validation story, and answers questions ajv does
68
+ * not (SchemaStore's own hygiene conventions).
69
+ *
70
+ * The shipped layer registers every declared {@link KeywordFamilies} keyword
71
+ * present in the document before compiling, so ajv strict mode does not
72
+ * reject the language-server families `DocumentLint` deliberately allows —
73
+ * one predicate governs both verdicts.
51
74
  *
52
75
  * @example
53
76
  * ```ts
@@ -59,7 +82,7 @@ const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`Schem
59
82
  * return yield* validator.validate({ type: "object" });
60
83
  * });
61
84
  *
62
- * Effect.runPromise(Effect.provide(program, SchemaValidator.noop));
85
+ * Effect.runPromise(Effect.provide(program, SchemaValidator.layer));
63
86
  * // => []
64
87
  * ```
65
88
  *
@@ -67,9 +90,45 @@ const notStubbed = (method) => () => Effect.die(/* @__PURE__ */ new Error(`Schem
67
90
  */
68
91
  var SchemaValidator = class SchemaValidator extends Context.Service()("@effected/schemastore/SchemaValidator") {
69
92
  /**
70
- * No-op default: `validate` always succeeds with no findings, never
71
- * consulting an engine. A pure `Layer.succeed`, bound to a const so the
72
- * layer memoizes by reference.
93
+ * The shipped ajv implementation the default a consumer provides.
94
+ *
95
+ * `validate` checks the document against the Draft-07 meta-schema and
96
+ * then compiles it, reporting BOTH as {@link ValidationFinding} values:
97
+ * meta-schema failures keep ajv's structured `instancePath` and
98
+ * `keyword`, while a strict-mode rejection (which ajv raises by throwing
99
+ * at compile time) becomes a root-pathed finding. The error channel
100
+ * stays reserved for the engine failing as a mechanism.
101
+ *
102
+ * `strict` defaults to `true` — SchemaStore's gate. Each call builds its
103
+ * own ajv instance, so documents sharing an `$id` never collide.
104
+ */
105
+ static layer = Layer.succeed(SchemaValidator, { validate: (document, options) => Effect.try({
106
+ try: () => {
107
+ const ajv = new Ajv({
108
+ strict: options?.strict ?? true,
109
+ allErrors: true
110
+ });
111
+ const declared = /* @__PURE__ */ new Set();
112
+ collectDeclaredKeywords(document, declared, 0);
113
+ for (const keyword of declared) ajv.addKeyword({ keyword });
114
+ if (!ajv.validateSchema(document)) return (ajv.errors ?? []).map(findingFromAjvError);
115
+ try {
116
+ ajv.compile(document);
117
+ } catch (cause) {
118
+ return [ValidationFinding.make({
119
+ path: "",
120
+ message: cause instanceof Error ? cause.message : String(cause)
121
+ })];
122
+ }
123
+ return [];
124
+ },
125
+ catch: (cause) => SchemaValidatorError.make({ cause })
126
+ }) });
127
+ /**
128
+ * No-op: `validate` always succeeds with no findings, never consulting an
129
+ * engine. A pure `Layer.succeed`, bound to a const so the layer memoizes
130
+ * by reference. Use it to switch validation off deliberately — for the
131
+ * real engine, provide {@link SchemaValidator.layer}.
73
132
  */
74
133
  static noop = Layer.succeed(SchemaValidator, { validate: () => Effect.succeed([]) });
75
134
  /**
@@ -2,19 +2,11 @@ import { Effect, Option, Order, Result, Schema } from "effect";
2
2
  import { SemVer } from "@effected/semver";
3
3
 
4
4
  //#region src/SchemaVersioning.ts
5
- /**
6
- * SchemaStore version labels are dotted numerics with one to three
7
- * components and an optional prerelease — `1`, `1.2`, `1.2.3`,
8
- * `1.2-beta.1` per the catalog's versioned-schema convention
9
- * (`agripparc-1.2.json`). They are deliberately NOT strict SemVer: most
10
- * real catalog labels are two-part. Leading zeros are rejected on every
11
- * numeric identifier — core components and numeric prerelease identifiers
12
- * alike (SemVer §9 semantics: `0` is legal, `01` is not, alphanumerics
13
- * like `0abc` are) — so no two distinct labels can collide under numeric
14
- * ordering, and every accepted label survives the SemVer pad the ordering
15
- * performs (see `orderingKey`).
16
- */
17
- const VERSION_PATTERN = /^(0|[1-9]\d*)(\.(0|[1-9]\d*)){0,2}(-(0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$/;
5
+ const isVersionLabel = (input) => {
6
+ if (!SemVer.isValid(input)) return false;
7
+ const parsed = SemVer.parseResult(input);
8
+ return Result.isSuccess(parsed) && parsed.success.build.length === 0;
9
+ };
18
10
  /**
19
11
  * Indicates that a string is not a valid SchemaStore version label.
20
12
  *
@@ -30,22 +22,23 @@ input: Schema.String }) {
30
22
  }
31
23
  };
32
24
  /**
33
- * A SchemaStore version label: a branded string validated against the
34
- * catalog's version grammar (`major[.minor[.patch]][-prerelease]`). The
35
- * label round-trips verbatim into file names and catalog `versions` keys;
36
- * ordering pads it to a full SemVer internally (see
37
- * {@link SchemaVersioning.Order}).
25
+ * A schema version label: a branded string holding a **full three-component
26
+ * SemVer** `major.minor.patch` with an optional prerelease, validated by
27
+ * `@effected/semver` itself. Build metadata is rejected (see below).
28
+ *
29
+ * `1.2` and `1` are NOT accepted, though SchemaStore's own corpus uses such
30
+ * labels: requiring all three components makes a label unambiguous to split
31
+ * back out of `<name>-<version>.json` or its URL, which is what consumers
32
+ * do with it. The file-name convention around the label stays SchemaStore's.
33
+ *
34
+ * The label round-trips verbatim into file names and catalog `versions`
35
+ * keys; ordering parses it directly (see {@link SchemaVersioning.Order}).
38
36
  *
39
37
  * @public
40
38
  */
41
- const SchemaVersion = Schema.String.check(Schema.isPattern(VERSION_PATTERN)).pipe(Schema.brand("SchemaVersion"));
39
+ const SchemaVersion = Schema.String.check(Schema.makeFilter((value) => isVersionLabel(value) ? void 0 : "must be a full major.minor.patch SemVer label")).pipe(Schema.brand("SchemaVersion"));
42
40
  const orderingKey = (label) => {
43
- const hyphen = label.indexOf("-");
44
- const core = hyphen === -1 ? label : label.slice(0, hyphen);
45
- const prerelease = hyphen === -1 ? "" : label.slice(hyphen);
46
- const parts = core.split(".");
47
- while (parts.length < 3) parts.push("0");
48
- const result = SemVer.parseResult(`${parts.join(".")}${prerelease}`);
41
+ const result = SemVer.parseResult(label);
49
42
  if (Result.isFailure(result)) throw new Error(`SchemaVersion ordering invariant violated for label "${label}"`);
50
43
  return result.success;
51
44
  };
@@ -59,11 +52,12 @@ const joinUrl = (baseUrl, file) => {
59
52
  };
60
53
  /**
61
54
  * Both SchemaStore catalog modes as pure derivations: unversioned (a plain
62
- * `name.json` file, `url` only) and versioned (`name-<version>.json` files,
63
- * a `versions` map, and `url` pointing at the latest version).
55
+ * `name.json` file, `url` only) and versioned (`name-<version>.json` files
56
+ * — SchemaStore's own suffix convention — a `versions` map, and `url`
57
+ * pointing at the latest version).
64
58
  *
65
- * Version ordering follows SemVer precedence over labels padded to three
66
- * components, so `1.10` sorts above `1.9` and `2-beta` below `2`.
59
+ * Version labels are full three-component SemVer, so ordering is plain
60
+ * SemVer precedence: `1.10.0` above `1.9.0`, `2.0.0-beta` below `2.0.0`.
67
61
  *
68
62
  * @public
69
63
  */
@@ -74,7 +68,7 @@ var SchemaVersioning = class SchemaVersioning {
74
68
  * {@link SchemaVersioning.parse} is the same check behind a span.
75
69
  */
76
70
  static parseResult(input) {
77
- return VERSION_PATTERN.test(input) ? Result.succeed(input) : Result.fail(InvalidSchemaVersionError.make({ input }));
71
+ return isVersionLabel(input) ? Result.succeed(input) : Result.fail(InvalidSchemaVersionError.make({ input }));
78
72
  }
79
73
  /**
80
74
  * Effect form of {@link SchemaVersioning.parseResult}, adding only the
@@ -83,9 +77,9 @@ var SchemaVersioning = class SchemaVersioning {
83
77
  */
84
78
  static parse = Effect.fn("SchemaVersioning.parse")((input) => Effect.fromResult(SchemaVersioning.parseResult(input)));
85
79
  /**
86
- * `Order` instance over version labels: SemVer precedence after padding
87
- * missing components with zeros. `1.10` sorts above `1.9` (numeric, not
88
- * lexical) and `2-beta` below `2` (prerelease precedence).
80
+ * `Order` instance over version labels: plain SemVer precedence.
81
+ * `1.10.0` sorts above `1.9.0` (numeric, not lexical) and `2.0.0-beta`
82
+ * below `2.0.0` (prerelease precedence).
89
83
  */
90
84
  static Order = Order.make((a, b) => SemVer.Order(orderingKey(a), orderingKey(b)));
91
85
  /**
@@ -123,16 +117,13 @@ var SchemaVersioning = class SchemaVersioning {
123
117
  * a contradiction (versioned mode with no versions) and throws — pass
124
118
  * `undefined` for the unversioned mode.
125
119
  *
126
- * Labels are inserted in ascending {@link SchemaVersioning.Order}, but
127
- * the SchemaStore catalog format requires `versions` to be a JSON
128
- * *object*, and JavaScript enumerates array-index-like keys first: a
129
- * bare-major label (`"2"`) always enumerates and therefore
130
- * serializes before every dotted label, regardless of insertion. The
131
- * resulting enumeration order is: bare-major labels ascending
132
- * numerically, then all other labels ascending. For label sets with no
133
- * bare majors this is fully ascending; mixed sets interleave, so
134
- * consumers must derive ordering from the labels themselves (as
135
- * {@link SchemaVersioning.latest} does), never from key position.
120
+ * Labels are inserted in ascending {@link SchemaVersioning.Order} and
121
+ * stay that way on serialization. Requiring three components is what
122
+ * buys this: JavaScript enumerates array-index-like keys first, so the
123
+ * old grammar's bare-major label (`"2"`) jumped ahead of every dotted
124
+ * one regardless of insertion order. No SemVer label is integer-like,
125
+ * so that hazard is gone. Deriving ordering from the labels themselves
126
+ * (as {@link SchemaVersioning.latest} does) is still the robust read.
136
127
  */
137
128
  static catalogUrls(options) {
138
129
  const { baseUrl, name, versions } = options;
package/StoreDocument.js CHANGED
@@ -79,6 +79,36 @@ var StoreDocument = class StoreDocument extends Schema.Class("StoreDocument")({
79
79
  /** The definitions pool, emitted under `$defs`. */
80
80
  defs: Schema.Record(Schema.String, Schema.Unknown)
81
81
  }) {
82
+ /**
83
+ * Builds a Draft-07 document from its parts, filling `$schema` with
84
+ * {@link DRAFT_07_META_SCHEMA}.
85
+ *
86
+ * `fromSchema` sets the meta-schema unconditionally; hand-building a
87
+ * value with `make` otherwise means importing the constant just to
88
+ * repeat what the package already knows. `$schema` stays a real field
89
+ * rather than a defaulted one — it declares the document's dialect, and
90
+ * a document that does not say which dialect it is written in is worse
91
+ * than one that repeats itself — so this is a constructor, not a
92
+ * default.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { StoreDocument } from "@effected/schemastore";
97
+ *
98
+ * const document = StoreDocument.draft07({
99
+ * $id: "https://example.com/config.schema.json",
100
+ * root: { type: "object" },
101
+ * });
102
+ * ```
103
+ */
104
+ static draft07(options) {
105
+ return StoreDocument.make({
106
+ $schema: DRAFT_07_META_SCHEMA,
107
+ $id: options.$id,
108
+ root: options.root,
109
+ defs: options.defs ?? {}
110
+ });
111
+ }
82
112
  /**
83
113
  * Builds the document for an Effect Schema source. Pure and
84
114
  * synchronous — the primitive form; {@link StoreDocument.fromSchema} is