@effected/yaml 0.6.1 → 0.8.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.
Files changed (40) hide show
  1. package/README.md +78 -7
  2. package/Yaml.js +87 -39
  3. package/YamlDocument.js +25 -6
  4. package/YamlFormat.js +171 -20
  5. package/YamlLint.js +239 -0
  6. package/YamlLintRule.js +36 -0
  7. package/YamlNode.js +36 -2
  8. package/YamlToken.js +139 -0
  9. package/YamlVisitor.js +40 -5
  10. package/index.d.ts +439 -12
  11. package/index.js +4 -1
  12. package/internal/composer/anchors.js +2 -27
  13. package/internal/composer/block.js +246 -58
  14. package/internal/composer/comments.js +187 -0
  15. package/internal/composer/document.js +49 -12
  16. package/internal/composer/flow.js +82 -9
  17. package/internal/composer/scalars.js +77 -28
  18. package/internal/composer/state.js +16 -2
  19. package/internal/cst-parser.js +11 -2
  20. package/internal/diagnostics.js +12 -3
  21. package/internal/fold.js +22 -10
  22. package/internal/lexer.js +23 -17
  23. package/internal/rules/catalog.js +39 -0
  24. package/internal/rules/colon-spacing.js +77 -0
  25. package/internal/rules/comments-spacing.js +68 -0
  26. package/internal/rules/document-end.js +65 -0
  27. package/internal/rules/document-start.js +64 -0
  28. package/internal/rules/empty-lines.js +70 -0
  29. package/internal/rules/eof-newline.js +33 -0
  30. package/internal/rules/hyphen-spacing.js +52 -0
  31. package/internal/rules/indentation.js +103 -0
  32. package/internal/rules/key-duplicates.js +49 -0
  33. package/internal/rules/line-length.js +36 -0
  34. package/internal/rules/parse-validity.js +33 -0
  35. package/internal/rules/quoted-strings.js +81 -0
  36. package/internal/rules/trailing-spaces.js +39 -0
  37. package/internal/rules/truthy.js +84 -0
  38. package/internal/rules/util.js +92 -0
  39. package/internal/stringifier.js +285 -69
  40. package/package.json +4 -3
package/README.md CHANGED
@@ -5,7 +5,7 @@
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
- Zero-dependency YAML 1.2 parsing, editing and formatting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` and decode straight into a validated domain schema.
8
+ Zero-dependency YAML 1.2 parsing, editing, formatting and linting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, read and preserve per-node comments, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` of AST events or positioned tokens, lint it against a rule catalog with surgical autofix, and decode straight into a validated domain schema.
9
9
 
10
10
  > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
11
  > development against a single pinned Effect v4 beta. Packages graduate to
@@ -106,18 +106,89 @@ Effect.runPromise(Effect.result(Yaml.parse(bomb))).then(console.log);
106
106
 
107
107
  Collection nesting past the depth cap behaves the same way, yielding a `NestingDepthExceeded` diagnostic instead of a stack overflow, and `Yaml.stringify` caps the mirror-image recursion when encoding a value back to text. The guarantee is the same everywhere: every fallible entry point carries a real error channel, and nothing reaches your process as a defect.
108
108
 
