@metamesh/testkit 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +121 -110
  3. package/dist/assertions.d.ts +8 -15
  4. package/dist/assertions.d.ts.map +1 -1
  5. package/dist/assertions.js +24 -70
  6. package/dist/assertions.js.map +1 -1
  7. package/dist/content-assertions.d.ts +28 -0
  8. package/dist/content-assertions.d.ts.map +1 -0
  9. package/dist/content-assertions.js +75 -0
  10. package/dist/content-assertions.js.map +1 -0
  11. package/dist/content-matchers.d.ts +12 -0
  12. package/dist/content-matchers.d.ts.map +1 -0
  13. package/dist/content-matchers.js +22 -0
  14. package/dist/content-matchers.js.map +1 -0
  15. package/dist/docs/guides/package.md +121 -110
  16. package/dist/docs/manifest.json +157 -45
  17. package/dist/docs/reference/font-fixtures/LoadedFontFixtures.md +1 -1
  18. package/dist/docs/reference/font-fixtures/loadFontFixtureServices.md +1 -1
  19. package/dist/docs/reference/font-fixtures/loadFontFixtures.md +1 -1
  20. package/dist/docs/reference/fonts/FontTestFixture.md +1 -1
  21. package/dist/docs/reference/fonts/prepareFontTestServices.md +1 -1
  22. package/dist/docs/reference/index/BufferContent.md +13 -0
  23. package/dist/docs/reference/index/ContentMismatchError.md +12 -0
  24. package/dist/docs/reference/index/GeneratorModelTestInput.md +13 -0
  25. package/dist/docs/reference/index/GeneratorModelTestOptions.md +10 -0
  26. package/dist/docs/reference/index/GeneratorTestResult.md +18 -0
  27. package/dist/docs/reference/index/GeneratorTestRunner.md +9 -11
  28. package/dist/docs/reference/index/GeneratorTestRunnerOptions.md +2 -3
  29. package/dist/docs/reference/index/MeshContent.md +13 -0
  30. package/dist/docs/reference/index/MeshMeasurementExpectation.md +3 -3
  31. package/dist/docs/reference/index/MeshMeasurementTolerances.md +1 -1
  32. package/dist/docs/reference/index/RunModelOptions.md +13 -0
  33. package/dist/docs/reference/index/assertEqualBuffer.md +15 -0
  34. package/dist/docs/reference/index/assertEqualMesh.md +14 -0
  35. package/dist/docs/reference/index/{expectMeshMeasurements.md → assertMeshMeasurements.md} +3 -3
  36. package/dist/docs/reference/index/{expectValidDefaults.md → assertValidDefaults.md} +3 -3
  37. package/dist/docs/reference/index/assertValidDefinition.md +10 -0
  38. package/dist/docs/reference/index/assertValidParts.md +12 -0
  39. package/dist/docs/reference/index/{expectValidPresets.md → assertValidPresets.md} +3 -3
  40. package/dist/docs/reference/index/contentMatchers.md +12 -0
  41. package/dist/docs/reference/index/createGeneratorTestRunner.md +2 -4
  42. package/dist/docs/reference/index/getTestEngine.md +1 -1
  43. package/dist/docs/reference/index/runModel.md +13 -0
  44. package/dist/generator-execution.d.ts +6 -0
  45. package/dist/generator-execution.d.ts.map +1 -0
  46. package/dist/generator-execution.js +20 -0
  47. package/dist/generator-execution.js.map +1 -0
  48. package/dist/generator-test-runner.d.ts +32 -0
  49. package/dist/generator-test-runner.d.ts.map +1 -0
  50. package/dist/generator-test-runner.js +109 -0
  51. package/dist/generator-test-runner.js.map +1 -0
  52. package/dist/index.d.ts +10 -7
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +7 -5
  55. package/dist/index.js.map +1 -1
  56. package/dist/measurements.d.ts +4 -4
  57. package/dist/measurements.d.ts.map +1 -1
  58. package/dist/measurements.js +10 -9
  59. package/dist/measurements.js.map +1 -1
  60. package/dist/run-model.d.ts +21 -0
  61. package/dist/run-model.d.ts.map +1 -0
  62. package/dist/run-model.js +39 -0
  63. package/dist/run-model.js.map +1 -0
  64. package/dist/{engine.d.ts → test-engine.d.ts} +1 -1
  65. package/dist/test-engine.d.ts.map +1 -0
  66. package/dist/{engine.js → test-engine.js} +1 -1
  67. package/dist/test-engine.js.map +1 -0
  68. package/package.json +8 -5
  69. package/dist/docs/reference/index/GeneratorTestExecution.md +0 -19
  70. package/dist/docs/reference/index/ModelTestOptions.md +0 -13
  71. package/dist/docs/reference/index/expectSerializableDefinition.md +0 -10
  72. package/dist/docs/reference/index/expectValidParts.md +0 -19
  73. package/dist/docs/reference/index/withModel.md +0 -17
  74. package/dist/engine.d.ts.map +0 -1
  75. package/dist/engine.js.map +0 -1
  76. package/dist/model.d.ts +0 -17
  77. package/dist/model.d.ts.map +0 -1
  78. package/dist/model.js +0 -22
  79. package/dist/model.js.map +0 -1
  80. package/dist/runner.d.ts +0 -38
  81. package/dist/runner.d.ts.map +0 -1
  82. package/dist/runner.js +0 -130
  83. package/dist/runner.js.map +0 -1
  84. package/dist/snapshot-scope.d.ts +0 -4
  85. package/dist/snapshot-scope.d.ts.map +0 -1
  86. package/dist/snapshot-scope.js +0 -10
  87. package/dist/snapshot-scope.js.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { parseGeneratorDefinition, validateGeneratorPresets, } from "@metamesh/generator-sdk/packages";
