geospec

中文文档
0.5.1发布于 2026/11/04Apache-2.0TypeScriptES Module构建来源已验证

Geometry specification API for CAD models: DSL, selector language, diagnostics schema, and the native runner integration

原始使用文档
文档目录5 节
  1. Running specs
  2. Subjects and host lifetime
  3. Runner configuration
  4. Checking a file with no spec
  5. License

geospec

GeoSpec is a CAD geometry testing library with one host-independent authoring API: loadModel from geospec/model and expectGeo from geospec.

This package is the matcher-API substrate (Apache-2.0): the authoring DSL, the selector language, the diagnostics and evidence schemas, the matcher registry, and host integration. It executes no geometry on its own: every claim is evaluated by the compiled geospec-engine core that the host passes to the runner. Tau's desktop/browser composition and the geospec CLI supply that engine. Authored tests do not import or select an engine, and nothing registers one globally.

The following is a *.geospec.ts module for the CLI or Tau test_model:

import { describe, expectGeo, it } from 'geospec';
import { loadModel } from 'geospec/model';

const code = `
  import { makeBaseBox } from 'replicad';
  export default function main() {
    return makeBaseBox(40, 20, 8);
  }
`;

describe('bracket', () => {
  it('has expected measurements', async () => {
    const model = await loadModel({
      code: { 'main.ts': code },
      file: 'main.ts',
      format: 'glb',
    });

    expectGeo(model).toHaveBoundingBox({ size: { x: 40, y: 20, z: 8 }, tolerance: 0.1 });
    expectGeo(model).toHaveSurfaceArea({ value: 2560, tolerance: 10 });
    expectGeo(model).toHaveVolume({ value: 6400, tolerance: 10 });
  });
});

Running specs

Subjects and host lifetime

Await loadModel, then make synchronous expectGeo assertions. Its GeoSpecSubject is opaque: do not read mesh fields, forge a subject hash, or reuse a subject after its owning loader/run is disposed. The host retains the source and parameter identity and owns geometry admission, reuse and release.

Standalone and Vitest hosts supply a managed loader through createModelLoader and the existing host integration; geospec/vitest supplies the framework adapter. Vitest's describe/it come from Vitest, while the helpers imported from geospec above belong to GeoSpec's VM runner. Ordinary assertions remain expectGeo; the optional Vitest expect extension has its own asynchronous settlement contract. See the host binding documentation.

Python uses load_model/expect_geo from geospec, with the wheel's pytest plugin scope or a standalone GeoSpecEngine context. Tau test_model discovers TypeScript/JavaScript tests, not Python files.

Read runStatus, complete accounting, discovery and source-lineage status when interpreting a run. Unsupported, inconclusive, skipped and not-run requirements are not passes. Compact test_model output can omit details; its retained fullResult remains the complete result. An empty failures list alone is not qualification, and filtered requirements remain outside the demonstrated scope.

Runner configuration

The geospec bin runs a project's specs on the compiled engine from geospec-engine: the native addon when this platform has one, else its WASM build. Models that are not STEP or GLB come from the exporters the project's geospec.config.* declares, imported once in each pool worker; without one, such a load refuses with GEOSPEC_EXPORTER_MISSING.

geospec run .
geospec run . --include "parts/**/*.geospec.ts"
geospec run . --file main.geospec.ts --test-name-pattern volume
geospec run . --workers            # worker pool, auto-sized
geospec run . --workers 4 --shard-timeout 600000
geospec run . --config ci.geospec.config.ts
geospec run . --json

Spec imports. The CLI bundles each spec with native esbuild over the project: a spec may import project files and the GeoSpec builtins (geospec, geospec/model, geospec/config). An npm package or URL import refuses with BUNDLER_FAILED and a message naming the specifier; nothing is fetched or installed. A host that resolves packages passes its own module VM as createModuleVm (CreateModuleVm in geospec/runner; for a Node pool, moduleVmModule). Tau's hosts inject Tau's esbuild VM this way. A browser host has no default VM and must inject one.

