@jasonbelmonti/markdown-engine 3.5.0 → 3.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 (100) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +40 -12
  3. package/dist/cli/declarative-validation.d.ts +3 -9
  4. package/dist/cli/declarative-validation.d.ts.map +1 -1
  5. package/dist/cli/declarative-validation.js +6 -16
  6. package/dist/cli/declarative-validation.js.map +1 -1
  7. package/dist/cli/run.d.ts.map +1 -1
  8. package/dist/cli/run.js +2 -0
  9. package/dist/cli/run.js.map +1 -1
  10. package/dist/cli/validate-args.d.ts +3 -1
  11. package/dist/cli/validate-args.d.ts.map +1 -1
  12. package/dist/cli/validate-args.js +45 -92
  13. package/dist/cli/validate-args.js.map +1 -1
  14. package/dist/cli/validation-output.d.ts +16 -0
  15. package/dist/cli/validation-output.d.ts.map +1 -0
  16. package/dist/cli/validation-output.js +26 -0
  17. package/dist/cli/validation-output.js.map +1 -0
  18. package/dist/cli/validation-report.d.ts +8 -0
  19. package/dist/cli/validation-report.d.ts.map +1 -0
  20. package/dist/cli/validation-report.js +24 -0
  21. package/dist/cli/validation-report.js.map +1 -0
  22. package/dist/cli/validation-summary.d.ts +47 -0
  23. package/dist/cli/validation-summary.d.ts.map +1 -0
  24. package/dist/cli/validation-summary.js +55 -0
  25. package/dist/cli/validation-summary.js.map +1 -0
  26. package/dist/declarative-validation/assertions/evaluator.d.ts.map +1 -1
  27. package/dist/declarative-validation/assertions/evaluator.js +3 -0
  28. package/dist/declarative-validation/assertions/evaluator.js.map +1 -1
  29. package/dist/declarative-validation/assertions/id-targets.d.ts +2 -0
  30. package/dist/declarative-validation/assertions/id-targets.d.ts.map +1 -1
  31. package/dist/declarative-validation/assertions/id-targets.js.map +1 -1
  32. package/dist/declarative-validation/assertions/table-column-coverage.d.ts.map +1 -1
  33. package/dist/declarative-validation/assertions/table-column-coverage.js +8 -10
  34. package/dist/declarative-validation/assertions/table-column-coverage.js.map +1 -1
  35. package/dist/declarative-validation/assertions/table-rows-complete.d.ts +4 -0
  36. package/dist/declarative-validation/assertions/table-rows-complete.d.ts.map +1 -0
  37. package/dist/declarative-validation/assertions/table-rows-complete.js +40 -0
  38. package/dist/declarative-validation/assertions/table-rows-complete.js.map +1 -0
  39. package/dist/declarative-validation/assertions/text.d.ts.map +1 -1
  40. package/dist/declarative-validation/assertions/text.js +5 -0
  41. package/dist/declarative-validation/assertions/text.js.map +1 -1
  42. package/dist/declarative-validation/assertions/visible-text.d.ts +5 -0
  43. package/dist/declarative-validation/assertions/visible-text.d.ts.map +1 -0
  44. package/dist/declarative-validation/assertions/visible-text.js +42 -0
  45. package/dist/declarative-validation/assertions/visible-text.js.map +1 -0
  46. package/dist/declarative-validation/compiler/assertion-builders.d.ts.map +1 -1
  47. package/dist/declarative-validation/compiler/assertion-builders.js +13 -2
  48. package/dist/declarative-validation/compiler/assertion-builders.js.map +1 -1
  49. package/dist/declarative-validation/compiler/assertion-shapes.d.ts.map +1 -1
  50. package/dist/declarative-validation/compiler/assertion-shapes.js +20 -4
  51. package/dist/declarative-validation/compiler/assertion-shapes.js.map +1 -1
  52. package/dist/declarative-validation/compiler/assertions.d.ts.map +1 -1
  53. package/dist/declarative-validation/compiler/assertions.js +1 -0
  54. package/dist/declarative-validation/compiler/assertions.js.map +1 -1
  55. package/dist/declarative-validation/compiler/compatibility.d.ts.map +1 -1
  56. package/dist/declarative-validation/compiler/compatibility.js +1 -0
  57. package/dist/declarative-validation/compiler/compatibility.js.map +1 -1
  58. package/dist/declarative-validation/compiler/plan.d.ts +6 -1
  59. package/dist/declarative-validation/compiler/plan.d.ts.map +1 -1
  60. package/dist/declarative-validation/compiler/table-rows-complete.d.ts +3 -0
  61. package/dist/declarative-validation/compiler/table-rows-complete.d.ts.map +1 -0
  62. package/dist/declarative-validation/compiler/table-rows-complete.js +17 -0
  63. package/dist/declarative-validation/compiler/table-rows-complete.js.map +1 -0
  64. package/dist/declarative-validation/profile/assertion-schema.d.ts.map +1 -1
  65. package/dist/declarative-validation/profile/assertion-schema.js +35 -6
  66. package/dist/declarative-validation/profile/assertion-schema.js.map +1 -1
  67. package/dist/declarative-validation/profile/cell-predicate-schema.d.ts +4 -0
  68. package/dist/declarative-validation/profile/cell-predicate-schema.d.ts.map +1 -0
  69. package/dist/declarative-validation/profile/cell-predicate-schema.js +29 -0
  70. package/dist/declarative-validation/profile/cell-predicate-schema.js.map +1 -0
  71. package/dist/declarative-validation/profile/index.d.ts +4 -0
  72. package/dist/declarative-validation/profile/index.d.ts.map +1 -1
  73. package/dist/declarative-validation/profile/selector-schema.d.ts.map +1 -1
  74. package/dist/declarative-validation/profile/selector-schema.js +1 -23
  75. package/dist/declarative-validation/profile/selector-schema.js.map +1 -1
  76. package/dist/declarative-validation/selectors/table-targets.d.ts +2 -1
  77. package/dist/declarative-validation/selectors/table-targets.d.ts.map +1 -1
  78. package/dist/declarative-validation/selectors/table-targets.js +13 -5
  79. package/dist/declarative-validation/selectors/table-targets.js.map +1 -1
  80. package/dist/internal/package-version.d.ts +1 -1
  81. package/dist/internal/package-version.js +1 -1
  82. package/dist-bundled/markdown-engine-cli.mjs +444 -215
  83. package/docs/contracts/declarative-validation.md +155 -8
  84. package/fixtures/declarative-validation/examples/table-rows-complete/README.md +65 -0
  85. package/fixtures/declarative-validation/examples/table-rows-complete/blockquote.md +3 -0
  86. package/fixtures/declarative-validation/examples/table-rows-complete/complete.md +3 -0
  87. package/fixtures/declarative-validation/examples/table-rows-complete/consumer-missing.md +50 -0
  88. package/fixtures/declarative-validation/examples/table-rows-complete/consumer-repaired.md +50 -0
  89. package/fixtures/declarative-validation/examples/table-rows-complete/excess.md +3 -0
  90. package/fixtures/declarative-validation/examples/table-rows-complete/explicit-empty.md +3 -0
  91. package/fixtures/declarative-validation/examples/table-rows-complete/header-only.md +2 -0
  92. package/fixtures/declarative-validation/examples/table-rows-complete/legacy.yaml +10 -0
  93. package/fixtures/declarative-validation/examples/table-rows-complete/missing.md +3 -0
  94. package/fixtures/declarative-validation/examples/table-rows-complete/multiple.md +5 -0
  95. package/fixtures/declarative-validation/examples/table-rows-complete/oracle.json +85 -0
  96. package/fixtures/declarative-validation/examples/table-rows-complete/profile.yaml +11 -0
  97. package/fixtures/declarative-validation/examples/table-rows-complete/shape.yaml +8 -0
  98. package/fixtures/declarative-validation/examples/table-rows-complete/syntax.md +3 -0
  99. package/package.json +2 -2
  100. package/scripts/install-markdown-engine-cli.sh +2 -2
@@ -7531,14 +7531,14 @@ function lists(document3, query = {}) {
7531
7531
  });
7532
7532
  }
