@fuaran-ui/conformance 0.1.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 (301) hide show
  1. package/LICENSE +215 -0
  2. package/README.md +85 -0
  3. package/corpus/elicitation/elc-ans-accept-full.json +1 -0
  4. package/corpus/elicitation/elc-ans-accept-int-for-float.json +1 -0
  5. package/corpus/elicitation/elc-ans-accept-minimal.json +1 -0
  6. package/corpus/elicitation/elc-ans-reject-enum.json +1 -0
  7. package/corpus/elicitation/elc-ans-reject-float-range.json +1 -0
  8. package/corpus/elicitation/elc-ans-reject-int-range.json +1 -0
  9. package/corpus/elicitation/elc-ans-reject-missing-required.json +1 -0
  10. package/corpus/elicitation/elc-ans-reject-string-len.json +1 -0
  11. package/corpus/elicitation/elc-ans-reject-type-mismatch.json +1 -0
  12. package/corpus/elicitation/elc-ans-reject-undeclared.json +1 -0
  13. package/corpus/elicitation/elc-full.json +1 -0
  14. package/corpus/elicitation/elc-minimal.json +1 -0
  15. package/corpus/elicitation/elc-out-answered.json +1 -0
  16. package/corpus/elicitation/elc-out-declined.json +1 -0
  17. package/corpus/elicitation/elc-out-reject-empty-id.json +1 -0
  18. package/corpus/elicitation/elc-out-reject-missing-answer.json +1 -0
  19. package/corpus/elicitation/elc-out-reject-stray-key.json +1 -0
  20. package/corpus/elicitation/elc-out-reject-unknown-type.json +1 -0
  21. package/corpus/elicitation/elc-out-superseded-anon.json +1 -0
  22. package/corpus/elicitation/elc-out-superseded-by.json +1 -0
  23. package/corpus/elicitation/elc-out-timedout.json +1 -0
  24. package/corpus/elicitation/elc-reject-bad-space.json +1 -0
  25. package/corpus/elicitation/elc-reject-bad-timeout.json +1 -0
  26. package/corpus/elicitation/elc-reject-bad-version.json +1 -0
  27. package/corpus/elicitation/elc-reject-default-nonconformant.json +1 -0
  28. package/corpus/elicitation/elc-reject-duplicate-field.json +1 -0
  29. package/corpus/elicitation/elc-reject-empty-contract.json +1 -0
  30. package/corpus/elicitation/elc-reject-empty-id.json +1 -0
  31. package/corpus/elicitation/elc-reject-missing-version.json +1 -0
  32. package/corpus/elicitation/elc-reject-stray-key.json +1 -0
  33. package/corpus/elicitation/elc-reject-tree-decode.json +1 -0
  34. package/corpus/elicitation/elc-reject-unknown-node.json +1 -0
  35. package/corpus/envelope/envelope-foreign-major.json +1 -0
  36. package/corpus/envelope/envelope-foreign-namespace.json +1 -0
  37. package/corpus/envelope/envelope-known-current.json +1 -0
  38. package/corpus/envelope/envelope-unknown-behind.json +1 -0
  39. package/corpus/envelope/envelope-unknown-malformed-required-profile.json +1 -0
  40. package/corpus/envelope/envelope-unknown-required-profile.json +1 -0
  41. package/corpus/lenient/legacy-upgrade-card.expected.json +1 -0
  42. package/corpus/lenient/legacy-upgrade-card.json +1 -0
  43. package/corpus/lenient/legacy-upgrade-dashboard.expected.json +1 -0
  44. package/corpus/lenient/legacy-upgrade-dashboard.json +1 -0
  45. package/corpus/lenient/legacy-upgrade-gridlayout.expected.json +1 -0
  46. package/corpus/lenient/legacy-upgrade-gridlayout.json +1 -0
  47. package/corpus/lenient/legacy-upgrade-stack.expected.json +1 -0
  48. package/corpus/lenient/legacy-upgrade-stack.json +1 -0
  49. package/corpus/lenient/legacy-upgrade-table.expected.json +1 -0
  50. package/corpus/lenient/legacy-upgrade-table.json +1 -0
  51. package/corpus/lenient/lenient-022-lvr-emphasis-loud.expected.json +1 -0
  52. package/corpus/lenient/lenient-022-lvr-emphasis-loud.json +1 -0
  53. package/corpus/lenient/lenient-022-lvr-emphasis-normal.expected.json +1 -0
  54. package/corpus/lenient/lenient-022-lvr-emphasis-normal.json +1 -0
  55. package/corpus/lenient/lenient-460-alias-emphasis-muted.expected.json +1 -0
  56. package/corpus/lenient/lenient-460-alias-emphasis-muted.json +1 -0
  57. package/corpus/lenient/lenient-460-alias-emphasis-strong.expected.json +1 -0
  58. package/corpus/lenient/lenient-460-alias-emphasis-strong.json +1 -0
  59. package/corpus/lenient/lenient-460-alias-tone-danger.expected.json +1 -0
  60. package/corpus/lenient/lenient-460-alias-tone-danger.json +1 -0
  61. package/corpus/lenient/lenient-460-alias-tone-positive.expected.json +1 -0
  62. package/corpus/lenient/lenient-460-alias-tone-positive.json +1 -0
  63. package/corpus/lenient/lenient-460-explicit-default-column.expected.json +1 -0
  64. package/corpus/lenient/lenient-460-explicit-default-column.json +1 -0
  65. package/corpus/lenient/lenient-460-explicit-default-metric.expected.json +1 -0
  66. package/corpus/lenient/lenient-460-explicit-default-metric.json +1 -0
  67. package/corpus/lenient/lenient-460-explicit-default-style.expected.json +1 -0
  68. package/corpus/lenient/lenient-460-explicit-default-style.json +1 -0
  69. package/corpus/lenient/lenient-596-form-explicit-auto-state.expected.json +1 -0
  70. package/corpus/lenient/lenient-596-form-explicit-auto-state.json +1 -0
  71. package/corpus/lenient/lenient-alias-call-url.expected.json +1 -0
  72. package/corpus/lenient/lenient-alias-call-url.json +1 -0
  73. package/corpus/lenient/lenient-alias-card-title-metric-value.expected.json +1 -0
  74. package/corpus/lenient/lenient-alias-card-title-metric-value.json +1 -0
  75. package/corpus/lenient/lenient-alias-datagrid-data-column-type.expected.json +1 -0
  76. package/corpus/lenient/lenient-alias-datagrid-data-column-type.json +1 -0
  77. package/corpus/lenient/lenient-alias-form-field-name.expected.json +1 -0
  78. package/corpus/lenient/lenient-alias-form-field-name.json +1 -0
  79. package/corpus/lenient/lenient-alias-grid-columns-row.expected.json +1 -0
  80. package/corpus/lenient/lenient-alias-grid-columns-row.json +1 -0
  81. package/corpus/lenient/lenient-alias-navigate-href.expected.json +1 -0
  82. package/corpus/lenient/lenient-alias-navigate-href.json +1 -0
  83. package/corpus/lenient/lenient-alias-select-options-query-deps.expected.json +1 -0
  84. package/corpus/lenient/lenient-alias-select-options-query-deps.json +1 -0
  85. package/corpus/lenient/lenient-bare-text-button-label.expected.json +1 -0
  86. package/corpus/lenient/lenient-bare-text-button-label.json +1 -0
  87. package/corpus/lenient/lenient-bare-text-callout.expected.json +1 -0
  88. package/corpus/lenient/lenient-bare-text-callout.json +1 -0
  89. package/corpus/lenient/lenient-bare-text-heading.expected.json +1 -0
  90. package/corpus/lenient/lenient-bare-text-heading.json +1 -0
  91. package/corpus/lenient/lenient-bare-text-markdown.expected.json +1 -0
  92. package/corpus/lenient/lenient-bare-text-markdown.json +1 -0
  93. package/corpus/lenient/lenient-binding-bound-wrapper.expected.json +1 -0
  94. package/corpus/lenient/lenient-binding-bound-wrapper.json +1 -0
  95. package/corpus/lenient/lenient-emphasis-cross-vocab.expected.json +1 -0
  96. package/corpus/lenient/lenient-emphasis-cross-vocab.json +1 -0
  97. package/corpus/lenient/lenient-fact-explicit-defaults.expected.json +1 -0
  98. package/corpus/lenient/lenient-fact-explicit-defaults.json +1 -0
  99. package/corpus/lenient/lenient-null-static-options.expected.json +1 -0
  100. package/corpus/lenient/lenient-null-static-options.json +1 -0
  101. package/corpus/lenient/lenient-opaque-static-markers.expected.json +1 -0
  102. package/corpus/lenient/lenient-opaque-static-markers.json +1 -0
  103. package/corpus/lenient/lenient-opaque-static-options.expected.json +1 -0
  104. package/corpus/lenient/lenient-opaque-static-options.json +1 -0
  105. package/corpus/lenient/lenient-opaque-static-series.expected.json +1 -0
  106. package/corpus/lenient/lenient-opaque-static-series.json +1 -0
  107. package/corpus/lenient/lenient-opaque-static-values.expected.json +1 -0
  108. package/corpus/lenient/lenient-opaque-static-values.json +1 -0
  109. package/corpus/lenient/lenient-shape-binding-scalar-fraction.expected.json +1 -0
  110. package/corpus/lenient/lenient-shape-binding-scalar-fraction.json +1 -0
  111. package/corpus/lenient/lenient-shape-grid-no-cols.expected.json +1 -0
  112. package/corpus/lenient/lenient-shape-grid-no-cols.json +1 -0
  113. package/corpus/lenient/lenient-shape-grid-template-no-cols.expected.json +1 -0
  114. package/corpus/lenient/lenient-shape-grid-template-no-cols.json +1 -0
  115. package/corpus/lenient/lenient-shape-options-bare-strings.expected.json +1 -0
  116. package/corpus/lenient/lenient-shape-options-bare-strings.json +1 -0
  117. package/corpus/lenient/lenient-shape-params-map.expected.json +1 -0
  118. package/corpus/lenient/lenient-shape-params-map.json +1 -0
  119. package/corpus/lenient/lenient-shape-segmented-orientation-omitted.expected.json +1 -0
  120. package/corpus/lenient/lenient-shape-segmented-orientation-omitted.json +1 -0
  121. package/corpus/lenient/lenient-shape-static-envelope-plain-scalars.expected.json +1 -0
  122. package/corpus/lenient/lenient-shape-static-envelope-plain-scalars.json +1 -0
  123. package/corpus/lenient/lenient-transform-bare-columns.expected.json +1 -0
  124. package/corpus/lenient/lenient-transform-bare-columns.json +1 -0
  125. package/corpus/lenient/lenient-transform-epoch-timestamps.expected.json +1 -0
  126. package/corpus/lenient/lenient-transform-epoch-timestamps.json +1 -0
  127. package/corpus/lenient/lenient-transform-expr-spellings.expected.json +1 -0
  128. package/corpus/lenient/lenient-transform-expr-spellings.json +1 -0
  129. package/corpus/lenient/lenient-transform-flat-contains.expected.json +1 -0
  130. package/corpus/lenient/lenient-transform-flat-contains.json +1 -0
  131. package/corpus/lenient/lenient-transform-flat-filter.expected.json +1 -0
  132. package/corpus/lenient/lenient-transform-flat-filter.json +1 -0
  133. package/corpus/lenient/lenient-transform-flat-or.expected.json +1 -0
  134. package/corpus/lenient/lenient-transform-flat-or.json +1 -0
  135. package/corpus/lenient/lenient-transform-flat-scalar-fn.expected.json +1 -0
  136. package/corpus/lenient/lenient-transform-flat-scalar-fn.json +1 -0
  137. package/corpus/lenient/lenient-transform-schemaless.expected.json +1 -0
  138. package/corpus/lenient/lenient-transform-schemaless.json +1 -0
  139. package/corpus/lenient/lenient-transform-step-aliases.expected.json +1 -0
  140. package/corpus/lenient/lenient-transform-step-aliases.json +1 -0
  141. package/corpus/lenient/lenient-transform-values-only-columns.expected.json +1 -0
  142. package/corpus/lenient/lenient-transform-values-only-columns.json +1 -0
  143. package/corpus/lenient/lenient-window-cumsum-legacy.expected.json +1 -0
  144. package/corpus/lenient/lenient-window-cumsum-legacy.json +1 -0
  145. package/corpus/manifest.json +2000 -0
  146. package/corpus/nodes/badge-1.json +1 -0
  147. package/corpus/nodes/boundary-1.json +1 -0
  148. package/corpus/nodes/btn-1.json +1 -0
  149. package/corpus/nodes/btn-copy-link.json +1 -0
  150. package/corpus/nodes/btn-invoke.json +1 -0
  151. package/corpus/nodes/btn-read-workbook.json +1 -0
  152. package/corpus/nodes/call-into.json +1 -0
  153. package/corpus/nodes/callout-1.json +1 -0
  154. package/corpus/nodes/card-1.json +1 -0
  155. package/corpus/nodes/chart-1.json +1 -0
  156. package/corpus/nodes/code-1.json +1 -0
  157. package/corpus/nodes/composite-root.json +1 -0
  158. package/corpus/nodes/controls-closure.json +1 -0
  159. package/corpus/nodes/controls-declarative.json +1 -0
  160. package/corpus/nodes/custom-1.json +1 -0
  161. package/corpus/nodes/custom-bounded-1.json +1 -0
  162. package/corpus/nodes/custom-bounded-advisory.json +1 -0
  163. package/corpus/nodes/dash-empty.json +1 -0
  164. package/corpus/nodes/discl-1.json +1 -0
  165. package/corpus/nodes/drawing-1.json +1 -0
  166. package/corpus/nodes/drawing-empty.json +1 -0
  167. package/corpus/nodes/fact-1.json +1 -0
  168. package/corpus/nodes/filterable-static-dashboard.json +1 -0
  169. package/corpus/nodes/filters-1.json +1 -0
  170. package/corpus/nodes/filters-declarative.json +1 -0
  171. package/corpus/nodes/filters-segmented.json +1 -0
  172. package/corpus/nodes/form-1.json +1 -0
  173. package/corpus/nodes/form-date.json +1 -0
  174. package/corpus/nodes/form-declarative-minimal.json +1 -0
  175. package/corpus/nodes/form-declarative.json +1 -0
  176. package/corpus/nodes/form-local-1.json +1 -0
  177. package/corpus/nodes/form-local-debounce.json +1 -0
  178. package/corpus/nodes/form-ranged.json +1 -0
  179. package/corpus/nodes/form-segmented.json +1 -0
  180. package/corpus/nodes/format-bindings.json +1 -0
  181. package/corpus/nodes/frag-decl-1.json +1 -0
  182. package/corpus/nodes/frag-decl-param.json +1 -0
  183. package/corpus/nodes/frag-ref-1.json +1 -0
  184. package/corpus/nodes/frag-ref-args.json +1 -0
  185. package/corpus/nodes/glayout-1.json +1 -0
  186. package/corpus/nodes/glayout-tpl-autofit.json +1 -0
  187. package/corpus/nodes/glayout-tpl-fixed.json +1 -0
  188. package/corpus/nodes/glayout-tpl-ratio.json +1 -0
  189. package/corpus/nodes/grid-1.json +1 -0
  190. package/corpus/nodes/grid-field-named.json +1 -0
  191. package/corpus/nodes/grid-transform-param.json +1 -0
  192. package/corpus/nodes/grid-transform.json +1 -0
  193. package/corpus/nodes/heading-1.json +1 -0
  194. package/corpus/nodes/image-1.json +1 -0
  195. package/corpus/nodes/link-1.json +1 -0
  196. package/corpus/nodes/list-1.json +1 -0
  197. package/corpus/nodes/lvr-1.json +1 -0
  198. package/corpus/nodes/map-1.json +1 -0
  199. package/corpus/nodes/markdown-1.json +1 -0
  200. package/corpus/nodes/master-detail-preselected.json +1 -0
  201. package/corpus/nodes/math-1.json +1 -0
  202. package/corpus/nodes/metric-1.json +1 -0
  203. package/corpus/nodes/metric-float-17sig.json +1 -0
  204. package/corpus/nodes/metric-float-bigint.json +1 -0
  205. package/corpus/nodes/metric-float-exp-neg.json +1 -0
  206. package/corpus/nodes/metric-float-exp-pos.json +1 -0
  207. package/corpus/nodes/metric-invoke.json +1 -0
  208. package/corpus/nodes/modal-1.json +1 -0
  209. package/corpus/nodes/mount-1.json +1 -0
  210. package/corpus/nodes/mount-2.json +1 -0
  211. package/corpus/nodes/multiselect-1.json +1 -0
  212. package/corpus/nodes/progress-1.json +1 -0
  213. package/corpus/nodes/query-dependson.json +1 -0
  214. package/corpus/nodes/scalar-transform-composition.json +1 -0
  215. package/corpus/nodes/scroll-1.json +1 -0
  216. package/corpus/nodes/select-1.json +1 -0
  217. package/corpus/nodes/skel-1.json +1 -0
  218. package/corpus/nodes/spark-1.json +1 -0
  219. package/corpus/nodes/split-1.json +1 -0
  220. package/corpus/nodes/stack-1.json +1 -0
  221. package/corpus/nodes/step-1.json +1 -0
  222. package/corpus/nodes/style-role-voice-1.json +1 -0
  223. package/corpus/nodes/summary-1.json +1 -0
  224. package/corpus/nodes/switch-1.json +1 -0
  225. package/corpus/nodes/table-1.json +1 -0
  226. package/corpus/nodes/tabs-1.json +1 -0
  227. package/corpus/nodes/tabs-explicit-1.json +1 -0
  228. package/corpus/nodes/toast-1.json +1 -0
  229. package/corpus/nodes/upload-1.json +1 -0
  230. package/corpus/ops/op-batch.json +1 -0
  231. package/corpus/ops/op-editnode.json +1 -0
  232. package/corpus/ops/op-insertchild.json +1 -0
  233. package/corpus/ops/op-movenode.json +1 -0
  234. package/corpus/ops/op-removenode.json +1 -0
  235. package/corpus/ops/op-reorderchildren.json +1 -0
  236. package/corpus/ops/op-replacebinding.json +1 -0
  237. package/corpus/ops/op-replaceroot.json +1 -0
  238. package/corpus/ops/op-updateprop-nested-badfield.json +1 -0
  239. package/corpus/ops/op-updateprop-nested-badindex.json +1 -0
  240. package/corpus/ops/op-updateprop-nested-column0-label.json +1 -0
  241. package/corpus/ops/op-updateprop-nested-column1-label.json +1 -0
  242. package/corpus/ops/op-updateprop-nested-field0-required.json +1 -0
  243. package/corpus/ops/op-updateprop-nested-field1-required.json +1 -0
  244. package/corpus/ops/op-updateprop-nested-malformed.json +1 -0
  245. package/corpus/ops/op-updateprop-nested-object-value.json +1 -0
  246. package/corpus/ops/op-updateprop-nested-yfield0.json +1 -0
  247. package/corpus/ops/op-updateprop-nested-yfield1.json +1 -0
  248. package/corpus/ops/op-updateprop.json +1 -0
  249. package/corpus/ops/op-updatestate.json +1 -0
  250. package/corpus/ops/op-updatestyle (phase 147 role=eyebrow + voice=structural).json +1 -0
  251. package/corpus/ops/op-updatestyle.json +1 -0
  252. package/corpus/reject/reject-emptynodeid.json +1 -0
  253. package/corpus/reject/reject-invalid-empty.json +0 -0
  254. package/corpus/reject/reject-invalid-garbage.json +1 -0
  255. package/corpus/reject/reject-invalid-truncated.json +1 -0
  256. package/corpus/reject/reject-missing-binding-type.json +1 -0
  257. package/corpus/reject/reject-missing-custom-moduleid.json +1 -0
  258. package/corpus/reject/reject-missing-markdown-text.json +1 -0
  259. package/corpus/reject/reject-missing-metric-value.json +1 -0
  260. package/corpus/reject/reject-missing-mount-scopeid.json +1 -0
  261. package/corpus/reject/reject-missing-node-id.json +1 -0
  262. package/corpus/reject/reject-missing-node-kind.json +1 -0
  263. package/corpus/reject/reject-missing-switch-cases.json +1 -0
  264. package/corpus/reject/reject-missing-switch-default.json +1 -0
  265. package/corpus/reject/reject-missing-switch-statekey.json +1 -0
  266. package/corpus/reject/reject-null-action-aitool-args.json +1 -0
  267. package/corpus/reject/reject-null-action-notify-payload.json +1 -0
  268. package/corpus/reject/reject-null-action-setstate-value.json +1 -0
  269. package/corpus/reject/reject-null-custom-prop.json +1 -0
  270. package/corpus/reject/reject-null-i18n-arg.json +1 -0
  271. package/corpus/reject/reject-null-updateprop-value.json +1 -0
  272. package/corpus/reject/reject-op-insertchild-empty-childid.json +1 -0
  273. package/corpus/reject/reject-op-insertchild-missing-parentid.json +1 -0
  274. package/corpus/reject/reject-op-missing-type.json +1 -0
  275. package/corpus/reject/reject-op-reorderchildren-wrongtype.json +1 -0
  276. package/corpus/reject/reject-op-unknown-yeet.json +1 -0
  277. package/corpus/reject/reject-op-updateprop-missing-path.json +1 -0
  278. package/corpus/reject/reject-unknown-binding.json +1 -0
  279. package/corpus/reject/reject-unknown-drawing-curve-command.json +1 -0
  280. package/corpus/reject/reject-unknown-drawing-shape.json +1 -0
  281. package/corpus/reject/reject-unknown-mount-direction.json +1 -0
  282. package/corpus/reject/reject-unknown-textsource.json +1 -0
  283. package/corpus/reject/reject-unknown-tone.json +1 -0
  284. package/corpus/reject/reject-wrongnodekind-sparkler.json +1 -0
  285. package/corpus/reject/reject-wrongnodekind-widget.json +1 -0
  286. package/corpus/reject/reject-wrongtype-dashboard-children.json +1 -0
  287. package/corpus/reject/reject-wrongtype-discriminator.json +1 -0
  288. package/corpus/reject/reject-wrongtype-heading-level.json +1 -0
  289. package/corpus/reject/reject-wrongtype-id.json +1 -0
  290. package/corpus/reject/reject-wrongtype-kind.json +1 -0
  291. package/corpus/reject/reject-wrongtype-style-tone.json +1 -0
  292. package/corpus/schema.json +4396 -0
  293. package/dist/cli.js +612 -0
  294. package/dist/cli.js.map +1 -0
  295. package/dist/index.cjs +579 -0
  296. package/dist/index.cjs.map +1 -0
  297. package/dist/index.d.cts +159 -0
  298. package/dist/index.d.ts +159 -0
  299. package/dist/index.js +570 -0
  300. package/dist/index.js.map +1 -0
  301. package/package.json +59 -0