FlagMeaning
--file <path>GeoSpec file or directory root; repeatable. Empty input discovers from the project root.
--include <glob> / --exclude <glob>Vitest-style file globs; repeatable. Include defaults to **/*.geospec.{ts,js}.
-t, --test-name-pattern <re>JavaScript regular expression matched against the full suite > test name.
--test-timeout <ms>Timeout for each async test callback.
--workers [n]Run in a worker pool. Default is one; omit the count after the flag to request available CPU parallelism.
--shard-timeout <ms>Per-shard non-verdict watchdog. Off by default.
--bailStop after the first red file. Interactive use only — a reward run wants the complete red set.
--config <path>The geospec.config.* to use instead of the one in the project root.
--jsonPrint exactly one JSON result document on stdout and nothing else.

A flag replaces the config field of the same name. The exit code is the verdict. 0 only when the run succeeded; any failure, any run-level issue, and an empty selection are all 1.

Checking a file with no spec

geospec check runs one bundled validity spec on a STEP or GLB file, so a first use needs no *.geospec.ts. It takes the same runner, engine and report as geospec run; the file's folder is the project root.

geospec check bracket.step
geospec check assembly.glb --components 3 --json

Each claim is its own test: no error diagnostics, closed edge incidence (toBeWatertight) and a bounding box with positive size on every axis, plus mesh integrity for a GLB or a valid BRep with closed shells for a STEP file. --components <n> adds an exact connected-component count. A claim the engine cannot decide is reported as inconclusive, never as a pass, and the exit code is the verdict.

To embed the pool, use createGeoSpecNodePoolRunner from geospec/runner/node; each worker owns one engine and its own exporters, and live subjects never cross worker boundaries. To run serially in this process, pass an engine from createNodeGeoSpecEngine to createNativeGeoSpecRunner from geospec/runner/native; it builds the native model loader and releases every admitted subject after each run. Other embeddings must qualify their actual binding, input representation and supported domain; shared authoring syntax alone does not establish verdict equivalence.

The filters below are the shared vocabulary of the CLI, the embedded runners and the Tau test_model tool:

  • files: GeoSpec files or directory roots to run. Empty input recursively discovers from the project root.
  • include: GeoSpec file include globs, defaulting to ["**/*.geospec.{ts,js}"]
  • exclude: GeoSpec file exclude globs
  • testNamePattern: JavaScript regular expression matched against full suite > test names
  • testTimeout: async test timeout in milliseconds

The host uses Tau runtime exports or already-exported GLB/glTF/STEP bytes. Authored tests use loadModel for both code and direct geometry inputs; low-level parsing/host APIs are not an alternate authored-test dialect.

Load failures carry structured diagnostics. Assertion reports retain the selected claim, evidence, provenance and diagnostics; inspect those reports rather than expecting mutable evidence fields on the opaque subject.

Keep each test readable with its own loadModel() call. Each load captures current source/parameter identity; hosts may reuse admitted computations only under matching identities. Do not assume a filename alone permits cross-file memoization:

import { describe, expectGeo, it } from 'geospec';
import { loadModel } from 'geospec/model';

describe('assembly', () => {
  it('has no global part interference', async () => {
    const model = await loadModel({ file: 'main.ts' });
    expectGeo(model).toHaveNoComponentInterference({ tolerance: 0.1 });
  });

  it('keeps the ring and planet free of interference', async () => {
    const model = await loadModel({ file: 'main.ts' });
    expectGeo(model).toHaveNoComponentInterference({
      tolerance: 0.05,
      pairs: [{ left: /ring/i, right: /planet gear/i }],
    });
  });
});

Production assembly suites can combine mesh-integrity, occurrence, spatial relationship, and exact BRep assertions without adding domain-specific matchers for each mechanical subsystem:

expectGeo(model).toHaveMeshIntegrity({
  finitePositions: true,
  degenerateTriangles: { count: 0 },
  duplicateFaces: { count: 0 },
});

expectGeo(model).toHaveAssemblyOccurrences({
  uniqueNames: true,
  occurrences: [
    { name: 'Housing', count: 1 },
    { name: /^Fastener \d+$/, count: 8 },
  ],
});

expectGeo(model).toHaveSpatialRelationships({
  relationships: [
    { id: 'shaft seats in bearing', kind: 'contact', subject: 'Shaft', target: 'Bearing', tolerance: 0.05 },
    { id: 'pin remains inside yoke', kind: 'containment', subject: 'Pin', target: 'Yoke', tolerance: 0.05 },
  ],
});

Choose selectors supported by the admitted source. A qualified mesh body selector identifies one material root with its cavity boundaries; disconnected roots and islands inside cavities remain separate Bodies. Its of scope matches the exact retained primitive label, including any ordinal suffix. Inspect that label and preserve cardinality instead of guessing an occurrence path. Mesh input does not invent assembly ancestry or analytic faces; missing measure, probe or ordering evidence refuses, even through query negation. The STEP Body index exposes per-occurrence solid aggregates, so do not assume its Body count maps one-to-one to mesh material roots.

Whole-Body contact requires no interior overlap and boundary separation within the declared inclusive tolerance. It does not establish a contact area or seal; finite-face angular requirements need their actual analytic evidence.

Load STEP through the same authoring API:

import { loadModel } from 'geospec/model';

const subject = await loadModel({ file: 'part.step', format: 'step' });

