statelyai 0.15.1 → 0.17.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/README.md +47 -21
- package/dist/bin.mjs +1 -1
- package/dist/{cli-CE7KhcOJ.mjs → cli-BBG--n3O.mjs} +10556 -10334
- package/dist/coverageWorker.d.mts +1 -0
- package/dist/coverageWorker.mjs +178 -0
- package/dist/index.d.mts +65 -17
- package/dist/index.mjs +2 -2
- package/dist/skills/setup-behavior-ci/SKILL.md +25 -0
- package/dist/skills/setup-behavior-ci/agents/openai.yaml +3 -0
- package/dist/skills/setup-behavior-ci/assets/campaign-adapter.mjs +203 -0
- package/dist/skills/setup-behavior-ci/assets/campaign-producer.mjs +63 -0
- package/dist/skills/setup-behavior-ci/assets/config.json +39 -0
- package/dist/skills/setup-behavior-ci/assets/estimated-model.json +12 -0
- package/dist/skills/setup-behavior-ci/assets/executable-config.json +42 -0
- package/dist/skills/setup-behavior-ci/assets/minimal.campaign.mjs +50 -0
- package/dist/skills/setup-behavior-ci/assets/plan.json +86 -0
- package/dist/skills/setup-behavior-ci/assets/vitest-adapter.mjs +47 -0
- package/dist/skills/setup-behavior-ci/assets/workflow.yml +28 -0
- package/dist/skills/setup-behavior-ci/references/contract.md +31 -0
- package/dist/skills/setup-behavior-ci/references/executable.md +41 -0
- package/dist/testing.d.mts +1 -0
- package/dist/testing.mjs +3 -0
- package/package.json +13 -8
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import {
|
|
5
|
+
campaignExitCode,
|
|
6
|
+
modelTestReportSchema,
|
|
7
|
+
reportReplays,
|
|
8
|
+
runCampaignFile,
|
|
9
|
+
} from 'statelyai/testing';
|
|
10
|
+
import normalize from './campaign-adapter.mjs';
|
|
11
|
+
|
|
12
|
+
const { positionals, values } = parseArgs({
|
|
13
|
+
allowPositionals: true,
|
|
14
|
+
options: { report: { type: 'string' }, budget: { type: 'string' } },
|
|
15
|
+
});
|
|
16
|
+
if (positionals.length !== 1 || !values.report)
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Usage: node campaign-producer.mjs <campaign.mjs> --report <file> [--budget <budget.json>]',
|
|
19
|
+
);
|
|
20
|
+
const file = path
|
|
21
|
+
.relative(process.cwd(), path.resolve(positionals[0]))
|
|
22
|
+
.split(path.sep)
|
|
23
|
+
.join('/');
|
|
24
|
+
if (
|
|
25
|
+
!file ||
|
|
26
|
+
file.startsWith('../') ||
|
|
27
|
+
path.isAbsolute(file) ||
|
|
28
|
+
!file.endsWith('.mjs')
|
|
29
|
+
)
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Campaign must be a repository-relative .mjs file; run from repository root.',
|
|
32
|
+
);
|
|
33
|
+
const budget = values.budget
|
|
34
|
+
? JSON.parse(await readFile(values.budget, 'utf8'))
|
|
35
|
+
: undefined;
|
|
36
|
+
const report = modelTestReportSchema.parse(
|
|
37
|
+
await runCampaignFile(file, {
|
|
38
|
+
budget,
|
|
39
|
+
onLog: (text) => process.stderr.write(text),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
const output = path.resolve(
|
|
43
|
+
process.env.STATELY_BEHAVIOR_OUTPUT ?? path.dirname(values.report),
|
|
44
|
+
);
|
|
45
|
+
const traces = path.join(output, 'traces');
|
|
46
|
+
await mkdir(traces, { recursive: true });
|
|
47
|
+
const writeJson = (filename, value) =>
|
|
48
|
+
writeFile(filename, `${JSON.stringify(value, null, 2)}\n`);
|
|
49
|
+
await writeJson(path.join(traces, 'campaign-report.json'), report);
|
|
50
|
+
await writeJson(path.join(traces, 'campaign-process.json'), {
|
|
51
|
+
nativeExitCode: campaignExitCode(report),
|
|
52
|
+
});
|
|
53
|
+
for (const [index, replay] of reportReplays(report).entries())
|
|
54
|
+
await writeJson(path.join(traces, `replay-${index + 1}.json`), replay);
|
|
55
|
+
await mkdir(path.dirname(path.resolve(values.report)), { recursive: true });
|
|
56
|
+
const envelope = { file, report };
|
|
57
|
+
await writeJson(values.report, envelope);
|
|
58
|
+
const results = normalize(envelope);
|
|
59
|
+
process.exitCode = results.some(
|
|
60
|
+
(result) => result.outcome === 'failed' || result.outcome === 'error',
|
|
61
|
+
)
|
|
62
|
+
? 1
|
|
63
|
+
: 0;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"name": "Selected behavior",
|
|
4
|
+
"spec": ".behavior-ci/feature/spec.json",
|
|
5
|
+
"model": ".behavior-ci/feature/model.json",
|
|
6
|
+
"adapter": ".behavior-ci/feature/vitest-adapter.mjs",
|
|
7
|
+
"stateRoot": ".behavior-ci/feature/state",
|
|
8
|
+
"baseline": null,
|
|
9
|
+
"command": {
|
|
10
|
+
"executable": "pnpm",
|
|
11
|
+
"args": [
|
|
12
|
+
"exec",
|
|
13
|
+
"vitest",
|
|
14
|
+
"run",
|
|
15
|
+
"src/feature.test.ts",
|
|
16
|
+
"--reporter=json",
|
|
17
|
+
"--outputFile={report}"
|
|
18
|
+
],
|
|
19
|
+
"cwd": "."
|
|
20
|
+
},
|
|
21
|
+
"inputs": [
|
|
22
|
+
"src/feature.ts",
|
|
23
|
+
"src/feature.test.ts",
|
|
24
|
+
"vitest.config.ts",
|
|
25
|
+
".github/workflows/behavior.yml"
|
|
26
|
+
],
|
|
27
|
+
"checks": [
|
|
28
|
+
{
|
|
29
|
+
"id": "feature.check",
|
|
30
|
+
"file": "src/feature.test.ts",
|
|
31
|
+
"fullName": "exact native reporter fullName"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"timeoutMs": 120000,
|
|
35
|
+
"assurance": {
|
|
36
|
+
"plan": ".behavior-ci/feature/plan.json",
|
|
37
|
+
"calibration": ".behavior-ci/feature/calibration.json"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{ "id": "feature", "type": "node", "parentId": null, "data": { "key": "feature" } },
|
|
5
|
+
{ "id": "feature.off", "type": "node", "parentId": "feature", "data": { "key": "off" } },
|
|
6
|
+
{ "id": "feature.on", "type": "node", "parentId": "feature", "data": { "key": "on" } }
|
|
7
|
+
],
|
|
8
|
+
"edges": [
|
|
9
|
+
{ "id": "feature.turn-on", "type": "edge", "sourceId": "feature.off", "targetId": "feature.on", "data": { "eventType": "ON" } }
|
|
10
|
+
],
|
|
11
|
+
"data": {}
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"name": "Executable campaign behavior",
|
|
4
|
+
"spec": ".behavior-ci/feature/spec.json",
|
|
5
|
+
"model": ".behavior-ci/feature/model.json",
|
|
6
|
+
"adapter": ".behavior-ci/feature/campaign-adapter.mjs",
|
|
7
|
+
"stateRoot": ".behavior-ci/feature/state",
|
|
8
|
+
"baseline": null,
|
|
9
|
+
"command": {
|
|
10
|
+
"executable": "node",
|
|
11
|
+
"args": [
|
|
12
|
+
".behavior-ci/feature/campaign-producer.mjs",
|
|
13
|
+
".behavior-ci/feature/feature.campaign.mjs",
|
|
14
|
+
"--report",
|
|
15
|
+
"{report}"
|
|
16
|
+
],
|
|
17
|
+
"cwd": "."
|
|
18
|
+
},
|
|
19
|
+
"inputs": [
|
|
20
|
+
"src/feature.ts",
|
|
21
|
+
".behavior-ci/feature/feature.campaign.mjs",
|
|
22
|
+
".behavior-ci/feature/sut.mjs",
|
|
23
|
+
".github/workflows/behavior.yml"
|
|
24
|
+
],
|
|
25
|
+
"checks": [
|
|
26
|
+
{
|
|
27
|
+
"id": "feature.assertion",
|
|
28
|
+
"file": ".behavior-ci/feature/feature.campaign.mjs",
|
|
29
|
+
"fullName": "campaign.assertion [\"feature.requirement\",\"feature.assertion\"]"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "feature.execution",
|
|
33
|
+
"file": ".behavior-ci/feature/feature.campaign.mjs",
|
|
34
|
+
"fullName": "campaign.execution"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"timeoutMs": 120000,
|
|
38
|
+
"assurance": {
|
|
39
|
+
"plan": ".behavior-ci/feature/plan.json",
|
|
40
|
+
"calibration": ".behavior-ci/feature/calibration.json"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createMachine, defineCampaign, defineRequirements, fc } from "statelyai/testing";
|
|
3
|
+
|
|
4
|
+
const requirements = defineRequirements({
|
|
5
|
+
"TOGGLE-001": ["initial.off", "on.turns-on"],
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export default defineCampaign({
|
|
9
|
+
id: "minimal-toggle",
|
|
10
|
+
intent: "A new toggle starts off; ON turns it on.",
|
|
11
|
+
scope: "API-shape example; replace with the real application adapter.",
|
|
12
|
+
assumptions: ["One ON operation; no persistence or concurrency."],
|
|
13
|
+
evidence: ["The explicit intent above is example-only."],
|
|
14
|
+
modelVersion: "1",
|
|
15
|
+
requirements,
|
|
16
|
+
model: createMachine({
|
|
17
|
+
initial: "off",
|
|
18
|
+
states: {
|
|
19
|
+
off: { on: { ON: () => ({ target: "on" }) } },
|
|
20
|
+
on: {},
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
options: {
|
|
24
|
+
events: {
|
|
25
|
+
ON: [{ case: "valid-on", generate: fc.constant({}) }],
|
|
26
|
+
},
|
|
27
|
+
sut: {
|
|
28
|
+
create() {
|
|
29
|
+
let value = "off";
|
|
30
|
+
return {
|
|
31
|
+
send(event) {
|
|
32
|
+
if (event.type === "ON") value = "on";
|
|
33
|
+
},
|
|
34
|
+
read() {
|
|
35
|
+
return value;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
states: {
|
|
41
|
+
"*": async (snapshot, session) => {
|
|
42
|
+
const actual = await session.read();
|
|
43
|
+
const check = snapshot.value === "off" ? "initial.off" : "on.turns-on";
|
|
44
|
+
await requirements.assert(check, () => {
|
|
45
|
+
assert.equal(actual, snapshot.value);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"runnerInputs": [
|
|
4
|
+
"vitest.config.ts"
|
|
5
|
+
],
|
|
6
|
+
"inputs": [
|
|
7
|
+
{
|
|
8
|
+
"kind": "mapped",
|
|
9
|
+
"category": "user",
|
|
10
|
+
"source": {
|
|
11
|
+
"authority": "cited-requirement",
|
|
12
|
+
"citation": "requirements.md#feature"
|
|
13
|
+
},
|
|
14
|
+
"requirementIds": [
|
|
15
|
+
"feature"
|
|
16
|
+
],
|
|
17
|
+
"checkIds": [
|
|
18
|
+
"feature.check"
|
|
19
|
+
],
|
|
20
|
+
"entityIds": [
|
|
21
|
+
"feature"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"kind": "gap",
|
|
26
|
+
"category": "host-update",
|
|
27
|
+
"source": {
|
|
28
|
+
"authority": "inference",
|
|
29
|
+
"citation": "Review external state owners in source"
|
|
30
|
+
},
|
|
31
|
+
"reason": "Discover whether host replacement can invalidate current selections"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "gap",
|
|
35
|
+
"category": "async-result",
|
|
36
|
+
"source": {
|
|
37
|
+
"authority": "inference",
|
|
38
|
+
"citation": "Review asynchronous completion handlers"
|
|
39
|
+
},
|
|
40
|
+
"reason": "Discover stale and out-of-order completions"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"kind": "gap",
|
|
44
|
+
"category": "cancellation",
|
|
45
|
+
"source": {
|
|
46
|
+
"authority": "inference",
|
|
47
|
+
"citation": "Review cancellation and abort handlers"
|
|
48
|
+
},
|
|
49
|
+
"reason": "Discover cancellation during pending work"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"kind": "gap",
|
|
53
|
+
"category": "timer",
|
|
54
|
+
"source": {
|
|
55
|
+
"authority": "inference",
|
|
56
|
+
"citation": "Review timers and deadlines"
|
|
57
|
+
},
|
|
58
|
+
"reason": "Discover expiry and clock-driven transitions"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"kind": "gap",
|
|
62
|
+
"category": "server-event",
|
|
63
|
+
"source": {
|
|
64
|
+
"authority": "inference",
|
|
65
|
+
"citation": "Review inbound server events"
|
|
66
|
+
},
|
|
67
|
+
"reason": "Discover retries, duplicate deliveries and server updates"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"kind": "gap",
|
|
71
|
+
"category": "lifecycle",
|
|
72
|
+
"source": {
|
|
73
|
+
"authority": "inference",
|
|
74
|
+
"citation": "Review initialization, teardown and restoration"
|
|
75
|
+
},
|
|
76
|
+
"reason": "Discover restart and disposal behavior"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"coverage": {
|
|
80
|
+
"kind": "selected-checks"
|
|
81
|
+
},
|
|
82
|
+
"dispositions": [],
|
|
83
|
+
"authorityInputs": [
|
|
84
|
+
"requirements.md"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export default function normalize(report, { root }) {
|
|
4
|
+
if (!report || !Array.isArray(report.testResults) || typeof report.success !== "boolean") {
|
|
5
|
+
throw new Error("Expected native Vitest JSON report");
|
|
6
|
+
}
|
|
7
|
+
if (report.numRuntimeErrorTestSuites > 0 || report.unhandledErrors?.length) {
|
|
8
|
+
throw new Error("Vitest reported runtime errors");
|
|
9
|
+
}
|
|
10
|
+
const results = [];
|
|
11
|
+
for (const suite of report.testResults) {
|
|
12
|
+
if (typeof suite.name !== "string" || !Array.isArray(suite.assertionResults)) {
|
|
13
|
+
throw new Error("Malformed Vitest suite");
|
|
14
|
+
}
|
|
15
|
+
if (suite.status === "failed" && suite.assertionResults.length === 0) {
|
|
16
|
+
throw new Error(`Suite failed before tests ran: ${suite.name}`);
|
|
17
|
+
}
|
|
18
|
+
for (const test of suite.assertionResults) {
|
|
19
|
+
if (typeof test.fullName !== "string" || !Array.isArray(test.failureMessages)) {
|
|
20
|
+
throw new Error("Malformed Vitest assertion");
|
|
21
|
+
}
|
|
22
|
+
const outcome = {
|
|
23
|
+
passed: "passed",
|
|
24
|
+
failed: "failed",
|
|
25
|
+
pending: "skipped",
|
|
26
|
+
skipped: "skipped",
|
|
27
|
+
todo: "skipped",
|
|
28
|
+
}[test.status];
|
|
29
|
+
if (!outcome) throw new Error(`Unknown Vitest status: ${test.status}`);
|
|
30
|
+
results.push({
|
|
31
|
+
file: path.relative(root, suite.name).split(path.sep).join("/"),
|
|
32
|
+
fullName: test.fullName,
|
|
33
|
+
outcome,
|
|
34
|
+
detail: test.failureMessages.join("\n").slice(0, 4000) || `Vitest: ${test.status}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const failed = results.filter((r) => r.outcome === "failed").length;
|
|
39
|
+
if (
|
|
40
|
+
report.numFailedTests !== failed ||
|
|
41
|
+
report.numTotalTests !== results.length ||
|
|
42
|
+
report.success === failed > 0
|
|
43
|
+
) {
|
|
44
|
+
throw new Error("Vitest report totals/success disagree with assertions");
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Behavior evidence
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
jobs:
|
|
8
|
+
behavior:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
|
+
with:
|
|
14
|
+
node-version: "24.17.0"
|
|
15
|
+
- uses: pnpm/action-setup@v4
|
|
16
|
+
- run: pnpm install --frozen-lockfile
|
|
17
|
+
- name: Check behavior
|
|
18
|
+
run: pnpm exec statelyai behavior check --config .behavior-ci/feature/config.json --output .scratch/behavior-run
|
|
19
|
+
- name: Job summary
|
|
20
|
+
if: always()
|
|
21
|
+
run: cat .scratch/behavior-run/summary.md >> "$GITHUB_STEP_SUMMARY"
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
if: always()
|
|
24
|
+
with:
|
|
25
|
+
name: behavior-${{ github.run_id }}-${{ github.run_attempt }}
|
|
26
|
+
path: .scratch/behavior-run
|
|
27
|
+
include-hidden-files: true
|
|
28
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Checked contract
|
|
2
|
+
|
|
3
|
+
The source of truth is `packages/cli/src/behaviorCi.ts` (`behaviorCiConfigSchema`, `behaviorCiResultsSchema`) and the public `@statelyai/mcp/loop` behavior record contract. JSON config is strict; unknown keys fail.
|
|
4
|
+
|
|
5
|
+
New config uses `schemaVersion: 2` and `assurance: {plan,calibration}` plus `name`, `spec`, `model`, `adapter`, `stateRoot`, `baseline`, `command: {executable,args,cwd}`, `inputs`, `checks`, `timeoutMs`. Paths are repository-relative without traversal or symlinks. `cwd: "."` is valid. `baseline` is initially null and subsequently `{machineId,versionId,sha256}` written by adoption. `checks` map stable check `id` to exact repository-relative `file` and native reporter `fullName`. A test maps once. Command argv includes exactly one `{report}` placeholder, usually `--outputFile={report}`. Spawn uses no shell. Timeout range is 100–600000 ms.
|
|
6
|
+
|
|
7
|
+
The `.mjs` module exports a default function `(rawReport, {root,cwd}) => results`, optionally async. Each result is `{file,fullName,outcome,detail}`. Outcomes are `passed`, `failed`, `skipped`, `error`. Detail is nonempty and at most 4000 characters. Keep the adapter self-contained except Node built-ins; its source is part of the binding. Config owns check IDs, never the adapter. The Vitest adapter handles native Vitest JSON. The executable adapter handles the native campaign envelope described in [executable mode](executable.md); this is not a framework registry.
|
|
8
|
+
|
|
9
|
+
`spec.json` contains `requirements` and `retire`. Each requirement is `{id,text,source:{authority,citation},entityIds,checks:[{id,acceptance}]}`. Authority is `cited-requirement`, `observation`, `inference` or `assumption`. Retirement is `{kind:"requirement"|"check",id,reason}`. Core omission retains prior requirements and checks. Record and requirement limits remain core limits, including 100 reports and 200 bound files; this pilot does not compact history automatically.
|
|
10
|
+
|
|
11
|
+
`model.json` is XGraph JSON. Start from the structurally tested [estimated model](../assets/estimated-model.json), then adapt it to the actual feature. Nodes use `parentId` for hierarchy; transitions use `sourceId` and `targetId`, not `source` and `target`. Use the public graph schema for other fields. Generated graph meaning deserves review; structural validity alone is insufficient.
|
|
12
|
+
|
|
13
|
+
Output is summary.md, run.json, the captured config.json, manifest.json, process.json, stdout/stderr, raw report.json and a `store/.statelyai` copy in the existing FileStore format. Run metadata references exact immutable model/record versions. Current verdict covers every retained active check on this run; historical reports and core aggregate gaps are displayed separately. Tests, sources, adapter, model, specification, config, selected manifests/locks and baseline files are hashed before and after execution. Runner metadata includes CLI package version and the SHA-256 of the executing source/bundle; built ignored distribution files need not become tracked application inputs. No transitive completeness claim. Core execution remains reported and review identity unavailable.
|
|
14
|
+
|
|
15
|
+
Inputs must be tracked; local staged/dirty bytes are allowed and labeled. CI requires clean bound inputs and actual HEAD equal to GITHUB_SHA. GitHub env is declared metadata, not authenticated proof. Authenticate the run, commit, logs and downloaded artifact externally before adopting. Artifact checksums detect corruption, not a malicious trusted runner forging an entire artifact.
|
|
16
|
+
|
|
17
|
+
Adoption reads data only; it never imports the downloaded adapter or runs artifact commands. Exact checked parent, record/model digests, checksums, recorded Git bytes (or unchanged local dirty bytes) and immutable file conflicts must validate. Green/red evidence can advance the baseline; setup/error evidence cannot. Repeated adoption of the same next ref is idempotent. CI never changes checked baseline.
|
|
18
|
+
|
|
19
|
+
Setup maps every selected native case, including existing public tests, using exact `(file, fullName)` pairs. A failed selected case outside those mappings produces ERROR, not an adoptable FAIL. Preserve that artifact, complete the mappings without weakening obligations, and rerun before application repair. A mapped native failure is a reported test failure; its detail may describe an assertion, runtime exception or timeout. The CLI does not establish a causal diagnosis from the failure status alone.
|
|
20
|
+
|
|
21
|
+
Schema 2 requires a discovery plan with `schemaVersion: 1`, `runnerInputs`, `authorityInputs`, `inputs`, `coverage` and `dispositions`. List checked authoritative intent documents in nonempty `authorityInputs`; changed intent requires recalibration even when structured spec is unchanged. List command producer/configuration modules in `runnerInputs`; mapped test files and adapter are automatic. All seven input categories must occur: `user`, `host-update`, `async-result`, `cancellation`, `timer`, `server-event`, `lifecycle`. Each entry has `source: {authority,citation}`. A `kind: "mapped"` entry adds nonempty `requirementIds`, `checkIds` and `entityIds`; referenced IDs must exist and selected checks must belong to the listed requirements. A `kind: "gap"` or `"not-applicable"` entry adds a reason. Both coverage modes retain disclosed discovery gaps. Complete bounded path retrieval does not establish complete feature input coverage. Coverage is `{kind:"selected-checks"}` or `{kind:"bounded-paths",artifact,caseMappings:[{pathId,checkIds}]}`. Complete bounded-path claims require complete bound generation evidence and a native mapping for every path; generated paths are not themselves executed cases.
|
|
22
|
+
|
|
23
|
+
`behavior calibrate --config <config> --output <fresh-directory>` runs the same deterministic native process as `check`, then saves `calibration.json`. The receipt includes original manifest, snapshots of original selected bytes, actual process outcome, raw native report, normalized results, contract digest and explicit implementation-defect dispositions. Checksums detect corruption; receipts do not attest that a trusted producer told the truth. `adopt` verifies the receipt against the current contract and local adapter, and installs it at the checked calibration path. It never executes downloaded code or artifact commands. It can import the current trusted repository adapter to verify native normalization. A failed calibration check requires a plan disposition `{checkId,kind:"implementation-defect",reason,source}` before it can be adopted. Skipped/missing/error calibration outcomes cannot establish a baseline. Red history remains red; a disposition never changes a failure to a pass.
|
|
24
|
+
|
|
25
|
+
`check` requires a valid receipt for schema 2. Model, spec, plan, authoritative intent files, mappings, native test producer files, command, adapter or runner identity changes invalidate it. Ordinary implementation edits do not invalidate the contract. The receipt and run store the original checkout root for native report normalization when CI or adoption uses another checkout path. Receipt snapshots keep original revision evidence inspectable even in shallow CI checkouts. Run output includes the used receipt. Schema 1 check/adopt remains compatible but reports assurance unavailable. Schema 2 rejects every extra selected native `(file,fullName)` pair, including passing cases.
|
|
26
|
+
|
|
27
|
+
A bounded-path archive is `{schemaVersion:1,source:{graph,kind:"simple"|"shortest"},pages:[...actual generate_graph_paths structuredContent]}`. Collect every continuation until null. Validation requires identical model, contiguous offsets, canonical cursor chain, matching generation hash, valid modeled states/transitions/events, complete retrieval, reported complete generation and exact path-to-native-check mappings. This is complete retrieval of the declared bounded generator result. Generator completeness is reported, not independently recomputed or attested. Checksums cannot authenticate a malicious trusted producer that rewrites an entire archive. The PR summary states this limit and separately counts generated paths and native passed/failed cases. The runner executable/version, Node version and CLI code digest are contract-bound; platform and architecture remain original/current evidence rather than invalidating the contract when CI uses another OS.
|
|
28
|
+
|
|
29
|
+
Calibration destinations must be isolated from every selected file, selected directory, auto-selected manifest/lockfile, stateRoot and run output. Parent/child overlaps and dot aliases are refused before native execution or receipt installation.
|
|
30
|
+
|
|
31
|
+
The native child receives `STATELY_BEHAVIOR_OUTPUT`, always overridden to its unique absolute run directory. Producers may write diagnostic files below its `traces/` directory on both success and failure. Keep assertion semantics independent of this path. The CLI hashes these files with the report and validates their complete inventory during adoption; extra, removed or changed trace files invalidate the artifact. Limits and symlink rules also apply to traces. Producers that ignore the variable are unchanged.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Executable campaign mode
|
|
2
|
+
|
|
3
|
+
Use for authored model-based tests of real functions, stores, services or UI. Keep an XGraph estimate in `model.json` for the existing behavior record. The executable XState model is a separate trusted `.mjs` program. Reconcile both against cited requirements; neither is automatically ground truth.
|
|
4
|
+
|
|
5
|
+
## Author the campaign
|
|
6
|
+
|
|
7
|
+
Install a pinned `statelyai` dependency in the application project, with compatible Node and package manager pins and a checked lockfile. An installed global CLI does not supply application imports. When validating an unpublished candidate, install the packed candidate and its pinned dependencies, record its SHA-256 and installation provenance, and call the result packed-candidate proof. Do not claim registry availability or publication from a local pack/install.
|
|
8
|
+
|
|
9
|
+
Use the runnable [minimal campaign](../assets/minimal.campaign.mjs) for the native callback and event descriptor syntax. Its in-memory toggle demonstrates API shape only; replace it with the application model and real adapter. Write `defineCampaign({...})`, a real model, native event descriptors and fast-check generators. Import `createMachine`, `defineCampaign`, `defineRequirements` and `fc` from `statelyai/testing`. A descriptor such as `{case: 'invalid-empty', generate: fc.constant({value: ''})}` belongs under its actual model event in `options.events`; add `when` or native `pick` only where source/spec establishes state-dependent applicability. Use application source and authoritative requirements to determine expected states and transitions. Include boundary and invalid payloads, repeated/ignored operations, cancellation, retries, timer/lifecycle interactions, and cross-owner sequences that apply to the feature. Record omissions and assumptions in the discovery plan.
|
|
10
|
+
|
|
11
|
+
Create a `defineRequirements` registry with stable requirement IDs mapped to nonempty arrays of globally unique assertion IDs. Pass that registry as `requirements` to `defineCampaign`. A requirement needing Enter and Blur uses distinct assertion IDs; one must never satisfy the other. Put runtime assertions inside awaited native `invariant`, state callbacks or `sut.check` callbacks:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const requirements = defineRequirements({
|
|
15
|
+
'FORM-001': ['validation.invalid-email'],
|
|
16
|
+
});
|
|
17
|
+
// Within a real assertion callback, after reading the application:
|
|
18
|
+
await requirements.assert('validation.invalid-email', () => {
|
|
19
|
+
assert.equal(observed.validation, 'Enter a valid email');
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The body must complete with `undefined` on success and throw on failure. Boolean predicates are not assertion bodies. Await or return `requirements.assert`, including nested asynchronous work. Only consumed assertions whose bodies complete within the active native callback count. Ignored, pending and late assertions make the run inconclusive. Assertions outside these callbacks, setup-only assertions, labels and phase PASS do not establish requirement execution. The tracker does not infer promise ownership: a detached assertion that finishes before the callback returns counts its actual result, and a recorded failure still fails the phase even if the detached task catches it. Counts include exploration and shrinking invocations; they are neither distinct cases nor attribution to the final shrunk witness. A saved replay supplies fresh evidence for only that concrete sequence and does not close unrelated full-campaign gaps.
|
|
24
|
+
|
|
25
|
+
Use fresh isolated SUT sessions for every path, trial, shrink and replay. `sut.send` drives the real app; `sut.read` observes it. Keep behavioral assertions in `sut.check`, `invariant`, state callbacks or model projections. Browser locator, navigation, setup and service transport errors remain infrastructure errors. Release contexts, services and resources in `dispose` and campaign `cleanup`. Do not add an Action DSL, replace the native engine or run an LLM in CI.
|
|
26
|
+
|
|
27
|
+
## Bridge into existing behavior CI
|
|
28
|
+
|
|
29
|
+
Copy [campaign-producer.mjs](../assets/campaign-producer.mjs) and [campaign-adapter.mjs](../assets/campaign-adapter.mjs) together into the checked feature directory. The producer imports the local installed package, calls `runCampaignFile`, validates the native report, then writes the `{file, report}` envelope at `--report {report}`. Its `file` is the repository-relative campaign path. Run from the repository root. An optional `--budget <budget.json>` supplies native budget overrides; check and bind that file. The producer writes the native report, native exit code and portable replays below `STATELY_BEHAVIOR_OUTPUT/traces`. Startup, malformed data or missing bindings are setup errors.
|
|
30
|
+
|
|
31
|
+
Adapt [executable-config.json](../assets/executable-config.json). Each declared assertion has exactly one selected `(file, fullName)` pair. For requirement `FORM-001` and assertion `validation.invalid-email`, the stable fullName is `campaign.assertion ["FORM-001","validation.invalid-email"]`. It is generated with `campaign.assertion ${JSON.stringify([requirementId, checkId])}`. Every campaign also has `campaign.execution`, mapped to an ancillary observation check, with its own requirement in `spec.json`. The config check IDs are stable specification IDs; the adapter never assigns them. Generate the complete inventory by importing `assertionFullName` and `executionFullName` from the copied adapter and reading the registry declarations. Then compare with actual adapter output. Selected schema-2 inventory must match exactly, including the execution guard. Missing declarations or phase binding metadata is invalid evidence and cannot pass.
|
|
32
|
+
|
|
33
|
+
The adapter folds actual assertion counts across paths and property. Any failed invocation is `failed`; positive passed count with no failure is `passed`; zero is `skipped`. Every phase must contain the exact declaration/check inventory, including zeros. The separate execution guard preserves projection, invariant, temporal and other unassigned native discrepancies as `failed`; it never assigns them to a requirement arbitrarily. Coverage-only `inconclusive` phases carry validated `requirementGaps` and yield a skipped guard. Other infrastructure or incomplete exploration yields `error`. A phase PASS cannot turn zero assertions into a requirement PASS.
|
|
34
|
+
|
|
35
|
+
The producer exits 1 when normalization contains `failed` or `error`, otherwise 0, including skipped-only results. This follows the existing behavior runner's process consistency contract. Required skips still make `behavior check` FAIL. Native exit 0/1/2 and raw status remain in traces. Producer exit 0 alone does not mean assurance PASS. Calibration cannot be adopted with skipped/error checks; add meaningful missing execution rather than weakening obligations. True failures require the existing cited implementation-defect disposition before red calibration adoption.
|
|
36
|
+
|
|
37
|
+
Set `plan.runnerInputs` to the campaign, every campaign import and adapter implementation, producer, native budget/config, relevant package manifests and lockfiles, and actual workflow. Include the application entry points and feature sources in config inputs. For packed candidates, bind package manifests, lockfile integrity entries and the compact SHA-256 provenance manifest. Do not snapshot large binary tarballs in runner inputs; their base64 bytes can exceed the 16 MiB receipt limit. CI must install with the frozen lockfile so altered tarballs fail integrity validation. Record authoritative intent files in `plan.authorityInputs`. Bind all producer/import dependencies explicitly; the native entry SHA hashes only the entry module. Preserve the existing 200-file limit and output/input separation. Use the shared workflow template with pinned installation and real clean-runner dependencies, including Playwright browsers only when the adapter needs them.
|
|
38
|
+
|
|
39
|
+
## Maintenance handoff
|
|
40
|
+
|
|
41
|
+
Check in a short feature maintenance document explaining the cited intent, executable and estimated model locations, real SUT adapter, event/generator inventory, requirement/assertion mapping, bounded coverage and gaps, commands, toolchain pins, output/replay locations and evolution procedure. Include it in authority inputs only for authoritative intent; runner maintenance instructions alone are not user requirements. Preserve requirement/assertion IDs for unchanged obligations. Review and explicitly retire removed obligations through `spec.retire`; omission never retires them. A changed campaign, import, producer, adapter, plan, manifest, lockfile, workflow, mapping or native runner requires explicit recalibration and inspected adoption. Preserve red history before the next evolution. Verify green, deliberate real assertion red, restored green, unexecuted assertion, missing binding metadata and concrete replay behavior locally. Remote green/red/fix/evolution/handoff claims require actual remote logs and downloaded artifacts against their exact checked parent.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@statelyai/mcp/testing";
|
package/dist/testing.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "statelyai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Command-line tools for Stately",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -17,23 +17,28 @@
|
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.mts",
|
|
19
19
|
"import": "./dist/index.mjs"
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/testing.d.mts",
|
|
23
|
+
"import": "./dist/testing.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./skills/setup-behavior-ci/SKILL.md": "./dist/skills/setup-behavior-ci/SKILL.md"
|
|
21
26
|
},
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"@oclif/core": "^4.10.3",
|
|
29
|
+
"tsx": "^4.23.13",
|
|
24
30
|
"typescript": "^5.9.3",
|
|
25
31
|
"ws": "^8.21.0",
|
|
26
32
|
"zod": "^4.4.1",
|
|
27
|
-
"@statelyai/mcp": "0.
|
|
28
|
-
"@statelyai/sdk": "0.
|
|
29
|
-
"@statelyai/model-testing": "0.2.0"
|
|
33
|
+
"@statelyai/mcp": "0.21.0",
|
|
34
|
+
"@statelyai/sdk": "0.41.0"
|
|
30
35
|
},
|
|
31
36
|
"devDependencies": {
|
|
32
37
|
"@types/ws": "^8.18.1",
|
|
33
38
|
"tsdown": "0.21.0-beta.2",
|
|
34
39
|
"vitest": "^3.2.4",
|
|
35
|
-
"@statelyai/api-contract": "0.
|
|
36
|
-
"@statelyai/editor-sync": "0.2.
|
|
40
|
+
"@statelyai/api-contract": "0.10.1",
|
|
41
|
+
"@statelyai/editor-sync": "0.2.8"
|
|
37
42
|
},
|
|
38
43
|
"oclif": {
|
|
39
44
|
"bin": "statelyai",
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
"topicSeparator": " "
|
|
46
51
|
},
|
|
47
52
|
"scripts": {
|
|
48
|
-
"build": "tsdown",
|
|
53
|
+
"build": "tsdown && node scripts/copy-testing-skill.mjs",
|
|
49
54
|
"test": "vitest run"
|
|
50
55
|
}
|
|
51
56
|
}
|