109
+ ## Comments survive an edit
110
+
111
+ A YAML file a human maintains is mostly explanation, and the explanation is the part a round-trip usually loses. `YamlFormat` computes its edits against the original bytes, so changing a value rewrites the value and leaves the header block, the trailing `# LTS`, the blank line and the indentation where they were. Setting `preserveComments: false` is how you ask for the other behavior:
112
+
113
+ ```ts
114
+ import { YamlFormat, YamlFormattingOptions } from "@effected/yaml";
115
+
116
+ const source = 'node: "20.11.0" # LTS\n\n# why the port\nport: 3000\n';
117
+
118
+ console.log(YamlFormat.formatToString(source));
119
+ // node: "20.11.0" # LTS
120
+ //
121
+ // # why the port
122
+ // port: 3000
123
+
124
+ console.log(YamlFormat.formatToString(source, undefined, YamlFormattingOptions.make({ preserveComments: false })));
125
+ // node: "20.11.0"
126
+ // port: 3000
127
+ ```
128
+
129
+ Comments are readable as data too. `YamlScalar`, `YamlMap`, `YamlSeq` and `YamlPair` each carry `commentBefore` (the own-line block above the node), `comment` (strictly the trailing comment on the node's own line) and `spaceBefore` (a blank line came first); `YamlDocument` adds `commentAfter` for a trailing block, and `YamlVisitor` tags every `Comment` event with `placement: "leading" | "trailing"`.
130
+
131
+ ## Lint and autofix
132
+
133
+ `YamlLint.run` checks a string against a rule catalog and returns positioned diagnostics; `YamlLint.fix` applies the fixes that can be expressed as surgical edits and returns the fixed text. Both are pure and synchronous. This is the lint engine, not a runner — no file discovery, no config-file loading, nothing written to disk:
134
+
135
+ ```ts
136
+ import { YamlLint, YamlLintConfig } from "@effected/yaml";
137
+ import { Result } from "effect";
138
+
139
+ const source = "# pinned\nname: demo \nport: yes\n";
140
+
141
+ for (const d of YamlLint.run(source, YamlLint.builtins, YamlLintConfig.default)) {
142
+ console.log(`${d.line}:${d.character} ${d.severity} ${d.rule} ${d.message}`);
143
+ }
144
+ // 1:5 error colon-spacing Too many spaces after ":" (2 > 1)
145
+ // 1:11 error trailing-spaces Trailing whitespace
146
+ // 2:6 error truthy Truthy value "yes" is not in the allowed spellings
147
+
148
+ const fixed = YamlLint.fix(source, YamlLint.builtins, YamlLintConfig.default);
149
+ console.log(Result.isSuccess(fixed) ? fixed.success : "fatal parse error — not safely fixable");
150
+ // # pinned
151
+ // name: demo
152
+ // port: "yes"
153
+ ```
154
+
155
+ Positions are zero-based throughout, so the first finding above sits on the second line of the source. Fixes route through `YamlEdit.applyAll` rather than a reformat, so an autofix cannot move a comment it was not asked to touch, and a rule whose repair would need reflowing the document ships without a fix at all. `YamlLintConfig.default` and `YamlLintConfig.relaxed` are the presets; compose your own by spreading a preset's `rules` into `YamlLintConfig.make`. Config validation is rule-aware for the built-in catalog — a misspelled option key on a built-in rule or an attempt to switch off the always-on `parse-validity` rule fails schema validation instead of being quietly ignored — while an unknown (custom) rule id passes through with its options treated as opaque, left for the custom rule itself to validate. Custom rules are plain array concatenation — `YamlLint.run(text, [...YamlLint.builtins, myRule], config)` — and configuring `myRule` in that config works precisely because of the pass-through.
156
+
157
+ ## Migrating from Prettier
158
+
159
+ `YamlFormat.formatToString(text, undefined, options)` replaces a `prettier --write` over YAML files. The option mapping:
160
+
161
+ | Prettier | `YamlFormattingOptions` |
162
+ | --- | --- |
163
+ | `tabWidth` | `indent` |
164
+ | `singleQuote` | `quoteStyle: "single" \| "double"` |
165
+ | indented block sequences (always on) | `indentSequences: true` |
166
+ | `printWidth` / `proseWrap` | no analog — see below |
167
+ | comments (best-effort) | `preserveComments: true` (the default) |
168
+
169
+ Two defaults differ from Prettier's output and are worth setting explicitly. The stringify default is `quoteStyle: "single"` for byte-compatibility with the kit's legacy form, so if you ran Prettier with its own default (`singleQuote: false`), set `quoteStyle: "double"`. Likewise `indentSequences` defaults to `false` — the flat legacy form with the `-` markers at the key's column — while Prettier always indented block sequences one level, so an ex-Prettier consumer almost certainly wants `indentSequences: true`.
170
+
171
+ One expectation to reset when carrying over `singleQuote`: on the format path, `quoteStyle` governs quotes the stringifier *introduces* — it never re-quotes scalars that already carry quotes in your source. Formatting `a: 'x'` with `quoteStyle: "double"` leaves `'x'` untouched; Prettier would have rewritten it to `"x"`. Existing quote styles are source, and the formatter preserves source. If it looks like the option "does nothing" on an already-quoted corpus, this is why — the same class of format-path inertness as `lineWidth` below.
172
+
173
+ `printWidth` and `proseWrap` have no analog here: `lineWidth` folding is a value-path feature only (`Yaml.stringify` / `Yaml.stringifyResult`), and the option is deliberately inert on the format path by contract — `YamlFormat` never reflows scalar content (the `lineWidth` TSDoc on `YamlStringifyOptions` pins this). Comments, which Prettier handled best-effort, are preserved exactly by default.
174
+
175
+ Multi-document streams — a Kubernetes manifest, or the two-document `pnpm-lock.yaml` pnpm 11 writes under `configDependencies` — format whole: every document is re-emitted in order with its own `---`/`...` framing and comments, so no document is ever dropped. Document detection is CST-level, so a `---` inside a block scalar or quoted string is content and formats normally. The formatter leaves input byte-identical rather than corrupting it in exactly two cases: a fatal parse error in any document, and a stream carrying `%YAML`/`%TAG` directives (which the emitter cannot re-emit faithfully). Validate a whole stream with `Yaml.parseAllResult`, which fails typed when any document carries a fatal diagnostic; `YamlFormat.modify` stays single-document, since a path names no particular document of a stream.
176
+
109
177
  ## Features
110
178
 
111
179
  - `Yaml.parse` / `Yaml.parseAll` — error-recovery parsing of a single document or a `---`-separated stream into plain values, resolving anchors and aliases and aggregating every diagnostic into one `YamlParseError`.
112
- - `Yaml.stringify` — serialize a plain value back to YAML, failing typed with `YamlStringifyError` on circular references or on excessively deep nesting.
113
- - `Yaml.parseResult` / `Yaml.stringifyResult` — the pure synchronous counterparts, returning a `Result` instead of an `Effect` for config-time callers that cannot await; each runs the same engine call as its `Effect` variant, so the two forms cannot diverge, and a fatal diagnostic, a duplicate key, an alias bomb or a circular reference still fails typed rather than throwing.
180
+ - `Yaml.stringify` — serialize a plain value back to YAML, failing typed with `YamlStringifyError` on circular references or on excessively deep nesting; a block-mapping key too long to render on one line spills into explicit-key form (`? key` / `: value`) so strict parsers still accept the output.
181
+ - `Yaml.parseResult` / `Yaml.parseAllResult` / `Yaml.stringifyResult` — the pure synchronous counterparts, returning a `Result` instead of an `Effect` for config-time callers that cannot await; each runs the same engine call as its `Effect` variant, so the two forms cannot diverge, and a fatal diagnostic, a duplicate key, an alias bomb or a circular reference still fails typed rather than throwing. `parseAllResult` doubles as a whole-stream validity check: it fails when any document in the stream is invalid.
114
182
  - `Yaml.stripComments` — quote-aware comment removal that keeps line numbers stable, or every byte offset stable when given a replacement character.
115
183
  - `Yaml.equals` / `Yaml.equalsValue` — semantic equality that ignores comments, whitespace, formatting and mapping key order while keeping sequence order significant.
116
184
  - `Yaml.schema` / `Yaml.fromString` / `Yaml.YamlFromString` / `Yaml.allFromString` — string→domain schema factories that decode a single document or a whole stream into a validated Effect `Schema` value.
117
- - `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path.
118
- - `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable.
119
- - `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch.
120
- - `YamlVisitor` — walk a parsed document as a `Stream` of a tagged-enum event union, with `Stream.take` early termination on large inputs.
185
+ - `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path, each collection and scalar carrying its own `commentBefore`, `comment` and `spaceBefore`.
186
+ - `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable, and the leading and trailing comment blocks as `comment` / `commentAfter`.
187
+ - `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch; `preserveComments: false` opts out. `format` handles multi-document streams whole, re-emitting every document and its framing; `modify` is single-document by contract (a path names no particular document of a stream) and fails typed with `MultiDocumentStream` rather than guessing.
188
+ - `YamlVisitor` — walk a parsed document as a `Stream` of a tagged-enum event union, with `Stream.take` early termination on large inputs and a `placement` on every comment event.
189
+ - `YamlToken` / `YamlTokens` — the positioned token stream the lint rules read: `tokenize` is a synchronous `Result` that always succeeds, surfacing lexical problems as `error`-kind tokens, and `stream` derives an Effect `Stream` from the same walk.
190
+ - `YamlLint` — `run`, `fix` and the `builtins` catalog: 14 rules covering parse validity, line length, trailing whitespace, blank lines, end-of-file newline, document markers, duplicate keys, quoting, truthy spellings, comment and colon and hyphen spacing, and indentation.
191
+ - `YamlLintConfig` / `YamlLintRule` / `YamlLintDiagnostic` — the `default` and `relaxed` presets, a config schema validated against the rule catalog, and the model a custom rule implements.
121
192
  - `YamlParseError` / `YamlStringifyError` / `YamlModificationError` — tagged errors carrying structured, positional `YamlDiagnostic` arrays rather than opaque messages.
122
193
 
123
194
  ## License
package/Yaml.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import { AliasExpansionBudgetExceeded, CollectionStyle, QuoteStyle, ScalarStyle, nodeToJsValue } from "./YamlNode.js";
2
- import { buildAnchorMap } from "./internal/composer/anchors.js";
3
2
  import { composeAllDocuments, composeFirstDocument } from "./internal/composer/document.js";
4
3
  import { isFatalCode } from "./internal/diagnostics.js";
5
4
  import { StringifyDepthExceeded, StringifyFailure, stringifyValue } from "./internal/stringifier.js";
6
5
  import { YamlDiagnostic } from "./YamlDiagnostic.js";
7
- import { Effect, Option, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
6
+ import { Effect, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
7
 
9
8
  //#region src/Yaml.ts
10
9
  /**
@@ -122,11 +121,13 @@ var YamlStringifyOptions = class extends Schema.Class("YamlStringifyOptions")({
122
121
  * Error-recovery parse failure: aggregates every fatal {@link YamlDiagnostic}
123
122
  * encountered, so a single failure reports the whole batch. Raised by
124
123
  * {@link Yaml.parse}, {@link Yaml.parseAll}, `YamlDocument.parse`/`parseAll`
125
- * and the decode direction of the schema factories.
124
+ * and the decode direction of the schema factories. The error itself has no
125
+ * `code` field: read the code from the diagnostics —
126
+ * `error.diagnostics[0].code` is the primary failure.
126
127
  *
127
128
  * @public
128
129
  */
129
- var YamlParseError = class extends Schema.TaggedErrorClass()("YamlParseError", {
130
+ var YamlParseError = class extends Schema.TaggedError()("YamlParseError", {
130
131
  diagnostics: Schema.Array(YamlDiagnostic),
131
132
  input: Schema.String
132
133
  }) {
@@ -140,11 +141,12 @@ var YamlParseError = class extends Schema.TaggedErrorClass()("YamlParseError", {
140
141
  * Stringification failure (the circular-reference guard), carrying structured
141
142
  * {@link YamlDiagnostic} entries and the offending value. Raised by
142
143
  * {@link Yaml.stringify}, `YamlDocument#stringify` and the encode direction of
143
- * the schema factories.
144
+ * the schema factories. The error itself has no `code` field: read the code
145
+ * from the diagnostics — `error.diagnostics[0].code` is the primary failure.
144
146
  *
145
147
  * @public
146
148
  */
147
- var YamlStringifyError = class extends Schema.TaggedErrorClass()("YamlStringifyError", {
149
+ var YamlStringifyError = class extends Schema.TaggedError()("YamlStringifyError", {
148
150
  diagnostics: Schema.Array(YamlDiagnostic),
149
151
  value: Schema.Unknown
150
152
  }) {
@@ -238,6 +240,35 @@ const parseResultImpl = (text, options) => {
238
240
  }
239
241
  };
240
242
  /**
243
+ * Synchronous multi-document parse returning a `Result`. Mirrors
244
+ * {@link parseResultImpl} over the whole stream: stream-level
245
+ * directive-placement errors and every document's failure records aggregate
246
+ * into one {@link YamlParseError}; each document's value is extracted against
247
+ * its own independent anchor map (anchors are document-scoped in a stream),
248
+ * with the alias-expansion budget materialized as a typed failure.
249
+ */
250
+ const parseAllResultImpl = (text, options) => {
251
+ const { documents, streamErrors } = composeAllDocuments(text, toParseInput(options));
252
+ const uniqueKeys = options?.uniqueKeys ?? true;
253
+ const failures = [...streamErrors.filter((e) => isFatalCode(e.code)), ...documents.flatMap((d) => failureRecords(d, uniqueKeys))];
254
+ if (failures.length > 0) return Result.fail(new YamlParseError({
255
+ diagnostics: toDiagnostics(text, failures),
256
+ input: text
257
+ }));
258
+ const maxAliasCount = options?.maxAliasCount ?? 100;
259
+ const values = [];
260
+ for (const d of documents) {
261
+ const anchors = /* @__PURE__ */ new Map();
262
+ try {
263
+ values.push(nodeToJsValue(d.contents, anchors, maxAliasCount));
264
+ } catch (defect) {
265
+ if (defect instanceof AliasExpansionBudgetExceeded) return Result.fail(aliasCountExceededError(defect.message, text));
266
+ throw defect;
267
+ }
268
+ }
269
+ return Result.succeed(values);
270
+ };
271
+ /**
241
272
  * Synchronous stringify returning a `Result`. Mirrors {@link Yaml.stringify}
242
273
  * without the Effect wrapper: a circular reference or a value nested past the
243
274
  * recursion budget yields a `Failure` carrying a typed {@link YamlStringifyError},
@@ -263,19 +294,6 @@ const failureRecords = (doc, uniqueKeys) => {
263
294
  if (fatal.length > 0) return fatal;
264
295
  return uniqueKeys ? doc.warnings.filter((w) => w.code === "DuplicateKey") : [];
265
296
  };
266
- /**
267
- * Extract a document's plain-JS value with an alias-expansion budget derived
268
- * from `maxAliasCount`, materializing a "billion laughs" blow-up as a typed
269
- * fatal {@link YamlParseError} (`AliasCountExceeded`) instead of letting the
270
- * heap-exhausting expansion escape as an unhandled defect.
271
- */
272
- const extractDocumentValue = (contents, anchors, maxAliasCount, text) => Effect.try({
273
- try: () => nodeToJsValue(contents, anchors, maxAliasCount),
274
- catch: (defect) => {
275
- if (defect instanceof AliasExpansionBudgetExceeded) return aliasCountExceededError(defect.message, text);
276
- throw defect;
277
- }
278
- });
279
297
  const stringifyOrFail = (value, options) => Effect.try({
280
298
  try: () => stringifyValue(value, toStringifyInput(options)),
281
299
  catch: (defect) => {
@@ -334,23 +352,11 @@ var Yaml = class Yaml {
334
352
  * A "billion laughs" alias-expansion blow-up in any document also fails
335
353
  * through {@link YamlParseError} with an `AliasCountExceeded` diagnostic,
336
354
  * never as an unhandled defect.
355
+ *
356
+ * Defined in terms of {@link Yaml.parseAllResult} — synchronous callers
357
+ * can use that variant directly.
337
358
  */
338
- static parseAll = Effect.fn("Yaml.parseAll")(function* (text, options) {
339
- const { documents, streamErrors } = composeAllDocuments(text, toParseInput(options));
340
- const uniqueKeys = options?.uniqueKeys ?? true;
341
- const failures = [...streamErrors.filter((e) => e.code === "InvalidDirective"), ...documents.flatMap((d) => failureRecords(d, uniqueKeys))];
342
- if (failures.length > 0) return yield* new YamlParseError({
343
- diagnostics: toDiagnostics(text, failures),
344
- input: text
345
- });
346
- const maxAliasCount = options?.maxAliasCount ?? 100;
347
- const values = [];
348
- for (const d of documents) {
349
- const anchors = buildAnchorMap(d.contents);
350
- values.push(yield* extractDocumentValue(d.contents, anchors, maxAliasCount, text));
351
- }
352
- return values;
353
- });
359
+ static parseAll = Effect.fn("Yaml.parseAll")((text, options) => Effect.fromResult(Yaml.parseAllResult(text, options)));
354
360
  /**
355
361
  * Stringify a plain JavaScript value as YAML. Fails with
356
362
  * {@link YamlStringifyError} on circular references (`CircularReference`)
@@ -406,6 +412,48 @@ var Yaml = class Yaml {
406
412
  return parseResultImpl(text, options);
407
413
  }
408
414
  /**
415
+ * Synchronous multi-document parse, returning a `Result` instead of an
416
+ * `Effect` — the {@link Yaml.parseResult} counterpart to
417
+ * {@link Yaml.parseAll}. Empty input succeeds with `[null]` (the engine
418
+ * reads `""` as one empty document, exactly as {@link Yaml.parse} yields
419
+ * `null` for it); a single-document stream succeeds with a one-element
420
+ * array whose value is exactly what {@link Yaml.parseResult} yields.
421
+ *
422
+ * @remarks
423
+ * This is the package's single multi-document parse path.
424
+ * {@link Yaml.parseAll} is defined in terms of it (`Effect.fromResult`
425
+ * behind the named span), so the two variants cannot diverge. Anchors are
426
+ * document-scoped: each document's aliases resolve against its own anchor
427
+ * map, never a neighbor's.
428
+ *
429
+ * Fails with the aggregate {@link YamlParseError} when **any** document in
430
+ * the stream carries a fatal diagnostic (or a stream-level
431
+ * directive-placement error), which makes it a whole-stream validity
432
+ * check: a `Success` means every document parsed clean. Preserves the
433
+ * package contract — malformed and adversarial input (including a
434
+ * "billion laughs" alias bomb in any document) fails typed, never as a
435
+ * defect; the method never throws.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * import { Yaml } from "@effected/yaml";
440
+ * import { Result } from "effect";
441
+ *
442
+ * // Whole-stream validity check: fails if ANY document is invalid.
443
+ * const result = Yaml.parseAllResult("a: 1\n---\nb: 2\n");
444
+ * if (Result.isSuccess(result)) {
445
+ * result.success; // [{ a: 1 }, { b: 2 }]
446
+ * } else {
447
+ * result.failure; // YamlParseError aggregating every fatal diagnostic
448
+ * }
449
+ * ```
450
+ *
451
+ * @public
452
+ */
453
+ static parseAllResult(text, options) {
454
+ return parseAllResultImpl(text, options);
455
+ }
456
+ /**
409
457
  * Synchronous stringify, returning a `Result` instead of an `Effect`.
410
458
  * The pure counterpart to {@link Yaml.stringify} for config-time callers
411
459
  * that cannot `await`.
@@ -516,8 +564,8 @@ var Yaml = class Yaml {
516
564
  */
517
565
  static fromString(options) {
518
566
  return Schema.String.pipe(Schema.decodeTo(Schema.Unknown, SchemaTransformation.transformOrFail({
519
- decode: (input) => Yaml.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
520
- encode: (value) => stringifyOrFail(value).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(value), { message: error.message })))
567
+ decode: (input) => Yaml.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, input))),
568
+ encode: (value) => stringifyOrFail(value).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value)))
521
569
  })));
522
570
  }
523
571
  /**
@@ -536,12 +584,12 @@ var Yaml = class Yaml {
536
584
  */
537
585
  static allFromString(options) {
538
586
  return Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.Unknown), SchemaTransformation.transformOrFail({
539
- decode: (input) => Yaml.parseAll(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
587
+ decode: (input) => Yaml.parseAll(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, input))),
540
588
  encode: (values) => Effect.gen(function* () {
541
589
  if (values.length === 0) return "";
542
590
  const parts = [];
543
591
  for (let index = 0; index < values.length; index++) {
544
- const yaml = yield* stringifyOrFail(values[index]).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(values), { message: error.message })));
592
+ const yaml = yield* stringifyOrFail(values[index]).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, values)));
545
593
  parts.push(index > 0 ? `---\n${yaml}` : yaml);
546
594
  }
547
595
  return parts.join("");
package/YamlDocument.js CHANGED
@@ -4,7 +4,7 @@ import { isFatalCode } from "./internal/diagnostics.js";
4
4
  import { StringifyDepthExceeded, StringifyFailure, stringifyDocument } from "./internal/stringifier.js";
5
5
  import { YamlDiagnostic } from "./YamlDiagnostic.js";
6
6
  import { YamlParseError, YamlStringifyError } from "./Yaml.js";
7
- import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
7
+ import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
8
 
9
9
  //#region src/YamlDocument.ts
10
10
  /**
@@ -22,8 +22,12 @@ var YamlDirective = class extends Schema.Class("YamlDirective")({
22
22
  /**
23
23
  * A parsed YAML document: the root {@link (YamlNode:type)} (or `null` when
24
24
  * empty), recovered `errors` and `warnings` as {@link YamlDiagnostic} data,
25
- * the {@link YamlDirective} list, an optional document-level comment, and the
26
- * `---`/`...` framing flags (absent flags read as `false`).
25
+ * the {@link YamlDirective} list, the optional document-level comments
26
+ * (`comment` is the LEADING header block, `commentAfter` the trailing block
27
+ * after the content or the `...` marker — the reference `yaml` package calls
28
+ * these `commentBefore`/`comment`; the naming here keeps the pre-existing
29
+ * `comment`-as-header field stable), and the `---`/`...` framing flags
30
+ * (absent flags read as `false`).
27
31
  *
28
32
  * Construct via `YamlDocument.parse` / `parseAll`; `YamlDocument.make` is for
29
33
  * synthetic documents.
@@ -36,6 +40,7 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
36
40
  warnings: Schema.Array(YamlDiagnostic),
37
41
  directives: Schema.Array(YamlDirective),
38
42
  comment: Schema.optionalKey(Schema.String),
43
+ commentAfter: Schema.optionalKey(Schema.String),
39
44
  hasDocumentStart: Schema.optionalKey(Schema.Boolean),
40
45
  hasDocumentEnd: Schema.optionalKey(Schema.Boolean),
41
46
  hasDocumentStartTab: Schema.optionalKey(Schema.Boolean)
@@ -80,8 +85,8 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
80
85
  */
81
86
  static schema(options) {
82
87
  return Schema.String.pipe(Schema.decodeTo(Schema.instanceOf(YamlDocument), SchemaTransformation.transformOrFail({
83
- decode: (input) => YamlDocument.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
84
- encode: (doc) => doc.stringify().pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(doc), { message: error.message })))
88
+ decode: (input) => YamlDocument.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, input))),
89
+ encode: (doc) => doc.stringify().pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, doc)))
85
90
  })));