For CAD source, loadModel({ file: 'main.ts', format: 'step' }) asks the host to export STEP. The configured compiled engine admits the exact returned bytes through its OCCT binding. Export and source provenance do not by themselves establish a geometric requirement.

Measurement matchers support mesh evidence and prefer exact BRep evidence when it is present:

expectGeo(subject).toHaveSurfaceArea({ value: 12_345, tolerance: 1 });
expectGeo(subject).toHaveVolume({ value: 120_000, tolerance: 10 });
expectGeo(subject).toHaveMass({ value: 94.2, density: 0.000_785, tolerance: 0.5 });
expectGeo(subject).toHaveCenterOfMass({ point: { x: 0, y: 0, z: 10 }, tolerance: 0.05 });

Initial BRep feature matchers are available when a loader provides BRep evidence:

import { loadModel } from 'geospec/model';

const subject = await loadModel({ file: 'main.ts', format: 'step' });

expectGeo(subject).toBeValidBrep({ maxTolerance: 0.01, closedShells: true });
expectGeo(subject).toHavePlanarFace({ normal: { x: 0, y: 0, z: 1 }, offset: 20, tolerance: 0.05 });
expectGeo(subject).toHaveCylindricalFace({ radius: 15, axis: 'z', tolerance: 0.05 });
expectGeo(subject).toHaveCircularHole({ diameter: 8, through: true, axis: 'z', center: { x: 25, y: 15 } });
expectGeo(subject).toHaveChamferFeature({ distance: 2, selection: 'outer top perimeter', tolerance: 0.05 });
expectGeo(subject).toHaveMinimumWallThickness({ value: { greaterThanOrEqual: 2 }, tolerance: 0.05 });
expectGeo(subject).toHaveVoidContinuity({
  path: [{ occurrence: 'Throttle Body 1' }, { occurrence: 'Cylinder Head R' }],
  material: ['Throttle Body 1', 'Intake Manifold 1', 'Cylinder Head R'],
  minCrossSection: 900,
  isolatedFrom: [[120, 0, 40]],
});

These are requirement examples, not a claim that every admitted model supports them. toHaveVoidContinuity requires the declared path, material and isolation proposition to be proved in a qualified domain. A sampled cross-section cannot establish a continuous minimum: the general sampled profile refuses a requested minCrossSection. Restricted nominal analytic domains have separate premises; general mesh lumen, wall and motion qualification remains incomplete.

Likewise, watertight edge incidence does not prove manifold validity, a Boolean fuse or material connectivity. Bounds and playback cannot establish contact, clearance, containment or sealing. Preserve intended geometry and tolerances; report unavailable evidence instead of replacing the requirement with a proxy.

The canonical API index describes ordinary authoring. The separate complete public API index retains host and framework contracts.

When a geometry assertion needs a parameter variant, pass that variant directly to loadModel. Omitting parameters exercises the defaults authored by the model:

import { describe, expectGeo, it } from 'geospec';
import { loadModel } from 'geospec/model';

describe('parameter variants', () => {
  it('uses the model defaults', async () => {
    const model = await loadModel({ file: 'main.ts' });
    expectGeo(model).toHaveBoundingBox({ size: { x: 40 }, tolerance: 1 });
  });

  it('accepts an explicit width', async () => {
    const width = 80;
    const model = await loadModel({
      file: 'main.ts',
      parameters: { width },
    });

    expectGeo(model).toHaveBoundingBox({ size: { x: width }, tolerance: 1 });
  });
});

License

Apache-2.0. This package is the permissive perimeter: your specs, your models and your verdicts do not inherit this package's license. See Licensing for the routing map and the internal-use FAQ.

版本与发布记录

geospec 共发布 20 个版本,首次发布于 2026/09/30;过去 12 个月发布了 20 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.1latest2026/11/04今天
0.5.1-next.361next2026/11/03今天
0.5.1-next.3512026/10/305 天前
0.5.1-next.3472026/10/295 天前
0.5.1-next.3422026/10/295 天前
0.5.1-next.3402026/10/295 天前

其他发布通道:bootstrap → 0.0.0,beta → 0.1.0-beta.9,next → 0.5.1-next.361。

关于 geospec 的常见问题

geospec 是什么?

用于CAD模型的几何规范API: DSL、选择器语言、诊断模式和本机运行器集成

如何安装 geospec?

在项目目录运行 npm install geospec,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 geospec,可以用 npx geospec 直接运行。

geospec 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

geospec 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

geospec 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

geospec 可以商用吗?

geospec 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

geospec 还在维护吗?

最新版本 0.5.1 发布于 2026/11/04(今天);至今共发布 20 个版本,其中过去 12 个月发布了 20 个;首次发布于 2026/09/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。