@roomwise/calc-engine 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +285 -2
- package/dist/index.cjs +1610 -0
- package/dist/index.d.cts +794 -0
- package/dist/index.d.ts +794 -0
- package/dist/index.js +1527 -0
- package/package.json +53 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oksana Vakuliak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,286 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @roomwise/calc-engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The calculation engine behind Roomwise, a step-by-step renovation cost configurator. It turns a configuration (apartment, rooms, chosen materials, options) and a snapshot of pricing data into an itemised estimate.
|
|
4
|
+
|
|
5
|
+
Rates, coefficients and formulas are plain data. Formulas are strings evaluated by a small purpose-built expression parser that only knows a fixed set of variables, coefficients and functions. There is no `eval` and no access to the JavaScript runtime. The same package computes live drafts in the browser and the authoritative result on the server, so both always agree.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @roomwise/calc-engine zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`zod` `^4.0.0` is a peer dependency. The package ships ESM and CommonJS builds with types.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
ConfigurationSchema,
|
|
20
|
+
PricingDataSchema,
|
|
21
|
+
checkLimits,
|
|
22
|
+
estimate,
|
|
23
|
+
} from '@roomwise/calc-engine';
|
|
24
|
+
|
|
25
|
+
const configuration = ConfigurationSchema.parse(configurationJson);
|
|
26
|
+
const data = PricingDataSchema.parse(pricingJson);
|
|
27
|
+
|
|
28
|
+
const violations = checkLimits(configuration, data.limits, data.options);
|
|
29
|
+
if (violations.length > 0) {
|
|
30
|
+
// [{ field: 'rooms[0].heightM', min: 2.2, max: 5 }]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = estimate(configuration, data);
|
|
34
|
+
result.totals.usdCents; // integer cents
|
|
35
|
+
result.totals.uahKopecks; // null when there is no exchange rate
|
|
36
|
+
result.incomplete; // true if any line failed or could not be calculated
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `estimate(configuration, data): Estimate`
|
|
42
|
+
|
|
43
|
+
Computes the estimate. It does not validate its input; parse with the schemas and call `checkLimits` first.
|
|
44
|
+
|
|
45
|
+
Lines are produced in this order: base cost, object rules (by `sortOrder`), materials (per room, per selected product), engineering (package items, then selected options), additional options.
|
|
46
|
+
|
|
47
|
+
- Base cost: the base rate matching the finish level with `areaFromSqm <= totalAreaSqm < areaToSqm`, times the total area. No matching rate gives a `NOT_CALCULATED` line with code `BASE_RATE_NOT_FOUND`.
|
|
48
|
+
- Object rules: `OBJECT` level rules, one line each.
|
|
49
|
+
- Materials: for every room selection with a product, the `MATERIAL_TYPE` rule for the category and the product's material type is used if one exists, otherwise the `CATEGORY` rule. Without either, the line is `NOT_CALCULATED` (`RULE_NOT_FOUND`). Unknown or unavailable products give `UNAVAILABLE`.
|
|
50
|
+
- Engineering and additional: package items marked `includedInBase` become `INCLUDED` lines with a zero amount. Options are priced by unit (`PIECE`, `ROOM_SQM`, `ROOM`, `PROJECT`).
|
|
51
|
+
|
|
52
|
+
Result fields:
|
|
53
|
+
|
|
54
|
+
| Field | Meaning |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `pricingVersion` | `data.version` |
|
|
57
|
+
| `currency` | the currency requested in the configuration |
|
|
58
|
+
| `exchangeRate` | `data.exchangeRate` as given |
|
|
59
|
+
| `lines` | `EstimateLine[]` with `id`, `section`, `status`, quantity, unit price, `amountCents`, `amountUahKopecks`, `warnings`, `error` |
|
|
60
|
+
| `totals` | `{ usdCents, uahKopecks, sections }`, sums of the line amounts |
|
|
61
|
+
| `warnings` | all line warnings (with `lineId`) plus estimate-level ones |
|
|
62
|
+
| `incomplete` | `true` if any line has status `ERROR` or `NOT_CALCULATED` |
|
|
63
|
+
|
|
64
|
+
Line statuses: `OK`, `INCLUDED`, `NOT_CALCULATED`, `UNAVAILABLE`, `ERROR`. A failed line contributes `0` to the totals and carries an `error` of the form `{ code, params? }`.
|
|
65
|
+
|
|
66
|
+
Warning codes:
|
|
67
|
+
|
|
68
|
+
- `HEATED_FLOOR_INCOMPATIBLE`: a floor product that does not suit a heated floor.
|
|
69
|
+
- `OPTION_ROOM_TYPE_NOT_ALLOWED`: an option applied to a room type it does not support; that room is left out of the quantity.
|
|
70
|
+
- `ROOMS_AREA_EXCEEDS_TOTAL`: the rooms add up to more than the total area.
|
|
71
|
+
- `EXCHANGE_RATE_UNAVAILABLE`: UAH requested but the pricing data has no rate.
|
|
72
|
+
|
|
73
|
+
### `checkLimits(configuration, limits, options?): LimitViolation[]`
|
|
74
|
+
|
|
75
|
+
Returns `{ field, min, max }` for every value outside its range. Checked fields: `totalAreaSqm`, `balconyAreaSqm` (0 to the total area), `rooms` (count), `rooms[i].lengthM`, `rooms[i].widthM`, `rooms[i].heightM`, and `options[i].quantity` for `PIECE` options (`minQuantity` defaults to 1, `maxQuantity` to unbounded). Pass `data.options` to enable the last check.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const violations = checkLimits(configuration, data.limits, data.options);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `validateFormula(input): ValidateFormulaResult`
|
|
82
|
+
|
|
83
|
+
Statically checks a formula for a given rule level. Meant for admin editing.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { validateFormula } from '@roomwise/calc-engine';
|
|
87
|
+
|
|
88
|
+
validateFormula({
|
|
89
|
+
level: 'CATEGORY',
|
|
90
|
+
kind: 'cost',
|
|
91
|
+
source: 'quantity * (price + coef.underlay)',
|
|
92
|
+
coefficientKeys: ['underlay', 'tile_glue'],
|
|
93
|
+
});
|
|
94
|
+
// { valid: true, coefficientKeys: ['underlay'], errors: [] }
|
|
95
|
+
|
|
96
|
+
validateFormula({
|
|
97
|
+
level: 'OBJECT',
|
|
98
|
+
kind: 'cost',
|
|
99
|
+
source: 'totlArea * coef.rate',
|
|
100
|
+
coefficientKeys: ['rate'],
|
|
101
|
+
});
|
|
102
|
+
// valid: false
|
|
103
|
+
// errors: [{ code: 'UNKNOWN_VARIABLE', position: 0, length: 8,
|
|
104
|
+
// params: { name: 'totlArea', suggestion: 'totalArea' } }]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `level`: `'OBJECT' | 'CATEGORY' | 'MATERIAL_TYPE'`.
|
|
108
|
+
- `kind`: `'quantity'` or `'cost'`. Only cost formulas of non-object rules may use `quantity`.
|
|
109
|
+
- `coefficientKeys`: the coefficient keys that exist; `coef.x` with another key is an error.
|
|
110
|
+
- The result's `coefficientKeys` lists the coefficients the formula references.
|
|
111
|
+
- Errors carry `code`, `position`, `length` (a span in the source) and optional `params`.
|
|
112
|
+
- The formula must type-check to a number.
|
|
113
|
+
|
|
114
|
+
### `previewRule(input): RulePreview`
|
|
115
|
+
|
|
116
|
+
Evaluates a rule against a sample, without any pricing data. It does not throw; failures are reported in `error`.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { previewRule } from '@roomwise/calc-engine';
|
|
120
|
+
|
|
121
|
+
const preview = previewRule({
|
|
122
|
+
level: 'CATEGORY',
|
|
123
|
+
quantityFormula: 'floorArea * (1 + waste)',
|
|
124
|
+
costFormula: 'quantity * (price + coef.underlay)',
|
|
125
|
+
coefficients: { underlay: 2 },
|
|
126
|
+
sample: {
|
|
127
|
+
room: {
|
|
128
|
+
roomTypeCode: 'LIVING_ROOM',
|
|
129
|
+
lengthM: 5,
|
|
130
|
+
widthM: 4,
|
|
131
|
+
heightM: 2.7,
|
|
132
|
+
heatedFloor: false,
|
|
133
|
+
},
|
|
134
|
+
product: { priceCents: 1800, unit: 'SQM', wastePercent: 5 },
|
|
135
|
+
object: {
|
|
136
|
+
totalAreaSqm: 62,
|
|
137
|
+
balconyAreaSqm: 4,
|
|
138
|
+
balconyTiles: true,
|
|
139
|
+
gas: false,
|
|
140
|
+
kitchenType: 'SEPARATE',
|
|
141
|
+
finishLevel: 'PRE_FINISH',
|
|
142
|
+
roomsCount: 1,
|
|
143
|
+
bathroomsCount: 1,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
// {
|
|
148
|
+
// quantity: 21,
|
|
149
|
+
// amountCents: 42000,
|
|
150
|
+
// variables: { floorArea: 20, waste: 0.05, price: 18, quantity: 21 },
|
|
151
|
+
// coefficients: { underlay: 2 },
|
|
152
|
+
// error: null,
|
|
153
|
+
// }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`variables` and `coefficients` in the result contain only what the formulas reference. For `OBJECT` rules `quantityFormula` must be `null` and `room`/`product` may be omitted. For the other levels `quantityFormula`, `room` and `product` are required; otherwise `error.code` is `QUANTITY_FORMULA_MISMATCH` or `SAMPLE_INCOMPLETE`.
|
|
157
|
+
|
|
158
|
+
### Lower-level formula API
|
|
159
|
+
|
|
160
|
+
- `parse(source)` returns `{ ok: true, ast }` or `{ ok: false, error }`; `FORMULA_LIMITS` holds the limits.
|
|
161
|
+
- `evaluate(ast, { variables, coefficients })` returns `{ ok: true, value }` or `{ ok: false, error }`.
|
|
162
|
+
- `FORMULA_VARIABLES`, `FORMULA_SCOPE_VARIABLES`, `FORMULA_QUANTITY_VARIABLE`, `FORMULA_FUNCTIONS`, `FORMULA_FUNCTION_TABLE`, `COEFFICIENT_NAMESPACE`.
|
|
163
|
+
- `suggestNearest(name, candidates, maxDistance = 2)` and `levenshtein(a, b)`.
|
|
164
|
+
- Types such as `FormulaNode`, `FormulaError`, `FormulaErrorCode`, `EvaluationError`, `FormulaValue`.
|
|
165
|
+
|
|
166
|
+
### Variables and money helpers
|
|
167
|
+
|
|
168
|
+
- `objectVariables(configuration)`, `roomVariables(room)`, `productVariables(product, category)` and `productVariablesOf(product, wastePercent)` build the variable scopes `estimate` uses.
|
|
169
|
+
- `toCents(dollars)` and `convertCentsToKopecks(cents, rate)`; see [Rounding and currency](#rounding-and-currency).
|
|
170
|
+
|
|
171
|
+
### Schemas and types
|
|
172
|
+
|
|
173
|
+
Every schema is a zod schema with an inferred type of the same name without the `Schema` suffix.
|
|
174
|
+
|
|
175
|
+
- Configuration: `ConfigurationSchema`, `RoomConfigurationSchema`, `RoomSelectionSchema`, `OptionSelectionSchema`. Duplicate room `clientId`s, category ids within a room and option ids are rejected.
|
|
176
|
+
- Pricing data: `PricingDataSchema`, `PricingLimitsSchema`, `BaseRateSchema`, `PricingRuleSchema`, `PricingRoomTypeSchema`, `PricingCategorySchema`, `PricingMaterialTypeSchema`, `PricingProductSchema`, `PricingPackageItemSchema`, `PricingOptionSchema`, `ExchangeRateSchema`.
|
|
177
|
+
- Result: `EstimateSchema`, `EstimateLineSchema`, `EstimateTotalsSchema`, `SectionTotalSchema`, `EstimateWarningSchema`, `EstimateMessageSchema`.
|
|
178
|
+
- Preview: `RuleSampleSchema`, `RulePreviewSchema`.
|
|
179
|
+
- Enums and primitives: `FinishLevelSchema`, `RuleLevelSchema`, `RateSourceSchema`, `SurfaceKindSchema`, `ProductUnitSchema`, `OptionUnitSchema`, `OptionKindSchema`, `RoomTypeCodeSchema`, `KitchenTypeSchema`, `CurrencySchema`, `SectionSchema`, `LineStatusSchema`, `LocalizedTextSchema`, `CentsSchema`, `RangeSchema`.
|
|
180
|
+
|
|
181
|
+
## Formula language
|
|
182
|
+
|
|
183
|
+
A formula is a single expression that produces a non-negative number. Each rule has a cost formula; material rules (`CATEGORY`, `MATERIAL_TYPE`) also have a quantity formula, evaluated first. Formulas are written in dollars; the engine converts the result to cents.
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
floorArea * (1 + waste)
|
|
187
|
+
quantity * (price + (heatedFloor ? coef.tile_glue_heated : coef.tile_glue))
|
|
188
|
+
balconyTiles ? balconyArea * coef.balcony_tile : 0
|
|
189
|
+
max(wallArea - 4, 0) * coef.paint
|
|
190
|
+
roomType == 'KITCHEN' ? ceil(floorArea / 2) : 0
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Variables
|
|
194
|
+
|
|
195
|
+
| Scope | Variable | Type |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| room | `length`, `width`, `height` | number, meters |
|
|
198
|
+
| room | `floorArea`, `perimeter`, `wallArea` | number: `length * width`, `2 * (length + width)`, `perimeter * height` |
|
|
199
|
+
| room | `heatedFloor` | boolean |
|
|
200
|
+
| room | `roomType` | `LIVING_ROOM`, `BEDROOM`, `KITCHEN`, `KITCHEN_LIVING`, `BATHROOM` |
|
|
201
|
+
| product | `price` | number, dollars per unit |
|
|
202
|
+
| product | `waste` | number, a fraction (5% is `0.05`): the product override, else the category value |
|
|
203
|
+
| product | `unit` | `SQM`, `LINEAR_M`, `PIECE` |
|
|
204
|
+
| object | `totalArea`, `balconyArea` | number |
|
|
205
|
+
| object | `balconyTiles`, `gas` | boolean |
|
|
206
|
+
| object | `kitchenType` | `SEPARATE`, `KITCHEN_LIVING` |
|
|
207
|
+
| object | `finishLevel` | `ROUGH`, `PRE_FINISH` |
|
|
208
|
+
| object | `roomsCount`, `bathroomsCount` | number (living rooms, bedrooms and kitchen-living rooms count as rooms) |
|
|
209
|
+
| cost formulas only | `quantity` | the result of the quantity formula |
|
|
210
|
+
|
|
211
|
+
Availability by rule level:
|
|
212
|
+
|
|
213
|
+
- `OBJECT`: object variables only. `quantity` is not available.
|
|
214
|
+
- `CATEGORY` and `MATERIAL_TYPE`: room, product and object variables. `quantity` is available in cost formulas only.
|
|
215
|
+
|
|
216
|
+
Coefficients are read as `coef.<key>`; keys come from the pricing data. The cost formula receives `quantity` unrounded, while the line shows it rounded to 3 decimals.
|
|
217
|
+
|
|
218
|
+
### Syntax
|
|
219
|
+
|
|
220
|
+
- Numbers and booleans (`true`, `false`). String literals are allowed only when comparing an enum variable.
|
|
221
|
+
- Arithmetic: `+ - * /` and unary `-`. Comparison: `< > <= >= == !=`. Logic: `&& || !`. Ternary: `test ? a : b`. Parentheses.
|
|
222
|
+
- `==` and `!=` work between an enum variable and a string literal (`roomType == 'BATHROOM'`), or between two values of the same type. The literal must be a valid value of that variable.
|
|
223
|
+
- Functions: `min(a, b, ...)` and `max(a, b, ...)` (one or more arguments), `round(x)`, `ceil(x)`, `floor(x)`. `round` rounds halves up.
|
|
224
|
+
- Anything else is a syntax error: `%`, `**`, bitwise operators, `null`, `in`, `instanceof`, unary `+`, indexing (`a[0]`), semicolons and sequences, method calls, other `Math` functions.
|
|
225
|
+
|
|
226
|
+
### Limits
|
|
227
|
+
|
|
228
|
+
| Limit | Value |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| Source length | 500 characters |
|
|
231
|
+
| AST depth | 32 |
|
|
232
|
+
| AST nodes | 200 |
|
|
233
|
+
|
|
234
|
+
### Errors from `validateFormula`
|
|
235
|
+
|
|
236
|
+
| Code | When |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `FORMULA_SYNTAX` | Not parsable, empty, or a type error. `params.detail` for parser failures; `params.reason` is `TYPE_MISMATCH` (with `expected`), `ARGUMENT_COUNT` (`min`, `max`) or `UNKNOWN_ENUM_VALUE` (`value`, `variable`) |
|
|
239
|
+
| `FORMULA_TOO_COMPLEX` | Over a limit; `params: { limit: 'LENGTH' \| 'DEPTH' \| 'NODES', max }` |
|
|
240
|
+
| `UNKNOWN_VARIABLE` | Name is not a variable; `params.suggestion` holds the nearest valid name when one is within edit distance 2 |
|
|
241
|
+
| `VARIABLE_NOT_ALLOWED_AT_LEVEL` | A real variable used on a level that does not provide it, such as `price` in an `OBJECT` rule; `params: { variable, level }` |
|
|
242
|
+
| `UNKNOWN_COEFFICIENT` | `coef.x` with `x` not in `coefficientKeys`; `params: { key }` |
|
|
243
|
+
| `UNKNOWN_FUNCTION` | Unknown function name; `params: { name }` |
|
|
244
|
+
| `QUANTITY_NOT_ALLOWED` | `quantity` in a quantity formula or in an `OBJECT` rule |
|
|
245
|
+
|
|
246
|
+
Rejected examples:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
floorArea ** 2 FORMULA_SYNTAX
|
|
250
|
+
sqrt(floorArea) UNKNOWN_FUNCTION
|
|
251
|
+
price * qty UNKNOWN_VARIABLE
|
|
252
|
+
price * totalArea VARIABLE_NOT_ALLOWED_AT_LEVEL (OBJECT level)
|
|
253
|
+
quantity * price QUANTITY_NOT_ALLOWED (in a quantity formula)
|
|
254
|
+
roomType == 'GARAGE' FORMULA_SYNTAX (UNKNOWN_ENUM_VALUE)
|
|
255
|
+
heatedFloor + 1 FORMULA_SYNTAX (TYPE_MISMATCH)
|
|
256
|
+
roomType FORMULA_SYNTAX (TYPE_MISMATCH: the result must be a number)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Runtime errors
|
|
260
|
+
|
|
261
|
+
Static validation does not catch everything. At evaluation time a line fails with `FORMULA_RUNTIME_ERROR` and `params.reason` set to `DIVISION_BY_ZERO`, `NOT_FINITE`, `TYPE_MISMATCH` or `NEGATIVE` (a rule result must not be negative). A line above 10,000,000,000 cents fails with `AMOUNT_OUT_OF_RANGE`.
|
|
262
|
+
|
|
263
|
+
## Rounding and currency
|
|
264
|
+
|
|
265
|
+
All money is integer cents (USD) or kopecks (UAH); amounts in the output are never floating point.
|
|
266
|
+
|
|
267
|
+
- Each estimate line is rounded to whole cents with `toCents`: `Math.round(Number((dollars * 100).toPrecision(15)))`. The `toPrecision(15)` step removes binary noise, so `1.005` becomes 101 cents rather than 100. Halves round up. Negative or non-finite values throw `RangeError`.
|
|
268
|
+
- Section totals and the grand total are the sum of the rounded lines. Nothing is rounded again at the total level.
|
|
269
|
+
- UAH: each line is converted on its own with `convertCentsToKopecks(cents, rate)`, and the UAH totals are the sums of those kopeck amounts. The rate is a string with up to four decimals (the pricing data format is `"41.2500"`), handled as a scaled integer with `BigInt`, so the result is exact with halves rounded up. An invalid rate throws `RangeError`.
|
|
270
|
+
- With no exchange rate, `amountUahKopecks` and `uahKopecks` are `null`; if the configuration asks for UAH, the estimate also carries the `EXCHANGE_RATE_UNAVAILABLE` warning.
|
|
271
|
+
- The quantity shown on a line is rounded to 3 decimals; formulas use the unrounded value.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { convertCentsToKopecks, toCents } from '@roomwise/calc-engine';
|
|
275
|
+
|
|
276
|
+
toCents(1.005); // 101
|
|
277
|
+
convertCentsToKopecks(10_000, '41.2500'); // 412_500
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Versioning and releases
|
|
281
|
+
|
|
282
|
+
The package follows semver. A release is published to npm by GitHub Actions from a tag named `calc-engine@X.Y.Z`, for example `calc-engine@0.1.0`. The tag must point to a commit on `main`, and its version must match `version` in `package.json`; both are updated together with [CHANGELOG.md](./CHANGELOG.md) in the pull request that prepares the release. Only stable `X.Y.Z` versions are published. The workflow lints, typechecks, tests and builds the package before publishing it with provenance. It authenticates with the `NPM_TOKEN` secret until the repository variable `NPM_TRUSTED_PUBLISHING` is set to `true`; from then on it uses npm trusted publishing and the token can be revoked.
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
[MIT](./LICENSE)
|