2
2
  import { createParamCodec, getDefaultParameters } from "@metamesh/generator-sdk/parameters";
3
- import { parseGeneratedParts } from "@metamesh/generator-sdk/runtime";
4
3
  import { generate } from "@metamesh/modeling";
5
- import { getTestEngine } from "./engine.js";
4
+ import * as mesh from "@metamesh/modeling/geometry/mesh";
5
+ import { assertEqualMesh } from "./content-assertions.js";
6
+ import { getTestEngine } from "./test-engine.js";
6
7
  function stringify(value, label) {
7
8
  try {
8
9
  const serialized = JSON.stringify(value);
@@ -26,8 +27,8 @@ function sortJson(value) {
26
27
  function stableStringify(value, label) {
27
28
  return stringify(sortJson(value), label);
28
29
  }
29
- /** Throws unless a definition already has the canonical package-serializable representation. */
30
- export function expectSerializableDefinition(definition) {
30
+ /** Strictly parses a definition and requires its JSON round trip to retain the canonical package representation. */
31
+ export function assertValidDefinition(definition) {
31
32
  parseGeneratorDefinition(definition);
32
33
  const decoded = JSON.parse(stringify(definition, "Generator definition"));
33
34
  const parsed = parseGeneratorDefinition(decoded);
@@ -37,7 +38,7 @@ export function expectSerializableDefinition(definition) {
37
38
  }
38
39
  }
39
40
  /** Throws unless defaults strictly decode and equal the defaults derived from the definition. */
40
- export function expectValidDefaults(definition, defaults) {
41
+ export function assertValidDefaults(definition, defaults) {
41
42
  const parsed = createParamCodec(definition).parse(defaults);
42
43
  if (stableStringify(parsed, "Generator defaults") !==
43
44
  stableStringify(getDefaultParameters(definition), "Derived Generator defaults")) {
@@ -45,74 +46,27 @@ export function expectValidDefaults(definition, defaults) {
45
46
  }
46
47
  }
47
48
  /** Checks preset metadata. Validate domain assertions through the generation test runner. */
48
- export function expectValidPresets(definition, presets) {
49
+ export function assertValidPresets(definition, presets) {
49
50
  validateGeneratorPresets(definition, presets);
50
51
  }
51
- export function expectValidParts(value, engine) {
52
- if (engine === undefined)
53
- return getTestEngine().then((host) => expectValidParts(value, host));
54
- const meshes = new WeakMap();
55
- const wireValue = Array.isArray(value)
56
- ? value.map((part, index) => {
57
- if (typeof part !== "object" || part === null || !Object.hasOwn(part, "measurements"))
58
- return part;
59
- const prototype = Object.getPrototypeOf(part);
60
- if (prototype !== null && prototype !== Object.prototype)
61
- return part;
62
- const descriptors = Object.getOwnPropertyDescriptors(part);
63
- if (Object.keys(descriptors).some((key) => ![
64
- "id",
65
- "positions",
66
- "indices",
67
- "color",
68
- "measurements",
69
- "geometry",
70
- "position",
71
- "rotation",
72
- ].includes(key)))
73
- return part;
74
- const resource = descriptors["geometry"];
75
- if (resource !== undefined && !("value" in resource))
76
- return part;
77
- const geometry = resource?.value;
78
- const source = typeof geometry === "object" && geometry !== null
79
- ? geometry
80
- : part;
81
- // Snapshot getters return copies. Read each local resource once to retain shared storage.
82
- let mesh = meshes.get(source);
83
- if (mesh === undefined) {
84
- mesh = { positions: source.positions, indices: source.indices };
85
- meshes.set(source, mesh);
86
- }
87
- descriptors["positions"] = { value: mesh.positions, enumerable: true };
88
- descriptors["indices"] = { value: mesh.indices, enumerable: true };
89
- if (source === part) {
90
- delete descriptors["position"];
91
- delete descriptors["rotation"];
92
- }
93
- delete descriptors["measurements"];
94
- delete descriptors["geometry"];
95
- return Object.defineProperties({
96
- geometryId: `assertion-${index}`,
97
- geometryRevision: 0,
98
- position: [0, 0, 0],
99
- rotation: [0, 0, 0],
100
- }, descriptors);
101
- })
102
- : value;
103
- const parts = parseGeneratedParts(wireValue, engine.limits);
104
- const session = engine.createSession(generate(({ part }) => parts.map((mesh) => part(mesh.id, {
105
- geometry: mesh,
106
- position: mesh.position,
107
- rotation: mesh.rotation,
108
- ...(mesh.color === undefined ? {} : { color: mesh.color }),
109
- }))));
110
- try {
111
- session.run({}).dispose();
112
- }
113
- finally {
114
- session.dispose();
52
+ /** Validates supplied world meshes and their exact agreement with reconstructed local geometry and placement.
53
+ * Resolves to the same parts or rejects on failure. A supplied engine is reused; otherwise the shared engine initializes lazily.
54
+ */
55
+ export async function assertValidParts(parts, engine) {
56
+ const host = engine ?? (await getTestEngine());
57
+ for (const part of parts) {
58
+ const unknown = Reflect.ownKeys(part).find((key) => typeof key !== "string" ||
59
+ !["id", "positions", "indices", "color", "measurements", "geometry", "transform"].includes(key));
60
+ if (unknown !== undefined)
61
+ throw new TypeError('Model part contains unknown field "' + String(unknown) + '".');
62
+ mesh.validate(part, { requireClosed: true });
115
63
  }
64
+ const reconstructed = host.run(generate(({ part }) => parts.map((value) => part(value.id, {
65
+ geometry: value.geometry,
66
+ transform: value.transform,
67
+ ...(value.color === undefined ? {} : { color: value.color }),
68
+ }))), undefined, { meshTopologyMode: "original" });
69
+ parts.forEach((part, index) => assertEqualMesh(part, reconstructed.parts[index]));
116
70
  return parts;
117
71
  }
118
72
  //# sourceMappingURL=assertions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assertions.js","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAsB,MAAM,iCAAiC,CAAC;AAC1F,OAAO,EAAE,QAAQ,EAAyC,MAAM,oBAAoB,CAAC;AAGrF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,SAAS,SAAS,CAAC,KAAc,EAAE,KAAa;IAC9C,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,UAAU,KAAK,SAAS;YAAE,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,4BAA4B,CAAC,CAAC;QACxF,OAAO,UAAU,CAAC;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,4BAA4B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SAClB,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;SACpD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CACjD,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAc,EAAE,KAAa;IACpD,OAAO,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,4BAA4B,CAAC,UAA+B;IAC1E,wBAAwB,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,OAAO,GAAY,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC,CAAC;IACnF,MAAM,MAAM,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;IACjD,IACE,eAAe,CAAC,MAAM,EAAE,6BAA6B,CAAC;QACtD,eAAe,CAAC,OAAO,EAAE,sBAAsB,CAAC,EAChD,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,sEAAsE,CAAC,CAAC;IAC9F,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,mBAAmB,CACjC,UAAa,EACb,QAA4B;IAE5B,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5D,IACE,eAAe,CAAC,MAAM,EAAE,oBAAoB,CAAC;QAC7C,eAAe,CAAC,oBAAoB,CAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC,EAC/E,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,mDAAmD,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,kBAAkB,CAChC,UAAa,EACb,OAAsC;IAEtC,wBAAwB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC;AAiBD,MAAM,UAAU,gBAAgB,CAC9B,KAAc,EACd,MAAoB;IAEpB,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,aAAa,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAC/F,MAAM,MAAM,GAAG,IAAI,OAAO,EAAwB,CAAC;IACnD,MAAM,SAAS,GAAY,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAC7C,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAa,EAAE,KAAK,EAAW,EAAE;YAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc,CAAC;gBACnF,OAAO,IAAI,CAAC;YACd,MAAM,SAAS,GAAY,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YACvD,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,MAAM,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YACtE,MAAM,WAAW,GAAG,MAAM,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;YAC3D,IACE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAC3B,CAAC,GAAG,EAAE,EAAE,CACN,CAAC;gBACC,IAAI;gBACJ,WAAW;gBACX,SAAS;gBACT,OAAO;gBACP,cAAc;gBACd,UAAU;gBACV,UAAU;gBACV,UAAU;aACX,CAAC,QAAQ,CAAC,GAAG,CAAC,CAClB;gBAED,OAAO,IAAI,CAAC;YACd,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;YACzC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,IAAI,QAAQ,CAAC;gBAAE,OAAO,IAAI,CAAC;YAClE,MAAM,QAAQ,GAAY,QAAQ,EAAE,KAAK,CAAC;YAC1C,MAAM,MAAM,GACV,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI;gBAC/C,CAAC,CAAE,QAAyB;gBAC5B,CAAC,CAAE,IAAsB,CAAC;YAC9B,0FAA0F;YAC1F,IAAI,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,IAAI,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;gBAChE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC3B,CAAC;YACD,WAAW,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;YACvE,WAAW,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;YACnE,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC;gBAC/B,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC;YACjC,CAAC;YACD,OAAO,WAAW,CAAC,cAAc,CAAC,CAAC;YACnC,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC;YAC/B,OAAO,MAAM,CAAC,gBAAgB,CAC5B;gBACE,UAAU,EAAE,aAAa,KAAK,EAAE;gBAChC,gBAAgB,EAAE,CAAC;gBACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;aACpB,EACD,WAAW,CACZ,CAAC;QACJ,CAAC,CAAC;QACJ,CAAC,CAAC,KAAK,CAAC;IACV,MAAM,KAAK,GAAG,mBAAmB,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,CAClC,QAAQ,CAAuB,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAC1C,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE;QACZ,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;KAC3D,CAAC,CACH,CACF,CACF,CAAC;IACF,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC5B,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"assertions.js","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /** Numeric contents readable without copying their storage, including immutable model buffers. */
2
+ export interface BufferContent {
3
+ readonly length: number;
4
+ at(index: number): number | undefined;
5
+ }
6
+ /** Ordered manufacturing geometry; placement, appearance and resource identities are separate. */
7
+ export interface MeshContent {
8
+ readonly positions: BufferContent;
9
+ readonly indices: BufferContent;
10
+ }
11
+ /** A difference in valid buffer contents, distinct from malformed inputs or reader failures. */
12
+ export declare class ContentMismatchError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ /**
16
+ * Asserts exact ordered numeric contents through length and at(index), without copying storage.
17
+ * Uses Object.is regardless of storage type: NaN equals NaN; signed zero differs.
18
+ * Throws ContentMismatchError for the first difference, TypeError for malformed inputs, and
19
+ * preserves reader failures. This assertion does not establish valid manufacturing geometry.
20
+ */
21
+ export declare function assertEqualBuffer(actual: BufferContent, expected: BufferContent): void;
22
+ /**
23
+ * Asserts identical positions and indices with buffer-specific mismatch diagnostics.
24
+ * Ignores metadata. Geometrically equivalent meshes with different triangulation or ordering can fail.
25
+ * Shares assertEqualBuffer's numeric equality, validation and error behavior without copying storage.
26
+ */
27
+ export declare function assertEqualMesh(actual: MeshContent, expected: MeshContent): void;
28
+ //# sourceMappingURL=content-assertions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-assertions.d.ts","sourceRoot":"","sources":["../src/content-assertions.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,kGAAkG;AAClG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CACjC;AAED,gGAAgG;AAChG,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AA8DD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAMtF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,CAKhF"}
@@ -0,0 +1,75 @@
1
+ /** A difference in valid buffer contents, distinct from malformed inputs or reader failures. */
2
+ export class ContentMismatchError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "ContentMismatchError";
6
+ }
7
+ }
8
+ function buffer(value, label) {
9
+ if (typeof value !== "object" ||
10
+ value === null ||
11
+ !("length" in value) ||
12
+ typeof value.length !== "number" ||
13
+ !Number.isSafeInteger(value.length) ||
14
+ value.length < 0 ||
15
+ !("at" in value) ||
16
+ typeof value.at !== "function") {
17
+ throw new TypeError(`${label} must provide a nonnegative integer length and at(index).`);
18
+ }
19
+ return value;
20
+ }
21
+ function numberAt(value, index, label) {
22
+ const element = value.at(index);
23
+ if (typeof element !== "number") {
24
+ throw new TypeError(`${label}.at(${index}) must return a number within its length.`);
25
+ }
26
+ return element;
27
+ }
28
+ function display(value) {
29
+ return Object.is(value, -0) ? "-0" : String(value);
30
+ }
31
+ function assertBufferContents(actual, expected, label) {
32
+ if (actual.length !== expected.length) {
33
+ throw new ContentMismatchError(`${label} length: expected ${expected.length}, received ${actual.length}.`);
34
+ }
35
+ for (let index = 0; index < actual.length; index += 1) {
36
+ const received = numberAt(actual, index, `Received ${label}`);
37
+ const wanted = numberAt(expected, index, `Expected ${label}`);
38
+ if (!Object.is(received, wanted)) {
39
+ throw new ContentMismatchError(`${label} differs at index ${index}: expected ${display(wanted)}, received ${display(received)} (length ${actual.length}).`);
40
+ }
41
+ }
42
+ }
43
+ function mesh(value, label) {
44
+ if (typeof value !== "object" ||
45
+ value === null ||
46
+ !("positions" in value) ||
47
+ !("indices" in value)) {
48
+ throw new TypeError(`${label} must provide positions and indices buffers.`);
49
+ }
50
+ return {
51
+ positions: buffer(value.positions, `${label} positions`),
52
+ indices: buffer(value.indices, `${label} indices`),
53
+ };
54
+ }
55
+ /**
56
+ * Asserts exact ordered numeric contents through length and at(index), without copying storage.
57
+ * Uses Object.is regardless of storage type: NaN equals NaN; signed zero differs.
58
+ * Throws ContentMismatchError for the first difference, TypeError for malformed inputs, and
59
+ * preserves reader failures. This assertion does not establish valid manufacturing geometry.
60
+ */
61
+ export function assertEqualBuffer(actual, expected) {
62
+ assertBufferContents(buffer(actual, "Received buffer"), buffer(expected, "Expected buffer"), "Buffer");
63
+ }
64
+ /**
65
+ * Asserts identical positions and indices with buffer-specific mismatch diagnostics.
66
+ * Ignores metadata. Geometrically equivalent meshes with different triangulation or ordering can fail.
67
+ * Shares assertEqualBuffer's numeric equality, validation and error behavior without copying storage.
68
+ */
69
+ export function assertEqualMesh(actual, expected) {
70
+ const received = mesh(actual, "Received mesh");
71
+ const wanted = mesh(expected, "Expected mesh");
72
+ assertBufferContents(received.positions, wanted.positions, "Mesh position buffer");
73
+ assertBufferContents(received.indices, wanted.indices, "Mesh index buffer");
74
+ }
75
+ //# sourceMappingURL=content-assertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-assertions.js","sourceRoot":"","sources":["../src/content-assertions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { type BufferContent, type MeshContent } from "./content-assertions.js";
2
+ interface ContentMatcherResult {
3
+ readonly pass: boolean;
4
+ readonly message: () => string;
5
+ }
6
+ /** Optional expect.extend adapters; only content mismatches become failed matcher results. */
7
+ export declare const contentMatchers: {
8
+ toEqualBuffer(received: BufferContent, expected: BufferContent): ContentMatcherResult;
9
+ toEqualMesh(received: MeshContent, expected: MeshContent): ContentMatcherResult;
10
+ };
11
+ export {};
12
+ //# sourceMappingURL=content-matchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-matchers.d.ts","sourceRoot":"","sources":["../src/content-matchers.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAC;AAEjC,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC;CAChC;AAYD,8FAA8F;AAC9F,eAAO,MAAM,eAAe;4BACF,aAAa,YAAY,aAAa,GAAG,oBAAoB;0BAM/D,WAAW,YAAY,WAAW,GAAG,oBAAoB;CAMhF,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { assertEqualBuffer, assertEqualMesh, ContentMismatchError, } from "./content-assertions.js";
2
+ function match(assertion, equalMessage) {
3
+ try {
4
+ assertion();
5
+ return { pass: true, message: () => equalMessage };
6
+ }
7
+ catch (error) {
8
+ if (!(error instanceof ContentMismatchError))
9
+ throw error;
10
+ return { pass: false, message: () => error.message };
11
+ }
12
+ }
13
+ /** Optional expect.extend adapters; only content mismatches become failed matcher results. */
14
+ export const contentMatchers = {
15
+ toEqualBuffer(received, expected) {
16
+ return match(() => assertEqualBuffer(received, expected), "Expected buffer contents to differ, but they match.");
17
+ },
18
+ toEqualMesh(received, expected) {
19
+ return match(() => assertEqualMesh(received, expected), "Expected mesh contents to differ, but they match.");
20
+ },
21
+ };
22
+ //# sourceMappingURL=content-matchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-matchers.js","sourceRoot":"","sources":["../src/content-matchers.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,oBAAoB,GAGrB,MAAM,yBAAyB,CAAC;AAOjC,SAAS,KAAK,CAAC,SAAqB,EAAE,YAAoB;IACxD,IAAI,CAAC;QACH,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,oBAAoB,CAAC;YAAE,MAAM,KAAK,CAAC;QAC1D,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;IACvD,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,aAAa,CAAC,QAAuB,EAAE,QAAuB;QAC5D,OAAO,KAAK,CACV,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAC3C,qDAAqD,CACtD,CAAC;IACJ,CAAC;IACD,WAAW,CAAC,QAAqB,EAAE,QAAqB;QACtD,OAAO,KAAK,CACV,GAAG,EAAE,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,EACzC,mDAAmD,CACpD,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -1,146 +1,157 @@
1
1
  # `@metamesh/testkit`
2
2
 
3
- Test MetaMesh models and Generators with the same generation engine used by their hosts. Check definitions,
4
- presets, mesh validity, and physical measurements with any test runner.
3
+ Check that your MetaMesh models produce the geometry you expect. Run models and Generators with
4
+ the production engine, check their measurements, and test definitions and presets with your
5
+ preferred test runner.
5
6
 
6
7
  > **Preview:** `0.x` minor releases may break compatibility. Patches remain compatible within the
7
8
  > same minor line.
8
9
 
9
10
  ## Installation
10
11
 
11
- Install the same exact SDK, Modeling, and Testkit release. Testkit is a development dependency:
12
+ Install matching SDK, Modeling, and Testkit releases, with Testkit as a development dependency:
12
13
 
13
14
  ```bash
14
15
  pnpm add @metamesh/generator-sdk @metamesh/modeling
15
16
  pnpm add -D @metamesh/testkit
16
17
  ```
17
18
 
18
- ## Execute models in tests
19
+ The package publishes ES modules only. Use Node.js 24 or newer. TypeScript consumers need
20
+ TypeScript 6.0 or newer, `strict` checking, and `NodeNext` or `Bundler` module resolution.
19
21
 
20
- Given a Generator module exporting its `definition` and default runtime, such as the
21
- [SDK example](https://github.com/JasonKoolman/metamesh/blob/main/packages/generator-sdk/README.md#define-controls):
22
+ The assertions work without a test framework and don't register globals. Testkit has no compiler,
23
+ renderer, or browser dependency. Use the [CLI](https://github.com/JasonKoolman/metamesh/blob/main/packages/cli/README.md)
24
+ to check source files, save captured runs, or render images.
22
25
 
23
- ```ts
24
- import { getDefaultParameters } from "@metamesh/generator-sdk/parameters";
25
- import { createGeneratorTestRunner, expectValidParts } from "@metamesh/testkit";
26
-
27
- import runtime, { definition } from "./generator.js";
28
-
29
- const runner = createGeneratorTestRunner(definition, runtime);
30
- try {
31
- await runner.withModel(getDefaultParameters(definition), async (model) => {
32
- await expectValidParts(model.parts);
33
- });
34
- } finally {
35
- runner.dispose();
36
- }
37
- ```
38
-
39
- `withModel` normalizes values, prepares services, and runs the persistent session. It awaits the
40
- callback, returns its result, and disposes the snapshot even when assertions throw or reject.
41
- Schema or generation assertion failures throw `GenerationError` with their structured diagnostics.
42
- Unexpected execution and malformed-output errors retain their original failures. The runner's
43
- session and caches remain reusable until `runner.dispose()`.
26
+ ## Test a model
44
27
 
45
- For a plain model program, import `withModel` from Testkit:
28
+ Build a plate, check its volume and shell count, then check that its meshes reconstruct correctly:
46
29
 
47
30
  ```ts
48
- import { box, generate } from "@metamesh/modeling";
49
- import { expectValidParts, withModel } from "@metamesh/testkit";
31
+ import { box } from "@metamesh/modeling";
32
+ import { assertMeshMeasurements, assertValidParts, runModel } from "@metamesh/testkit";
50
33
 
51
- const model = generate<{ width: number }>(({ params, part }) =>
52
- part("plate", box({ size: [params.width, 20, 4] }))
34
+ const result = await runModel<{ width: number }>(
35
+ ({ params, part }) => part("plate", box({ size: [params.width, 20, 4] })),
36
+ { width: 30 }
53
37
  );
54
- await withModel(model, { width: 30 }, async (snapshot) => {
55
- await expectValidParts(snapshot.parts);
56
- });
57
- ```
58
-
59
- The plain-model helper also disposes its temporary session. Its optional fourth argument accepts
60
- session policy, prepared `services`, and a test-owned `engine`; declared services are required.
61
- It passes model parameters directly without Generator normalization and never disposes the engine.
62
- Previously obtained parts remain usable after snapshot disposal; returning parts does not eagerly
63
- copy their buffers.
64
-
65
- Snapshot parts include session-specific geometry resource identities. Compare manufacturing
66
- fields such as world positions, indices, measurements, IDs and colors for equality across runs.
67
- Each positions or indices read copies its buffer; capture the buffer once before a dense loop.
68
-
69
- Use `runner.execute` for explicit snapshot ownership or invalid-result assertions. It returns
70
- `invalid` for schema or generation assertion errors. Dispose successful snapshots separately from
71
- the runner; captured snapshots remain valid after runner disposal.
72
-
73
- Use `runner.validate(parameters, { normalize: false })` to probe deliberately invalid typed values
74
- without finalizing parts. It checks declarations and reached assertions; observing geometry can
75
- still initialize the engine. Creating a runner alone loads no engine or assets. Pass `options.engine`
76
- to reuse a test-owned engine and `options.limits` to lower host ceilings.
77
-
78
- ## Assertions
79
38
 
80
- Helpers throw on failure without depending on your test framework:
81
-
82
- | Helper | Checks |
83
- | ------------------------------ | ------------------------------------------------------------------ |
84
- | `expectSerializableDefinition` | Strict parsing and a canonical JSON round trip |
85
- | `expectValidDefaults` | Strict parameter decoding and agreement with definition defaults |
86
- | `expectValidPresets` | Serializable preset metadata; use the runner for domain assertions |
87
- | `expectValidParts` | Strict part metadata, closed topology, and native reconstruction |
88
- | `expectMeshMeasurements` | Bounds, area, signed volume, and connected-component count |
89
-
90
- `expectValidParts(parts, engine)` is synchronous and uses that engine's limits. Without an engine,
91
- it returns a promise using the shared test engine and default limits. It also accepts finalized
92
- parts, stripping measurement facts before strict validation; other unknown fields reject.
93
- Snapshot inputs retain shared local mesh buffers and rigid placements in the returned parts.
94
-
95
- Compare physical measurements with explicit tolerances. For a 30 × 20 × 4 mm box mesh:
96
-
97
- ```ts
98
- import { expectMeshMeasurements } from "@metamesh/testkit";
99
-
100
- expectMeshMeasurements(
101
- mesh,
102
- { signedVolume: 30 * 20 * 4, connectedComponentCount: 1 },
39
+ assertMeshMeasurements(
40
+ result.part("plate"),
41
+ { signedVolume: 2400, shellCount: 1 },
103
42
  { signedVolume: 0.05 }
104
43
  );
44
+ await assertValidParts(result.parts);
105
45
  ```
106
46
 
107
- Bounds use mm, area mm², and volume mm³. Tolerances are absolute and default to zero; component
108
- counts are exact. Measurements check closed oriented topology without a native engine. Matching
109
- measurements alone do not establish geometric equivalence or exclude self-intersections; use
110
- `expectValidParts` for native reconstruction.
47
+ `runModel` returns immutable parts, measurements, diagnostics, and dependencies. Results need no
48
+ cleanup. Use `result.part(id)` to find a required part by its ID, or iterate over `result.parts`.
111
49
 
112
- ## Font fixtures
50
+ Always await `assertValidParts(parts)`. It checks the world-space buffers for closed topology,
51
+ then reconstructs each part from its local geometry and rigid transform and compares the positions
52
+ and indices exactly. It resolves to the same parts or rejects on failure. Pass an engine as the
53
+ second argument to reuse one you own; otherwise, the shared test engine initializes when needed.
113
54
 
114
- Supply `prepareServices` in runner options for models with fonts. It receives the definition and
115
- current parameters, and can return services synchronously or asynchronously. Missing fixtures or
116
- service failures reject before generation. A different services object starts a fresh session;
117
- return the same immutable object when capabilities are unchanged to retain computation caches.
55
+ You can pass an existing Modeling program: `await runModel(program, parameters)` uses the same
56
+ program and parameter types as `engine.run`. For a small test, pass a synchronous callback
57
+ directly. Without parameters, no type argument or wrapper is needed:
58
+ `await runModel(({ part }) => part("cube", box({ size: 2 })))`.
59
+ Parameters keep the program or callback's declared type. Use
60
+ `GenerateCallback<Parameters, Services>` for a reusable callback or supply
61
+ `runModel<Parameters, Services>` inline. Options stay in the third argument; use `undefined` for
62
+ omitted parameters when supplying an engine, limits, or prepared services.
118
63
 
119
- `prepareFontTestServices` from `/fonts` checks immutable font revisions and prepares services from
120
- checked-in bytes. Install Modeling's optional `fontkit` peer, plus `woff2-encoder` for WOFF2 fixtures.
121
- Tests need no font downloads or browser cache; retain the fixtures' licenses.
64
+ ## Assertions
122
65
 
123
- For local JSON fixture descriptors, `loadFontFixtureServices` from
124
- `@metamesh/testkit/font-fixtures` returns the runner's `prepareServices` adapter. Descriptor paths
125
- resolve beside the JSON file, and every file's SHA-256 must match its declared selection revision.
126
- `loadFontFixtures` additionally returns the verified selections. These loaders are Node-only;
127
- use `/fonts` when the host already supplies the bytes.
66
+ | Helper | Checks |
67
+ | ------------------------ | -------------------------------------------------------------- |
68
+ | `assertValidDefinition` | Strict parsing and a canonical JSON round trip. |
69
+ | `assertValidDefaults` | Strict decoding and agreement with definition defaults. |
70
+ | `assertValidPresets` | Complete, normalized preset metadata. |
71
+ | `assertValidParts` | Part metadata, closed topology, and native reconstruction. |
72
+ | `assertMeshMeasurements` | Bounds, surface area, signed volume, and surface shell count. |
73
+ | `assertEqualBuffer` | Exact ordered numeric buffer contents without copying storage. |
74
+ | `assertEqualMesh` | Exact positions and indices, independently of mesh metadata. |
75
+
76
+ `assertValidDefinition` requires strict SDK parsing and a canonical JSON round trip; it does not
77
+ execute the Generator. Check model assertions and domain constraints through the Generator runner.
78
+
79
+ Measurement tolerances are absolute: millimetres for bounds, mm² for area, and mm³ for volume.
80
+ They default to zero; shell counts are exact. Matching measurements do not prove geometric
81
+ equivalence or rule out self-intersections. Use native reconstruction checks alongside them.
82
+
83
+ Use `assertEqualBuffer(actual, expected)` or `assertEqualMesh(actual, expected)` for exact content
84
+ comparisons. They read `length` and `at(index)` without copying storage and throw
85
+ `ContentMismatchError` with the first differing index or length. Malformed representations throw
86
+ `TypeError`; reader failures propagate. Numeric equality uses `Object.is`: `NaN` equals `NaN` and
87
+ signed zero differs. Mesh comparisons ignore metadata and require identical position and index
88
+ contents. Geometrically equivalent meshes with different triangulation or ordering can fail.
89
+
90
+ Register `contentMatchers` with your runner's `expect.extend(contentMatchers)` to use
91
+ `toEqualBuffer` and `toEqualMesh`, including `.not`. Only `ContentMismatchError` becomes a failed
92
+ matcher result; invalid inputs and unexpected errors still throw. Follow your runner's
93
+ [matcher extension guidance](https://vitest.dev/guide/extending-matchers) for TypeScript declarations.
94
+
95
+ ## Test Generators and repeated edits
96
+
97
+ For a Generator, import its definition and default runtime, then call
98
+ `runModel({ definition, runtime }, parameters)`. This validates the inputs and prepares services
99
+ before generation. Omit parameters or pass `undefined` to use defaults. Supplied inputs use the
100
+ definition's inferred types and must form a complete canonical map.
101
+
102
+ To change one value, spread `getDefaultParameters(definition)` and apply your override. The runner
103
+ doesn't merge defaults into supplied values. Use SDK `normalizeParameters` when recovering saved
104
+ values. Plain model parameters pass through directly.
105
+
106
+ Import the actual Generator entry point in integration tests so they exercise its input handling
107
+ and model together. Direct callbacks are useful for testing construction helpers or mesh reconstruction.
108
+ For a deliberate schema bypass, `runModel(runtime, parameters, { services })` executes
109
+ the Generator's model behavior without claiming those values pass its parameter schema.
110
+ See the [SDK example](https://github.com/JasonKoolman/metamesh/blob/main/packages/generator-sdk/README.md#define-controls).
111
+
112
+ Use `createGeneratorTestRunner(definition, runtime)` for repeated parameter cases. Its `run` method
113
+ returns a generated result or a structured validation failure. Call `runner.run(record)` to test
114
+ unknown keys, missing fields, or wrong value types. Use `check` to test declarations and reached
115
+ assertions without finalizing parts; geometry observations can still require the engine.
116
+ Dispose the runner in `finally`; returned results remain usable.
117
+ Use `check(parameters, { validateInput: false })` only to probe model assertions with deliberately
118
+ invalid typed values, bypassing the parameter schema.
119
+
120
+ ### Check cache reuse
121
+
122
+ For models using `memo`, include repeated-edit tests with `{ verifyCache: true }` on the runner.
123
+ Change geometry inputs and placement inputs independently. Verification reruns cached calculations
124
+ and rejects changed results for unchanged inputs. Keep pure helpers stable and pass changing values
125
+ as arguments; pass the current params/context to track fields read inside the helper.
126
+ Verification has bounded comparison work. For outputs it can't compare, use `{ cache: false }`
127
+ on a separate runner and compare the resulting geometry. Neither check proves arbitrary JavaScript pure.
128
+ Keep prepared service identity stable for unchanged assets; replacing services creates a new session
129
+ and prevents testing cache reuse. Isolated `runModel` calls do not verify repeated-edit correctness.
130
+
131
+ For plain models, use `engine.createSession(program, options)` for repeated edits or cache tests
132
+ and dispose the session in `finally`. Session results remain usable after later runs and disposal.
133
+ Both `runModel` and Generator runners can reuse a test-owned engine and lower its resource ceilings.
134
+ They accept the same physical `resolution`, `meshValidation`, and `meshTopologyMode` policy.
135
+
136
+ ## Fonts and entry points
137
+
138
+ | Import | Responsibility |
139
+ | --------------------------------- | ------------------------------------------------------------------------------ |
140
+ | `@metamesh/testkit` | Model execution, Generator runners, assertions, and optional content matchers. |
141
+ | `@metamesh/testkit/fonts` | Prepared font services from supplied fixture bytes. |
142
+ | `@metamesh/testkit/font-fixtures` | Node-only JSON fixture loading with SHA-256 verification. |
143
+
144
+ For a Generator with Font parameters, supply `prepareServices` to prepare its font services before
145
+ generation. Tests use checked-in bytes without CDN access; retain their licenses. Font preparation
146
+ needs Modeling's optional `fontkit` peer, plus `woff2-encoder` for WOFF2 fixtures.
128
147
 
129
148
  ## Documentation
130
149
 
131
- Resolve `@metamesh/testkit/docs/manifest.json` for this installed version's Markdown
132
- reference and testing guidance. Testkit has no compiler, bundler, renderer, or browser dependencies.
133
- It shares the Modeling engine and Generator SDK contracts rather than duplicating runtime rules.
134
-
135
- For source checking, captured runs, documentation lookup, and images, install
136
- [`@metamesh/cli`](https://github.com/JasonKoolman/metamesh/blob/main/packages/cli/README.md).
137
- Its executable is `metamesh`; reusable Node authoring functions live in `@metamesh/cli/authoring`.
138
-
139
- - [Generator SDK](https://github.com/JasonKoolman/metamesh/blob/main/packages/generator-sdk/README.md) — definitions, parameter values, and module authoring.
140
- - [Modeling](https://github.com/JasonKoolman/metamesh/blob/main/packages/modeling/README.md) — geometry, sessions, and snapshots.
141
- - [API source](https://github.com/JasonKoolman/metamesh/blob/main/packages/testkit/src/index.ts) — runner and assertion exports; [font helpers](https://github.com/JasonKoolman/metamesh/blob/main/packages/testkit/src/fonts.ts) live in `/fonts`.
142
-
143
- For package development, see [Contributing](https://github.com/JasonKoolman/metamesh/blob/main/CONTRIBUTING.md).
150
+ Resolve `@metamesh/testkit/docs/manifest.json` for exact-version Markdown guidance and annotated
151
+ API references. With CLI installed, read `pnpm exec metamesh docs show testkit/guide/testing`.
152
+ See [Modeling](https://github.com/JasonKoolman/metamesh/blob/main/packages/modeling/README.md)
153
+ for geometry and [Contributing](https://github.com/JasonKoolman/metamesh/blob/main/CONTRIBUTING.md)
154
+ for package development.
144
155
 
145
156
  ## License
146
157