Skip to the guide
‹ Back to the app CivilKit Buckling · Guide

The ckext command line

tools/ckext.mjs packs a module folder into a .ckext and tests it with no browser: the same MicroPython runtime and the same cufsm-rs engine as the app's sandbox, under Node. It is the loop an author, a reviewer or a CI job runs.

Setting up#

You need Node 20 or later and a checkout of CivilKit Buckling's source. The tool has no dependencies of its own beyond what is committed (the engine cufsm.wasm and vendor/civilkit):

cd civilkit-buckling
node tools/ckext.mjs test extensions/dsm-compression

npm run ckext -- test … runs the same. Paths are relative to where you run it.

pack#

node tools/ckext.mjs pack <folder> [-o out.ckext]

Zips manifest.json, main.py and everything under tests/ from the folder, checks the result exactly as the app's installer does (the manifest's required fields, a semantic version, a buckling.tool contribution, a main.py), and writes it. With no -o the file is named after the folder, in the current directory.

$ node tools/ckext.mjs pack squash-load -o squash-load.ckext
packed yourname.squash-load 1.0.0 -> squash-load.ckext (1467 bytes)

A bundle that would not install is not written; the error names why (see install errors). Other files in the folder, such as a README, are not packed.

test#

node tools/ckext.mjs test <file.ckext | folder> [--inputs '{"fy":450}'] [--model model.json] [--json]

It does four things, in order:

  1. runs every worked example, which decides the badge;
  2. calls build_ui({}, [], []) and lists the form's fields and buttons;
  3. runs check once: on the first example's inputs and fixture by default, on --inputs if given, against the model in --model if given (a JSON file in the model shape);
  4. reports which capabilities were declared, which were used, and any called without being declared.

A folder is packed in memory first, so testing a folder and testing its packed file give the same answer.

$ node tools/ckext.mjs test squash-load
verified
Squash load 1.0.0 (yourname.squash-load)
worked examples: 1 of 1 pass
  pass lipped-c-150-60-40
form: fy; buttons: Run
run on {"fy":450} (the first worked example and its fixture):
  Py = 236.25 kN
    Squash load: Py = A fy = 236.25 kN
capabilities: declared firstYield; used firstYield
The run used the first worked example and its fixture, which is never solved, so hasResults is false and getResults is empty. Pass --model (and --inputs) to run on another model.

The first line is the badge. A failing example prints FAIL and its diffs under it; a run that stops prints error: and the message (a Python error with its main.py line); calc lines are indented under the results, with their clause in brackets; proposals print as proposal: 25 nodes, 24 elements or proposal: refused: ‹reason›; civilkit.log messages as log:; and anything the module printed under stdout:. Numbers are shown to 12 significant figures.

--json#

Prints the same report as JSON, the shape CivilKit Studio's run_extension returns:

{
  "id": "yourname.squash-load",
  "name": "Squash load",
  "version": "1.0.0",
  "badge": "verified",
  "examples": [
    {
      "id": "lipped-c-150-60-40",
      "pass": true,
      "diffs": [],
      "source": "Hand calc: lipped C 150 x 60 x 40 mm (centreline), t = 1.5 mm, sharp corners. A = (150 + 2 x 60 + 2 x 40) x 1.5 = 525 mm2; Py = A fy = 525 x 450 = 236 250 N = 236.25 kN."
    }
  ],
  "form": {
    "title": "Squash load",
    "fields": [
      {
        "id": "fy",
        "label": "fy (MPa)",
        "inputType": "number",
        "default": 450
      }
    ],
    "buttons": [
      {
        "id": "run",
        "label": "Run",
        "action": "run-check"
      }
    ]
  },
  "run": {
    "inputs": {
      "fy": 450
    },
    "fixture": true,
    "result": [
      {
        "unit": "kN",
        "label": "Py",
        "value": 236.25
      }
    ],
    "calcLines": [
      {
        "eq": "Py = A fy = 236.25 kN",
        "label": "Squash load"
      }
    ],
    "proposals": [],
    "logs": [],
    "error": null
  },
  "capabilities": {
    "declared": [
      "firstYield"
    ],
    "used": [
      "firstYield"
    ],
    "undeclaredCalls": [],
    "unusedInThisRun": []
  },
  "note": "The run used the first worked example and its fixture, which is never solved, so hasResults is false and getResults is empty. Pass --model (and --inputs) to run on another model."
}
FieldMeaning
badge"verified" or "community".
examples[]Each example's id, pass, diffs and source.
formThe title, fields (id, label, inputType, default) and buttons; {"error": …} when build_ui failed.
runThe one check: its inputs, whether it ran on the first example's fixture, and its result, calcLines, proposals, logs and error.
capabilitiesdeclared; used (anywhere: examples and run); undeclaredCalls; unusedInThisRun, the declared ones the single run did not call.
noteSet when the run used a fixture, which is never solved, so the results capabilities show unused there.

Exit codes#

CodeWhen
0Verified, and no capability was called without being declared.
1Community, an undeclared call, or a bundle that cannot be read.
2A usage error: no command, an unknown option, --inputs that is not JSON. The usage is printed.

So node tools/ckext.mjs test my-module && echo ok is a complete CI gate.

The loop#

  1. Write the three files, with at least one worked example and its cited source.
  2. node tools/ckext.mjs test my-module, and read the diffs and the error.
  3. Fix, and repeat until the first line is verified and nothing is listed under called without declaring.
  4. Run it on another section: --model with a model JSON (copy one out of a fixture), and --inputs.
  5. pack it, install it in the app, and hand it to a reviewer with its README.

The same loop works for an AI agent: give it this guide, ask for a module, and run test on what it writes.

Building the store#

node tools/build-store.mjs

Packs every folder under extensions/ into store/<id>.ckext, tests each with the command above, and writes store/index.json ([{ "id", "name", "version", "description", "url" }]) listing only the Verified ones. A module that is not Verified is left out and named, and the script exits 1:

store: 3 module(s): civilkit.dsm-compression 1.0.0, civilkit.section-rounded-cz 1.0.0, civilkit.thickness-study 1.0.0

Re-run it after changing a module, and commit store/ with the change. The app's Extensions list reads store/index.json from the site, so the store is whatever was committed and deployed.

CivilKit Buckling runs cufsm-rs, a Rust port of CUFSM, the finite strip software by Benjamin W. Schafer and co-workers (constrained FSM with Sándor Ádány), compiled to WebAssembly. An analysis tool, not a design certificate: check what you use.