@effected/schemastore-cli 0.17.0 → 0.19.0

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,73 @@
1
+ import { makeAjv } from "./internal/ajv.js";
2
+ import { InstanceFinding, InstanceValidator, InstanceValidatorError } from "@effected/schemastore";
3
+ import { Effect, Layer } from "effect";
4
+
5
+ //#region src/AjvInstanceValidator.ts
6
+ const findingFromAjvError = (error) => InstanceFinding.make({
7
+ path: error.instancePath,
8
+ message: error.message ?? "instance is not valid",
9
+ keyword: error.keyword
10
+ });
11
+ /**
12
+ * The shipped `InstanceValidator` implementation: the same ajv strict-mode
13
+ * setup `AjvValidator` gates documents with — the declared `KeywordFamilies`
14
+ * keywords and the standard `ajv-formats` vocabulary registered, through the
15
+ * one shared `makeAjv` — pointed at a payload instance instead of the
16
+ * meta-schema.
17
+ *
18
+ * @remarks
19
+ * Lives in the CLI, not the library, for the same reason `AjvValidator`
20
+ * does: `@effected/schemastore` owns the `InstanceValidator` contract and
21
+ * its doubles, an application that imports it at runtime never pulls an
22
+ * engine, and the `schemastore validate` command composes this layer at its
23
+ * edge. It is exported for a program that validates payloads itself and
24
+ * wants the same verdict the command gives — a document the `check` gate
25
+ * admits always compiles here, so the two engines cannot drift.
26
+ *
27
+ * `validate` compiles the document and runs the instance against it. An
28
+ * instance the document rejects answers `InstanceFinding` values — ajv's
29
+ * structured `instancePath` pointer and `keyword` preserved, `allErrors`
30
+ * on, so one run reports every problem, not just the first. A document the
31
+ * engine cannot compile (a meta-schema failure, a strict-mode rejection, a
32
+ * declared keyword whose NAME ajv's grammar refuses) fails
33
+ * `InstanceValidatorError`: unlike `AjvValidator`, whose subject IS the
34
+ * document, this contract's subject is the instance, and a document that
35
+ * yields no verdict is the engine failing to run as a mechanism. The
36
+ * document's own gate is `SchemaValidator`'s job — `schemastore check` runs
37
+ * it before a document is ever published.
38
+ *
39
+ * `strict` defaults to `true` — SchemaStore's gate. Each call builds its own
40
+ * ajv instance, so documents sharing an `$id` never collide.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { InstanceValidator } from "@effected/schemastore";
45
+ * import { AjvInstanceValidator } from "@effected/schemastore-cli";
46
+ * import { Effect } from "effect";
47
+ *
48
+ * const program = Effect.gen(function* () {
49
+ * const validator = yield* InstanceValidator;
50
+ * return yield* validator.validate({ type: "object" }, { any: "payload" });
51
+ * });
52
+ *
53
+ * Effect.runPromise(Effect.provide(program, AjvInstanceValidator.layer));
54
+ * // => []
55
+ * ```
56
+ *
57
+ * @public
58
+ */
59
+ var AjvInstanceValidator = class {
60
+ constructor() {}
61
+ /** The engine, as an `InstanceValidator` layer. */
62
+ static layer = Layer.succeed(InstanceValidator, { validate: (document, instance, options) => Effect.try({
63
+ try: () => {
64
+ const validate = makeAjv(document, options?.strict ?? true).compile(document);
65
+ if (validate(instance)) return [];
66
+ return (validate.errors ?? []).map(findingFromAjvError);
67
+ },
68
+ catch: (cause) => InstanceValidatorError.make({ cause })
69
+ }) });
70
+ };
71
+
72
+ //#endregion
73
+ export { AjvInstanceValidator };
package/AjvValidator.js CHANGED
@@ -1,25 +1,8 @@
1
- import { KeywordFamilies, SchemaValidator, SchemaValidatorError, ValidationFinding } from "@effected/schemastore";
2
- import { Ajv } from "ajv";
3
- import ajvFormats from "ajv-formats";
1
+ import { makeAjv } from "./internal/ajv.js";
2
+ import { SchemaValidator, SchemaValidatorError, ValidationFinding } from "@effected/schemastore";
4
3
  import { Effect, Layer } from "effect";
5
4
 
6
5
  //#region src/AjvValidator.ts
