@jasonbelmonti/markdown-engine 3.0.0 → 3.1.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +77 -8
  3. package/dist/api/contracts.d.ts +2 -1
  4. package/dist/api/contracts.d.ts.map +1 -1
  5. package/dist/api/contracts.js +1 -0
  6. package/dist/api/contracts.js.map +1 -1
  7. package/dist/api/declarative-validation.d.ts +1 -1
  8. package/dist/api/declarative-validation.d.ts.map +1 -1
  9. package/dist/api/declarative-validation.js.map +1 -1
  10. package/dist/api/document-set-validation-types.d.ts +32 -0
  11. package/dist/api/document-set-validation-types.d.ts.map +1 -0
  12. package/dist/api/document-set-validation-types.js +2 -0
  13. package/dist/api/document-set-validation-types.js.map +1 -0
  14. package/dist/api/document-set-validation.d.ts +4 -0
  15. package/dist/api/document-set-validation.d.ts.map +1 -0
  16. package/dist/api/document-set-validation.js +60 -0
  17. package/dist/api/document-set-validation.js.map +1 -0
  18. package/dist/api/serialize.d.ts +2 -1
  19. package/dist/api/serialize.d.ts.map +1 -1
  20. package/dist/api/serialize.js.map +1 -1
  21. package/dist/declarative-validation/assertions/evaluator.d.ts.map +1 -1
  22. package/dist/declarative-validation/assertions/evaluator.js +6 -0
  23. package/dist/declarative-validation/assertions/evaluator.js.map +1 -1
  24. package/dist/declarative-validation/assertions/frontmatter-shape.d.ts +9 -0
  25. package/dist/declarative-validation/assertions/frontmatter-shape.d.ts.map +1 -0
  26. package/dist/declarative-validation/assertions/frontmatter-shape.js +105 -0
  27. package/dist/declarative-validation/assertions/frontmatter-shape.js.map +1 -0
  28. package/dist/declarative-validation/assertions/text-format.d.ts +9 -0
  29. package/dist/declarative-validation/assertions/text-format.d.ts.map +1 -0
  30. package/dist/declarative-validation/assertions/text-format.js +75 -0
  31. package/dist/declarative-validation/assertions/text-format.js.map +1 -0
  32. package/dist/declarative-validation/compiler/assertion-builders.d.ts.map +1 -1
  33. package/dist/declarative-validation/compiler/assertion-builders.js +38 -1
  34. package/dist/declarative-validation/compiler/assertion-builders.js.map +1 -1
  35. package/dist/declarative-validation/compiler/assertion-shapes.d.ts +3 -1
  36. package/dist/declarative-validation/compiler/assertion-shapes.d.ts.map +1 -1
  37. package/dist/declarative-validation/compiler/assertion-shapes.js +30 -0
  38. package/dist/declarative-validation/compiler/assertion-shapes.js.map +1 -1
  39. package/dist/declarative-validation/compiler/assertions.d.ts.map +1 -1
  40. package/dist/declarative-validation/compiler/assertions.js +2 -0
  41. package/dist/declarative-validation/compiler/assertions.js.map +1 -1
  42. package/dist/declarative-validation/compiler/compatibility.d.ts.map +1 -1
  43. package/dist/declarative-validation/compiler/compatibility.js +2 -0
  44. package/dist/declarative-validation/compiler/compatibility.js.map +1 -1
  45. package/dist/declarative-validation/compiler/plan.d.ts +8 -1
  46. package/dist/declarative-validation/compiler/plan.d.ts.map +1 -1
  47. package/dist/declarative-validation/evidence/index.d.ts.map +1 -1
  48. package/dist/declarative-validation/evidence/index.js +2 -1
  49. package/dist/declarative-validation/evidence/index.js.map +1 -1
  50. package/dist/declarative-validation/profile/assertion-schema.d.ts.map +1 -1
  51. package/dist/declarative-validation/profile/assertion-schema.js +48 -3
  52. package/dist/declarative-validation/profile/assertion-schema.js.map +1 -1
  53. package/dist/declarative-validation/profile/frontmatter-shape-schema.d.ts +8 -0
  54. package/dist/declarative-validation/profile/frontmatter-shape-schema.d.ts.map +1 -0
  55. package/dist/declarative-validation/profile/frontmatter-shape-schema.js +178 -0
  56. package/dist/declarative-validation/profile/frontmatter-shape-schema.js.map +1 -0
  57. package/dist/declarative-validation/profile/index.d.ts +21 -0
  58. package/dist/declarative-validation/profile/index.d.ts.map +1 -1
  59. package/dist/declarative-validation/profile/text-format-contract.d.ts +4 -0
  60. package/dist/declarative-validation/profile/text-format-contract.d.ts.map +1 -0
  61. package/dist/declarative-validation/profile/text-format-contract.js +6 -0
  62. package/dist/declarative-validation/profile/text-format-contract.js.map +1 -0
  63. package/dist/internal/package-version.d.ts +2 -0
  64. package/dist/internal/package-version.d.ts.map +1 -0
  65. package/dist/internal/package-version.js +2 -0
  66. package/dist/internal/package-version.js.map +1 -0
  67. package/dist-bundled/markdown-engine-cli.mjs +556 -6
  68. package/docs/contracts/api.md +60 -0
  69. package/docs/contracts/declarative-validation.md +161 -22
  70. package/fixtures/declarative-validation/examples/okf-v0.1/fail/invalid-log-date/log.md +5 -0
  71. package/fixtures/declarative-validation/examples/okf-v0.1/fail/missing-concept-type/concepts/customer-metric.md +8 -0
  72. package/fixtures/declarative-validation/examples/okf-v0.1/fail/non-root-index-frontmatter/datasets/index.md +7 -0
  73. package/fixtures/declarative-validation/examples/okf-v0.1/pass/datasets/index.md +3 -0
  74. package/fixtures/declarative-validation/examples/okf-v0.1/pass/datasets/sales.md +16 -0
  75. package/fixtures/declarative-validation/examples/okf-v0.1/pass/index.md +8 -0
  76. package/fixtures/declarative-validation/examples/okf-v0.1/pass/log.md +9 -0
  77. package/fixtures/declarative-validation/examples/okf-v0.1/pass/playbooks/incident-response.md +17 -0
  78. package/fixtures/declarative-validation/examples/okf-v0.1/profiles/concept.yaml +14 -0
  79. package/fixtures/declarative-validation/examples/okf-v0.1/profiles/log.yaml +10 -0
  80. package/fixtures/declarative-validation/examples/okf-v0.1/profiles/non-root-index.yaml +9 -0
  81. package/fixtures/declarative-validation/examples/okf-v0.1/profiles/root-index.yaml +12 -0
  82. package/package.json +3 -2
  83. package/scripts/install-markdown-engine-cli.sh +113 -0