86
91
  }
87
92
  /**
@@ -159,6 +164,18 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
159
164
  finalNewline: options.finalNewline,
160
165
  forceDefaultStyles: options.forceDefaultStyles
161
166
  };
167
+ /**
168
+ * Materialize a raw engine document into the public class — including
169
+ * documents whose diagnostics are fatal, which `YamlDocument.parse` refuses.
170
+ * The lint layer builds its context through this so linting runs on
171
+ * malformed input; it is NOT a second public parse entry point (not
172
+ * re-exported from the package index).
173
+ *
174
+ * @internal
175
+ */
176
+ function documentFromRaw(raw, text) {
177
+ return fromRawDocument(raw, text);
178
+ }
162
179
  /** Materialize a raw engine document into the public class. */
163
180
  function fromRawDocument(raw, text) {
164
181
  return new YamlDocument({
@@ -170,6 +187,7 @@ function fromRawDocument(raw, text) {
170
187
  parameters: d.parameters
171
188
  })),
172
189
  ...raw.comment !== void 0 ? { comment: raw.comment } : {},
190
+ ...raw.commentAfter !== void 0 ? { commentAfter: raw.commentAfter } : {},
173
191
  hasDocumentStart: raw.hasDocumentStart,
174
192
  hasDocumentEnd: raw.hasDocumentEnd,
175
193
  hasDocumentStartTab: raw.hasDocumentStartTab
@@ -183,6 +201,7 @@ function toRawDocument(doc) {
183
201
  warnings: [],
184
202
  directives: doc.directives,
185
203
  ...doc.comment !== void 0 ? { comment: doc.comment } : {},
204
+ ...doc.commentAfter !== void 0 ? { commentAfter: doc.commentAfter } : {},
186
205
  hasDocumentStart: doc.hasDocumentStart ?? false,
187
206
  hasDocumentEnd: doc.hasDocumentEnd ?? false,
188
207
  hasDocumentStartTab: doc.hasDocumentStartTab ?? false
@@ -190,4 +209,4 @@ function toRawDocument(doc) {
190
209
  }
191
210
 
192
211
  //#endregion
193
- export { YamlDirective, YamlDocument };
212
+ export { YamlDirective, YamlDocument, documentFromRaw };