7
- const addFormats = ajvFormats.default;
8
- const MAX_KEYWORD_WALK_DEPTH = 256;
9
- const collectDeclaredKeywords = (node, into, depth) => {
10
- if (depth >= MAX_KEYWORD_WALK_DEPTH || typeof node !== "object" || node === null) return;
11
- if (Array.isArray(node)) {
12
- for (const element of node) collectDeclaredKeywords(element, into, depth + 1);
13
- return;
14
- }
15
- for (const [key, value] of Object.entries(node)) {
16
- if (KeywordFamilies.isDeclared(key)) {
17
- into.add(key);
18
- continue;
19
- }
20
- collectDeclaredKeywords(value, into, depth + 1);
21
- }
22
- };
23
6
  const findingFromAjvError = (error) => ValidationFinding.make({
24
7
  path: error.instancePath,
25
8
  message: error.message ?? "schema is not valid",
@@ -47,13 +30,16 @@ const findingFromAjvError = (error) => ValidationFinding.make({
47
30
  * carrying a dot or a space. The error channel stays reserved for the engine
48
31
  * failing as a mechanism (`SchemaValidatorError`).
49
32
  *
33
+ * The ajv setup is `internal/ajv.ts`'s `makeAjv`, shared with
34
+ * `AjvInstanceValidator` so a document this gate admits always compiles in
35
+ * the instance engine too — the two verdicts cannot drift.
36
+ *
50
37
  * `strict` defaults to `true` — SchemaStore's gate. Each call builds its own
51
38
  * ajv instance, so documents sharing an `$id` never collide. Registering the
52
39
  * declared families keeps the engine's verdict consistent with
53
40
  * `DocumentLint`'s through the same predicate, so the two cannot drift; the
54
41
  * plugin's `formatMaximum` / `formatMinimum` limit keywords are deliberately
55
42
  * NOT registered, because the lint answers those as unknown keywords.
56
- *
57
43
  * @example
58
44
  * ```ts
59
45
  * import { SchemaValidator } from "@effected/schemastore";
@@ -76,15 +62,8 @@ var AjvValidator = class {
76
62
  /** The engine, as a `SchemaValidator` layer. */
77
63
  static layer = Layer.succeed(SchemaValidator, { validate: (document, options) => Effect.try({
78
64
  try: () => {
79
- const ajv = new Ajv({
80
- strict: options?.strict ?? true,
81
- allErrors: true
82
- });
83
- addFormats(ajv, { keywords: false });
84
- const declared = /* @__PURE__ */ new Set();
85
- collectDeclaredKeywords(document, declared, 0);
86
65
  try {
87
- for (const keyword of declared) ajv.addKeyword({ keyword });
66
+ const ajv = makeAjv(document, options?.strict ?? true);
88
67
  if (!ajv.validateSchema(document)) return (ajv.errors ?? []).map(findingFromAjvError);
89
68
  ajv.compile(document);
90
69
  } catch (cause) {
package/ConfigLoader.js CHANGED
@@ -28,7 +28,9 @@ searched: Schema.Array(Schema.String) }) {
28
28
  * @public
29
29
  */
30
30
  var ConfigLoadError = class extends Schema.TaggedError()("ConfigLoadError", {
31
+ /** The config file that failed to load. */
31
32
  path: Schema.String,
33
+ /** Why it failed; the first line is the message, any remainder is detail such as a stack. */
32
34
  reason: Schema.String
33
35
  }) {
34
36
  get message() {
package/README.md CHANGED
@@ -5,12 +5,12 @@
5
5
  [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
6
  [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
7
7
 
8
- The `schemastore` command: build and check SchemaStore-shaped JSON Schema documents from a `schemastore.config.ts`. It is the companion to [`@effected/schemastore`](https://www.npmjs.com/package/@effected/schemastore), which owns the pipeline and every type a config needs; this package ships the plumbing every consumer used to write by hand — the config loader, the drift policy, the frozen-label checks, the catalog file, exit codes, a GitHub step summary — once, as a `bin`. It is also where the validation engine lives: `AjvValidator`, ajv in strict mode, composed by the command and exported for a program that drives the pipeline itself, so the library stays free of ajv.
8
+ The `schemastore` command: build and check SchemaStore-shaped JSON Schema documents from a `schemastore.config.ts`, and validate a payload against a published document. It is the companion to [`@effected/schemastore`](https://www.npmjs.com/package/@effected/schemastore), which owns the pipeline and every type a config needs; this package ships the plumbing every consumer used to write by hand — the config loader, the drift policy, the frozen-label checks, the catalog file, exit codes, a GitHub step summary — once, as a `bin`. It is also where the validation engines live: `AjvValidator` (documents) and `AjvInstanceValidator` (payloads), both ajv in strict mode over one shared setup, composed by the commands and exported for a program that drives the pipeline or validates payloads itself, so the library stays free of ajv.
9
9
 
10
- > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 prerelease. Packages graduate to
12
- > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
- > exactly the ones the kit is built and tested against, install
10
+ > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
+ > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
12
+ > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
+ > `@effect/*` versions on the line the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
@@ -114,17 +114,19 @@ export default defineConfig({
114
114
  ```text
115
115
  schemastore build [config] [--drift=strict|semantic|allow] [--on-drift=error|warn] [--force] [--format=human|json]
116
116
  schemastore check [config] [--drift=strict|semantic|allow] [--on-drift=error|warn] [--force] [--format=human|json]
117
+ schemastore validate <payload.json> [config] [--schema <path|$id|url>] [--format=human|json]
117
118
  ```
118
119
 
119
120
  - Before anything is generated, every frozen label is verified — present on disk, and self-identified by the derived `$id`.
120
121
  - `build` generates every schema, runs the gates (the structural lint and ajv strict mode), applies the drift policy, and writes what passes — content-compared, so an unchanged file is untouched — plus the config's catalog slice when any entry declares one, and the merged catalog over every slice.
121
122
  - `check` is the identical walk with no writes: it reports what `build` would do under the same flags and exits the same way, and also fails (exit `1`) whenever a build would write anything — a stale or missing document is fixed by running `schemastore build` and committing the result. A catalog slice left behind after the config's last `catalog` block was removed is reported `orphaned` and fails `check` the same way, but `build` never deletes it — and the merged catalog keeps advertising its entries until it is gone: delete the file by hand, or restore a `catalog` block; a merged `catalog.json` with no slice left is orphaned the same way. So is a document left behind under an old derived name — an `appendVersion` flip or a `layout` change moved its path, and nothing claims the old file any more: both commands probe the sibling shapes (`<name>.json`, `<name>-<v>.json`, `<v>/<name>.json`, `<v>/<name>-<v>.json`) of every label the config still declares and report each one that exists as an orphaned document, failed by `check`, never deleted by `build`. Nothing else in `outputDir` is looked at, so sharing it with another config, a deploy folder, or the repository root is safe (unless two configs derive the same schema name and version under different layouts into it); a `name` change or a dropped label leaves a file the command cannot know about — delete those by hand. A catalog URL advertised by two slices, or a slice that cannot be read or is not a catalog entry array (an undeclared key included), blocks the merged catalog: both commands fail (exit `1`) naming the URL and its slices or the invalid slice, and the merged file is left as it is until the configs or slices are fixed.
123
+ - `validate` answers the question the publication story exists for: does THIS payload conform to the published document it names? The reference is the `--schema` flag or the payload's own `$schema`, resolved file-first and then against every identity a config schema derives (a target `$id`, a frozen version's `$id`/`url`, the catalog `url`) — CI validates an action's output against the committed document with no third-party tool and no network fetch. The payload's `$schema` self-reference is the pointer naming the document: it is stripped before validating only when the resolved document does not declare `$schema` as a root property, since a generated document's `additionalProperties: false` would otherwise reject the very self-reference that names it. A document that does declare `$schema` — the `HostedSchema` pattern above, where the source struct carries `$schema: Schema.Literal(...)` and the generated document requires and const-constrains the key — validates the payload verbatim. A non-conforming payload fails (exit `1`) with one finding per problem, each carrying the JSON pointer into the instance and the keyword; `--format=json` writes one report document to stdout and moves the human lines to stderr.
122
124
  - `--drift` and `--on-drift` override the config for one run; `--force` is sugar for `--drift=allow` (combined with a different explicit `--drift` it is a usage error).
123
125
  - `--format=json` emits one JSON document on stdout (per-schema outcome and effective tolerance, the catalog slice and merged-catalog outcomes, the `orphaned` document paths when any, `drift: { onDrift, policy? }`); human text moves to stderr. When `GITHUB_STEP_SUMMARY` is set, both commands append a markdown table.
124
126
 
125
- ## The engine, as a library export
127
+ ## The engines, as library exports
126
128
 
127
- The command validates with ajv in strict mode — SchemaStore's own gate — registering the keyword families `@effected/schemastore` declares and the standard `ajv-formats` vocabulary (formats only, never the `formatMaximum` family), one fresh instance per document. The same layer is this package's one export, for a program composing the pipeline directly:
129
+ The commands validate with ajv in strict mode — SchemaStore's own gate — registering the keyword families `@effected/schemastore` declares and the standard `ajv-formats` vocabulary (formats only, never the `formatMaximum` family), one fresh instance per document, through one shared setup both engines use so a document the `check` gate admits always compiles in the instance engine too. The two engine layers are this package's only exports, for a program composing the pipeline or validating payloads directly:
128
130
 
129
131
  ```ts
130
132
  import { SchemaFile, SchemaPipeline } from "@effected/schemastore";
@@ -137,17 +139,28 @@ const AppLayer = Layer.mergeAll(SchemaFile.layer, AjvValidator.layer).pipe(Layer
137
139
  const program = SchemaPipeline.run(targets).pipe(Effect.provide(AppLayer));
138
140
  ```
139
141
 
140
- Findings come back as values; the error channel carries `SchemaValidatorError` only when the engine fails as a mechanism.
142
+ ```ts
143
+ import { InstanceValidator } from "@effected/schemastore";
144
+ import { AjvInstanceValidator } from "@effected/schemastore-cli";
145
+ import { Effect } from "effect";
146
+
147
+ const program = Effect.gen(function* () {
148
+ const validator = yield* InstanceValidator;
149
+ return yield* validator.validate(document, payload);
150
+ }).pipe(Effect.provide(AjvInstanceValidator.layer));
151
+ ```
152
+
153
+ Findings come back as values — `ValidationFinding` pointers into the document for `AjvValidator`, `InstanceFinding` pointers into the payload for `AjvInstanceValidator`; the error channel carries `SchemaValidatorError` / `InstanceValidatorError` only when an engine fails as a mechanism.
141
154
 
142
155
  ## Exit codes
143
156
 
144
157
  | code | meaning |
145
158
  | ---- | -------------------------------------------------------------------------- |
146
159
  | 0 | success, including drift under `onDrift: warn` |
147
- | 1 | drift under `onDrift: error` (one line per drifting schema: `$id`, change, current and next version), a gate failure, a missing or mis-identified frozen version, a merged catalog blocked by a URL two slices advertise or an invalid slice, or — for `check` — anything `build` would write or an output nothing claims (an orphaned catalog slice or merged catalog, or an orphaned document at a sibling shape of a derived path) |
148
- | 2 | config not found, failed to load, failed `defineConfig` validation, or a `catalogDir` that is a file or cannot be listed (checked before anything is written) |
149
- | 3 | infrastructure failure |
150
- | 64 | usage error |
160
+ | 1 | drift under `onDrift: error` (one line per drifting schema: `$id`, change, current and next version), a gate failure, a missing or mis-identified frozen version, a merged catalog blocked by a URL two slices advertise or an invalid slice, — for `check` — anything `build` would write or an output nothing claims (an orphaned catalog slice or merged catalog, or an orphaned document at a sibling shape of a derived path), or — for `validate` — a payload that does not conform to the resolved document (one finding per problem, pointer and keyword each) |
161
+ | 2 | config not found, failed to load, failed `defineConfig` validation, a `catalogDir` that is a file or cannot be listed (checked before anything is written), or — for `validate` — a payload that cannot be read or parsed, or a `--schema`/`$schema` reference that is neither an existing file nor an identity any config schema derives, or names a document that cannot be read or parsed |
162
+ | 3 | infrastructure failure (for `validate`, an engine mechanism failure — a document the instance engine cannot compile — included) |
163
+ | 64 | usage error (for `validate`, a payload with no `$schema` and no `--schema` given, included) |
151
164
 
152
165
  ## License
153
166
 
@@ -0,0 +1,257 @@
1
+ import { AjvInstanceValidator } from "../../AjvInstanceValidator.js";
2
+ import { ConfigLoader } from "../../ConfigLoader.js";
3
+ import { configArgument, formatFlag } from "../flags.js";
4
+ import { InstanceValidator } from "@effected/schemastore";
5
+ import { Console, Effect, FileSystem, JsonPointer, Option, Path, Predicate, Schema } from "effect";
6
+ import { CliRuntime } from "@effected/cli";
7
+ import { Argument, Command, Flag } from "effect/cli";
8
+
9
+ //#region src/cli/commands/validate.ts
10
+ /**
11
+ * The payload file could not be read or parsed: it does not exist, the
12
+ * filesystem refused it, or its contents are not JSON. Nothing was
13
+ * validated. Exit `2`.
14
+ *
15
+ * @public
16
+ */
17
+ var PayloadError = class extends Schema.TaggedError()("PayloadError", {
18
+ path: Schema.String,
19
+ reason: Schema.String
20
+ }) {
21
+ get message() {
22
+ return `Failed to read payload ${this.path}: ${this.reason}`;
23
+ }
24
+ };
25
+ /**
26
+ * The payload names no schema and `--schema` was not given, so there is
27
+ * nothing to validate against. A usage error — the remedy is in how the
28
+ * command is invoked. Exit `64`.
29
+ *
30
+ * @public
31
+ */
32
+ var MissingSchemaRefError = class extends Schema.TaggedError()("MissingSchemaRefError", { path: Schema.String }) {
33
+ get message() {
34
+ return `Payload ${this.path} has no "$schema" property; pass --schema <path|$id> to name the document to validate against.`;
35
+ }
36
+ };
37
+ /**
38
+ * The schema reference — `--schema` or the payload's `$schema` — resolved to
39
+ * no readable document: it is neither an existing file nor an identity any
40
+ * config schema derives (`$id`, a frozen `$id`/`url`, a catalog `url`), or
41
+ * the document it names could not be read or parsed. Exit `2`.
42
+ *
43
+ * @public
44
+ */
45
+ var SchemaResolutionError = class extends Schema.TaggedError()("SchemaResolutionError", {
46
+ $ref: Schema.String,
47
+ reason: Schema.String
48
+ }) {
49
+ get message() {
50
+ return `Failed to resolve schema "${this.$ref}": ${this.reason}`;
51
+ }
52
+ };
53
+ /**
54
+ * The payload does not conform to the resolved document: the engine
55
+ * answered findings, each already reported. Exit `1` — a validation
56
+ * verdict, the reason CI runs the command.
57
+ *
58
+ * @public
59
+ */
60
+ var ValidationFailedError = class extends Schema.TaggedError()("ValidationFailedError", {
61
+ count: Schema.Number,
62
+ payload: Schema.String,
63
+ schema: Schema.String
64
+ }) {
65
+ get message() {
66
+ return `${this.count} finding(s): ${this.payload} does not conform to ${this.schema}`;
67
+ }
68
+ };
69
+ var ReadJsonError = class extends Schema.TaggedError()("ReadJsonError", {
70
+ path: Schema.String,
71
+ reason: Schema.String
72
+ }) {};
73
+ const readJson = Effect.fn("schemastore.validate.readJson")(function* (file) {
74
+ const text = yield* (yield* FileSystem.FileSystem).readFileString(file).pipe(Effect.mapError((cause) => new ReadJsonError({
75
+ path: file,
76
+ reason: cause.message
77
+ })));
78
+ return yield* Effect.try({
79
+ try: () => JSON.parse(text),
80
+ catch: (cause) => new ReadJsonError({
81
+ path: file,
82
+ reason: cause instanceof Error ? cause.message : String(cause)
83
+ })
84
+ });
85
+ });
86
+ const identityMap = (config) => {
87
+ const identities = /* @__PURE__ */ new Map();
88
+ const add = (identity, name, path) => {
89
+ if (identity !== void 0 && !identities.has(identity)) identities.set(identity, {
90
+ name,
91
+ path
92
+ });
93
+ };
94
+ for (const schema of config.schemas) {
95
+ add(schema.target.$id, schema.name, schema.target.path);
96
+ for (const frozen of schema.frozen) {
97
+ add(frozen.$id, schema.name, frozen.path);
98
+ add(frozen.url, schema.name, frozen.path);
99
+ }
100
+ add(schema.catalog?.url, schema.name, schema.target.path);
101
+ }
102
+ return identities;
103
+ };
104
+ const resolveDocument = Effect.fn("schemastore.validate.resolveDocument")(function* ($ref, input, deps) {
105
+ const path = yield* Path.Path;
106
+ const fs = yield* FileSystem.FileSystem;
107
+ const unresolved = (reason) => Effect.fail(CliRuntime.reported(new SchemaResolutionError({
108
+ $ref,
109
+ reason
110
+ }), 2));
111
+ const candidate = path.resolve(deps.cwd, $ref);
112
+ if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) return {
113
+ parsed: yield* readJson(candidate).pipe(Effect.catchTag("ReadJsonError", ({ reason }) => unresolved(reason))),
114
+ source: candidate
115
+ };
116
+ const loaded = yield* ConfigLoader.load({
117
+ cwd: deps.cwd,
118
+ ...Option.isSome(input.config) ? { explicit: input.config.value } : {},
119
+ ...deps.importModule !== void 0 ? { importModule: deps.importModule } : {}
120
+ });
121
+ const match = identityMap(loaded.config).get($ref);
122
+ if (match === void 0) return yield* unresolved("not an existing file, and no schema in the config derives this $id or url");
123
+ return {
124
+ parsed: yield* readJson(match.path).pipe(Effect.catchTag("ReadJsonError", ({ reason }) => unresolved(`schema "${match.name}" resolves to ${match.path}, which ${reason}`))),
125
+ source: match.path
126
+ };
127
+ });
128
+ const findingLine = (finding) => ` ${finding.path === "" ? "(root)" : finding.path}: ${finding.message}${finding.keyword !== void 0 ? ` [${finding.keyword}]` : ""}`;
129
+ const jsonReport = (payload, $ref, source, findings) => JSON.stringify({
130
+ payload,
131
+ schema: $ref,
132
+ document: source,
133
+ valid: findings.length === 0,
134
+ findings: findings.map((finding) => ({
135
+ path: finding.path,
136
+ message: finding.message,
137
+ ...finding.keyword !== void 0 ? { keyword: finding.keyword } : {}
138
+ }))
139
+ });
140
+ const schemaPropertyOf = (payload) => Predicate.isObject(payload) && !Array.isArray(payload) && typeof payload.$schema === "string" ? payload.$schema : void 0;
141
+ const defsTarget = (document, ref) => {
142
+ if (!ref.startsWith("#/")) return;
143
+ let name;
144
+ try {
145
+ const tokens = ref.slice(2).split("/").map((token) => JsonPointer.unescapeToken(decodeURIComponent(token)));
146
+ if (tokens.length !== 2 || tokens[0] !== "$defs") return;
147
+ name = tokens[1];
148
+ } catch {
149
+ return;
150
+ }
151
+ const defs = document.$defs;
152
+ if (!Predicate.isObject(defs) || Array.isArray(defs) || !Object.hasOwn(defs, name)) return;
153
+ const entry = defs[name];
154
+ return Predicate.isObject(entry) && !Array.isArray(entry) ? entry : void 0;
155
+ };
156
+ const declaresSchemaProperty = (document) => {
157
+ const declares = (node) => {
158
+ const properties = node.properties;
159
+ return Predicate.isObject(properties) && !Array.isArray(properties) && "$schema" in properties;
160
+ };
161
+ const seen = /* @__PURE__ */ new Set();
162
+ const queue = [document];
163
+ for (let node = queue.shift(); node !== void 0; node = queue.shift()) {
164
+ if (declares(node)) return true;
165
+ if (typeof node.$ref === "string" && !seen.has(node.$ref)) {
166
+ seen.add(node.$ref);
167
+ const target = defsTarget(document, node.$ref);
168
+ if (target !== void 0) queue.push(target);
169
+ }
170
+ if (Array.isArray(node.allOf)) {
171
+ for (const member of node.allOf) if (Predicate.isObject(member) && !Array.isArray(member)) queue.push(member);
172
+ }
173
+ }
174
+ return false;
175
+ };
176
+ const withoutSchemaRef = (payload, document) => {
177
+ if (schemaPropertyOf(payload) === void 0 || declaresSchemaProperty(document)) return payload;
178
+ const rest = { ...payload };
179
+ delete rest.$schema;
180
+ return rest;
181
+ };
182
+ /**
183
+ * Run one `schemastore validate`.
184
+ *
185
+ * @remarks
186
+ * Reads the payload, resolves the schema reference (`--schema`, else the
187
+ * payload's `$schema`) file-first then against the config's derived
188
+ * identities, validates with `deps.instanceValidator` or the real engine,
189
+ * and reports. A top-level string `$schema` on the payload is the pointer
190
+ * naming the document: it is stripped from the instance before validating
191
+ * only when the resolved document does not declare `$schema` as a root
192
+ * property; a document that does declare it (the HostedSchema pattern,
193
+ * where the key is required and const-constrained) sees the payload
194
+ * verbatim, and the rest of the payload always goes verbatim. Findings fail
195
+ * `ValidationFailedError` at exit `1`; a
196
+ * payload that cannot be read or parsed is `PayloadError` at `2`, an
197
+ * unresolvable reference `SchemaResolutionError` at `2`, a payload naming
198
+ * nothing `MissingSchemaRefError` at `64`, and an engine mechanism failure
199
+ * (`InstanceValidatorError`) flows unmarked to the runtime's exit `3`.
200
+ * `--format json` writes one report document to stdout and moves the human
201
+ * lines to stderr, exactly as `build`/`check` do.
202
+ *
203
+ * @public
204
+ */
205
+ const runValidate = Effect.fn("schemastore.validate")(function* (input, deps) {
206
+ const payloadPath = (yield* Path.Path).resolve(deps.cwd, input.payload);
207
+ const payload = yield* readJson(payloadPath).pipe(Effect.catchTag("ReadJsonError", ({ reason }) => Effect.fail(CliRuntime.reported(new PayloadError({
208
+ path: payloadPath,
209
+ reason
210
+ }), 2))));
211
+ const $ref = Option.getOrUndefined(input.schema) ?? schemaPropertyOf(payload);
212
+ if ($ref === void 0) return yield* Effect.fail(CliRuntime.reported(new MissingSchemaRefError({ path: payloadPath }), 64));
213
+ const resolved = yield* resolveDocument($ref, input, deps);
214
+ const document = resolved.parsed;
215
+ if (!Predicate.isObject(document) || Array.isArray(document)) return yield* Effect.fail(CliRuntime.reported(new SchemaResolutionError({
216
+ $ref,
217
+ reason: `${resolved.source} is not a JSON object`
218
+ }), 2));
219
+ const findings = yield* Effect.gen(function* () {
220
+ return yield* (yield* InstanceValidator).validate(document, withoutSchemaRef(payload, document));
221
+ }).pipe(Effect.provide(deps.instanceValidator ?? AjvInstanceValidator.layer));
222
+ const human = findings.length === 0 ? [`valid ${payloadPath} against ${resolved.source}`] : [...findings.map(findingLine), `${findings.length} finding(s): ${payloadPath} does not conform to ${$ref}`];
223
+ if (input.format === "json") {
224
+ yield* Console.log(jsonReport(payloadPath, $ref, resolved.source, findings));
225
+ for (const line of human) yield* Effect.logInfo(line);
226
+ } else for (const line of human) yield* Console.log(line);
227
+ if (findings.length > 0) return yield* Effect.fail(CliRuntime.reported(new ValidationFailedError({
228
+ count: findings.length,
229
+ payload: payloadPath,
230
+ schema: $ref
231
+ }), 1));
232
+ });
233
+ /**
234
+ * `schemastore validate`: check a payload against a published schema
235
+ * document. The reference (`--schema`, else the payload's `$schema`) is an
236
+ * existing file path or an identity the config derives — a schema's `$id`,
237
+ * a frozen version's `$id`/`url`, or a catalog `url` — so CI validates an
238
+ * action's output against the committed document without a third-party
239
+ * tool or a network fetch. A top-level string `$schema` on the payload is
240
+ * consumed as that pointer; it is stripped before validating only when the
241
+ * resolved document does not declare `$schema` as a root property, since a
242
+ * generated document's `additionalProperties: false` would otherwise
243
+ * reject the very self-reference that names it. A document that declares
244
+ * `$schema` — the HostedSchema pattern — validates the payload verbatim
245
+ * and enforces its own const constraint on the key.
246
+ *
247
+ * @public
248
+ */
249
+ const makeValidateCommand = (deps) => Command.make("validate", {
250
+ payload: Argument.String("payload").pipe(Argument.withDescription("Path to the JSON payload to validate")),
251
+ schema: Flag.String("schema").pipe(Flag.optional, Flag.withDescription("The document to validate against: a file path, or a $id/url a config schema derives; omitted, the payload's own $schema property")),
252
+ config: configArgument,
253
+ format: formatFlag
254
+ }, (input) => runValidate(input, deps)).pipe(Command.withDescription("Validate a JSON payload against a published schema document, resolved by path or by the config's derived $id/url identities"));
255
+
256
+ //#endregion
257
+ export { MissingSchemaRefError, PayloadError, SchemaResolutionError, ValidationFailedError, makeValidateCommand, runValidate };
package/cli/root.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { makeBuildCommand } from "./commands/build.js";
2
2
  import { makeCheckCommand } from "./commands/check.js";
3
+ import { makeValidateCommand } from "./commands/validate.js";
3
4
  import { Command } from "effect/cli";
4
5
 
5
6
  //#region src/cli/root.ts
@@ -12,10 +13,16 @@ import { Command } from "effect/cli";
12
13
  const makeCommands = (deps) => {
13
14
  const build = makeBuildCommand(deps);
14
15
  const check = makeCheckCommand(deps);
16
+ const validate = makeValidateCommand(deps);
15
17
  return {
16
- root: Command.make("schemastore", {}).pipe(Command.withDescription("Build and check SchemaStore-shaped JSON Schema documents from a schemastore.config.ts"), Command.withSubcommands([build, check])),
18
+ root: Command.make("schemastore", {}).pipe(Command.withDescription("Build and check SchemaStore-shaped JSON Schema documents from a schemastore.config.ts, and validate payloads against them"), Command.withSubcommands([
19
+ build,
20
+ check,
21
+ validate
22
+ ])),
17
23
  build,
18
- check
24
+ check,
25
+ validate
19
26
  };
20
27
  };
21
28
 
package/index.d.ts CHANGED
@@ -1,5 +1,60 @@
1
- import { SchemaValidator } from "@effected/schemastore";
1
+ import { InstanceValidator, SchemaValidator } from "@effected/schemastore";
2
2
  import { Layer } from "effect";
3
+ //#region src/AjvInstanceValidator.d.ts
4
+ /**
5
+ * The shipped `InstanceValidator` implementation: the same ajv strict-mode
6
+ * setup `AjvValidator` gates documents with — the declared `KeywordFamilies`
7
+ * keywords and the standard `ajv-formats` vocabulary registered, through the
8
+ * one shared `makeAjv` — pointed at a payload instance instead of the
9
+ * meta-schema.
10
+ *
11
+ * @remarks
12
+ * Lives in the CLI, not the library, for the same reason `AjvValidator`
13
+ * does: `@effected/schemastore` owns the `InstanceValidator` contract and
14
+ * its doubles, an application that imports it at runtime never pulls an
15
+ * engine, and the `schemastore validate` command composes this layer at its
16
+ * edge. It is exported for a program that validates payloads itself and
17
+ * wants the same verdict the command gives — a document the `check` gate
18
+ * admits always compiles here, so the two engines cannot drift.
19
+ *
20
+ * `validate` compiles the document and runs the instance against it. An
21
+ * instance the document rejects answers `InstanceFinding` values — ajv's
22
+ * structured `instancePath` pointer and `keyword` preserved, `allErrors`
23
+ * on, so one run reports every problem, not just the first. A document the
24
+ * engine cannot compile (a meta-schema failure, a strict-mode rejection, a
25
+ * declared keyword whose NAME ajv's grammar refuses) fails
26
+ * `InstanceValidatorError`: unlike `AjvValidator`, whose subject IS the
27
+ * document, this contract's subject is the instance, and a document that
28
+ * yields no verdict is the engine failing to run as a mechanism. The
29
+ * document's own gate is `SchemaValidator`'s job — `schemastore check` runs
30
+ * it before a document is ever published.
31
+ *
32
+ * `strict` defaults to `true` — SchemaStore's gate. Each call builds its own
33
+ * ajv instance, so documents sharing an `$id` never collide.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { InstanceValidator } from "@effected/schemastore";
38
+ * import { AjvInstanceValidator } from "@effected/schemastore-cli";
39
+ * import { Effect } from "effect";
40
+ *
41
+ * const program = Effect.gen(function* () {
42
+ * const validator = yield* InstanceValidator;
43
+ * return yield* validator.validate({ type: "object" }, { any: "payload" });
44
+ * });
45
+ *
46
+ * Effect.runPromise(Effect.provide(program, AjvInstanceValidator.layer));
47
+ * // => []
48
+ * ```
49
+ *
50
+ * @public
51
+ */
52
+ export declare class AjvInstanceValidator {
53
+ private constructor();
54
+ /** The engine, as an `InstanceValidator` layer. */
55
+ static readonly layer: Layer.Layer<InstanceValidator>;
56
+ }
57
+ //#endregion
3
58
  //#region src/AjvValidator.d.ts
4
59
  /**
5
60
  * The shipped `SchemaValidator` implementation: ajv strict mode over the
@@ -23,13 +78,16 @@ import { Layer } from "effect";
23
78
  * carrying a dot or a space. The error channel stays reserved for the engine
24
79
  * failing as a mechanism (`SchemaValidatorError`).
25
80
  *
81
+ * The ajv setup is `internal/ajv.ts`'s `makeAjv`, shared with
82
+ * `AjvInstanceValidator` so a document this gate admits always compiles in
83
+ * the instance engine too — the two verdicts cannot drift.
84
+ *
26
85
  * `strict` defaults to `true` — SchemaStore's gate. Each call builds its own
27
86
  * ajv instance, so documents sharing an `$id` never collide. Registering the
28
87
  * declared families keeps the engine's verdict consistent with
29
88
  * `DocumentLint`'s through the same predicate, so the two cannot drift; the
30
89
  * plugin's `formatMaximum` / `formatMinimum` limit keywords are deliberately
31
90
  * NOT registered, because the lint answers those as unknown keywords.
32
- *
33
91
  * @example
34
92
  * ```ts
35
93
  * import { SchemaValidator } from "@effected/schemastore";
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { AjvInstanceValidator } from "./AjvInstanceValidator.js";
1
2
  import { AjvValidator } from "./AjvValidator.js";
2
3
 
3
- export { AjvValidator };
4
+ export { AjvInstanceValidator, AjvValidator };
@@ -0,0 +1,51 @@
1
+ import { KeywordFamilies } from "@effected/schemastore";
2
+ import { Ajv } from "ajv";
3
+ import ajvFormats from "ajv-formats";
4
+
5
+ //#region src/internal/ajv.ts
6
+ const addFormats = ajvFormats.default;
7
+ const MAX_KEYWORD_WALK_DEPTH = 256;
8
+ const collectDeclaredKeywords = (node, into, depth) => {
9
+ if (depth >= MAX_KEYWORD_WALK_DEPTH || typeof node !== "object" || node === null) return;
10
+ if (Array.isArray(node)) {
11
+ for (const element of node) collectDeclaredKeywords(element, into, depth + 1);
12
+ return;
13
+ }
14
+ for (const [key, value] of Object.entries(node)) {
15
+ if (KeywordFamilies.isDeclared(key)) {
16
+ into.add(key);
17
+ continue;
18
+ }
19
+ collectDeclaredKeywords(value, into, depth + 1);
20
+ }
21
+ };
22
+ /**
23
+ * Build the shared strict-mode ajv instance over a document: the standard
24
+ * `ajv-formats` vocabulary registered (`keywords: false` is load-bearing —
25
+ * the plugin's default ALSO registers `formatMaximum` / `formatMinimum` and
26
+ * their exclusive variants, which `DocumentLint` answers as unknown
27
+ * keywords, and registering them would drift the two verdicts apart), plus
28
+ * every declared `KeywordFamilies` keyword the document carries.
29
+ *
30
+ * THROWS when ajv itself refuses the setup — most often a declared keyword
31
+ * whose NAME ajv's own grammar (`/^[a-z_$][a-z0-9_$:-]*$/i`) rejects, such
32
+ * as an `x-ai-*` key carrying a dot or a space. Each caller decides what
33
+ * that throw means for its subject: a finding for `AjvValidator` (the
34
+ * document IS the subject there), an engine failure for
35
+ * `AjvInstanceValidator` (there the subject is the instance, and a document
36
+ * that will not compile yields no verdict about it).
37
+ */
38
+ const makeAjv = (document, strict) => {
39
+ const ajv = new Ajv({
40
+ strict,
41
+ allErrors: true
42
+ });
43
+ addFormats(ajv, { keywords: false });
44
+ const declared = /* @__PURE__ */ new Set();
45
+ collectDeclaredKeywords(document, declared, 0);
46
+ for (const keyword of declared) ajv.addKeyword({ keyword });
47
+ return ajv;
48
+ };
49
+
50
+ //#endregion
51
+ export { makeAjv };
package/main.js CHANGED
@@ -6,16 +6,11 @@ import { CliRuntime } from "@effected/cli";
6
6
  import { CliError } from "effect/cli";
7
7
 
8
8
  //#region src/main.ts
9
- /**
10
- * The assembled schemastore CLI program.
11
- *
12
- * @packageDocumentation
13
- */
14
9
  const render = (error) => CliError.isCliError(error) && error._tag === "ShowHelp" ? [] : [error instanceof Error ? error.message : String(error)];
15
10
  const main = () => {
16
11
  const run = program(process.argv.slice(2), {
17
12
  cwd: process.cwd(),
18
- version: "0.17.0"
13
+ version: "0.19.0"
19
14
  }).pipe(Effect.provide(NodeServices.layer), CliRuntime.reportFailures({
20
15
  exitCode: 3,
21
16
  render
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/schemastore-cli",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "private": false,
5
5
  "description": "The schemastore command: build and check SchemaStore-shaped JSON Schema documents from a schemastore.config.ts, with a per-schema published flag and a drift policy.",
6
6
  "keywords": [
@@ -40,15 +40,18 @@
40
40
  "schemastore": "bin/schemastore.js"
41
41
  },
42
42
  "dependencies": {
43
- "@effect/platform-node": "4.0.0-rc.118",
44
- "@effected/cli": "^0.10.0",
43
+ "@effect/platform-node": "^4.0.0",
44
+ "@effected/cli": "^0.11.0",
45
+ "@effected/env": "^0.1.0",
46
+ "@effected/glob": "^0.10.0",
47
+ "@effected/walker": "^0.15.0",
45
48
  "ajv": "^8.20.0",
46
49
  "ajv-formats": "^3.0.1",
47
50
  "jiti": "^2.6.0"
48
51
  },
49
52
  "peerDependencies": {
50
- "@effected/schemastore": "0.17.0",
51
- "effect": "4.0.0-rc.118"
53
+ "@effected/schemastore": "0.19.0",
54
+ "effect": "^4.0.0"
52
55
  },
53
56
  "engines": {
54
57
  "node": ">=24.11.0"