@@ -22,6 +22,7 @@ The package root exports the API functions, helpers, and types from `src/api/**`
22
22
  - `validateAnnotations(document, annotations)`
23
23
  - `parseValidationProfile(input, options?)`
24
24
  - `validateWithProfile(document, profile, options?)`
25
+ - `validateDocumentSet(entries, options?)`
25
26
 
26
27
  The package root also exports the public result, document, diagnostic, config,
27
28
  and function types declared in `src/api/**`.
@@ -249,6 +250,64 @@ defaults, so an omitted `documentVersion` and an explicit matching
249
250
  `documentVersion` produce the same profile hash for the same document version
250
251
  and rules.
251
252
 
253
+ ## `validateDocumentSet`
254
+
255
+ Signature:
256
+
257
+ ```ts
258
+ validateDocumentSet(
259
+ entries: readonly ValidateDocumentSetEntry[],
260
+ options?: ValidateDocumentSetOptions,
261
+ ): ValidateDocumentSetResult
262
+ ```
263
+
264
+ `validateDocumentSet` is a pure aggregate API for caller-supplied Markdown
265
+ documents and caller-supplied validation profiles. Each entry contains:
266
+
267
+ - `path`: caller-owned document path or stable identifier
268
+ - `markdown`: Markdown text to parse and normalize
269
+ - `profile`: YAML profile text, JSON-safe profile data, or parsed
270
+ `ValidationProfile`
271
+ - `profilePath`: optional caller-owned profile path or stable identifier for
272
+ profile diagnostics
273
+
274
+ The function processes entries in the supplied order and returns `entries` in
275
+ that same order. It does not read directories, expand globs, watch files, write
276
+ persistent state, add CLI behavior, classify OKF paths, or choose profiles for
277
+ callers. Consumers own discovery, routing, and IO.
278
+
279
+ For OKF-style bundles, `validateDocumentSet` stays a generic aggregation API.
280
+ Callers must classify each path before calling the function and provide the
281
+ profile for that caller-classified role. A non-reserved concept document can use
282
+ a concept profile that requires `type` frontmatter, the bundle-root `index.md`
283
+ can use a root-index profile that accepts the `okf_version: "0.1"` exception, a
284
+ non-root `index.md` can use a no-frontmatter profile, and `log.md` can use a
285
+ log profile that checks date headings.
286
+
287
+ The API does not infer those roles from paths.
288
+
289
+ `ValidateDocumentSetOptions` forwards supported document normalization and
290
+ validation options:
291
+
292
+ - `documentVersion`
293
+ - `preserveSourceLocations`
294
+ - `includeEvidence`
295
+
296
+ Each `ValidateDocumentSetEntryResult` contains:
297
+
298
+ - `path` and optional `profilePath`
299
+ - `parseDiagnostics`
300
+ - `normalizationDiagnostics`
301
+ - `profileDiagnostics`
302
+ - `validationDiagnostics`
303
+ - aggregate entry `diagnostics`
304
+ - `validationResult` when profile parsing succeeds without error-severity
305
+ diagnostics
306
+
307
+ Profile-stage failures on one entry do not stop later entries from processing.
308
+ The aggregate `valid` field is `false` when any aggregate diagnostic has
309
+ severity `error`; otherwise it is `true`.
310
+
252
311
  ## `serialize`
