@teurons/vds 0.0.0-stage → 0.0.2
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 +51 -2
- package/dist/vds.js +182 -0
- package/dist/vds_mapper.wasm +0 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @teurons/vds
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `vds` command. It starts, checks and indexes a VDS workspace on your machine.
|
|
4
|
+
|
|
5
|
+
VDS keeps a product's spec as files: capabilities that hold rules written in EARS,
|
|
6
|
+
scenarios in Gherkin, and flows that point at those rules in order.
|
|
7
|
+
This package is an early alpha and is published on the `next` tag.
|
|
8
|
+
|
|
9
|
+
## Set it up with your AI agent
|
|
10
|
+
|
|
11
|
+
Copy this prompt into Claude Code, Codex or any coding agent, run from the root of your project:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
Set up VDS in this project.
|
|
15
|
+
|
|
16
|
+
1. Run `npx -y @teurons/vds@next init vds-workspace`. It creates the
|
|
17
|
+
capabilities, flows and scenarios folders, a VDS.md guide and a .vds.json.
|
|
18
|
+
If it says a workspace already exists, stop and tell me what it found.
|
|
19
|
+
2. Add `"vds": "vds"` to package.json scripts and @teurons/vds@next to
|
|
20
|
+
devDependencies if this project uses npm, pnpm or yarn. Otherwise skip this.
|
|
21
|
+
3. Read vds-workspace/VDS.md and follow it from now on whenever you work on
|
|
22
|
+
the spec.
|
|
23
|
+
4. Add a short section to this project's AGENTS.md (or CLAUDE.md if that is
|
|
24
|
+
what exists) saying: the product spec lives in vds-workspace; read
|
|
25
|
+
vds-workspace/VDS.md before changing it; run `vds index vds-workspace`
|
|
26
|
+
first and `vds check vds-workspace` before finishing.
|
|
27
|
+
5. Run `npx @teurons/vds@next check vds-workspace` and confirm it prints valid.
|
|
28
|
+
6. Tell me what you created and changed, file by file.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx @teurons/vds@next init [folder] # start a workspace (never overwrites one)
|
|
35
|
+
npx @teurons/vds@next check [folder] # print every problem; exit 1 if any
|
|
36
|
+
npx @teurons/vds@next index [folder] # print the index: every flow and rule, one line each
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`folder` defaults to the current folder.
|
|
40
|
+
|
|
41
|
+
## A workspace
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
vds-workspace/
|
|
45
|
+
VDS.md how agents and people work on this spec
|
|
46
|
+
.vds.json local settings, such as the studio port
|
|
47
|
+
capabilities/<CODE>.yaml rules, each one EARS statement with an id like ONB-01
|
|
48
|
+
flows/<id>.yaml versioned flows whose steps point at rule ids
|
|
49
|
+
scenarios/<RULE-ID>.feature Gherkin scenarios, one file per rule
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Needs Node 20 or later.
|
package/dist/vds.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.js
|
|
4
|
+
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
5
|
+
import { join as join2, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
// ../../specs/js/load.js
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
var compiled = /* @__PURE__ */ new Map();
|
|
10
|
+
function compile(url, build) {
|
|
11
|
+
const key = String(url);
|
|
12
|
+
if (!compiled.has(key)) {
|
|
13
|
+
let bytes;
|
|
14
|
+
try {
|
|
15
|
+
bytes = readFileSync(url);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(`${key.split("/").pop()} is missing. Build it with: ${build}`);
|
|
18
|
+
}
|
|
19
|
+
compiled.set(key, new WebAssembly.Module(bytes));
|
|
20
|
+
}
|
|
21
|
+
return compiled.get(key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ../../specs/js/bridge.js
|
|
25
|
+
var enc = new TextEncoder();
|
|
26
|
+
var dec = new TextDecoder();
|
|
27
|
+
function instanceOf(module) {
|
|
28
|
+
return new WebAssembly.Instance(module, {});
|
|
29
|
+
}
|
|
30
|
+
function bridge(instance) {
|
|
31
|
+
const x = instance.exports;
|
|
32
|
+
const read = (packed) => {
|
|
33
|
+
const ptr = Number(packed >> 32n);
|
|
34
|
+
const len = Number(packed & 0xffffffffn);
|
|
35
|
+
const text = dec.decode(new Uint8Array(x.memory.buffer, ptr, len));
|
|
36
|
+
x.dealloc(ptr, len);
|
|
37
|
+
return text;
|
|
38
|
+
};
|
|
39
|
+
const call = (fn, input) => {
|
|
40
|
+
const bytes = enc.encode(input);
|
|
41
|
+
const ptr = x.alloc(bytes.length);
|
|
42
|
+
new Uint8Array(x.memory.buffer, ptr, bytes.length).set(bytes);
|
|
43
|
+
return read(x[fn](ptr, bytes.length));
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
call,
|
|
47
|
+
/** Calls an export with a value as JSON and returns its answer parsed. */
|
|
48
|
+
json: (fn, input) => JSON.parse(call(fn, JSON.stringify(input))),
|
|
49
|
+
/** Calls an export that takes nothing and returns its answer parsed. */
|
|
50
|
+
none: (fn) => JSON.parse(read(x[fn]()))
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../../mapper/js/index.js
|
|
55
|
+
function bind(instance) {
|
|
56
|
+
const { call, json } = bridge(instance);
|
|
57
|
+
return {
|
|
58
|
+
validate: (files) => json("validate", files),
|
|
59
|
+
v2Check: (files) => json("v2_check", files),
|
|
60
|
+
v2Index: (files) => call("v2_index", JSON.stringify(files)),
|
|
61
|
+
load: (files) => json("load", files),
|
|
62
|
+
check: (changes) => json("check", changes),
|
|
63
|
+
apply: (changes) => json("apply", changes),
|
|
64
|
+
toYaml: (doc) => call("to_yaml", typeof doc === "string" ? doc : JSON.stringify(doc)),
|
|
65
|
+
toYamlOver: (original, doc) => call("to_yaml_over", JSON.stringify({ original, doc })),
|
|
66
|
+
parseYaml: (text) => {
|
|
67
|
+
const r = JSON.parse(call("parse_yaml", text));
|
|
68
|
+
if ("error" in r) throw new Error(r.error);
|
|
69
|
+
return r.ok;
|
|
70
|
+
},
|
|
71
|
+
nextId: (kind, parent) => json("next_id", { kind, parent: parent ?? null }).id ?? null,
|
|
72
|
+
readSettings: (text) => json("read_settings", { settings: text ?? null }),
|
|
73
|
+
workspaceFiles: (paths) => json("workspace_files", paths),
|
|
74
|
+
classify: (files) => json("classify", files),
|
|
75
|
+
pathOf: (input) => json("path_of", input).path,
|
|
76
|
+
describe: (settings, path) => json("path_of", { settings: settings ?? null, of: "describe", path }),
|
|
77
|
+
parseFeature: (text) => JSON.parse(call("parse_feature", text)),
|
|
78
|
+
writeFeature: (input) => call("write_feature", JSON.stringify(input)),
|
|
79
|
+
retitleFeature: (text, requirement, title) => call("retitle_feature", JSON.stringify({ text, requirement, title })),
|
|
80
|
+
putScenario: (text, scenario) => call("put_scenario", JSON.stringify({ text, scenario })),
|
|
81
|
+
removeScenario: (text, id) => call("remove_scenario", JSON.stringify({ text, id }))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function instantiate(module) {
|
|
85
|
+
return bind(instanceOf(module));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../../mapper/js/node.js
|
|
89
|
+
function mapperModule() {
|
|
90
|
+
return compile(new URL("./vds_mapper.wasm", import.meta.url), "pnpm --filter @vds/mapper build");
|
|
91
|
+
}
|
|
92
|
+
function loadMapper() {
|
|
93
|
+
return instantiate(mapperModule());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/init.js
|
|
97
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
98
|
+
import { createServer } from "node:net";
|
|
99
|
+
import { join } from "node:path";
|
|
100
|
+
var FOLDERS = ["capabilities", "flows", "scenarios"];
|
|
101
|
+
var FIRST_PORT = 4600;
|
|
102
|
+
var GUIDE = `# Working in this VDS workspace
|
|
103
|
+
|
|
104
|
+
This folder holds the product's requirements, flows and scenarios as plain files.
|
|
105
|
+
Read and write them with the vds command.
|
|
106
|
+
|
|
107
|
+
## Layout
|
|
108
|
+
|
|
109
|
+
- capabilities/<CODE>.yaml: one capability and its requirements (ids like ONB-01).
|
|
110
|
+
- flows/<id>.yaml: a journey made of states; each state points at requirement ids.
|
|
111
|
+
- scenarios/<REQ-ID>.feature: Given, When, Then scenarios for one requirement.
|
|
112
|
+
|
|
113
|
+
## How to work
|
|
114
|
+
|
|
115
|
+
1. Run \`vds index\` first. It prints one line per flow and requirement, so you know what exists.
|
|
116
|
+
2. Open one file at a time. Read only the file you are changing.
|
|
117
|
+
3. Write requirements in EARS: "When <trigger>, the system SHALL <response>." The system is always the subject.
|
|
118
|
+
4. Point flows at requirement ids. A state lists the ids it needs under \`requirements:\`.
|
|
119
|
+
5. Run \`vds check\` before you finish. It prints "valid" when nothing is wrong; fix every problem it lists.
|
|
120
|
+
`;
|
|
121
|
+
async function freePort() {
|
|
122
|
+
for (let port = FIRST_PORT; ; port++) {
|
|
123
|
+
const free = await new Promise((resolve2) => {
|
|
124
|
+
const server = createServer();
|
|
125
|
+
server.once("error", () => resolve2(false));
|
|
126
|
+
server.listen(port, () => server.close(() => resolve2(true)));
|
|
127
|
+
});
|
|
128
|
+
if (free) return port;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function init(root) {
|
|
132
|
+
const found = [...FOLDERS, "VDS.md"].filter((name) => existsSync(join(root, name)));
|
|
133
|
+
if (found.length > 0) {
|
|
134
|
+
console.error(`nothing changed; this folder already has: ${found.join(", ")}`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
const port = await freePort();
|
|
138
|
+
for (const dir of FOLDERS) mkdirSync(join(root, dir), { recursive: true });
|
|
139
|
+
writeFileSync(join(root, "VDS.md"), GUIDE);
|
|
140
|
+
writeFileSync(join(root, ".vds.json"), `${JSON.stringify({ port })}
|
|
141
|
+
`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/bin.js
|
|
146
|
+
var FOLDERS2 = ["capabilities", "flows", "scenarios"];
|
|
147
|
+
function readWorkspace(root) {
|
|
148
|
+
const files = {};
|
|
149
|
+
for (const dir of FOLDERS2) {
|
|
150
|
+
let names = [];
|
|
151
|
+
try {
|
|
152
|
+
names = readdirSync(join2(root, dir));
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const name of names) files[`${dir}/${name}`] = readFileSync2(join2(root, dir, name), "utf8");
|
|
157
|
+
}
|
|
158
|
+
return files;
|
|
159
|
+
}
|
|
160
|
+
async function main([command, folder = "."]) {
|
|
161
|
+
if (command === "init") return init(resolve(folder));
|
|
162
|
+
const mapper = await loadMapper();
|
|
163
|
+
const files = readWorkspace(resolve(folder));
|
|
164
|
+
if (command === "check") {
|
|
165
|
+
const findings = mapper.v2Check(files);
|
|
166
|
+
if ("error" in findings) throw new Error(findings.error);
|
|
167
|
+
if (findings.length === 0) {
|
|
168
|
+
console.log("valid");
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
console.error(`${findings.length} problem(s):`);
|
|
172
|
+
for (const f of findings) console.error(`- ${f.path}: ${f.message} [${f.code}]`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (command === "index") {
|
|
176
|
+
process.stdout.write(mapper.v2Index(files));
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
console.error("usage: vds init [folder] | vds check [folder] | vds index [folder]");
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teurons/vds",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "The vds command: check, index and run a VDS workspace on your machine.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vds": "dist/vds.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {},
|
|
16
|
+
"license": "UNLICENSED",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/teurons/vds.git"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"tag": "next"
|
|
24
|
+
}
|
|
25
|
+
}
|