7533
7533
  function links(document3, query = {}) {
7534
- return (document3.links ?? []).filter((link2) => {
7535
- if (query.targetId !== void 0 && link2.target.id !== query.targetId) {
7534
+ return (document3.links ?? []).filter((link3) => {
7535
+ if (query.targetId !== void 0 && link3.target.id !== query.targetId) {
7536
7536
  return false;
7537
7537
  }
7538
- if (query.url !== void 0 && link2.url !== query.url) {
7538
+ if (query.url !== void 0 && link3.url !== query.url) {
7539
7539
  return false;
7540
7540
  }
7541
- return query.text === void 0 || link2.text === query.text;
7541
+ return query.text === void 0 || link3.text === query.text;
7542
7542
  });
7543
7543
  }
7544
7544
  function linkReferences(document3, query = {}) {
@@ -8047,7 +8047,8 @@ function tableColumnTargets(document3, source) {
8047
8047
  }
8048
8048
  const resolvedColumns = tableResolution.tables.flatMap((table) => {
8049
8049
  const columnIndex = columnIndexForHeader(table, source.column);
8050
- return columnIndex === void 0 ? [] : [{ table, columnIndex }];
8050
+ const predicateColumnMissing = source.rowWhere !== void 0 && columnIndexForHeader(table, source.rowWhere.column) === void 0;
8051
+ return columnIndex === void 0 || predicateColumnMissing ? [] : [{ table, columnIndex }];
8051
8052
  });
8052
8053
  if (resolvedColumns.length === 0) {
8053
8054
  return {
@@ -8060,7 +8061,7 @@ function tableColumnTargets(document3, source) {
8060
8061
  status: "resolved",
8061
8062
  source,
8062
8063
  targets: resolvedColumns.flatMap(
8063
- ({ table, columnIndex }) => tableCellTargetsForColumn(document3, table, columnIndex)
8064
+ ({ table, columnIndex }) => tableCellTargetsForColumn(document3, table, columnIndex, source.rowWhere)
8064
8065
  )
8065
8066
  };
8066
8067
  }
@@ -8110,9 +8111,11 @@ function rowMatchesPredicate(table, cells, predicate) {
8110
8111
  function columnIndexForHeader(table, column) {
8111
8112
  return headerCells(table).find((cell) => cell.text === column)?.columnIndex;
8112
8113
  }
8113
- function tableCellTargetsForColumn(document3, table, columnIndex) {
8114
- return dataRows(table).flatMap((row) => {
8115
- const cell = row.cells.find((candidate) => candidate.columnIndex === columnIndex);
8114
+ function tableCellTargetsForColumn(document3, table, columnIndex, rowWhere) {
8115
+ return dataRows(table).filter((row) => rowMatchesPredicate(table, row.cells, rowWhere)).flatMap((row) => {
8116
+ const cell = row.cells.find(
8117
+ (candidate) => candidate.columnIndex === columnIndex
8118
+ );
8116
8119
  return cell === void 0 ? [] : [tableCellSelectionTarget(document3, table, cell)];
8117
8120
  });
8118
8121
  }
@@ -9000,11 +9003,11 @@ function linkTargets(document3, selector) {
9000
9003
  return documentQueries.links(document3, {
9001
9004
  ...selector.text !== void 0 ? { text: selector.text } : {},
9002
9005
  ...selector.url !== void 0 ? { url: selector.url } : {}
9003
- }).filter((link2) => targetMatchesSection(document3, link2.target.id, selector.section)).map((link2) => ({
9006
+ }).filter((link3) => targetMatchesSection(document3, link3.target.id, selector.section)).map((link3) => ({
9004
9007
  kind: "link",
9005
- link: link2,
9006
- text: link2.text,
9007
- ...sourceFromTarget(document3, link2.target)
9008
+ link: link3,
9009
+ text: link3.text,
9010
+ ...sourceFromTarget(document3, link3.target)
9008
9011
  }));
9009
9012
  }
9010
9013
  function listTargets(document3, selector) {
@@ -9603,8 +9606,16 @@ function sourceLengthRequirement(assertion) {
9603
9606
  // src/declarative-validation/assertions/table-column-coverage.ts
9604
9607
  function evaluateTableColumnCoverage(assertion, context) {
9605
9608
  const options = idTokenOptions(assertion);
9606
- const sourceIds = sourceIdsForCoverage(assertion, context, options);
9609
+ const sourceResolution = resolveTableColumnIdTokens(
9610
+ context.selection.document,
9611
+ assertion.source,
9612
+ options
9613
+ );
9614
+ const sourceIds = sourceResolution.status === "resolved" ? uniqueSourceIds2(sourceResolution.tokens) : [];
9607
9615
  if (sourceIds.length === 0) {
9616
+ if (assertion.allowEmptySource === true && sourceResolution.status === "resolved") {
9617
+ return [];
9618
+ }
9608
9619
  return [emptySelectionDiagnostic(context.rule, context.assertionIndex)];
9609
9620
  }
9610
9621
  const targetResolution = resolveTableColumnIdTokens(
@@ -9614,12 +9625,22 @@ function evaluateTableColumnCoverage(assertion, context) {
9614
9625
  );
9615
9626
  if (targetResolution.status === "missingSection") {
9616
9627
  return sourceIds.map(
9617
- (sourceId, sourceIdOrder) => missingTargetSectionDiagnostic(assertion, context, sourceId, sourceIdOrder)
9628
+ (sourceId, sourceIdOrder) => missingTargetSectionDiagnostic(
9629
+ assertion,
9630
+ context,
9631
+ sourceId,
9632
+ sourceIdOrder
9633
+ )
9618
9634
  );
9619
9635
  }
9620
9636
  if (targetResolution.status === "missingColumn") {
9621
9637
  return sourceIds.map(
9622
- (sourceId, sourceIdOrder) => missingTargetColumnDiagnostic(assertion, context, sourceId, sourceIdOrder)
9638
+ (sourceId, sourceIdOrder) => missingTargetColumnDiagnostic(
9639
+ assertion,
9640
+ context,
9641
+ sourceId,
9642
+ sourceIdOrder
9643
+ )
9623
9644
  );
9624
9645
  }
9625
9646
  const targetComparisonValues = new Set(
@@ -9641,17 +9662,6 @@ function evaluateTableColumnCoverage(assertion, context) {
9641
9662
  }
9642
9663
  return diagnostics;
9643
9664
  }
9644
- function sourceIdsForCoverage(assertion, context, options) {
9645
- const sourceResolution = resolveTableColumnIdTokens(
9646
- context.selection.document,
9647
- {
9648
- section: assertion.source.section,
9649
- column: assertion.source.column
9650
- },
9651
- options
9652
- );
9653
- return sourceResolution.status === "resolved" ? uniqueSourceIds2(sourceResolution.tokens) : [];
9654
- }
9655
9665
  function uniqueSourceIds2(tokens) {
9656
9666
  const sourceIdsByComparisonValue = /* @__PURE__ */ new Map();
9657
9667
  for (const token of tokens) {
@@ -9741,6 +9751,49 @@ function targetColumnSource(assertion) {
9741
9751
  };
9742
9752
  }
9743
9753
 
9754
+ // src/declarative-validation/assertions/table-rows-complete.ts
9755
+ function evaluateTableRowsComplete(context) {
9756
+ if (context.selection.targets.length === 0) {
9757
+ return [emptySelectionDiagnostic(context.rule, context.assertionIndex)];
9758
+ }
9759
+ return context.selection.targets.flatMap(
9760
+ (target, targetOrder) => target.kind === "table" ? incompleteRows(target.table, context, targetOrder) : []
9761
+ );
9762
+ }
9763
+ function incompleteRows(table, context, targetOrder) {
9764
+ const expected = columnIndexes(table.cells.filter((cell) => cell.header));
9765
+ const rows = /* @__PURE__ */ new Map();
9766
+ for (const cell of table.cells) {
9767
+ if (!cell.header) {
9768
+ const cells = rows.get(cell.rowIndex) ?? [];
9769
+ cells.push(cell);
9770
+ rows.set(cell.rowIndex, cells);
9771
+ }
9772
+ }
9773
+ return [...rows].sort(([left], [right]) => left - right).flatMap(([rowIndex, cells]) => {
9774
+ const actual = columnIndexes(cells);
9775
+ if (expected.length === actual.length && expected.every((column, i) => column === actual[i])) {
9776
+ return [];
9777
+ }
9778
+ const sourceRange = [...cells].sort((left, right) => left.columnIndex - right.columnIndex).find((cell) => cell.sourceRange !== void 0)?.sourceRange;
9779
+ return [validationDiagnostic(
9780
+ "profile.validation.assertionFailed",
9781
+ `Selected table "${table.target.id}" body row ${rowIndex} must have column positions ${JSON.stringify(expected)}; found ${JSON.stringify(actual)}.`,
9782
+ context.rule,
9783
+ {
9784
+ assertionIndex: context.assertionIndex,
9785
+ targetOrder,
9786
+ targetKey: `table:${table.target.id}:row:${rowIndex}`,
9787
+ diagnosticOrder: rowIndex,
9788
+ ...sourceRange === void 0 ? {} : { sourceRange }
9789
+ }
9790
+ )];
9791
+ });
9792
+ }
9793
+ function columnIndexes(cells) {
9794
+ return cells.map((cell) => cell.columnIndex).sort((left, right) => left - right);
9795
+ }
9796
+
9744
9797
  // src/declarative-validation/assertions/table-columns-exact.ts
9745
9798
  function evaluateTableColumnsExact(assertion, context) {
9746
9799
  if (context.selection.targets.length === 0) {
@@ -9824,6 +9877,40 @@ function tableColumnSortKey(table, diagnosticOrder, column) {
9824
9877
  return `table:${table.target.id}:column:${paddedDiagnosticOrder}:${column}`;
9825
9878
  }
9826
9879
 
9880
+ // src/declarative-validation/assertions/visible-text.ts
9881
+ function visibleText(document3, target) {
9882
+ const textAt = (nodeTarget) => documentQueries.nodes(document3, { targetId: nodeTarget.id }).map(nodeText2).join("");
9883
+ switch (target.kind) {
9884
+ case "document":
9885
+ return document3.children.map(nodeText2).join("\n");
9886
+ case "section":
9887
+ return [
9888
+ textAt(target.section.headingTarget),
9889
+ ...target.section.bodyTargets.map(textAt)
9890
+ ].join("\n");
9891
+ case "heading":
9892
+ return textAt(target.section.headingTarget);
9893
+ case "table":
9894
+ return textAt(target.table.target);
9895
+ case "tableRow":
9896
+ return target.cells.map((cell) => textAt(cell.target)).join(" ");
9897
+ case "tableCell":
9898
+ return textAt(target.cell.target);
9899
+ case "textSpan":
9900
+ return textAt(target.span.target);
9901
+ case "link":
9902
+ return textAt(target.link.target);
9903
+ case "list":
9904
+ return textAt(target.list.target);
9905
+ }
9906
+ }
9907
+ function nodeText2(node2) {
9908
+ if (node2.type === "html" || node2.type === "definition" || node2.type === "yaml") {
9909
+ return "";
9910
+ }
9911
+ return node2.children === void 0 ? node2.text ?? "" : node2.children.map(nodeText2).join("");
9912
+ }
9913
+
9827
9914
  // src/declarative-validation/assertions/literal-text.ts
9828
9915
  function countNonOverlappingLiteralOccurrences(text5, literal) {
9829
9916
  let count = 0;
@@ -9850,6 +9937,16 @@ function evaluateText(assertion, context) {
9850
9937
  }
9851
9938
  function evaluateTextTarget(assertion, context, target, targetOrder) {
9852
9939
  const diagnostics = [];
9940
+ if (assertion.nonBlank === true && visibleText(context.selection.document, target).trim().length === 0) {
9941
+ diagnostics.push(
9942
+ validationDiagnostic(
9943
+ "profile.validation.textBlank",
9944
+ `Selected ${target.kind} must contain non-whitespace text outside raw HTML.`,
9945
+ context.rule,
9946
+ { assertionIndex: context.assertionIndex, target, targetOrder }
9947
+ )
9948
+ );
9949
+ }
9853
9950
  if (assertion.contains !== void 0 && !target.text.includes(assertion.contains)) {
9854
9951
  diagnostics.push(
9855
9952
  validationDiagnostic(
@@ -10083,6 +10180,8 @@ function evaluateAssertion(assertion, context) {
10083
10180
  return evaluateFrontmatterRequired(assertion, context);
10084
10181
  case "tableColumnsRequired":
10085
10182
  return evaluateTableColumnsRequired(assertion, context);
10183
+ case "tableRowsComplete":
10184
+ return evaluateTableRowsComplete(context);
10086
10185
  case "tableColumnsExact":
10087
10186
  return evaluateTableColumnsExact(assertion, context);
10088
10187
  case "ids":
@@ -10625,6 +10724,48 @@ function stringArray(value) {
10625
10724
  return values;
10626
10725
  }
10627
10726
 
10727
+ // src/declarative-validation/profile/cell-predicate-schema.ts
10728
+ function cellPredicateFromValue(value, key, diagnostics) {
10729
+ if (!isPlainRecord(value)) {
10730
+ diagnostics.push(invalidShape(`Selector ${key} must be an object.`));
10731
+ return void 0;
10732
+ }
10733
+ unsupportedKeys(value, ["column", "equals", "includes"], diagnostics);
10734
+ const column = requiredNonEmptyStringField(
10735
+ value.column,
10736
+ `Selector ${key}.column`,
10737
+ diagnostics,
10738
+ (field) => `${field} must be a non-empty string.`
10739
+ );
10740
+ if (column === void 0) {
10741
+ return void 0;
10742
+ }
10743
+ const predicate = {
10744
+ ...optionalPredicateString(value, "equals", diagnostics),
10745
+ ...optionalPredicateString(value, "includes", diagnostics)
10746
+ };
10747
+ if (predicate.equals === void 0 && predicate.includes === void 0) {
10748
+ diagnostics.push(
10749
+ invalidShape(
10750
+ `Selector ${key} must include at least one of equals or includes.`
10751
+ )
10752
+ );
10753
+ return void 0;
10754
+ }
10755
+ return {
10756
+ column,
10757
+ ...predicate
10758
+ };
10759
+ }
10760
+ function optionalPredicateString(record, key, diagnostics) {
10761
+ return optionalStringField(
10762
+ record,
10763
+ key,
10764
+ diagnostics,
10765
+ (field) => `Selector ${field} must be a non-empty string when provided.`
10766
+ );
10767
+ }
10768
+
10628
10769
  // src/declarative-validation/profile/selector-schema.ts
10629
10770
  function selectorFromValue(value, diagnostics) {
10630
10771
  if (!isPlainRecord(value)) {
@@ -10746,33 +10887,6 @@ function optionalCellPredicate(record, key, diagnostics) {
10746
10887
  const predicate = cellPredicateFromValue(value, key, diagnostics);
10747
10888
  return predicate === void 0 ? {} : { [key]: predicate };
10748
10889
  }
10749
- function cellPredicateFromValue(value, key, diagnostics) {
10750
- if (!isPlainRecord(value)) {
10751
- diagnostics.push(invalidShape(`Selector ${key} must be an object.`));
10752
- return void 0;
10753
- }
10754
- unsupportedKeys(value, ["column", "equals", "includes"], diagnostics);
10755
- const column = requiredSelectorString(value.column, `${key}.column`, diagnostics);
10756
- if (column === void 0) {
10757
- return void 0;
10758
- }
10759
- const predicate = {
10760
- ...optionalSelectorString(value, "equals", diagnostics),
10761
- ...optionalSelectorString(value, "includes", diagnostics)
10762
- };
10763
- if (predicate.equals === void 0 && predicate.includes === void 0) {
10764
- diagnostics.push(
10765
- invalidShape(
10766
- `Selector ${key} must include at least one of equals or includes.`
10767
- )
10768
- );
10769
- return void 0;
10770
- }
10771
- return {
10772
- column,
10773
- ...predicate
10774
- };
10775
- }
10776
10890
  function optionalBoolean(record, key, diagnostics) {
10777
10891
  return optionalBooleanField(
10778
10892
  record,
@@ -11122,7 +11236,22 @@ function pushObjectDiagnostic(fieldName, value, ruleId, diagnostics) {
11122
11236
  }
11123
11237
  function pushTextShapeDiagnostics(assertion, ruleId, diagnostics) {
11124
11238
  let valid = true;
11125
- if (assertion?.contains !== void 0 && !pushNonEmptyStringDiagnostic("contains", assertion.contains, ruleId, diagnostics)) {
11239
+ if (assertion?.nonBlank !== void 0 && assertion.nonBlank !== true) {
11240
+ diagnostics.push(
11241
+ compileDiagnostic(
11242
+ "profile.config.invalidShape",
11243
+ "text.nonBlank must be true when provided.",
11244
+ ruleId
11245
+ )
11246
+ );
11247
+ valid = false;
11248
+ }
11249
+ if (assertion?.contains !== void 0 && !pushNonEmptyStringDiagnostic(
11250
+ "contains",
11251
+ assertion.contains,
11252
+ ruleId,
11253
+ diagnostics
11254
+ )) {
11126
11255
  valid = false;
11127
11256
  }
11128
11257
  if (assertion?.excludes !== void 0 && closedStringArray("excludes", assertion.excludes, ruleId, diagnostics) === void 0) {
@@ -11191,7 +11320,13 @@ function closedTableColumnCoverage(value, ruleId, diagnostics) {
11191
11320
  const record = value;
11192
11321
  const hasSupportedKeys = pushUnsupportedKeyDiagnostics(
11193
11322
  record,
11194
- ["source", "target", "require"],
11323
+ ["source", "target", "require", "allowEmptySource"],
11324
+ diagnostics
11325
+ );
11326
+ pushOptionalBooleanDiagnostic(
11327
+ "tableColumnCoverage.allowEmptySource",
11328
+ record.allowEmptySource,
11329
+ ruleId,
11195
11330
  diagnostics
11196
11331
  );
11197
11332
  const source = closedTableColumnCoverageSource(
@@ -11217,7 +11352,8 @@ function closedTableColumnCoverage(value, ruleId, diagnostics) {
11217
11352
  return hasSupportedKeys && diagnostics.length === diagnosticCountBefore && source !== void 0 && target !== void 0 && require2 !== void 0 ? {
11218
11353
  source,
11219
11354
  target,
11220
- require: require2
11355
+ require: require2,
11356
+ ...record.allowEmptySource === void 0 ? {} : { allowEmptySource: record.allowEmptySource }
11221
11357
  } : void 0;
11222
11358
  }
11223
11359
  function closedTableColumnCoverageSource(value, ruleId, diagnostics) {
@@ -11232,7 +11368,7 @@ function closedTableColumnCoverageSource(value, ruleId, diagnostics) {
11232
11368
  const source = value;
11233
11369
  const hasSupportedKeys = pushUnsupportedKeyDiagnostics(
11234
11370
  source,
11235
- ["section", "column", "prefix", "caseSensitive"],
11371
+ ["section", "column", "prefix", "caseSensitive", "rowWhere"],
11236
11372
  diagnostics
11237
11373
  );
11238
11374
  const hasValidFields = pushNonEmptyStringDiagnostic(
@@ -11256,7 +11392,9 @@ function closedTableColumnCoverageSource(value, ruleId, diagnostics) {
11256
11392
  ruleId,
11257
11393
  diagnostics
11258
11394
  );
11259
- return hasSupportedKeys && hasValidFields ? {
11395
+ const rowWhere = source.rowWhere === void 0 ? void 0 : cellPredicateFromValue(source.rowWhere, "rowWhere", diagnostics);
11396
+ return hasSupportedKeys && hasValidFields && (source.rowWhere === void 0 || rowWhere !== void 0) ? {
11397
+ ...rowWhere === void 0 ? {} : { rowWhere },
11260
11398
  section: source.section,
11261
11399
  column: source.column,
11262
11400
  ...optionalString("prefix", source.prefix),
@@ -11431,7 +11569,7 @@ function optionalNumber2(key, value) {
11431
11569
  return value === void 0 ? {} : { [key]: value };
11432
11570
  }
11433
11571
  function hasTextPredicate(assertion) {
11434
- return assertion?.contains !== void 0 || assertion?.excludes !== void 0 && assertion.excludes.length > 0;
11572
+ return assertion?.nonBlank === true || assertion?.contains !== void 0 || assertion?.excludes !== void 0 && assertion.excludes.length > 0;
11435
11573
  }
11436
11574
 
11437
11575
  // src/declarative-validation/profile/ids-assertion-contract.ts
@@ -11475,6 +11613,7 @@ function selectorAssertionCompatibilityError(assertionName, selector) {
11475
11613
  return selector.target === "document" ? void 0 : selectorCompatibilityMessage(assertionName, "document");
11476
11614
  case "tableColumnsRequired":
11477
11615
  case "tableColumnsExact":
11616
+ case "tableRowsComplete":
11478
11617
  return selector.target === "table" ? void 0 : selectorCompatibilityMessage(assertionName, "table");
11479
11618
  case "ids":
11480
11619
  case "text":
@@ -11596,12 +11735,29 @@ function hasLengthBound(bounds) {
11596
11735
  return bounds.min !== void 0 || bounds.max !== void 0;
11597
11736
  }
11598
11737
 
11738
+ // src/declarative-validation/compiler/table-rows-complete.ts
11739
+ var buildTableRowsCompleteAssertion = (assertion, selector, ruleId, syntaxVersion, diagnostics) => {
11740
+ if (assertion.tableRowsComplete === void 0 || syntaxVersion !== PROFILE_SYNTAX_VERSION_V2) {
11741
+ return void 0;
11742
+ }
11743
+ if (assertion.tableRowsComplete !== true) {
11744
+ diagnostics.push(compileDiagnostic(
11745
+ "profile.config.invalidShape",
11746
+ "tableRowsComplete must be true.",
11747
+ ruleId
11748
+ ));
11749
+ return void 0;
11750
+ }
11751
+ return pushCompatibilityDiagnostic("tableRowsComplete", selector, ruleId, diagnostics) ? { kind: "tableRowsComplete" } : void 0;
11752
+ };
11753
+
11599
11754
  // src/declarative-validation/compiler/assertion-builders.ts
11600
11755
  var ASSERTION_BUILDERS = [
11601
11756
  buildExistsAssertion,
11602
11757
  buildSectionsRequiredAssertion,
11603
11758
  buildTableColumnsRequiredAssertion,
11604
11759
  buildTableColumnsExactAssertion,
11760
+ buildTableRowsCompleteAssertion,
11605
11761
  buildIdsAssertion,
11606
11762
  buildReferencesAssertion,
11607
11763
  buildTableColumnCoverageAssertion,
@@ -11856,7 +12012,9 @@ function buildTableColumnCoverageAssertion(assertion, selector, ruleId, syntaxVe
11856
12012
  )) {
11857
12013
  return {
11858
12014
  kind: "tableColumnCoverage",
12015
+ ...tableColumnCoverage.allowEmptySource === void 0 ? {} : { allowEmptySource: tableColumnCoverage.allowEmptySource },
11859
12016
  source: {
12017
+ ...tableColumnCoverage.source.rowWhere === void 0 ? {} : { rowWhere: tableColumnCoverage.source.rowWhere },
11860
12018
  section: tableColumnCoverage.source.section,
11861
12019
  column: tableColumnCoverage.source.column,
11862
12020
  caseSensitive: tableColumnCoverage.source.caseSensitive ?? true,
@@ -11895,13 +12053,13 @@ function buildFrontmatterShapeAssertion(assertion, selector, ruleId, syntaxVersi
11895
12053
  }
11896
12054
  return void 0;
11897
12055
  }
11898
- function buildTextAssertion(assertion, selector, ruleId, _syntaxVersion, diagnostics) {
12056
+ function buildTextAssertion(assertion, selector, ruleId, syntaxVersion, diagnostics) {
11899
12057
  if (assertion.text === void 0) {
11900
12058
  return void 0;
11901
12059
  }
11902
12060
  if (!pushObjectDiagnostic("text", assertion.text, ruleId, diagnostics) || !pushUnsupportedKeyDiagnostics(
11903
12061
  assertion.text,
11904
- ["contains", "excludes"],
12062
+ syntaxVersion === PROFILE_SYNTAX_VERSION_V2 ? ["contains", "excludes", "nonBlank"] : ["contains", "excludes"],
11905
12063
  diagnostics
11906
12064
  ) || !pushTextShapeDiagnostics(assertion.text, ruleId, diagnostics)) {
11907
12065
  return void 0;
@@ -11919,6 +12077,7 @@ function buildTextAssertion(assertion, selector, ruleId, _syntaxVersion, diagnos
11919
12077
  if (pushCompatibilityDiagnostic("text", selector, ruleId, diagnostics)) {
11920
12078
  return {
11921
12079
  kind: "text",
12080
+ ...assertion.text.nonBlank === true ? { nonBlank: true } : {},
11922
12081
  ...optionalString("contains", assertion.text.contains),
11923
12082
  ...optionalStringArray("excludes", assertion.text.excludes)
11924
12083
  };
@@ -12026,6 +12185,7 @@ var ASSERTION_KEYS_V2 = [
12026
12185
  "selectionCount",
12027
12186
  "sourceLength",
12028
12187
  "tableColumnsExact",
12188
+ "tableRowsComplete",
12029
12189
  "tableColumnCoverage",
12030
12190
  "frontmatterShape",
12031
12191
  "textFormat"
@@ -12797,6 +12957,7 @@ var SUPPORTED_ASSERTION_KEYS_V2 = [
12797
12957
  "selectionCount",
12798
12958
  "sourceLength",
12799
12959
  "tableColumnsExact",
12960
+ "tableRowsComplete",
12800
12961
  "tableColumnCoverage",
12801
12962
  "frontmatterShape",
12802
12963
  "textFormat"
@@ -12817,12 +12978,13 @@ function assertionFromValue(value, syntaxVersion, diagnostics) {
12817
12978
  ...tableColumnsRequiredFromValue(value.tableColumnsRequired, diagnostics),
12818
12979
  ...idsFromValue(value.ids, syntaxVersion, diagnostics),
12819
12980
  ...referencesFromValue(value.references, diagnostics),
12820
- ...textAssertionFromValue(value.text, diagnostics),
12981
+ ...textAssertionFromValue(value.text, syntaxVersion, diagnostics),
12821
12982
  ...textOccurrenceCountFromValue(value.textOccurrenceCount, diagnostics),
12822
12983
  ...textLengthFromValue(value.textLength, diagnostics),
12823
12984
  ...supportsV2AssertionSurface(syntaxVersion) ? selectionCountFromValue(value.selectionCount, diagnostics) : {},
12824
12985
  ...supportsV2AssertionSurface(syntaxVersion) ? sourceLengthFromValue(value.sourceLength, diagnostics) : {},
12825
12986
  ...supportsV2AssertionSurface(syntaxVersion) ? tableColumnsExactFromValue(value.tableColumnsExact, diagnostics) : {},
12987
+ ...supportsV2AssertionSurface(syntaxVersion) ? tableRowsCompleteFromValue(value.tableRowsComplete, diagnostics) : {},
12826
12988
  ...frontmatterRequiredFromValue(value.frontmatterRequired, diagnostics),
12827
12989
  ...supportsV2AssertionSurface(syntaxVersion) ? tableColumnCoverageFromValue(value.tableColumnCoverage, diagnostics) : {},
12828
12990
  ...supportsV2AssertionSurface(syntaxVersion) ? frontmatterShapeFromValue2(value.frontmatterShape, diagnostics) : {},
@@ -12833,7 +12995,9 @@ function assertionFromValue(value, syntaxVersion, diagnostics) {
12833
12995
  return void 0;
12834
12996
  }
12835
12997
  diagnostics.push(
12836
- invalidShape("Rule assert must include at least one supported assertion.")
12998
+ invalidShape(
12999
+ "Rule assert must include at least one supported assertion."
13000
+ )
12837
13001
  );
12838
13002
  return void 0;
12839
13003
  }
@@ -12876,6 +13040,16 @@ function existsFromValue(value, diagnostics) {
12876
13040
  }
12877
13041
  return { exists: true };
12878
13042
  }
13043
+ function tableRowsCompleteFromValue(value, diagnostics) {
13044
+ if (value === void 0) {
13045
+ return {};
13046
+ }
13047
+ if (value !== true) {
13048
+ diagnostics.push(invalidShape("tableRowsComplete must be true."));
13049
+ return {};
13050
+ }
13051
+ return { tableRowsComplete: true };
13052
+ }
12879
13053
  function sectionsRequiredFromValue(value, diagnostics) {
12880
13054
  if (value === void 0) {
12881
13055
  return {};
@@ -13014,7 +13188,12 @@ function tableColumnCoverageFromValue(value, diagnostics) {
13014
13188
  diagnostics.push(invalidShape("tableColumnCoverage must be an object."));
13015
13189
  return {};
13016
13190
  }
13017
- unsupportedKeys(value, ["source", "target", "require"], diagnostics);
13191
+ unsupportedKeys(
13192
+ value,
13193
+ ["source", "target", "require", "allowEmptySource"],
13194
+ diagnostics
13195
+ );
13196
+ const emptyPolicy = optionalBoolean2(value, "allowEmptySource", diagnostics);
13018
13197
  const source = tableColumnCoverageSourceFromValue(value.source, diagnostics);
13019
13198
  const target = tableColumnCoverageTargetFromValue(value.target, diagnostics);
13020
13199
  const require2 = value.require === "everySourceId" ? "everySourceId" : void 0;
@@ -13025,6 +13204,7 @@ function tableColumnCoverageFromValue(value, diagnostics) {
13025
13204
  }
13026
13205
  return source === void 0 || target === void 0 || require2 === void 0 ? {} : {
13027
13206
  tableColumnCoverage: {
13207
+ ...emptyPolicy,
13028
13208
  source,
13029
13209
  target,
13030
13210
  require: require2
@@ -13041,7 +13221,7 @@ function tableColumnCoverageSourceFromValue(value, diagnostics) {
13041
13221
  }
13042
13222
  unsupportedKeys(
13043
13223
  value,
13044
- ["section", "column", "prefix", "caseSensitive"],
13224
+ ["section", "column", "prefix", "caseSensitive", "rowWhere"],
13045
13225
  diagnostics
13046
13226
  );
13047
13227
  const section = requiredAssertionString(
@@ -13054,7 +13234,9 @@ function tableColumnCoverageSourceFromValue(value, diagnostics) {
13054
13234
  "tableColumnCoverage.source.column",
13055
13235
  diagnostics
13056
13236
  );
13237
+ const rowWhere = value.rowWhere === void 0 ? void 0 : cellPredicateFromValue(value.rowWhere, "rowWhere", diagnostics);
13057
13238
  const source = {
13239
+ ...rowWhere === void 0 ? {} : { rowWhere },
13058
13240
  ...optionalTableColumnCoverageSourceString(value, "prefix", diagnostics),
13059
13241
  ...optionalTableColumnCoverageSourceBoolean(
13060
13242
  value,
@@ -13137,7 +13319,7 @@ function frontmatterShapeFromValue2(value, diagnostics) {
13137
13319
  }
13138
13320
  return result.shape === void 0 ? {} : { frontmatterShape: result.shape };
13139
13321
  }
13140
- function textAssertionFromValue(value, diagnostics) {
13322
+ function textAssertionFromValue(value, syntaxVersion, diagnostics) {
13141
13323
  if (value === void 0) {
13142
13324
  return {};
13143
13325
  }
@@ -13145,16 +13327,22 @@ function textAssertionFromValue(value, diagnostics) {
13145
13327
  diagnostics.push(invalidShape("text must be an object."));
13146
13328
  return {};
13147
13329
  }
13148
- unsupportedKeys(value, ["contains", "excludes"], diagnostics);
13330
+ unsupportedKeys(
13331
+ value,
13332
+ syntaxVersion === PROFILE_SYNTAX_VERSION_V2 ? ["contains", "excludes", "nonBlank"] : ["contains", "excludes"],
13333
+ diagnostics
13334
+ );
13335
+ if (value.nonBlank !== void 0 && value.nonBlank !== true) {
13336
+ diagnostics.push(invalidShape("text.nonBlank must be true when provided."));
13337
+ }
13149
13338
  const text5 = {
13339
+ ...value.nonBlank === true ? { nonBlank: true } : {},
13150
13340
  ...optionalAssertionString(value, "contains", diagnostics),
13151
13341
  ...optionalStringArray2(value, "excludes", diagnostics)
13152
13342
  };
13153
13343
  if (!hasTextPredicate2(text5)) {
13154
13344
  diagnostics.push(
13155
- invalidShape(
13156
- "text must include contains or a non-empty excludes array."
13157
- )
13345
+ invalidShape("text must include contains or a non-empty excludes array.")
13158
13346
  );
13159
13347
  return {};
13160
13348
  }
@@ -13299,7 +13487,7 @@ function requiredAssertionString(value, field, diagnostics) {
13299
13487
  );
13300
13488
  }
13301
13489
  function hasTextPredicate2(text5) {
13302
- return text5?.contains !== void 0 || text5?.excludes !== void 0;
13490
+ return text5?.nonBlank === true || text5?.contains !== void 0 || text5?.excludes !== void 0;
13303
13491
  }
13304
13492
  function isNonNegativeInteger2(value) {
13305
13493
  return Number.isInteger(value) && value >= 0;
@@ -13814,7 +14002,7 @@ function cloneSourceRange2(sourceRange) {
13814
14002
  import { createHash } from "node:crypto";
13815
14003
 
13816
14004
  // src/internal/package-version.ts
13817
- var MARKDOWN_ENGINE_PACKAGE_VERSION = "3.5.0";
14005
+ var MARKDOWN_ENGINE_PACKAGE_VERSION = "3.8.0";
13818
14006
 
13819
14007
  // src/internal/stable-json.ts
13820
14008
  function normalizeStableJsonValue(value) {
@@ -19628,10 +19816,10 @@ function resolveAll(constructs2, events, context) {
19628
19816
  const called = [];
19629
19817
  let index2 = -1;
19630
19818
  while (++index2 < constructs2.length) {
19631
- const resolve2 = constructs2[index2].resolveAll;
19632
- if (resolve2 && !called.includes(resolve2)) {
19633
- events = resolve2(events, context);
19634
- called.push(resolve2);
19819
+ const resolve3 = constructs2[index2].resolveAll;
19820
+ if (resolve3 && !called.includes(resolve3)) {
19821
+ events = resolve3(events, context);
19822
+ called.push(resolve3);
19635
19823
  }
19636
19824
  }
19637
19825
  return events;
@@ -24495,7 +24683,7 @@ function compiler(options) {
24495
24683
  transforms: [],
24496
24684
  canContainEols: ["emphasis", "fragment", "heading", "paragraph", "strong"],
24497
24685
  enter: {
24498
- autolink: opener(link2),
24686
+ autolink: opener(link3),
24499
24687
  autolinkProtocol: onenterdata,
24500
24688
  autolinkEmail: onenterdata,
24501
24689
  atxHeading: opener(heading2),
@@ -24523,7 +24711,7 @@ function compiler(options) {
24523
24711
  htmlTextData: onenterdata,
24524
24712
  image: opener(image2),
24525
24713
  label: buffer,
24526
- link: opener(link2),
24714
+ link: opener(link3),
24527
24715
  listItem: opener(listItem2),
24528
24716
  listItemValue: onenterlistitemvalue,
24529
24717
  listOrdered: opener(list3, onenterlistordered),
@@ -25075,7 +25263,7 @@ function compiler(options) {
25075
25263
  alt: null
25076
25264
  };
25077
25265
  }
25078
- function link2() {
25266
+ function link3() {
25079
25267
  return {
25080
25268
  type: "link",
25081
25269
  title: null,
@@ -25576,9 +25764,9 @@ var VFile = class {
25576
25764
  * @returns {undefined}
25577
25765
  * Nothing.
25578
25766
  */
25579
- set dirname(dirname) {
25767
+ set dirname(dirname2) {
25580
25768
  assertPath(this.basename, "dirname");
25581
- this.path = default2.join(dirname || "", this.basename);
25769
+ this.path = default2.join(dirname2 || "", this.basename);
25582
25770
  }
25583
25771
  /**
25584
25772
  * Get the extname (including dot) (example: `'.js'`).
@@ -26177,7 +26365,7 @@ var Processor = class _Processor extends CallableInstance {
26177
26365
  assertParser("process", this.parser || this.Parser);
26178
26366
  assertCompiler("process", this.compiler || this.Compiler);
26179
26367
  return done ? executor(void 0, done) : new Promise(executor);
26180
- function executor(resolve2, reject) {
26368
+ function executor(resolve3, reject) {
26181
26369
  const realFile = vfile(file);
26182
26370
  const parseTree = (
26183
26371
  /** @type {HeadTree extends undefined ? Node : HeadTree} */
@@ -26208,8 +26396,8 @@ var Processor = class _Processor extends CallableInstance {
26208
26396
  function realDone(error, file2) {
26209
26397
  if (error || !file2) {
26210
26398
  reject(error);
26211
- } else if (resolve2) {
26212
- resolve2(file2);
26399
+ } else if (resolve3) {
26400
+ resolve3(file2);
26213
26401
  } else {
26214
26402
  ok(done, "`done` is defined if `resolve` is not");
26215
26403
  done(void 0, file2);
@@ -26311,7 +26499,7 @@ var Processor = class _Processor extends CallableInstance {
26311
26499
  file = void 0;
26312
26500
  }
26313
26501
  return done ? executor(void 0, done) : new Promise(executor);
26314
- function executor(resolve2, reject) {
26502
+ function executor(resolve3, reject) {
26315
26503
  ok(
26316
26504
  typeof file !== "function",
26317
26505
  "`file` can\u2019t be a `done` anymore, we checked"
@@ -26325,8 +26513,8 @@ var Processor = class _Processor extends CallableInstance {
26325
26513
  );
26326
26514
  if (error) {
26327
26515
  reject(error);
26328
- } else if (resolve2) {
26329
- resolve2(resultingTree);
26516
+ } else if (resolve3) {
26517
+ resolve3(resultingTree);
26330
26518
  } else {
26331
26519
  ok(done, "`done` is defined if `resolve` is not");
26332
26520
  done(void 0, resultingTree, file2);
@@ -26608,7 +26796,7 @@ function toEngineNode(node2, options) {
26608
26796
  const type = typeof node2.type === "string" ? node2.type : "unknown";
26609
26797
  const attributes = nodeAttributes(type, node2, options);
26610
26798
  const sourceRange = toSourceRange(node2.position, options);
26611
- const text5 = nodeText2(type, node2);
26799
+ const text5 = nodeText3(type, node2);
26612
26800
  const children = childNodes(node2).map((child) => toEngineNode(child, options));
26613
26801
  return {
26614
26802
  type,
@@ -26715,7 +26903,7 @@ function assignStringAttribute(attributes, name, value) {
26715
26903
  function isTableAlignment(alignment) {
26716
26904
  return alignment === "left" || alignment === "right" || alignment === "center" || alignment === null;
26717
26905
  }
26718
- function nodeText2(type, node2) {
26906
+ function nodeText3(type, node2) {
26719
26907
  if (typeof node2.value === "string") {
26720
26908
  return node2.value;
26721
26909
  }
@@ -27337,13 +27525,7 @@ function compatibilityModeForDocumentVersion(documentVersion) {
27337
27525
  }
27338
27526
 
27339
27527
  // src/cli/validate-args.ts
27340
- var fileFlag = "--file";
27341
- var fileAssignmentPrefix = `${fileFlag}=`;
27342
- var profileFlag = "--profile";
27343
- var profileAssignmentPrefix = `${profileFlag}=`;
27344
- var formatFlag = "--format";
27345
- var formatAssignmentPrefix = `${formatFlag}=`;
27346
- var validateCliUsage = `Usage: markdown-engine validate --file <markdown-file> --profile <profile-file> [--format json]
27528
+ var validateCliUsage = `Usage: markdown-engine validate --file <markdown-file> --profile <profile-file> [--format json] [--output full|summary] [--report-file <new-file>]
27347
27529
 
27348
27530
  Runs declarative validation for one Markdown file and one validation profile.
27349
27531
 
@@ -27351,117 +27533,76 @@ Options:
27351
27533
  --file <markdown-file> Markdown file to validate.
27352
27534
  --profile <profile-file> Declarative validation profile to apply.
27353
27535
  --format json Output JSON. This is the default and only supported format.
27536
+ --output full|summary Full JSON (default) or a compact validation summary.
27537
+ --report-file <new-file> Save full JSON; required for summary. Never overwrites.
27354
27538
  -h, --help Show this help message.
27355
27539
 
27540
+ Summary includes at most 10 top-level diagnostics, errors first, with explicit
27541
+ omission/truncation counts. The report's parent directory must already exist.
27542
+
27356
27543
  Exit status:
27357
27544
  0 Validation completed with no error diagnostics.
27358
27545
  1 Profile/config/compile or validation diagnostics failed.
27359
- 2 Usage, unsupported format, unknown args, or file read error.
27546
+ 2 Usage, unsupported format, unknown args, or file read/write error.
27360
27547
  `;
27361
27548
  function parseValidateCliArgs(args) {
27362
- const filePaths = [];
27363
- const profilePaths = [];
27364
- let format;
27549
+ const values = {
27550
+ "--file": [],
27551
+ "--profile": [],
27552
+ "--format": [],
27553
+ "--output": [],
27554
+ "--report-file": []
27555
+ };
27365
27556
  for (let index2 = 0; index2 < args.length; index2 += 1) {
27366
27557
  const arg = args[index2];
27367
27558
  if (arg === "-h" || arg === "--help") {
27368
27559
  return { kind: "help", usage: validateCliUsage };
27369
27560
  }
27370
- if (arg === fileFlag) {
27371
- const value = readRequiredFlagValue(arg, args, index2);
27372
- if (value.kind === "error") {
27373
- return validateError(value.message);
27374
- }
27375
- filePaths.push(value.value);
27376
- index2 += 1;
27377
- continue;
27378
- }
27379
- if (arg?.startsWith(fileAssignmentPrefix) === true) {
27380
- filePaths.push(arg.slice(fileAssignmentPrefix.length));
27381
- continue;
27382
- }
27383
- if (arg === profileFlag) {
27384
- const value = readRequiredFlagValue(arg, args, index2);
27385
- if (value.kind === "error") {
27386
- return validateError(value.message);
27387
- }
27388
- profilePaths.push(value.value);
27389
- index2 += 1;
27390
- continue;
27391
- }
27392
- if (arg?.startsWith(profileAssignmentPrefix) === true) {
27393
- profilePaths.push(arg.slice(profileAssignmentPrefix.length));
27394
- continue;
27395
- }
27396
- if (arg === formatFlag) {
27397
- if (format !== void 0) {
27398
- return validateError("Expected at most one --format selector.");
27399
- }
27400
- const value = readRequiredFlagValue(arg, args, index2);
27401
- if (value.kind === "error") {
27402
- return validateError(value.message);
27403
- }
27404
- const parsedFormat = parseDeclarativeOutputFormat(value.value);
27405
- if (parsedFormat.kind === "error") {
27406
- return validateError(parsedFormat.message);
27407
- }
27408
- format = parsedFormat.format;
27409
- index2 += 1;
27410
- continue;
27411
- }
27412
- if (arg?.startsWith(formatAssignmentPrefix) === true) {
27413
- if (format !== void 0) {
27414
- return validateError("Expected at most one --format selector.");
27415
- }
27416
- const parsedFormat = parseDeclarativeOutputFormat(
27417
- arg.slice(formatAssignmentPrefix.length)
27418
- );
27419
- if (parsedFormat.kind === "error") {
27420
- return validateError(parsedFormat.message);
27421
- }
27422
- format = parsedFormat.format;
27423
- continue;
27424
- }
27425
- return validateError(`Unknown argument: ${arg ?? ""}`);
27426
- }
27427
- if (filePaths.length === 0) {
27428
- return validateError("Expected exactly one --file target.");
27429
- }
27430
- if (filePaths.length > 1) {
27431
- return validateError("Expected one Markdown file target, received multiple.");
27432
- }
27433
- if (profilePaths.length === 0) {
27434
- return validateError("Expected exactly one --profile target.");
27435
- }
27436
- if (profilePaths.length > 1) {
27437
- return validateError("Expected one profile file target, received multiple.");
27438
- }
27439
- const [filePath] = filePaths;
27440
- const [profilePath] = profilePaths;
27441
- if (filePath === void 0 || filePath.trim() === "") {
27442
- return validateError("File path cannot be empty.");
27443
- }
27444
- if (profilePath === void 0 || profilePath.trim() === "") {
27445
- return validateError("Profile path cannot be empty.");
27561
+ const equals = arg.indexOf("=");
27562
+ const flag = equals === -1 ? arg : arg.slice(0, equals);
27563
+ if (!Object.hasOwn(values, flag)) return validateError(`Unknown argument: ${arg}`);
27564
+ const entries = values[flag];
27565
+ if (flag !== "--file" && flag !== "--profile" && entries.length > 0) {
27566
+ return validateError(`Expected at most one ${flag} selector.`);
27567
+ }
27568
+ const value = equals === -1 ? args[index2 + 1] : arg.slice(equals + 1);
27569
+ if (value === void 0 || equals === -1 && value.startsWith("-")) {
27570
+ return validateError(`Missing value for ${flag}.`);
27571
+ }
27572
+ if (flag === "--format" && value !== "json") {
27573
+ return validateError(`Unsupported validation output format: ${value}.`);
27574
+ }
27575
+ if (flag === "--output" && value !== "full" && value !== "summary") {
27576
+ return validateError(`Unsupported validation output mode: ${value}.`);
27577
+ }
27578
+ entries.push(value);
27579
+ if (equals === -1) index2 += 1;
27580
+ }
27581
+ const files = values["--file"];
27582
+ const profiles = values["--profile"];
27583
+ if (files.length === 0) return validateError("Expected exactly one --file target.");
27584
+ if (files.length > 1) return validateError("Expected one Markdown file target, received multiple.");
27585
+ if (profiles.length === 0) return validateError("Expected exactly one --profile target.");
27586
+ if (profiles.length > 1) return validateError("Expected one profile file target, received multiple.");
27587
+ const filePath = files[0];
27588
+ const profilePath = profiles[0];
27589
+ if (filePath.trim() === "") return validateError("File path cannot be empty.");
27590
+ if (profilePath.trim() === "") return validateError("Profile path cannot be empty.");
27591
+ const output = values["--output"][0] ?? "full";
27592
+ const reportFile = values["--report-file"][0];
27593
+ if (reportFile !== void 0 && reportFile.trim() === "") {
27594
+ return validateError("Report file path cannot be empty.");
27595
+ }
27596
+ if (output === "summary" && reportFile === void 0) {
27597
+ return validateError("--output summary requires --report-file to retain the full result.");
27446
27598
  }
27447
27599
  return {
27448
27600
  kind: "validate",
27449
27601
  filePath,
27450
- format: format ?? "json",
27451
- profilePath
27452
- };
27453
- }
27454
- function readRequiredFlagValue(arg, args, index2) {
27455
- const value = args[index2 + 1];
27456
- if (value === void 0 || value.startsWith("-")) {
27457
- return { kind: "error", message: `Missing value for ${arg}.` };
27458
- }
27459
- return { kind: "ok", value };
27460
- }
27461
- function parseDeclarativeOutputFormat(value) {
27462
- return value === "json" ? { kind: "ok", format: value } : {
27463
- kind: "error",
27464
- message: `Unsupported validation output format: ${value}.`
27602
+ profilePath,
27603
+ format: "json",
27604
+ output,
27605
+ ...reportFile !== void 0 ? { reportFile } : {}
27465
27606
  };
27466
27607
  }
27467
27608
  function validateError(message) {
@@ -27473,8 +27614,8 @@ var documentVersionFlag = "--document-version";
27473
27614
  var documentVersionAssignmentPrefix = `${documentVersionFlag}=`;
27474
27615
  var duplicateDocumentVersionMessage = "Expected at most one --document-version selector.";
27475
27616
  var validateCommand = "validate";
27476
- var fileFlag2 = "--file";
27477
- var fileAssignmentPrefix2 = `${fileFlag2}=`;
27617
+ var fileFlag = "--file";
27618
+ var fileAssignmentPrefix = `${fileFlag}=`;
27478
27619
  var pathFlag = "--path";
27479
27620
  var pathAssignmentPrefix = `${pathFlag}=`;
27480
27621
  var normalizeCliUsage = `Usage: markdown-engine [--document-version <version>] (--file <markdown-file> | --path <markdown-file>)
@@ -27504,7 +27645,7 @@ function parseNormalizeCliArgs(args) {
27504
27645
  if (arg === "-h" || arg === "--help") {
27505
27646
  return { kind: "help", usage: cliUsage };
27506
27647
  }
27507
- if (arg === fileFlag2 || arg === pathFlag) {
27648
+ if (arg === fileFlag || arg === pathFlag) {
27508
27649
  const value = args[index2 + 1];
27509
27650
  if (value === void 0 || value.startsWith("-")) {
27510
27651
  return normalizeError(`Missing value for ${arg}.`);
@@ -27531,8 +27672,8 @@ function parseNormalizeCliArgs(args) {
27531
27672
  }
27532
27673
  continue;
27533
27674
  }
27534
- if (arg?.startsWith(fileAssignmentPrefix2) === true) {
27535
- targets.push(arg.slice(fileAssignmentPrefix2.length));
27675
+ if (arg?.startsWith(fileAssignmentPrefix) === true) {
27676
+ targets.push(arg.slice(fileAssignmentPrefix.length));
27536
27677
  continue;
27537
27678
  }
27538
27679
  if (arg?.startsWith(pathAssignmentPrefix) === true) {
@@ -27619,6 +27760,99 @@ function normalizeMarkdown(input) {
27619
27760
  });
27620
27761
  }
27621
27762
 
27763
+ // src/cli/validation-report.ts
27764
+ import { createHash as createHash2 } from "node:crypto";
27765
+ import { link as link2, mkdtemp, rm, writeFile } from "node:fs/promises";
27766
+ import { dirname, join, resolve as resolve2 } from "node:path";
27767
+ async function saveValidationReport(cwd, reportFile, content3) {
27768
+ const path2 = resolve2(cwd, reportFile);
27769
+ const temporaryDirectory = await mkdtemp(join(dirname(path2), ".markdown-engine-report-"));
27770
+ try {
27771
+ const temporaryFile = join(temporaryDirectory, "report.json");
27772
+ await writeFile(temporaryFile, content3, { encoding: "utf8", mode: 384, flag: "wx" });
27773
+ await link2(temporaryFile, path2);
27774
+ } finally {
27775
+ await rm(temporaryDirectory, { recursive: true, force: true });
27776
+ }
27777
+ return {
27778
+ path: path2,
27779
+ sha256: createHash2("sha256").update(content3, "utf8").digest("hex"),
27780
+ bytes: Buffer.byteLength(content3, "utf8")
27781
+ };
27782
+ }
27783
+
27784
+ // src/cli/validation-summary.ts
27785
+ var diagnosticLimit = 10;
27786
+ var severityOrder = { error: 0, warning: 1, info: 2 };
27787
+ function createValidationSummary(result, exitCode, report) {
27788
+ const diagnostics = [...result.diagnostics].sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]).slice(0, diagnosticLimit).map(compactDiagnostic);
27789
+ const counts = { total: result.diagnostics.length, error: 0, warning: 0, info: 0 };
27790
+ for (const diagnostic of result.diagnostics) counts[diagnostic.severity] += 1;
27791
+ const evidence = result.evidence;
27792
+ return {
27793
+ schemaVersion: "markdown-engine.validation-summary.v1",
27794
+ valid: result.valid,
27795
+ stage: "stage" in result ? result.stage : "validation",
27796
+ exitCode,
27797
+ engineVersion: MARKDOWN_ENGINE_PACKAGE_VERSION,
27798
+ runtimeVersion: process.version,
27799
+ ...result.profile !== void 0 ? { profile: result.profile } : {},
27800
+ ...evidence !== void 0 ? {
27801
+ evidence: {
27802
+ inputHash: evidence.inputHash,
27803
+ profileHash: evidence.profileHash,
27804
+ engineVersion: evidence.engineVersion,
27805
+ runtimeVersion: evidence.runtimeVersion,
27806
+ ...evidence.sourceLength !== void 0 ? { sourceLength: evidence.sourceLength } : {}
27807
+ }
27808
+ } : {},
27809
+ diagnosticCounts: counts,
27810
+ diagnostics,
27811
+ diagnosticsOmitted: counts.total - diagnostics.length,
27812
+ diagnosticsTruncated: diagnostics.filter((d) => d.truncatedFields.length > 0).length,
27813
+ report
27814
+ };
27815
+ }
27816
+ function compactDiagnostic(diagnostic) {
27817
+ const truncatedFields = [];
27818
+ const bound = (field, value, max) => {
27819
+ if (value.length <= max) return value;
27820
+ truncatedFields.push(field);
27821
+ return `${value.slice(0, max - 1)}\u2026`;
27822
+ };
27823
+ return {
27824
+ code: bound("code", diagnostic.code, 128),
27825
+ message: bound("message", diagnostic.message, 512),
27826
+ severity: diagnostic.severity,
27827
+ ...diagnostic.ruleId !== void 0 ? { ruleId: bound("ruleId", diagnostic.ruleId, 128) } : {},
27828
+ ...diagnostic.sourceRange !== void 0 ? { sourceRange: diagnostic.sourceRange } : {},
27829
+ truncatedFields
27830
+ };
27831
+ }
27832
+
27833
+ // src/cli/validation-output.ts
27834
+ async function outputValidationResult(result, exitCode, options) {
27835
+ const output = JSON.stringify(normalizeStableJsonValue(result), null, 2) ?? "null";
27836
+ try {
27837
+ const report = options.reportFile === void 0 ? void 0 : await saveValidationReport(options.cwd, options.reportFile, `${output}
27838
+ `);
27839
+ if (options.output === "summary") {
27840
+ if (report === void 0) throw new Error("--output summary requires --report-file.");
27841
+ return {
27842
+ kind: "output",
27843
+ exitCode,
27844
+ output: JSON.stringify(normalizeStableJsonValue(createValidationSummary(result, exitCode, report)))
27845
+ };
27846
+ }
27847
+ return { kind: "output", exitCode, output };
27848
+ } catch (error) {
27849
+ return {
27850
+ kind: "fileError",
27851
+ message: `Unable to save validation report "${options.reportFile ?? ""}": ${error instanceof Error ? error.message : String(error)}`
27852
+ };
27853
+ }
27854
+ }
27855
+
27622
27856
  // src/cli/declarative-validation.ts
27623
27857
  async function runDeclarativeValidationCli(input) {
27624
27858
  const profileText = await readValidationFile(input.profilePath, input.cwd);
@@ -27629,13 +27863,14 @@ async function runDeclarativeValidationCli(input) {
27629
27863
  path: input.profilePath
27630
27864
  });
27631
27865
  if (profileResult.profile === void 0 || hasErrorDiagnostic(profileResult.diagnostics)) {
27632
- return outputResult(profileStageResult(profileResult.diagnostics), 1);
27866
+ return outputValidationResult(profileStageResult(profileResult.diagnostics), 1, input);
27633
27867
  }
27634
27868
  const compileDiagnostics = compileProfileForCli(profileResult.profile);
27635
27869
  if (hasErrorDiagnostic(compileDiagnostics)) {
27636
- return outputResult(
27870
+ return outputValidationResult(
27637
27871
  profileStageResult([...profileResult.diagnostics, ...compileDiagnostics]),
27638
- 1
27872
+ 1,
27873
+ input
27639
27874
  );
27640
27875
  }
27641
27876
  const markdown = await readValidationFile(input.filePath, input.cwd);
@@ -27652,7 +27887,7 @@ async function runDeclarativeValidationCli(input) {
27652
27887
  ...normalizeResult.diagnostics
27653
27888
  ];
27654
27889
  if (hasErrorDiagnostic(normalizeResult.diagnostics)) {
27655
- return outputResult(
27890
+ return outputValidationResult(
27656
27891
  documentDiagnosticsResult(
27657
27892
  normalizeResult.document,
27658
27893
  profileResult.profile,
@@ -27665,7 +27900,8 @@ async function runDeclarativeValidationCli(input) {
27665
27900
  )
27666
27901
  ]
27667
27902
  ),
27668
- 1
27903
+ 1,
27904
+ input
27669
27905
  );
27670
27906
  }
27671
27907
  const validationResult = validateWithProfile(
@@ -27677,9 +27913,10 @@ async function runDeclarativeValidationCli(input) {
27677
27913
  validationResult,
27678
27914
  documentDiagnostics
27679
27915
  );
27680
- return outputResult(
27916
+ return outputValidationResult(
27681
27917
  result,
27682
- hasErrorDiagnostic(result.diagnostics) ? 1 : 0
27918
+ hasErrorDiagnostic(result.diagnostics) ? 1 : 0,
27919
+ input
27683
27920
  );
27684
27921
  }
27685
27922
  function compileProfileForCli(profile) {
@@ -27732,16 +27969,6 @@ function mergeDocumentDiagnostics(validationResult, documentDiagnostics) {
27732
27969
  } : {}
27733
27970
  };
27734
27971
  }
27735
- function outputResult(result, exitCode) {
27736
- return {
27737
- kind: "output",
27738
- exitCode,
27739
- output: serializeDeclarativeValidationCliJsonResult(result)
27740
- };
27741
- }
27742
- function serializeDeclarativeValidationCliJsonResult(result) {
27743
- return JSON.stringify(normalizeStableJsonValue(result), null, 2) ?? "null";
27744
- }
27745
27972
  async function readValidationFile(path2, cwd) {
27746
27973
  try {
27747
27974
  return {
@@ -27774,7 +28001,9 @@ ${argsResult.usage}`);
27774
28001
  cwd: input.cwd,
27775
28002
  filePath: argsResult.filePath,
27776
28003
  format: argsResult.format,
27777
- profilePath: argsResult.profilePath
28004
+ profilePath: argsResult.profilePath,
28005
+ output: argsResult.output,
28006
+ ...argsResult.reportFile !== void 0 ? { reportFile: argsResult.reportFile } : {}
27778
28007
  });
27779
28008
  if (validationResult.kind === "fileError") {
27780
28009
  input.stderr.write(`${validationResult.message}