253
312
 
254
313
  Signature:
@@ -260,6 +319,7 @@ serialize(
260
319
  | NormalizeResult
261
320
  | ValidationResult
262
321
  | DeclarativeValidationResult
322
+ | ValidateDocumentSetResult
263
323
  | EngineDocument
264
324
  | AnnotationValidationResult,
265
325
  options?: SerializeOptions,
@@ -1,11 +1,13 @@
1
1
  # Declarative Validation Contract
2
2
 
3
3
  Status: package 3.0.0, v1 profile syntax with v2 Conditional V2, document contract 1.0.0
4
- Last updated: 2026-06-05
4
+ Last updated: 2026-06-15
5
5
  Current v2 surface: flat-rule result/evidence shell, ID count-bound schema and
6
6
  runtime evaluator contract, plus `tableColumnCoverage` schema, compiled-plan,
7
- and runtime evaluator contract, grouped rule runtime contract, and rule-level
8
- `when` schema, matcher, public skipped-rule result, skipped counts, and evidence
7
+ and runtime evaluator contract, `frontmatterShape` schema, compiled-plan, and
8
+ runtime evaluator contract, `textFormat` schema, compiled-plan, and runtime
9
+ evaluator contract, grouped rule runtime contract, and rule-level `when`
10
+ schema, matcher, public skipped-rule result, skipped counts, and evidence
9
11
  cloning contract.
10
12
 
11
13
  This document defines the public declarative validation contract for
@@ -25,8 +27,11 @@ optional rule-level `when`. The admitted v2 path exposes the result and evidence
25
27
  shell needed to distinguish assertion, grouped, and skipped evaluation output
26
28
  from v1 output, plus the ID count-bound schema, compiled-plan, and runtime
27
29
  evaluator contract; the `tableColumnCoverage` schema, compiled-plan, and
28
- runtime evaluator contract; and the `when` schema plus private compiled-plan and
29
- matcher contract. Matched applicability continues into normal rule evaluation.
30
+ runtime evaluator contract; the `frontmatterShape` schema, private
31
+ compiled-plan, and runtime evaluator contract; the `textFormat` schema,
32
+ private compiled-plan, and runtime evaluator contract; and the `when` schema
33
+ plus private compiled-plan and matcher contract. Matched applicability
34
+ continues into normal rule evaluation.
30
35
  Non-matching applicability returns a public skipped rule result with
31
36
  `status: "skipped"`, `passed: true`, `evaluation.kind: "skipped"`,
32
37
  `reason: "whenNotMatched"`, `skippedRuleCount`, no top-level diagnostics, and a
@@ -53,6 +58,11 @@ validateWithProfile(
53
58
  profile: ValidationProfile,
54
59
  options?: DeclarativeValidationOptions,
55
60
  ): DeclarativeValidationResult
61
+
62
+ validateDocumentSet(
63
+ entries: readonly ValidateDocumentSetEntry[],
64
+ options?: ValidateDocumentSetOptions,
65
+ ): ValidateDocumentSetResult
56
66
  ```
57
67
 
58
68
  Compiled declarative validation plans are internal. They are not exported from
@@ -79,13 +89,16 @@ syntaxVersion: markdown-engine.validation@v2
79
89
  This release recognizes v2 as a distinct syntax version at profile admission,
80
90
  admits ID count bounds at the schema, compiled-plan, and runtime evaluator
81
91
  layers, admits `tableColumnCoverage` at the schema, internal compiled-plan, and
82
- runtime evaluator layers, admits non-recursive grouped rules at the schema,
83
- compiled-plan, and runtime evaluator layers, and admits optional rule-level
84
- `when` at the schema, internal compiled-plan, and matcher layers. Matching
85
- `when` rules continue through normal flat or grouped evaluation and do not add a
86
- public `when` field to the evaluated rule result. Non-matching `when` rules are
87
- not evaluated; they return the public skipped-rule result shape, increment
88
- `skippedRuleCount`, and leave `evaluatedRuleCount` unchanged.
92
+ runtime evaluator layers, admits `frontmatterShape` at the schema, internal
93
+ compiled-plan, and runtime evaluator layers, admits `textFormat` at the schema,
94
+ internal compiled-plan, and runtime evaluator layers, admits non-recursive
95
+ grouped rules at the schema, compiled-plan, and runtime evaluator layers, and
96
+ admits optional rule-level `when` at the schema, internal compiled-plan, and
97
+ matcher layers.
98
+ Matching `when` rules continue through normal flat or grouped evaluation and do
99
+ not add a public `when` field to the evaluated rule result. Non-matching `when`
100
+ rules are not evaluated; they return the public skipped-rule result shape,
101
+ increment `skippedRuleCount`, and leave `evaluatedRuleCount` unchanged.
89
102
 
90
103
  The admitted v1/v2 flat vocabulary is closed. Unknown profile keys, rule keys,
91
104
  selector keys, known assertion keys, and nested assertion keys emit
@@ -207,8 +220,8 @@ emit `profile.config.invalidShape`.
207
220
 
208
221
  Rule-level `when` is allowed only on v2 rules. Branch-level `when` remains
209
222
  unsupported. V1 profiles preserve the original flat rule authoring contract;
210
- grouped `anyOf` / `allOf`, ID count bounds, `tableColumnCoverage`, and
211
- rule-level `when` are v2 additions.
223
+ grouped `anyOf` / `allOf`, ID count bounds, `tableColumnCoverage`,
224
+ `frontmatterShape`, `textFormat`, and rule-level `when` are v2 additions.
212
225
 
213
226
  Profile values must be JSON-safe data properties after YAML materialization.
214
227
  Functions, accessors, proxies, cyclic structures, sparse arrays, `undefined`
@@ -300,6 +313,15 @@ interface DeclarativeAssertion {
300
313
  };
301
314
  require: "everySourceId";
302
315
  };
316
+ frontmatterShape?: {
317
+ presence?: "required" | "forbidden";
318
+ fields?: readonly {
319
+ field: string;
320
+ required?: true;
321
+ valueType?: "string" | "number" | "boolean" | "array" | "object" | "null";
322
+ nonEmpty?: true;
323
+ }[];
324
+ };
303
325
  text?: {
304
326
  contains?: string;
305
327
  excludes?: readonly string[];
@@ -312,6 +334,9 @@ interface DeclarativeAssertion {
312
334
  min?: number;
313
335
  max?: number;
314
336
  };
337
+ textFormat?: {
338
+ format: "isoDate";
339
+ };
315
340
  frontmatterRequired?: {
316
341
  fields: readonly string[];
317
342
  };
@@ -335,9 +360,11 @@ Selector/assertion compatibility is part of the public contract:
335
360
  | `ids` | all supported selector targets |
336
361
  | `references` | `document` |
337
362
  | `tableColumnCoverage` | `document` |
363
+ | `frontmatterShape` | `document` |
338
364
  | `text` | all supported selector targets |
339
365
  | `textOccurrenceCount` | all supported selector targets |
340
366
  | `textLength` | all supported selector targets |
367
+ | `textFormat` | all supported selector targets |
341
368
  | `frontmatterRequired` | `document` |
342
369
 
343
370
  Incompatible supported selector/assertion pairs emit
@@ -378,6 +405,35 @@ target section do not satisfy coverage. Missing target sections, missing target
378
405
  columns, and missing target-column IDs emit deterministic validation diagnostics
379
406
  source-grounded to the source ID when source evidence is available.
380
407
 
408
+ For v2 profiles, `frontmatterShape` is admitted as a flat-rule schema and
409
+ internal compiled-plan assertion. It is compatible only with a `document`
410
+ selector because frontmatter is document metadata. `presence` is optional and
411
+ must be exactly `"required"` or `"forbidden"` when provided. `fields` is an
412
+ optional non-empty array of field constraints. Each field constraint has a
413
+ required non-empty `field` name and must include at least one effective
414
+ constraint: `required: true`, `valueType`, or `nonEmpty: true`. Field names
415
+ within one `frontmatterShape.fields` array must be unique. `valueType` must be
416
+ one of `"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, or `"null"`.
417
+ `nonEmpty: true` is a string predicate; when it is combined with `valueType`,
418
+ `valueType` must be `"string"`. `presence: "forbidden"` cannot be combined with
419
+ `fields`.
420
+
421
+ Runtime evaluation treats frontmatter as present when
422
+ `document.frontmatter !== undefined`. `presence: "required"` fails absent
423
+ frontmatter with `profile.validation.frontmatterMissing`.
424
+ `presence: "forbidden"` fails any present frontmatter value, including empty
425
+ frontmatter, with `profile.validation.frontmatterForbidden`. Field constraints
426
+ evaluate only top-level frontmatter fields and do not coerce values. A required
427
+ field that is absent, or any required field on non-object frontmatter, emits
428
+ `profile.validation.frontmatterFieldMissing`. Optional fields are ignored when
429
+ absent. `valueType` checks distinguish `"string"`, finite `"number"`,
430
+ `"boolean"`, `"array"`, plain `"object"`, and `"null"` values and emit
431
+ `profile.validation.frontmatterFieldTypeMismatch` on mismatch. `nonEmpty: true`
432
+ requires a present field value to be a non-empty string and emits
433
+ `profile.validation.frontmatterFieldEmpty` when that predicate fails. When
434
+ `valueType: "string"` and `nonEmpty: true` are combined, a non-string value
435
+ emits the type-mismatch diagnostic without a duplicate empty-string diagnostic.
436
+
381
437
  `text` must include `contains` or a non-empty `excludes` array.
382
438
  `textOccurrenceCount.count` is a finite number and counts non-overlapping
383
439
  literal occurrences per selected target.
@@ -386,10 +442,22 @@ integers, `min` must be less than or equal to `max` when both are present, and
386
442
  evaluation uses JavaScript string `.length` for each selected target's
387
443
  normalized text.
388
444
 
445
+ For v2 profiles, `textFormat` is admitted as a flat-rule schema and internal
446
+ compiled-plan assertion. `textFormat.format` must be exactly `"isoDate"`;
447
+ custom formats, locale options, regex-like formats, profile-supplied patterns,
448
+ and additional `textFormat` members are unsupported. Runtime evaluation checks
449
+ each selected target's normalized text as an exact real calendar date in
450
+ `YYYY-MM-DD` form. Valid dates such as `2026-06-14` and leap dates such as
451
+ `2024-02-29` pass. Non-dates, invalid calendar dates, timestamps, partial
452
+ dates, and strings with extra text fail with
453
+ `profile.validation.assertionFailed`. The evaluator does not compile
454
+ profile-supplied regular expressions, call `Date.parse`, perform locale
455
+ parsing, or implement date ordering.
456
+
389
457
  Empty selector results produce `profile.validation.emptySelection` for exists,
390
- table, ID, reference, text, occurrence, and text-length assertions.
391
- Document-scoped required-section and required-frontmatter assertions evaluate
392
- against the document.
458
+ table, ID, reference, text, occurrence, text-length, and text-format
459
+ assertions. Document-scoped required-section, required-frontmatter, and
460
+ frontmatter-shape assertions evaluate against the document.
393
461
 
394
462
  ## Diagnostics
395
463
 
@@ -418,9 +486,13 @@ diagnostic can still leave the aggregate `valid` value `true`.
418
486
  | `profile.compile.unsupportedAssertion` | `error` | Parsed YAML or JSON-safe `assert` input contains an unsupported first-level assertion member that does not have unsupported-key precedence. |
419
487
  | `profile.compile.incompatibleSelectorAssertion` | `error` | A supported selector target is paired with an incompatible supported assertion. |
420
488
  | `profile.validation.emptySelection` | Rule severity | A rule cannot evaluate because its selector matches no applicable target. |
421
- | `profile.validation.assertionFailed` | Rule severity | A supported assertion evaluates and fails without a more specific diagnostic code, including missing table columns, exact occurrence-count mismatches, and text-length bound failures. |
489
+ | `profile.validation.assertionFailed` | Rule severity | A supported assertion evaluates and fails without a more specific diagnostic code, including missing table columns, exact occurrence-count mismatches, text-length bound failures, and text-format `isoDate` failures. |
422
490
  | `profile.validation.duplicateId` | Rule severity | An `ids.unique` assertion finds repeated IDs. |
491
+ | `profile.validation.frontmatterForbidden` | Rule severity | A `frontmatterShape` assertion with `presence: "forbidden"` finds present frontmatter. |
492
+ | `profile.validation.frontmatterFieldEmpty` | Rule severity | A frontmatter field configured with `nonEmpty: true` is not a non-empty string. |
423
493
  | `profile.validation.frontmatterFieldMissing` | Rule severity | A required frontmatter field is absent. |
494
+ | `profile.validation.frontmatterFieldTypeMismatch` | Rule severity | A frontmatter field value does not match the configured `frontmatterShape.fields[].valueType` without coercion. |
495
+ | `profile.validation.frontmatterMissing` | Rule severity | A `frontmatterShape` assertion with `presence: "required"` finds absent frontmatter. |
424
496
  | `profile.validation.idCountTooHigh` | Rule severity | Unique ID count after filtering is higher than `ids.maxCount`. |
425
497
  | `profile.validation.idCountTooLow` | Rule severity | Unique ID count after filtering is lower than `ids.minCount`. |
426
498
  | `profile.validation.referenceMissing` | Rule severity | A source ID is absent from a required target section. |
@@ -552,8 +624,8 @@ serialization of the resolved `ValidationProfile` after applying the resolved
552
624
  `documentVersion` and an explicit matching `documentVersion` therefore produce
553
625
  the same profile hash for the same document version and rules.
554
626
 
555
- `engineVersion` records the package version that produced the evidence. In the
556
- 3.0 release line this is `"3.0.0"` even though `documentVersion` remains
627
+ `engineVersion` records the package version that produced the evidence. This
628
+ matches the package metadata version even though `documentVersion` remains
557
629
  `"1.0.0"`.
558
630
 
559
631
  Raw Markdown bytes, raw YAML bytes, YAML comments, caller file paths, and
@@ -665,8 +737,21 @@ rules:
665
737
  exists: true
666
738
  ```
667
739
 
668
- The v1 profile above continues to emit the v1 validation-result shape. The v2
669
- profile above emits syntax-versioned v2 result metadata with
740
+ ```yaml
741
+ # v2 date-heading profile: validates exact ISO date heading text.
742
+ syntaxVersion: markdown-engine.validation@v2
743
+ rules:
744
+ - id: log.date-heading
745
+ select:
746
+ target: heading
747
+ depth: 2
748
+ assert:
749
+ textFormat:
750
+ format: isoDate
751
+ ```
752
+
753
+ The v1 profile above continues to emit the v1 validation-result shape. V2
754
+ profiles emit syntax-versioned v2 result metadata with
670
755
  `evaluatedRuleCount` and `skippedRuleCount`; v2 rule results include `status`
671
756
  and `evaluation`. The CLI does not add a second discriminator for v2; consumers
672
757
  branch on
@@ -778,6 +863,60 @@ rules:
778
863
  exists: true
779
864
  ```
780
865
 
866
+ ### OKF v0.1 Hard-Validation Profile Composition
867
+
868
+ `markdown-engine` does not ship a built-in OKF validator. Consumers that want to
869
+ validate OKF v0.1 hard conformance compose generic declarative-validation
870
+ profiles and route documents to those profiles themselves. The caller owns
871
+ bundle traversal, path classification, IO, and role selection before invoking
872
+ `validateDocumentSet`.
873
+
874
+ The packaged OKF proof under
875
+ `fixtures/declarative-validation/examples/okf-v0.1/**` uses four generic
876
+ profiles:
877
+
878
+ | Caller-classified role | Example profile | Generic checks |
879
+ | --- | --- | --- |
880
+ | Concept document | `profiles/concept.yaml` | `frontmatterShape` requires parseable frontmatter with a non-empty string `type`. |
881
+ | Bundle-root `index.md` | `profiles/root-index.yaml` | `frontmatterShape` accepts a non-empty string `okf_version` when root version frontmatter is present. |
882
+ | Non-root `index.md` | `profiles/non-root-index.yaml` | `frontmatterShape` forbids frontmatter on directory index files. |
883
+ | `log.md` | `profiles/log.yaml` | `textFormat` checks level-2 log headings as real `YYYY-MM-DD` dates. |
884
+
885
+ Concept validation applies only to non-reserved concept documents. Reserved
886
+ `index.md` and `log.md` files are not routed through the concept profile and do
887
+ not fail merely because they lack concept `type` frontmatter. The bundle-root
888
+ `index.md` may carry the OKF version exception `okf_version: "0.1"` through the
889
+ root-index profile; non-root `index.md` files use the non-root index profile and
890
+ forbid frontmatter. `log.md` is validated as a log with `textFormat`, not as a
891
+ concept document.
892
+
893
+ A caller-owned routing layer can classify entries by path and choose the
894
+ matching profile before validation:
895
+
896
+ ```ts
897
+ const entries = bundleMarkdownFiles.map((file) => {
898
+ const role = classifyOkfPath(file.path);
899
+
900
+ return {
901
+ path: file.path,
902
+ markdown: file.markdown,
903
+ profile: profilesByRole[role],
904
+ profilePath: profilePathsByRole[role],
905
+ };
906
+ });
907
+
908
+ const result = validateDocumentSet(entries, {
909
+ documentVersion: "1.0.0",
910
+ includeEvidence: true,
911
+ });
912
+ ```
913
+
914
+ This composition proves hard OKF conformance only. It does not add CLI
915
+ traversal, an OKF adapter, runtime OKF semantics, directory watching, bundle
916
+ discovery, link checking, unknown-type rejection, optional metadata quality
917
+ checks, or rejection of missing optional `index.md` files. It also does not add
918
+ engine-owned path classification.
919
+
781
920
  CLI invocation:
782
921
 
783
922
  ```sh
@@ -0,0 +1,5 @@
1
+ # Directory Update Log
2
+
3
+ ## 2026-02-30
4
+
5
+ * **Update**: This date heading is intentionally invalid.
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: Customer Metric
3
+ description: A concept fixture that omits the required type field.
4
+ ---
5
+
6
+ # Definition
7
+
8
+ This fixture is intentionally missing `type` frontmatter for OKF concept validation.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Datasets
3
+ ---
4
+
5
+ # Datasets
6
+
7
+ * [Customer metric](customer-metric.md) - This non-root index intentionally has frontmatter.
@@ -0,0 +1,3 @@
1
+ # Datasets
2
+
3
+ * [Sales dataset](sales.md) - Sales knowledge assets.
@@ -0,0 +1,16 @@
1
+ ---
2
+ type: BigQuery Dataset
3
+ title: Sales Dataset
4
+ description: Sales reporting assets for the example bundle.
5
+ resource: https://example.com/bigquery/datasets/sales
6
+ tags: [sales, reporting]
7
+ timestamp: 2026-06-15T00:00:00Z
8
+ ---
9
+
10
+ # Schema
11
+
12
+ The sales dataset contains curated order and customer reporting tables.
13
+
14
+ # Citations
15
+
16
+ [1] [Internal sales catalog](https://example.com/catalog/sales)
@@ -0,0 +1,8 @@
1
+ ---
2
+ okf_version: "0.1"
3
+ ---
4
+
5
+ # Knowledge Bundle
6
+
7
+ * [Datasets](datasets/) - Curated data assets and related concepts.
8
+ * [Incident response](playbooks/incident-response.md) - Operational playbook concept.
@@ -0,0 +1,9 @@
1
+ # Directory Update Log
2
+
3
+ ## 2026-06-15
4
+
5
+ * **Creation**: Added the initial OKF v0.1 example bundle.
6
+
7
+ ## 2026-06-01
8
+
9
+ * **Initialization**: Established the example knowledge bundle structure.
@@ -0,0 +1,17 @@
1
+ ---
2
+ type: Playbook
3
+ title: Incident Response
4
+ description: Triage steps for a data freshness incident.
5
+ tags: [oncall, freshness]
6
+ timestamp: 2026-06-15T00:00:00Z
7
+ ---
8
+
9
+ # Trigger
10
+
11
+ A freshness alert fires when an expected dataset update is delayed.
12
+
13
+ # Steps
14
+
15
+ 1. Check the ingestion dashboard.
16
+ 2. Notify the on-call owner.
17
+ 3. Record the incident in the bundle log.
@@ -0,0 +1,14 @@
1
+ syntaxVersion: markdown-engine.validation@v2
2
+ documentVersion: 1.0.0
3
+ rules:
4
+ - id: okf.concept.frontmatter
5
+ select:
6
+ target: document
7
+ assert:
8
+ frontmatterShape:
9
+ presence: required
10
+ fields:
11
+ - field: type
12
+ required: true
13
+ valueType: string
14
+ nonEmpty: true
@@ -0,0 +1,10 @@
1
+ syntaxVersion: markdown-engine.validation@v2
2
+ documentVersion: 1.0.0
3
+ rules:
4
+ - id: okf.log.date-heading
5
+ select:
6
+ target: heading
7
+ depth: 2
8
+ assert:
9
+ textFormat:
10
+ format: isoDate
@@ -0,0 +1,9 @@
1
+ syntaxVersion: markdown-engine.validation@v2
2
+ documentVersion: 1.0.0
3
+ rules:
4
+ - id: okf.non-root-index.no-frontmatter
5
+ select:
6
+ target: document
7
+ assert:
8
+ frontmatterShape:
9
+ presence: forbidden
@@ -0,0 +1,12 @@
1
+ syntaxVersion: markdown-engine.validation@v2
2
+ documentVersion: 1.0.0
3
+ rules:
4
+ - id: okf.root-index.version-frontmatter
5
+ select:
6
+ target: document
7
+ assert:
8
+ frontmatterShape:
9
+ fields:
10
+ - field: okf_version
11
+ valueType: string
12
+ nonEmpty: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonbelmonti/markdown-engine",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Deterministic Markdown parsing and validation engine package.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  "files": [
33
33
  "dist",
34
34
  "dist-bundled",
35
+ "scripts/install-markdown-engine-cli.sh",
35
36
  "skills",
36
37
  "docs/contracts",
37
38
  "fixtures/declarative-validation/examples",
@@ -69,7 +70,7 @@
69
70
  "test:validation:profile": "npm run build && vitest run tests/declarative-validation-profile.test.ts \"--exclude=.worktrees/**\"",
70
71
  "test:validation:compiler": "npm run build && vitest run tests/declarative-validation-compiler.test.ts \"--exclude=.worktrees/**\"",
71
72
  "test:validation:selectors": "npm run build && vitest run tests/declarative-validation-selectors.test.ts \"--exclude=.worktrees/**\"",
72
- "test:validation:assertions": "npm run build && vitest run tests/declarative-validation-assertions.test.ts tests/declarative-validation-table-column-coverage-fixtures.test.ts tests/declarative-validation-grouped-rules-fixtures.test.ts tests/declarative-validation-when-skipped-rules-fixtures.test.ts \"--exclude=.worktrees/**\"",
73
+ "test:validation:assertions": "npm run build && vitest run tests/declarative-validation-assertions.test.ts tests/declarative-validation-frontmatter-shape-assertions.test.ts tests/declarative-validation-table-column-coverage-fixtures.test.ts tests/declarative-validation-grouped-rules-fixtures.test.ts tests/declarative-validation-when-skipped-rules-fixtures.test.ts \"--exclude=.worktrees/**\"",
73
74
  "test:validation:diagnostics": "npm run build && vitest run tests/declarative-validation-diagnostics.test.ts \"--exclude=.worktrees/**\"",
74
75
  "test:validation:cli": "npm run build && vitest run tests/declarative-validation-cli.test.ts \"--exclude=.worktrees/**\"",
75
76
  "test:validation:examples": "npm run build && vitest run tests/declarative-validation-examples.test.ts \"--exclude=.worktrees/**\"",
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ VERSION="3.1.1"
5
+ PACKAGE="@jasonbelmonti/markdown-engine"
6
+ EXPECTED_SHA256="8c7ff572765cbdc6c38b94de52d69709f583ee7b4d2b7e25ba6be8c92125addf"
7
+ DEFAULT_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
8
+ MARKDOWN_ENGINE_HOME="${MARKDOWN_ENGINE_HOME:-$DEFAULT_DATA_HOME/markdown-engine}"
9
+ MARKDOWN_ENGINE_BIN_DIR="${MARKDOWN_ENGINE_BIN_DIR:-$HOME/.local/bin}"
10
+ INSTALL_DIR="$MARKDOWN_ENGINE_HOME/tools/markdown-engine/$VERSION"
11
+ INSTALL_CLI="$INSTALL_DIR/markdown-engine-cli.mjs"
12
+ BIN_DIR="$MARKDOWN_ENGINE_BIN_DIR"
13
+ BIN_PATH="$BIN_DIR/markdown-engine"
14
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
15
+ REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
16
+ LOCAL_CLI="$REPO_ROOT/dist-bundled/markdown-engine-cli.mjs"
17
+ PACKED_CLI_ENTRY="package/dist-bundled/markdown-engine-cli.mjs"
18
+ TMP_DIR=""
19
+ SOURCE_CLI=""
20
+
21
+ cleanup() {
22
+ if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
23
+ rm -rf "$TMP_DIR"
24
+ fi
25
+ }
26
+ trap cleanup EXIT HUP INT TERM
27
+
28
+ fail() {
29
+ printf 'install-markdown-engine-cli: %s\n' "$1" >&2
30
+ exit 1
31
+ }
32
+
33
+ require_command() {
34
+ command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
35
+ }
36
+
37
+ sha256_of() {
38
+ shasum -a 256 "$1" | awk '{print $1}'
39
+ }
40
+
41
+ shell_quote() {
42
+ printf "'"
43
+ printf '%s' "$1" | sed "s/'/'\\\\''/g"
44
+ printf "'"
45
+ }
46
+
47
+ has_expected_sha() {
48
+ candidate_sha=$(sha256_of "$1")
49
+
50
+ if [ "$candidate_sha" = "$EXPECTED_SHA256" ]; then
51
+ return 0
52
+ fi
53
+
54
+ printf 'install-markdown-engine-cli: ignoring local CLI with unexpected SHA-256 for %s: %s\n' "$1" "$candidate_sha" >&2
55
+ return 1
56
+ }
57
+
58
+ resolve_source_cli() {
59
+ if [ -f "$LOCAL_CLI" ]; then
60
+ if has_expected_sha "$LOCAL_CLI"; then
61
+ SOURCE_CLI="$LOCAL_CLI"
62
+ return 0
63
+ fi
64
+ fi
65
+
66
+ require_command npm
67
+ require_command tar
68
+
69
+ TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/markdown-engine-cli.XXXXXX")
70
+ pack_file=$(npm pack "$PACKAGE@$VERSION" --ignore-scripts --pack-destination "$TMP_DIR" --silent)
71
+
72
+ [ -n "$pack_file" ] || fail "npm pack did not return a tarball name"
73
+ [ -f "$TMP_DIR/$pack_file" ] || fail "npm pack tarball not found: $TMP_DIR/$pack_file"
74
+
75
+ tar -xzf "$TMP_DIR/$pack_file" -C "$TMP_DIR" "$PACKED_CLI_ENTRY" ||
76
+ fail "packed CLI artifact not found in $PACKAGE@$VERSION"
77
+
78
+ packed_cli="$TMP_DIR/$PACKED_CLI_ENTRY"
79
+ [ -f "$packed_cli" ] || fail "packed CLI artifact not found in $PACKAGE@$VERSION"
80
+ SOURCE_CLI="$packed_cli"
81
+ }
82
+
83
+ require_command shasum
84
+ require_command node
85
+ require_command sed
86
+
87
+ resolve_source_cli
88
+ source_cli="$SOURCE_CLI"
89
+ [ -n "$source_cli" ] || fail "unable to resolve bundled CLI source"
90
+ actual_sha=$(sha256_of "$source_cli")
91
+
92
+ if [ "$actual_sha" != "$EXPECTED_SHA256" ]; then
93
+ fail "unexpected CLI SHA-256 for $source_cli: $actual_sha"
94
+ fi
95
+
96
+ mkdir -p "$INSTALL_DIR" "$BIN_DIR"
97
+ cp "$source_cli" "$INSTALL_CLI"
98
+ chmod 755 "$INSTALL_CLI"
99
+
100
+ quoted_install_cli=$(shell_quote "$INSTALL_CLI")
101
+ {
102
+ printf '%s\n' '#!/usr/bin/env sh'
103
+ printf 'DEFAULT_MARKDOWN_ENGINE_CLI=%s\n' "$quoted_install_cli"
104
+ printf '%s\n' 'MARKDOWN_ENGINE_CLI="${MARKDOWN_ENGINE_CLI:-$DEFAULT_MARKDOWN_ENGINE_CLI}"'
105
+ printf '%s\n' 'exec "${NODE_BINARY:-node}" "$MARKDOWN_ENGINE_CLI" "$@"'
106
+ } > "$BIN_PATH"
107
+ chmod 755 "$BIN_PATH"
108
+
109
+ "${NODE_BINARY:-node}" "$INSTALL_CLI" --help >/dev/null
110
+
111
+ printf 'Installed markdown-engine CLI: %s\n' "$INSTALL_CLI"
112
+ printf 'Installed markdown-engine wrapper: %s\n' "$BIN_PATH"
113
+ printf 'SHA-256: %s\n' "$actual_sha"