package/dist/cli.js ADDED
@@ -0,0 +1,612 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync, readFileSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+ import { pathToFileURL, fileURLToPath } from 'url';
5
+ import { Ajv2020 } from 'ajv/dist/2020.js';
6
+ import { createHash } from 'crypto';
7
+
8
+ // src/report.ts
9
+ var computeVerdict = (legs) => {
10
+ if (legs.some((l) => l.status === "fail")) return "non-conformant";
11
+ if (legs.some((l) => l.mandatory && l.status === "skipped")) return "partially-conformant";
12
+ return "conformant";
13
+ };
14
+ var statusGlyph = { pass: "\u2713", fail: "\u2717", skipped: "\u2013" };
15
+ var formatReport = (report2) => {
16
+ const lines = [];
17
+ const impl = report2.implementation.version === void 0 ? report2.implementation.name : `${report2.implementation.name} ${report2.implementation.version}`;
18
+ lines.push(`Fuaran wire-format conformance report`);
19
+ lines.push(` kit ${report2.kit.name}@${report2.kit.version}`);
20
+ lines.push(` implementation ${impl}`);
21
+ lines.push(
22
+ ` corpus v${report2.corpus.manifestVersion} (${report2.corpus.fixtureCount} fixtures, ${report2.corpus.source})`
23
+ );
24
+ lines.push(` corpus digest sha256:${report2.corpus.digest}`);
25
+ lines.push(` generated ${report2.generatedAt}`);
26
+ lines.push("");
27
+ for (const leg of report2.legs) {
28
+ const tier = leg.mandatory ? "mandatory" : "optional ";
29
+ const counts = leg.status === "skipped" ? leg.skipReason ?? "skipped" : `${leg.fixturesPassed}/${leg.fixturesTotal}`;
30
+ lines.push(` ${statusGlyph[leg.status]} ${leg.leg.padEnd(20)} ${tier} ${counts}`);
31
+ for (const f of leg.failures) {
32
+ lines.push(` ${f.fixtureId}: ${f.summary}`);
33
+ if (f.detail !== void 0)
34
+ for (const detailLine of f.detail.split("\n")) lines.push(` ${detailLine}`);
35
+ }
36
+ }
37
+ lines.push("");
38
+ lines.push(` verdict: ${report2.verdict.toUpperCase()}`);
39
+ if (report2.verdict === "partially-conformant") {
40
+ const skipped = report2.legs.filter((l) => l.mandatory && l.status === "skipped").map((l) => l.leg);
41
+ lines.push(` (mandatory legs not attempted: ${skipped.join(", ")})`);
42
+ }
43
+ lines.push(
44
+ ` Certification is per corpus version \u2014 re-certify when the corpus advances (WIRE_FORMAT.md \xA711).`
45
+ );
46
+ return lines.join("\n");
47
+ };
48
+ var bundledRoot = () => (
49
+ // dist/index.js → ../corpus (tsup `shims: true` provides import.meta.url under CJS).
50
+ fileURLToPath(new URL("../corpus/", import.meta.url))
51
+ );
52
+ var loadCorpus = (options) => {
53
+ const external = options?.corpusRoot;
54
+ const root = external ?? bundledRoot();
55
+ const read = (relPath) => readFileSync(join(root, relPath), "utf8");
56
+ const manifestText = read("manifest.json");
57
+ const manifest = JSON.parse(manifestText);
58
+ const hash = createHash("sha256");
59
+ hash.update(manifestText);
60
+ hash.update(read(manifest.schema));
61
+ for (const f of manifest.fixtures) {
62
+ hash.update(read(f.inputFile));
63
+ if (f.expectedFile !== void 0 && f.expectedFile !== f.inputFile)
64
+ hash.update(read(f.expectedFile));
65
+ }
66
+ return {
67
+ root,
68
+ source: external === void 0 ? "bundled" : "external",
69
+ manifestVersion: manifest.version,
70
+ fixtures: manifest.fixtures,
71
+ digest: hash.digest("hex"),
72
+ schema: JSON.parse(read(manifest.schema)),
73
+ read
74
+ };
75
+ };
76
+
77
+ // src/run.ts
78
+ var KIT_NAME = "@fuaran-ui/conformance";
79
+ var KIT_VERSION = "0.1.0";
80
+ var canon = (s) => s.replace(/\r?\n$/, "");
81
+ var firstDiff = (expected, actual) => {
82
+ const n = Math.min(expected.length, actual.length);
83
+ let i = 0;
84
+ while (i < n && expected[i] === actual[i]) i++;
85
+ const from = Math.max(0, i - 30);
86
+ const win = (s) => JSON.stringify(s.slice(from, i + 30));
87
+ return `byte offset ${i} (length canonical=${expected.length}, host=${actual.length})
88
+ canonical \u2026${win(expected)}\u2026
89
+ host \u2026${win(actual)}\u2026`;
90
+ };
91
+ var messageOf = (err) => err instanceof Error ? err.message : String(err);
92
+ var newAcc = () => ({ total: 0, passed: 0, failures: [] });
93
+ var toLeg = (leg, mandatory, acc, skipReason) => acc === void 0 ? {
94
+ leg,
95
+ mandatory,
96
+ status: "skipped",
97
+ ...skipReason === void 0 ? {} : { skipReason },
98
+ fixturesTotal: 0,
99
+ fixturesPassed: 0,
100
+ failures: []
101
+ } : {
102
+ leg,
103
+ mandatory,
104
+ status: acc.failures.length === 0 ? "pass" : "fail",
105
+ fixturesTotal: acc.total,
106
+ fixturesPassed: acc.passed,
107
+ failures: acc.failures
108
+ };
109
+ var runConformance = (adapter2, options) => {
110
+ const corpus = loadCorpus(
111
+ options.corpusRoot === void 0 ? void 0 : { corpusRoot: options.corpusRoot }
112
+ );
113
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
114
+ const schemaValid = ajv.compile(corpus.schema);
115
+ const accepts = (decoder) => corpus.fixtures.filter(
116
+ (f) => f.decoder === decoder && (f.kind === "node-round-trip" || f.kind === "op-round-trip")
117
+ );
118
+ const rejects = (decoder) => corpus.fixtures.filter((f) => f.decoder === decoder && f.kind === "reject");
119
+ const lenients = (decoder) => corpus.fixtures.filter((f) => f.decoder === decoder && f.kind === "lenient-accept");
120
+ const accs = /* @__PURE__ */ new Map();
121
+ const acc = (leg) => {
122
+ let a = accs.get(leg);
123
+ if (a === void 0) {
124
+ a = newAcc();
125
+ accs.set(leg, a);
126
+ }
127
+ return a;
128
+ };
129
+ const runSide = (decoder, decode, encode) => {
130
+ const decodeLeg = decoder === "node" ? "node-decode" : "op-decode";
131
+ const byteLeg = decoder === "node" ? "node-byte-identity" : "op-byte-identity";
132
+ const rejectLeg = decoder === "node" ? "node-reject" : "op-reject";
133
+ if (decode === void 0) return;
134
+ for (const f of accepts(decoder)) {
135
+ const input = corpus.read(f.inputFile);
136
+ const d = acc(decodeLeg);
137
+ d.total++;
138
+ let decoded;
139
+ try {
140
+ decoded = decode(input);
141
+ } catch (err) {
142
+ d.failures.push({
143
+ fixtureId: f.id,
144
+ summary: "decoder THREW on an accept fixture (a conformant decoder never throws)",
145
+ detail: messageOf(err)
146
+ });
147
+ continue;
148
+ }
149
+ if (!decoded.ok) {
150
+ d.failures.push({
151
+ fixtureId: f.id,
152
+ summary: "decoder rejected an accept fixture",
153
+ detail: `${decoded.error.code} at ${decoded.error.path}${decoded.error.message === void 0 ? "" : ` \u2014 ${decoded.error.message}`}`
154
+ });
155
+ continue;
156
+ }
157
+ d.passed++;
158
+ if (encode === void 0) continue;
159
+ const b = acc(byteLeg);
160
+ b.total++;
161
+ const expected = canon(corpus.read(f.expectedFile ?? f.inputFile));
162
+ let actual;
163
+ try {
164
+ actual = encode(decoded.value);
165
+ } catch (err) {
166
+ b.failures.push({
167
+ fixtureId: f.id,
168
+ summary: "encoder THREW re-encoding a decoded accept fixture",
169
+ detail: messageOf(err)
170
+ });
171
+ continue;
172
+ }
173
+ if (actual !== expected) {
174
+ b.failures.push({
175
+ fixtureId: f.id,
176
+ summary: "encoder output diverges from the canonical form",
177
+ detail: firstDiff(expected, actual)
178
+ });
179
+ continue;
180
+ }
181
+ b.passed++;
182
+ const s = acc("schema-validation");
183
+ s.total++;
184
+ if (schemaValid(JSON.parse(actual))) {
185
+ s.passed++;
186
+ } else {
187
+ s.failures.push({
188
+ fixtureId: f.id,
189
+ summary: "encoder output fails the canonical JSON Schema (schema.json)",
190
+ detail: (schemaValid.errors ?? []).map((e) => `${e.instancePath === "" ? "$" : e.instancePath} ${e.message ?? ""}`).join("\n")
191
+ });
192
+ }
193
+ }
194
+ if (encode !== void 0) {
195
+ for (const f of lenients(decoder)) {
196
+ const input = corpus.read(f.inputFile);
197
+ const l = acc("lenient-accept");
198
+ l.total++;
199
+ let decoded;
200
+ try {
201
+ decoded = decode(input);
202
+ } catch (err) {
203
+ l.failures.push({
204
+ fixtureId: f.id,
205
+ summary: "decoder THREW on a \xA716 shorthand (a conformant decoder accepts it)",
206
+ detail: messageOf(err)
207
+ });
208
+ continue;
209
+ }
210
+ if (!decoded.ok) {
211
+ l.failures.push({
212
+ fixtureId: f.id,
213
+ summary: "decoder rejected a \xA716 shorthand (MUST accept \u2014 WIRE_FORMAT \xA716)",
214
+ detail: `${decoded.error.code} at ${decoded.error.path}${decoded.error.message === void 0 ? "" : ` \u2014 ${decoded.error.message}`}`
215
+ });
216
+ continue;
217
+ }
218
+ let actual;
219
+ try {
220
+ actual = encode(decoded.value);
221
+ } catch (err) {
222
+ l.failures.push({
223
+ fixtureId: f.id,
224
+ summary: "encoder THREW re-encoding a decoded \xA716 shorthand",
225
+ detail: messageOf(err)
226
+ });
227
+ continue;
228
+ }
229
+ const expected = canon(corpus.read(f.expectedFile ?? f.inputFile));
230
+ if (actual !== expected) {
231
+ l.failures.push({
232
+ fixtureId: f.id,
233
+ summary: "shorthand normalisation diverges from the verbose canonical form",
234
+ detail: firstDiff(expected, actual)
235
+ });
236
+ continue;
237
+ }
238
+ l.passed++;
239
+ }
240
+ }
241
+ for (const f of rejects(decoder)) {
242
+ const input = corpus.read(f.inputFile);
243
+ const r = acc(rejectLeg);
244
+ r.total++;
245
+ let decoded;
246
+ try {
247
+ decoded = decode(input);
248
+ } catch (err) {
249
+ r.failures.push({
250
+ fixtureId: f.id,
251
+ summary: "decoder THREW on a reject fixture (must return a structured error)",
252
+ detail: messageOf(err)
253
+ });
254
+ continue;
255
+ }
256
+ if (decoded.ok) {
257
+ r.failures.push({
258
+ fixtureId: f.id,
259
+ summary: "decoder ACCEPTED a reject fixture",
260
+ detail: `expected ${f.expectedErrorCode} at ${f.expectedPath}`
261
+ });
262
+ continue;
263
+ }
264
+ if (decoded.error.code !== f.expectedErrorCode) {
265
+ r.failures.push({
266
+ fixtureId: f.id,
267
+ summary: "decode-error CODE diverges from the canonical error",
268
+ detail: `expected ${f.expectedErrorCode}, got ${decoded.error.code} (path ${decoded.error.path})`
269
+ });
270
+ continue;
271
+ }
272
+ if (!decoded.error.path.startsWith(f.expectedPath ?? "$")) {
273
+ r.failures.push({
274
+ fixtureId: f.id,
275
+ summary: "decode-error PATH diverges from the canonical error",
276
+ detail: `expected prefix ${f.expectedPath}, got ${decoded.error.path} (code ${decoded.error.code})`
277
+ });
278
+ continue;
279
+ }
280
+ r.passed++;
281
+ }
282
+ };
283
+ runSide("node", adapter2.decodeNode, adapter2.encodeNode);
284
+ runSide("op", adapter2.decodeOp, adapter2.encodeOp);
285
+ const negotiateEnvelope = adapter2.negotiateEnvelope;
286
+ if (negotiateEnvelope !== void 0) {
287
+ const byKind = (kind) => corpus.fixtures.filter((f) => f.kind === kind);
288
+ for (const f of byKind("envelope-round-trip")) {
289
+ const a = acc("envelope-round-trip");
290
+ a.total++;
291
+ const input = corpus.read(f.inputFile);
292
+ let r;
293
+ try {
294
+ r = negotiateEnvelope(input);
295
+ } catch (err) {
296
+ a.failures.push({
297
+ fixtureId: f.id,
298
+ summary: "negotiateEnvelope THREW on a round-trip fixture (must return a structured result)",
299
+ detail: messageOf(err)
300
+ });
301
+ continue;
302
+ }
303
+ if (!r.ok) {
304
+ a.failures.push({
305
+ fixtureId: f.id,
306
+ summary: "negotiateEnvelope refused a round-trip fixture (Current/Behind must be tolerated)",
307
+ detail: `${r.error.code} at ${r.error.path}`
308
+ });
309
+ continue;
310
+ }
311
+ const expected = canon(corpus.read(f.expectedFile ?? f.inputFile));
312
+ if (typeof r.value !== "string" || r.value !== expected) {
313
+ a.failures.push({
314
+ fixtureId: f.id,
315
+ summary: "envelope re-encode diverges from the canonical form (must-ignore-but-preserve)",
316
+ detail: typeof r.value === "string" ? firstDiff(expected, r.value) : "value was not a string"
317
+ });
318
+ continue;
319
+ }
320
+ a.passed++;
321
+ }
322
+ for (const f of byKind("envelope-reject")) {
323
+ const a = acc("envelope-reject");
324
+ a.total++;
325
+ const input = corpus.read(f.inputFile);
326
+ let r;
327
+ try {
328
+ r = negotiateEnvelope(input);
329
+ } catch (err) {
330
+ a.failures.push({
331
+ fixtureId: f.id,
332
+ summary: "negotiateEnvelope THREW on a reject fixture (must return a structured error)",
333
+ detail: messageOf(err)
334
+ });
335
+ continue;
336
+ }
337
+ if (r.ok) {
338
+ a.failures.push({
339
+ fixtureId: f.id,
340
+ summary: "negotiateEnvelope ACCEPTED a Foreign artifact (must hard-refuse)",
341
+ detail: `expected ${f.expectedErrorCode} at ${f.expectedPath}`
342
+ });
343
+ continue;
344
+ }
345
+ if (r.error.code !== f.expectedErrorCode) {
346
+ a.failures.push({
347
+ fixtureId: f.id,
348
+ summary: "refusal CODE diverges from the canonical error",
349
+ detail: `expected ${f.expectedErrorCode}, got ${r.error.code} (path ${r.error.path})`
350
+ });
351
+ continue;
352
+ }
353
+ if (!r.error.path.startsWith(f.expectedPath ?? "$")) {
354
+ a.failures.push({
355
+ fixtureId: f.id,
356
+ summary: "refusal PATH diverges from the canonical error",
357
+ detail: `expected prefix ${f.expectedPath}, got ${r.error.path}`
358
+ });
359
+ continue;
360
+ }
361
+ a.passed++;
362
+ }
363
+ }
364
+ const roundTripElicitation = adapter2.roundTripElicitation;
365
+ if (roundTripElicitation !== void 0) {
366
+ const elDecoder = (f) => f.decoder === "elicitation-outcome" ? "elicitation-outcome" : "elicitation";
367
+ for (const f of corpus.fixtures.filter((x) => x.kind === "elicitation-round-trip")) {
368
+ const a = acc("elicitation-round-trip");
369
+ a.total++;
370
+ const input = corpus.read(f.inputFile);
371
+ let r;
372
+ try {
373
+ r = roundTripElicitation(elDecoder(f), input);
374
+ } catch (err) {
375
+ a.failures.push({
376
+ fixtureId: f.id,
377
+ summary: "roundTripElicitation THREW on a round-trip fixture (must return a structured result)",
378
+ detail: messageOf(err)
379
+ });
380
+ continue;
381
+ }
382
+ if (!r.ok) {
383
+ a.failures.push({
384
+ fixtureId: f.id,
385
+ summary: "roundTripElicitation refused a round-trip fixture",
386
+ detail: `${r.error.code} at ${r.error.path}`
387
+ });
388
+ continue;
389
+ }
390
+ const expected = canon(corpus.read(f.expectedFile ?? f.inputFile));
391
+ if (typeof r.value !== "string" || r.value !== expected) {
392
+ a.failures.push({
393
+ fixtureId: f.id,
394
+ summary: "elicitation re-encode diverges from the canonical form",
395
+ detail: typeof r.value === "string" ? firstDiff(expected, r.value) : "value was not a string"
396
+ });
397
+ continue;
398
+ }
399
+ a.passed++;
400
+ }
401
+ for (const f of corpus.fixtures.filter((x) => x.kind === "elicitation-reject")) {
402
+ const a = acc("elicitation-reject");
403
+ a.total++;
404
+ const input = corpus.read(f.inputFile);
405
+ let r;
406
+ try {
407
+ r = roundTripElicitation(elDecoder(f), input);
408
+ } catch (err) {
409
+ a.failures.push({
410
+ fixtureId: f.id,
411
+ summary: "roundTripElicitation THREW on a reject fixture (must return a structured error)",
412
+ detail: messageOf(err)
413
+ });
414
+ continue;
415
+ }
416
+ if (r.ok) {
417
+ a.failures.push({
418
+ fixtureId: f.id,
419
+ summary: "roundTripElicitation ACCEPTED a reject fixture",
420
+ detail: `expected ${f.expectedErrorCode} at ${f.expectedPath}`
421
+ });
422
+ continue;
423
+ }
424
+ if (r.error.code !== f.expectedErrorCode) {
425
+ a.failures.push({
426
+ fixtureId: f.id,
427
+ summary: "refusal CODE diverges from the canonical error",
428
+ detail: `expected ${f.expectedErrorCode}, got ${r.error.code} (path ${r.error.path})`
429
+ });
430
+ continue;
431
+ }
432
+ if (!r.error.path.startsWith(f.expectedPath ?? "$")) {
433
+ a.failures.push({
434
+ fixtureId: f.id,
435
+ summary: "refusal PATH diverges from the canonical error",
436
+ detail: `expected prefix ${f.expectedPath}, got ${r.error.path}`
437
+ });
438
+ continue;
439
+ }
440
+ a.passed++;
441
+ }
442
+ }
443
+ const validateAnswerDocument = adapter2.validateAnswerDocument;
444
+ if (validateAnswerDocument !== void 0) {
445
+ for (const f of corpus.fixtures.filter(
446
+ (x) => x.kind === "elicitation-answer-accept" || x.kind === "elicitation-answer-reject"
447
+ )) {
448
+ const a = acc("elicitation-answer");
449
+ a.total++;
450
+ const input = corpus.read(f.inputFile);
451
+ let r;
452
+ try {
453
+ r = validateAnswerDocument(input);
454
+ } catch (err) {
455
+ a.failures.push({
456
+ fixtureId: f.id,
457
+ summary: "validateAnswerDocument THREW (must return a structured result)",
458
+ detail: messageOf(err)
459
+ });
460
+ continue;
461
+ }
462
+ if (f.kind === "elicitation-answer-accept") {
463
+ if (!r.ok) {
464
+ a.failures.push({
465
+ fixtureId: f.id,
466
+ summary: "validateAnswerDocument refused a conforming answer",
467
+ detail: `${r.error.code} at ${r.error.path}`
468
+ });
469
+ continue;
470
+ }
471
+ a.passed++;
472
+ continue;
473
+ }
474
+ if (r.ok) {
475
+ a.failures.push({
476
+ fixtureId: f.id,
477
+ summary: "validateAnswerDocument ACCEPTED a nonconforming answer",
478
+ detail: `expected ${f.expectedErrorCode} at ${f.expectedPath}`
479
+ });
480
+ continue;
481
+ }
482
+ if (r.error.code !== f.expectedErrorCode) {
483
+ a.failures.push({
484
+ fixtureId: f.id,
485
+ summary: "refusal CODE diverges from the canonical error",
486
+ detail: `expected ${f.expectedErrorCode}, got ${r.error.code} (path ${r.error.path})`
487
+ });
488
+ continue;
489
+ }
490
+ if (!r.error.path.startsWith(f.expectedPath ?? "$")) {
491
+ a.failures.push({
492
+ fixtureId: f.id,
493
+ summary: "refusal PATH diverges from the canonical error",
494
+ detail: `expected prefix ${f.expectedPath}, got ${r.error.path}`
495
+ });
496
+ continue;
497
+ }
498
+ a.passed++;
499
+ }
500
+ }
501
+ const need = (hooks) => `adapter does not provide ${hooks.join(" + ")}`;
502
+ const hasSchemaInput = adapter2.encodeNode !== void 0 || adapter2.encodeOp !== void 0;
503
+ const legs = [
504
+ toLeg("node-decode", true, accs.get("node-decode"), need(["decodeNode"])),
505
+ toLeg(
506
+ "node-byte-identity",
507
+ true,
508
+ accs.get("node-byte-identity"),
509
+ need(["decodeNode", "encodeNode"])
510
+ ),
511
+ toLeg("node-reject", true, accs.get("node-reject"), need(["decodeNode"])),
512
+ toLeg("op-decode", true, accs.get("op-decode"), need(["decodeOp"])),
513
+ toLeg("op-byte-identity", true, accs.get("op-byte-identity"), need(["decodeOp", "encodeOp"])),
514
+ toLeg("op-reject", true, accs.get("op-reject"), need(["decodeOp"])),
515
+ toLeg(
516
+ "lenient-accept",
517
+ true,
518
+ accs.get("lenient-accept"),
519
+ need(["a decode + encode hook pair (node and/or op)"])
520
+ ),
521
+ toLeg(
522
+ "envelope-round-trip",
523
+ true,
524
+ accs.get("envelope-round-trip"),
525
+ need(["negotiateEnvelope"])
526
+ ),
527
+ toLeg("envelope-reject", true, accs.get("envelope-reject"), need(["negotiateEnvelope"])),
528
+ toLeg(
529
+ "elicitation-round-trip",
530
+ true,
531
+ accs.get("elicitation-round-trip"),
532
+ need(["roundTripElicitation"])
533
+ ),
534
+ toLeg(
535
+ "elicitation-reject",
536
+ true,
537
+ accs.get("elicitation-reject"),
538
+ need(["roundTripElicitation"])
539
+ ),
540
+ toLeg(
541
+ "elicitation-answer",
542
+ true,
543
+ accs.get("elicitation-answer"),
544
+ need(["validateAnswerDocument"])
545
+ ),
546
+ toLeg(
547
+ "schema-validation",
548
+ true,
549
+ accs.get("schema-validation"),
550
+ hasSchemaInput ? need(["a decode hook for the encode hooks provided"]) : need(["encodeNode and/or encodeOp"])
551
+ ),
552
+ toLeg("apply", false, void 0, "corpus v1 ships no apply fixtures \u2014 leg reserved")
553
+ ];
554
+ return {
555
+ kit: { name: KIT_NAME, version: KIT_VERSION },
556
+ implementation: options.implementation,
557
+ corpus: {
558
+ manifestVersion: corpus.manifestVersion,
559
+ digest: corpus.digest,
560
+ fixtureCount: corpus.fixtures.length,
561
+ source: corpus.source
562
+ },
563
+ legs,
564
+ verdict: computeVerdict(legs),
565
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
566
+ };
567
+ };
568
+
569
+ // src/cli.ts
570
+ var usage = () => {
571
+ console.error(
572
+ "Usage: fuaran-conformance <adapter-module> [--corpus <dir>] [--json <file>]\n\n <adapter-module> module exporting `adapter` (decode/encode hooks) and\n `implementation` ({ name, version? })\n --corpus <dir> certify against an external corpus checkout instead of\n the snapshot bundled with this package\n --json <file> also write the structured report as JSON\n"
573
+ );
574
+ process.exit(1);
575
+ };
576
+ var args = process.argv.slice(2);
577
+ var modulePath;
578
+ var corpusRoot;
579
+ var jsonOut;
580
+ for (let i = 0; i < args.length; i++) {
581
+ const arg = args[i];
582
+ if (arg === "--corpus") corpusRoot = args[++i] ?? usage();
583
+ else if (arg === "--json") jsonOut = args[++i] ?? usage();
584
+ else if (modulePath === void 0 && arg !== void 0 && !arg.startsWith("--")) {
585
+ modulePath = arg;
586
+ } else usage();
587
+ }
588
+ if (modulePath === void 0) usage();
589
+ var loaded = await import(pathToFileURL(resolve(modulePath)).href);
590
+ var adapter = loaded.adapter ?? loaded.default?.adapter;
591
+ var implementation = loaded.implementation ?? loaded.default?.implementation;
592
+ if (adapter === void 0 || implementation === void 0) {
593
+ console.error(
594
+ `The adapter module must export \`adapter\` and \`implementation\` (named or on the default export). Loaded: ${modulePath}`
595
+ );
596
+ process.exit(1);
597
+ }
598
+ var report = runConformance(adapter, {
599
+ implementation,
600
+ ...corpusRoot === void 0 ? {} : { corpusRoot }
601
+ });
602
+ console.log(formatReport(report));
603
+ if (jsonOut !== void 0) {
604
+ writeFileSync(jsonOut, JSON.stringify(report, null, 2) + "\n", "utf8");
605
+ console.log(`
606
+ Structured report written to ${jsonOut}`);
607
+ }
608
+ process.exit(
609
+ report.verdict === "conformant" ? 0 : report.verdict === "partially-conformant" ? 2 : 1
610
+ );
611
+ //# sourceMappingURL=cli.js.map
612
+ //# sourceMappingURL=cli.js.map