@teurons/vds 0.0.7 → 0.0.8
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/dist/CHANGELOG.md +22 -1
- package/dist/vds.js +275 -201
- package/dist/vds_mapper.wasm +0 -0
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -11,7 +11,28 @@ Each release has two parts:
|
|
|
11
11
|
and the one it installed.
|
|
12
12
|
"None" means there is nothing to do.
|
|
13
13
|
|
|
14
|
-
## 0.0.
|
|
14
|
+
## 0.0.8 (2026-11-03)
|
|
15
|
+
|
|
16
|
+
### What changed
|
|
17
|
+
|
|
18
|
+
- Flows are now journeys. The folder is `journeys/<id>.yaml`, the top key is `journey:`, and every
|
|
19
|
+
journey file names its kind with `kind: journey` (the checker refuses a missing or unknown kind).
|
|
20
|
+
In states, `next_flow` is `next_journey` and the `subflow` type is `subjourney`. The index, checker
|
|
21
|
+
codes (`journey_*`), studio pages (`/journeys/<id>`), MCP tools (`vds_mark_built(journey)`), the
|
|
22
|
+
command (`vds built <journey>`) and VDS.md all say journey. `vds update` moves `flows/` to
|
|
23
|
+
`journeys/` and rewrites the keys, including in `versions/`, and prints each file. `vds check` on a
|
|
24
|
+
workspace that still has `flows/` and no `journeys/` tells you to run `vds update`.
|
|
25
|
+
- The studio's "Group by capability" works: the server now passes the page its full address. Each
|
|
26
|
+
journey on the dashboard shows its id. An empty workspace shows this studio's own MCP address
|
|
27
|
+
instead of `<port>`. Changelog entries are dated in the maintainer's local time.
|
|
28
|
+
|
|
29
|
+
### Agent steps
|
|
30
|
+
|
|
31
|
+
- Run `npx -y @teurons/vds@latest update`; it moves flows/ to journeys/ for you and prints each file.
|
|
32
|
+
- Change the wording from flow to journey in the project's AGENTS.md and CLAUDE.md, and re-read VDS.md.
|
|
33
|
+
- Rename tests named after FLOW-xx scenario ids only if the workspace is VDS's own (FLOW is now JRN); other workspaces keep their ids.
|
|
34
|
+
|
|
35
|
+
## 0.0.7 (2026-11-03)
|
|
15
36
|
|
|
16
37
|
### What changed
|
|
17
38
|
|
package/dist/vds.js
CHANGED
|
@@ -7510,17 +7510,17 @@ function stateRules(state) {
|
|
|
7510
7510
|
...(state.choices ?? []).flatMap((c) => c.requirements ?? [])
|
|
7511
7511
|
];
|
|
7512
7512
|
}
|
|
7513
|
-
function ruleIdsOf(
|
|
7514
|
-
return new Set(Object.values(
|
|
7513
|
+
function ruleIdsOf(journey) {
|
|
7514
|
+
return new Set(Object.values(journey.states ?? {}).flatMap(stateRules));
|
|
7515
7515
|
}
|
|
7516
|
-
function
|
|
7517
|
-
return Object.keys(files).filter((p) => p.startsWith("
|
|
7518
|
-
const
|
|
7519
|
-
return { path, id:
|
|
7516
|
+
function journeysOf(files) {
|
|
7517
|
+
return Object.keys(files).filter((p) => p.startsWith("journeys/") && p.endsWith(".yaml")).sort().map((path) => {
|
|
7518
|
+
const journey = (0, import_yaml.parse)(files[path]) ?? {};
|
|
7519
|
+
return { path, id: journey.journey ?? path.slice(6, -5), journey };
|
|
7520
7520
|
});
|
|
7521
7521
|
}
|
|
7522
|
-
function isBuilt(
|
|
7523
|
-
return !
|
|
7522
|
+
function isBuilt(journey) {
|
|
7523
|
+
return !journey.built || journey.built.version === journey.version;
|
|
7524
7524
|
}
|
|
7525
7525
|
function inline(doc, value) {
|
|
7526
7526
|
const node2 = doc.createNode(value);
|
|
@@ -7529,11 +7529,11 @@ function inline(doc, value) {
|
|
|
7529
7529
|
}
|
|
7530
7530
|
function recordChange(text, rule, at) {
|
|
7531
7531
|
const doc = (0, import_yaml.parseDocument)(text);
|
|
7532
|
-
const
|
|
7532
|
+
const journey = doc.toJS();
|
|
7533
7533
|
const writes = {};
|
|
7534
|
-
if (isBuilt(
|
|
7535
|
-
writes.old = { version:
|
|
7536
|
-
doc.set("version",
|
|
7534
|
+
if (isBuilt(journey)) {
|
|
7535
|
+
writes.old = { version: journey.version, text };
|
|
7536
|
+
doc.set("version", journey.version + 1);
|
|
7537
7537
|
}
|
|
7538
7538
|
const change = inline(doc, { version: doc.get("version"), rule, at });
|
|
7539
7539
|
const changes = doc.get("changes");
|
|
@@ -7547,8 +7547,8 @@ function impact(before, after, at = today()) {
|
|
|
7547
7547
|
const affected = [];
|
|
7548
7548
|
const unaffected = [];
|
|
7549
7549
|
const moved = {};
|
|
7550
|
-
for (const { path, id,
|
|
7551
|
-
const used = ruleIdsOf(
|
|
7550
|
+
for (const { path, id, journey } of journeysOf(after)) {
|
|
7551
|
+
const used = ruleIdsOf(journey);
|
|
7552
7552
|
const hits = rules.filter((r) => used.has(r));
|
|
7553
7553
|
if (hits.length === 0) {
|
|
7554
7554
|
unaffected.push(id);
|
|
@@ -7566,17 +7566,17 @@ function impact(before, after, at = today()) {
|
|
|
7566
7566
|
}
|
|
7567
7567
|
return { writes, affected, unaffected, rules, moved };
|
|
7568
7568
|
}
|
|
7569
|
-
function markBuilt(files,
|
|
7570
|
-
const found =
|
|
7569
|
+
function markBuilt(files, journeyId, by, at = today()) {
|
|
7570
|
+
const found = journeysOf(files).find((f) => f.id === journeyId);
|
|
7571
7571
|
if (!found) return null;
|
|
7572
7572
|
const doc = (0, import_yaml.parseDocument)(files[found.path]);
|
|
7573
|
-
doc.set("built", inline(doc, { version: found.
|
|
7573
|
+
doc.set("built", inline(doc, { version: found.journey.version, by, at }));
|
|
7574
7574
|
return { [found.path]: String(doc) };
|
|
7575
7575
|
}
|
|
7576
|
-
function versionsOf(files,
|
|
7577
|
-
const kept = Object.keys(files).map((p) => p.match(new RegExp(`^versions/${
|
|
7578
|
-
const current =
|
|
7579
|
-
if (current) kept.push({ version: current.
|
|
7576
|
+
function versionsOf(files, journeyId) {
|
|
7577
|
+
const kept = Object.keys(files).map((p) => p.match(new RegExp(`^versions/${journeyId}/v(\\d+)\\.yaml$`))).filter(Boolean).map((m) => ({ version: Number(m[1]), text: files[m[0]] }));
|
|
7578
|
+
const current = journeysOf(files).find((f) => f.id === journeyId);
|
|
7579
|
+
if (current) kept.push({ version: current.journey.version, text: files[current.path] });
|
|
7580
7580
|
return kept.sort((a, b) => a.version - b.version);
|
|
7581
7581
|
}
|
|
7582
7582
|
function diffState(id, was, now) {
|
|
@@ -7593,7 +7593,7 @@ function diffState(id, was, now) {
|
|
|
7593
7593
|
const differs = entry.titleChanged || !sameShape(was, now, before, after);
|
|
7594
7594
|
return { ...entry, status: differs ? "changed" : "same" };
|
|
7595
7595
|
}
|
|
7596
|
-
function
|
|
7596
|
+
function compareJourneys(aText, bText) {
|
|
7597
7597
|
const a = (0, import_yaml.parse)(aText)?.states ?? {};
|
|
7598
7598
|
const b = (0, import_yaml.parse)(bText)?.states ?? {};
|
|
7599
7599
|
return [.../* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(a)])].map(
|
|
@@ -7623,8 +7623,8 @@ function whoIsBuilding() {
|
|
|
7623
7623
|
}
|
|
7624
7624
|
return process.env.USER || process.env.USERNAME || "unknown";
|
|
7625
7625
|
}
|
|
7626
|
-
function builtWrites(files,
|
|
7627
|
-
return markBuilt(files,
|
|
7626
|
+
function builtWrites(files, journeyId) {
|
|
7627
|
+
return markBuilt(files, journeyId, whoIsBuilding());
|
|
7628
7628
|
}
|
|
7629
7629
|
function writeAll(root, writes) {
|
|
7630
7630
|
for (const [path, text] of Object.entries(writes)) {
|
|
@@ -7735,45 +7735,45 @@ var init_evidence = __esm({
|
|
|
7735
7735
|
});
|
|
7736
7736
|
|
|
7737
7737
|
// src/workspace.js
|
|
7738
|
-
import { readdirSync as
|
|
7739
|
-
import { join as
|
|
7738
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
7739
|
+
import { join as join6 } from "node:path";
|
|
7740
7740
|
function readWorkspace(root) {
|
|
7741
7741
|
const files = {};
|
|
7742
|
-
for (const dir of FOLDERS2) {
|
|
7742
|
+
for (const dir of [...FOLDERS2, "flows"]) {
|
|
7743
7743
|
let names = [];
|
|
7744
7744
|
try {
|
|
7745
|
-
names =
|
|
7745
|
+
names = readdirSync3(join6(root, dir));
|
|
7746
7746
|
} catch {
|
|
7747
7747
|
continue;
|
|
7748
7748
|
}
|
|
7749
|
-
for (const name of names) files[`${dir}/${name}`] =
|
|
7749
|
+
for (const name of names) files[`${dir}/${name}`] = readFileSync7(join6(root, dir, name), "utf8");
|
|
7750
7750
|
}
|
|
7751
7751
|
return Object.assign(files, readVersions(root));
|
|
7752
7752
|
}
|
|
7753
7753
|
function readVersions(root) {
|
|
7754
7754
|
const files = {};
|
|
7755
|
-
let
|
|
7755
|
+
let journeys = [];
|
|
7756
7756
|
try {
|
|
7757
|
-
|
|
7757
|
+
journeys = readdirSync3(join6(root, "versions"));
|
|
7758
7758
|
} catch {
|
|
7759
7759
|
return files;
|
|
7760
7760
|
}
|
|
7761
|
-
for (const id of
|
|
7761
|
+
for (const id of journeys) {
|
|
7762
7762
|
let names = [];
|
|
7763
7763
|
try {
|
|
7764
|
-
names =
|
|
7764
|
+
names = readdirSync3(join6(root, "versions", id));
|
|
7765
7765
|
} catch {
|
|
7766
7766
|
continue;
|
|
7767
7767
|
}
|
|
7768
7768
|
for (const n of names.filter((x) => x.endsWith(".yaml")))
|
|
7769
|
-
files[`versions/${id}/${n}`] =
|
|
7769
|
+
files[`versions/${id}/${n}`] = readFileSync7(join6(root, "versions", id, n), "utf8");
|
|
7770
7770
|
}
|
|
7771
7771
|
return files;
|
|
7772
7772
|
}
|
|
7773
7773
|
var FOLDERS2;
|
|
7774
7774
|
var init_workspace = __esm({
|
|
7775
7775
|
"src/workspace.js"() {
|
|
7776
|
-
FOLDERS2 = ["capabilities", "
|
|
7776
|
+
FOLDERS2 = ["capabilities", "journeys", "scenarios"];
|
|
7777
7777
|
}
|
|
7778
7778
|
});
|
|
7779
7779
|
|
|
@@ -7783,12 +7783,12 @@ __export(server_exports, {
|
|
|
7783
7783
|
portFor: () => portFor,
|
|
7784
7784
|
startServer: () => startServer
|
|
7785
7785
|
});
|
|
7786
|
-
import { readFileSync as
|
|
7786
|
+
import { readFileSync as readFileSync8, watch } from "node:fs";
|
|
7787
7787
|
import { createServer as createServer2 } from "node:http";
|
|
7788
|
-
import { join as
|
|
7788
|
+
import { join as join7 } from "node:path";
|
|
7789
7789
|
function portFor(root) {
|
|
7790
7790
|
try {
|
|
7791
|
-
const { port } = JSON.parse(
|
|
7791
|
+
const { port } = JSON.parse(readFileSync8(join7(root, ".vds.json"), "utf8"));
|
|
7792
7792
|
return Number.isInteger(port) ? port : FIRST_PORT2;
|
|
7793
7793
|
} catch {
|
|
7794
7794
|
return FIRST_PORT2;
|
|
@@ -7837,7 +7837,8 @@ async function startServer({
|
|
|
7837
7837
|
return;
|
|
7838
7838
|
}
|
|
7839
7839
|
const files = readWorkspace(root);
|
|
7840
|
-
const
|
|
7840
|
+
const address = new URL(req.url, `http://${req.headers.host ?? "localhost"}`).href;
|
|
7841
|
+
const { status, html } = render(address, files, check4(files), loadEvidence(root));
|
|
7841
7842
|
res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
7842
7843
|
res.end(withReloadScript(html));
|
|
7843
7844
|
} catch (e) {
|
|
@@ -7885,37 +7886,37 @@ var init_server = __esm({
|
|
|
7885
7886
|
});
|
|
7886
7887
|
|
|
7887
7888
|
// src/studio/versionPages.js
|
|
7888
|
-
function withVersionInfo(
|
|
7889
|
-
const info =
|
|
7890
|
-
const raw = (0, import_yaml2.parse)(files[`
|
|
7891
|
-
const versions = versionsOf(files,
|
|
7889
|
+
function withVersionInfo(journeys, files) {
|
|
7890
|
+
const info = journeys.map((journey) => {
|
|
7891
|
+
const raw = (0, import_yaml2.parse)(files[`journeys/${journey.id}.yaml`] ?? "") ?? {};
|
|
7892
|
+
const versions = versionsOf(files, journey.id).map((v) => v.version);
|
|
7892
7893
|
return {
|
|
7893
|
-
...
|
|
7894
|
+
...journey,
|
|
7894
7895
|
vinfo: { built: raw.built, changes: raw.changes ?? [], versions, unbuilt: !isBuilt(raw) }
|
|
7895
7896
|
};
|
|
7896
7897
|
});
|
|
7897
7898
|
return [...info.filter((f) => f.vinfo.unbuilt), ...info.filter((f) => !f.vinfo.unbuilt)];
|
|
7898
7899
|
}
|
|
7899
|
-
function changedBadge(
|
|
7900
|
-
if (!
|
|
7900
|
+
function changedBadge(journey, m, { esc: esc3, plainOf: plainOf2 }) {
|
|
7901
|
+
if (!journey.vinfo?.unbuilt) return "";
|
|
7901
7902
|
const rules = [
|
|
7902
7903
|
...new Set(
|
|
7903
|
-
|
|
7904
|
+
journey.vinfo.changes.filter((c) => c.version > (journey.vinfo.built?.version ?? 0)).map((c) => c.rule)
|
|
7904
7905
|
)
|
|
7905
7906
|
];
|
|
7906
7907
|
const lines = rules.map((id) => m.rules.has(id) ? plainOf2(m.rules.get(id)) : id);
|
|
7907
|
-
return `<div class="changed"><b>Changed</b> \xB7 v${esc3(
|
|
7908
|
+
return `<div class="changed"><b>Changed</b> \xB7 v${esc3(journey.version)} is not built yet${lines.length ? `: ${lines.map(esc3).join("; ")}` : ""}</div>`;
|
|
7908
7909
|
}
|
|
7909
|
-
function defaultPair(
|
|
7910
|
-
const { versions, built } =
|
|
7911
|
-
const current =
|
|
7910
|
+
function defaultPair(journey) {
|
|
7911
|
+
const { versions, built } = journey.vinfo;
|
|
7912
|
+
const current = journey.version;
|
|
7912
7913
|
const before = built && built.version < current ? built.version : versions.filter((v) => v < current).at(-1);
|
|
7913
7914
|
return [before, current];
|
|
7914
7915
|
}
|
|
7915
|
-
function compareLink(
|
|
7916
|
-
if (!
|
|
7917
|
-
const [a, b] = defaultPair(
|
|
7918
|
-
return ` \xB7 <a href="/
|
|
7916
|
+
function compareLink(journey) {
|
|
7917
|
+
if (!journey.vinfo || journey.vinfo.versions.length < 2) return "";
|
|
7918
|
+
const [a, b] = defaultPair(journey);
|
|
7919
|
+
return ` \xB7 <a href="/journeys/${encodeURIComponent(journey.id)}/compare?a=${a}&b=${b}">Compare versions</a>`;
|
|
7919
7920
|
}
|
|
7920
7921
|
function rowOf(d, m, tools) {
|
|
7921
7922
|
const { esc: esc3, plainOf: plainOf2 } = tools;
|
|
@@ -7929,25 +7930,25 @@ function rowOf(d, m, tools) {
|
|
|
7929
7930
|
const tag = LABELS[d.status] ? ` <span class="tag">${LABELS[d.status]}</span>` : "";
|
|
7930
7931
|
return `<li class="diff ${d.status}"><b>${name}</b>${tag}${notes.length ? `<small>${notes.join("<br>")}</small>` : ""}</li>`;
|
|
7931
7932
|
}
|
|
7932
|
-
function compareRoute(
|
|
7933
|
-
if (!
|
|
7934
|
-
const [a0, b0] = defaultPair(
|
|
7933
|
+
function compareRoute(journey, m, query, tools) {
|
|
7934
|
+
if (!journey.vinfo || journey.vinfo.versions.length < 2) return void 0;
|
|
7935
|
+
const [a0, b0] = defaultPair(journey);
|
|
7935
7936
|
const a = Number(query.get("a") ?? a0);
|
|
7936
7937
|
const b = Number(query.get("b") ?? b0);
|
|
7937
|
-
const kept = versionsOf(m.files,
|
|
7938
|
+
const kept = versionsOf(m.files, journey.id);
|
|
7938
7939
|
const [va, vb] = [a, b].map((n) => kept.find((v) => v.version === n));
|
|
7939
7940
|
if (!va || !vb) return void 0;
|
|
7940
|
-
const rows =
|
|
7941
|
+
const rows = compareJourneys(va.text, vb.text);
|
|
7941
7942
|
const { esc: esc3 } = tools;
|
|
7942
7943
|
const shown = rows.map((r) => rowOf(r, m, tools));
|
|
7943
|
-
const body = `<p class="sub"><a href="/
|
|
7944
|
+
const body = `<p class="sub"><a href="/journeys/${encodeURIComponent(journey.id)}">${esc3(journey.title)}</a></p>
|
|
7944
7945
|
<h1>Compare versions</h1><p class="sub">Version ${a} against Version ${b}: what was added, removed or changed.</p>
|
|
7945
7946
|
<ul class="list">${shown.join("")}</ul>`;
|
|
7946
|
-
return { title: `${
|
|
7947
|
+
return { title: `${journey.title}: versions`, body };
|
|
7947
7948
|
}
|
|
7948
7949
|
function versionModel(m, files) {
|
|
7949
|
-
const
|
|
7950
|
-
return { ...m, files,
|
|
7950
|
+
const journeys = withVersionInfo(m.journeys, files);
|
|
7951
|
+
return { ...m, files, journeys, journeyById: new Map(journeys.map((f) => [f.id, f])) };
|
|
7951
7952
|
}
|
|
7952
7953
|
var import_yaml2, LABELS;
|
|
7953
7954
|
var init_versionPages = __esm({
|
|
@@ -7974,7 +7975,7 @@ function layout(title, body, findings) {
|
|
|
7974
7975
|
return `<!doctype html>
|
|
7975
7976
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7976
7977
|
<meta name="color-scheme" content="light dark"><title>${esc(title)}</title><style>${CSS}</style></head>
|
|
7977
|
-
<body><header><b>VDS</b><a href="/">
|
|
7978
|
+
<body><header><b>VDS</b><a href="/">Journeys</a><a href="/product-wide">Product-wide rules</a></header>${banner}
|
|
7978
7979
|
<main>${body}</main></body></html>`;
|
|
7979
7980
|
}
|
|
7980
7981
|
function indexModel(model, evidence2 = []) {
|
|
@@ -7991,25 +7992,25 @@ function indexModel(model, evidence2 = []) {
|
|
|
7991
7992
|
latest: latest(evidence2),
|
|
7992
7993
|
rules,
|
|
7993
7994
|
capabilityOf,
|
|
7994
|
-
|
|
7995
|
+
journeyById: new Map(model.journeys.map((f) => [f.id, f]))
|
|
7995
7996
|
};
|
|
7996
7997
|
}
|
|
7997
7998
|
function plainOf(rule) {
|
|
7998
7999
|
return rule.plain ?? `${rule.statement} (not written as an EARS statement)`;
|
|
7999
8000
|
}
|
|
8000
|
-
function stepCount(
|
|
8001
|
-
return Object.values(
|
|
8001
|
+
function stepCount(journey) {
|
|
8002
|
+
return Object.values(journey.states).filter(isStep).length;
|
|
8002
8003
|
}
|
|
8003
8004
|
function isTicked(state, m) {
|
|
8004
8005
|
const rules = stateRules2(state).map((r) => m.rules.get(r));
|
|
8005
8006
|
if (rules.length === 0 || rules.some((r) => !r || r.scenarios.length === 0)) return false;
|
|
8006
8007
|
return rules.every((r) => r.scenarios.every((s) => m.latest[s.id]?.status === "passed"));
|
|
8007
8008
|
}
|
|
8008
|
-
function walkedCount(
|
|
8009
|
-
return Object.values(
|
|
8009
|
+
function walkedCount(journey, m) {
|
|
8010
|
+
return Object.values(journey.states).filter((s) => isStep(s) && isTicked(s, m)).length;
|
|
8010
8011
|
}
|
|
8011
|
-
function walkedText(
|
|
8012
|
-
return `${walkedCount(
|
|
8012
|
+
function walkedText(journey, m) {
|
|
8013
|
+
return `${walkedCount(journey, m)} of ${stepCount(journey)} steps walked`;
|
|
8013
8014
|
}
|
|
8014
8015
|
function runSummary(rule, m) {
|
|
8015
8016
|
const ran = rule.scenarios.filter((s) => m.latest[s.id]);
|
|
@@ -8021,109 +8022,110 @@ function stateRules2(state) {
|
|
|
8021
8022
|
const viaChoice = (state.choices ?? []).flatMap((c) => c.requirements ?? []);
|
|
8022
8023
|
return [...state.requirements ?? [], ...viaChoice];
|
|
8023
8024
|
}
|
|
8024
|
-
function walk(
|
|
8025
|
+
function walk(journey) {
|
|
8025
8026
|
const order = [];
|
|
8026
8027
|
const seen = /* @__PURE__ */ new Set();
|
|
8027
8028
|
const visit2 = (id) => {
|
|
8028
|
-
const state =
|
|
8029
|
+
const state = journey.states[id];
|
|
8029
8030
|
if (!id || seen.has(id) || !state) return;
|
|
8030
8031
|
seen.add(id);
|
|
8031
8032
|
order.push(id);
|
|
8032
8033
|
if (state.type === "choice") for (const c of state.choices ?? []) visit2(c.next);
|
|
8033
8034
|
else visit2(state.next);
|
|
8034
8035
|
};
|
|
8035
|
-
visit2(
|
|
8036
|
+
visit2(journey.start_at);
|
|
8036
8037
|
return order;
|
|
8037
8038
|
}
|
|
8038
|
-
function ruleLink(rule,
|
|
8039
|
-
const query =
|
|
8039
|
+
function ruleLink(rule, journeyId) {
|
|
8040
|
+
const query = journeyId ? `?from=${encodeURIComponent(journeyId)}` : "";
|
|
8040
8041
|
return `<a href="/rules/${encodeURIComponent(rule.id)}${query}">${esc(plainOf(rule))}</a>`;
|
|
8041
8042
|
}
|
|
8042
|
-
function
|
|
8043
|
-
return `<li><a href="/
|
|
8044
|
-
<div class="muted">${esc(
|
|
8043
|
+
function journeyCard(journey, m) {
|
|
8044
|
+
return `<li><a href="/journeys/${encodeURIComponent(journey.id)}"><b>${esc(journey.title)}</b></a>
|
|
8045
|
+
<div class="muted"><code>${esc(journey.id)}</code> \xB7 ${esc(journey.persona)} \xB7 v${esc(journey.version)} \xB7 ${walkedText(journey, m)}</div>${changedBadge(journey, m, TOOLS)}</li>`;
|
|
8045
8046
|
}
|
|
8046
|
-
function
|
|
8047
|
+
function journeysByCapability(m) {
|
|
8047
8048
|
const sections = m.capabilities.map((c) => {
|
|
8048
8049
|
const mine = new Set(c.requirements.map((r) => r.id));
|
|
8049
|
-
return { c,
|
|
8050
|
-
}).filter((s) => s.
|
|
8051
|
-
const bare = m.
|
|
8050
|
+
return { c, journeys: m.journeys.filter((f) => f.requirements.some((id) => mine.has(id))) };
|
|
8051
|
+
}).filter((s) => s.journeys.length);
|
|
8052
|
+
const bare = m.journeys.filter((f) => f.requirements.length === 0);
|
|
8052
8053
|
const html = sections.map(
|
|
8053
|
-
(s) => `<h2>${esc(s.c.title)}</h2><ul class="list">${s.
|
|
8054
|
+
(s) => `<h2>${esc(s.c.title)}</h2><ul class="list">${s.journeys.map((f) => journeyCard(f, m)).join("")}</ul>`
|
|
8054
8055
|
);
|
|
8055
8056
|
if (bare.length)
|
|
8056
8057
|
html.push(
|
|
8057
|
-
`<h2>No rules yet</h2><ul class="list">${bare.map((f) =>
|
|
8058
|
+
`<h2>No rules yet</h2><ul class="list">${bare.map((f) => journeyCard(f, m)).join("")}</ul>`
|
|
8058
8059
|
);
|
|
8059
8060
|
return html.join("");
|
|
8060
8061
|
}
|
|
8061
|
-
function emptyWorkspace() {
|
|
8062
|
-
|
|
8063
|
-
|
|
8062
|
+
function emptyWorkspace(origin) {
|
|
8063
|
+
const address = origin ? `${esc(origin)}/mcp` : "http://localhost:<port>/mcp";
|
|
8064
|
+
return `<h1>No journeys yet</h1>
|
|
8065
|
+
<p class="sub">Connect an AI app to this studio, then ask it to write the first journey through MCP.</p>
|
|
8064
8066
|
<div class="card"><h3>Connect an AI app</h3>
|
|
8065
|
-
<p>Add this MCP address to your AI app
|
|
8066
|
-
<pre
|
|
8067
|
-
<p class="muted">
|
|
8067
|
+
<p>Add this MCP address to your AI app:</p>
|
|
8068
|
+
<pre>${address}</pre>
|
|
8069
|
+
<p class="muted">Then ask the app to write the first journey: who it is for, the steps they take, and the rules on each step.</p></div>`;
|
|
8068
8070
|
}
|
|
8069
|
-
function dashboard(m, query) {
|
|
8070
|
-
if (m.
|
|
8071
|
+
function dashboard(m, query, origin) {
|
|
8072
|
+
if (m.journeys.length === 0) return emptyWorkspace(origin);
|
|
8071
8073
|
const grouped = query.get("group") === "capability";
|
|
8072
|
-
const switcher = grouped ? '<a href="/">List all
|
|
8073
|
-
const list = grouped ?
|
|
8074
|
-
return `<h1>
|
|
8074
|
+
const switcher = grouped ? '<a href="/">List all journeys</a>' : '<a href="/?group=capability">Group by capability</a>';
|
|
8075
|
+
const list = grouped ? journeysByCapability(m) : `<ul class="list">${m.journeys.map((f) => journeyCard(f, m)).join("")}</ul>`;
|
|
8076
|
+
return `<h1>Journeys</h1><p class="sub">${switcher} \xB7 <a href="/product-wide">Product-wide rules</a></p>${list}`;
|
|
8075
8077
|
}
|
|
8076
|
-
function stopBox(id,
|
|
8077
|
-
const stop =
|
|
8078
|
+
function stopBox(id, journey, m) {
|
|
8079
|
+
const stop = journey.states[id];
|
|
8078
8080
|
const lines = (stop.requirements ?? []).map((r) => m.rules.get(r)).filter(Boolean).map(plainOf);
|
|
8079
|
-
const back = stop.next &&
|
|
8081
|
+
const back = stop.next && journey.states[stop.next] ? ` Back to ${journey.states[stop.next].title ?? stop.next}.` : "";
|
|
8080
8082
|
return `<div class="stop"><b>${esc(stop.title ?? id)}</b><small>${esc(lines.join(" "))}${esc(back)}</small></div>`;
|
|
8081
8083
|
}
|
|
8082
|
-
function choiceBox(state,
|
|
8084
|
+
function choiceBox(state, journey, numbers) {
|
|
8083
8085
|
const lines = (state.choices ?? []).map((c) => {
|
|
8084
|
-
const target =
|
|
8086
|
+
const target = journey.states[c.next];
|
|
8085
8087
|
const n = numbers.get(c.next);
|
|
8086
8088
|
return `${esc(c.when)} \u2192 ${esc(target?.title ?? c.next)}${n ? ` (${n})` : ""}`;
|
|
8087
8089
|
});
|
|
8088
8090
|
return `<div class="choice">${lines.join("<br>")}</div>`;
|
|
8089
8091
|
}
|
|
8090
|
-
function stepRow(id,
|
|
8091
|
-
const state =
|
|
8092
|
-
const href = `/
|
|
8092
|
+
function stepRow(id, journey, m, numbers) {
|
|
8093
|
+
const state = journey.states[id];
|
|
8094
|
+
const href = `/journeys/${encodeURIComponent(journey.id)}/steps/${encodeURIComponent(id)}`;
|
|
8093
8095
|
const tick = isTicked(state, m) ? ' <span class="tick" title="Every scenario passed">\u2713</span>' : "";
|
|
8094
8096
|
const left = `<a class="box ${esc(state.type)}" href="${href}"><b><span class="num">${numbers.get(id)}</span>${esc(state.title ?? id)}${tick}</b>${state.shows ? `<small>${esc(state.shows)}</small>` : ""}</a>`;
|
|
8095
|
-
const stops = (state
|
|
8096
|
-
const notes = stateRules2(state).map((r) => m.rules.get(r)).filter(Boolean).map((r) => `<li>${ruleLink(r,
|
|
8097
|
+
const stops = stopBoxes(state, journey, m);
|
|
8098
|
+
const notes = stateRules2(state).map((r) => m.rules.get(r)).filter(Boolean).map((r) => `<li>${ruleLink(r, journey.id)}</li>`);
|
|
8097
8099
|
return `<div class="row"><div>${left}</div><div>${stops.join("")}<ul class="notes">${notes.join("")}</ul></div></div>`;
|
|
8098
8100
|
}
|
|
8099
|
-
function
|
|
8100
|
-
const order = walk(
|
|
8101
|
+
function journeyBody(journey, m) {
|
|
8102
|
+
const order = walk(journey);
|
|
8101
8103
|
const numbers = /* @__PURE__ */ new Map();
|
|
8102
|
-
for (const id of order) if (isStep(
|
|
8103
|
-
const rows = order.filter((id) =>
|
|
8104
|
-
const state =
|
|
8105
|
-
if (isStep(state)) return stepRow(id,
|
|
8104
|
+
for (const id of order) if (isStep(journey.states[id])) numbers.set(id, numbers.size + 1);
|
|
8105
|
+
const rows = order.filter((id) => journey.states[id].type !== "stop").map((id) => {
|
|
8106
|
+
const state = journey.states[id];
|
|
8107
|
+
if (isStep(state)) return stepRow(id, journey, m, numbers);
|
|
8106
8108
|
if (state.type === "choice")
|
|
8107
|
-
return `<div class="row"><div>${choiceBox(state,
|
|
8108
|
-
const
|
|
8109
|
-
return `<div class="row"><div><span class="end">${esc(state.title ?? "Done")}${
|
|
8109
|
+
return `<div class="row"><div>${choiceBox(state, journey, numbers)}</div><div></div></div>`;
|
|
8110
|
+
const journeyOut = state.next_journey ? ` \xB7 next: <a href="/journeys/${esc(state.next_journey)}">${esc(state.next_journey)}</a>` : "";
|
|
8111
|
+
return `<div class="row"><div><span class="end">${esc(state.title ?? "Done")}${journeyOut}</span></div><div></div></div>`;
|
|
8110
8112
|
});
|
|
8111
8113
|
const reached = new Set(order);
|
|
8112
|
-
const loose = Object.keys(
|
|
8113
|
-
(id) =>
|
|
8114
|
+
const loose = Object.keys(journey.states).filter(
|
|
8115
|
+
(id) => journey.states[id].type === "stop" && !order.some((o) => (journey.states[o].catch ?? []).includes(id))
|
|
8114
8116
|
);
|
|
8115
|
-
const looseHtml = loose.length ? `<h2>Other places it can stop</h2>${loose.map((s) => stopBox(s,
|
|
8116
|
-
const unreached = Object.keys(
|
|
8117
|
-
(id) => isStep(
|
|
8117
|
+
const looseHtml = loose.length ? `<h2>Other places it can stop</h2>${loose.map((s) => stopBox(s, journey, m)).join("")}` : "";
|
|
8118
|
+
const unreached = Object.keys(journey.states).filter(
|
|
8119
|
+
(id) => isStep(journey.states[id]) && !reached.has(id)
|
|
8118
8120
|
);
|
|
8119
|
-
const unreachedHtml = unreached.length ? `<p class="muted">Not reached from the start: ${unreached.map((id) => esc(
|
|
8120
|
-
const checklist =
|
|
8121
|
-
(r) => `<li>${ruleLink(r,
|
|
8121
|
+
const unreachedHtml = unreached.length ? `<p class="muted">Not reached from the start: ${unreached.map((id) => esc(journey.states[id].title ?? id)).join(", ")}.</p>` : "";
|
|
8122
|
+
const checklist = journey.requirements.map((id) => m.rules.get(id)).filter(Boolean).map(
|
|
8123
|
+
(r) => `<li>${ruleLink(r, journey.id)} <span class="tag">${r.scenarios.length} ${r.scenarios.length === 1 ? "scenario" : "scenarios"} \xB7 ${runSummary(r, m)}</span></li>`
|
|
8122
8124
|
);
|
|
8123
|
-
return `<h1>${esc(
|
|
8125
|
+
return `<h1>${esc(journey.title)}</h1><p class="sub">${esc(journey.persona)} \xB7 v${esc(journey.version)} \xB7 ${walkedText(journey, m)}${compareLink(journey)}</p>${changedBadge(journey, m, TOOLS)}
|
|
8124
8126
|
<div class="row"><h3>Steps</h3><h3>Where it can stop, and the rules</h3></div>
|
|
8125
8127
|
${rows.join('<div class="row"><div class="arrow">\u2193</div><div></div></div>')}${looseHtml}${unreachedHtml}
|
|
8126
|
-
<h2>Checklist</h2><ul class="list">${checklist.join("") || '<li class="muted">This
|
|
8128
|
+
<h2>Checklist</h2><ul class="list">${checklist.join("") || '<li class="muted">This journey has no rules yet.</li>'}</ul>`;
|
|
8127
8129
|
}
|
|
8128
8130
|
function scenarioRun(id, m) {
|
|
8129
8131
|
const run = m.latest[id];
|
|
@@ -8135,54 +8137,57 @@ function scenarioList(rule, m) {
|
|
|
8135
8137
|
(s) => `<li><b>${esc(s.id)}</b> ${esc(s.name)} <span class="tag">${scenarioRun(s.id, m)}</span></li>`
|
|
8136
8138
|
).join("")}</ul>`;
|
|
8137
8139
|
}
|
|
8138
|
-
function ruleCard(rule,
|
|
8140
|
+
function ruleCard(rule, journeyId, m) {
|
|
8139
8141
|
return `<div class="card"><h3>${esc(rule.id)} \xB7 ${esc(rule.title)}</h3>
|
|
8140
|
-
<p>${esc(rule.statement)}</p><p class="muted">${ruleLink(rule,
|
|
8142
|
+
<p>${esc(rule.statement)}</p><p class="muted">${ruleLink(rule, journeyId)}</p>${scenarioList(rule, m)}</div>`;
|
|
8141
8143
|
}
|
|
8142
|
-
function
|
|
8143
|
-
|
|
8144
|
+
function stopBoxes(state, journey, m) {
|
|
8145
|
+
return (state.catch ?? []).filter((s) => journey.states[s]).map((s) => stopBox(s, journey, m));
|
|
8146
|
+
}
|
|
8147
|
+
function stepBody(journey, id, m) {
|
|
8148
|
+
const state = journey.states[id];
|
|
8144
8149
|
const rules = stateRules2(state).map((r) => m.rules.get(r)).filter(Boolean);
|
|
8145
|
-
const stops = (state
|
|
8146
|
-
return `<p class="sub"><a href="/
|
|
8150
|
+
const stops = stopBoxes(state, journey, m);
|
|
8151
|
+
return `<p class="sub"><a href="/journeys/${encodeURIComponent(journey.id)}">${esc(journey.title)}</a></p>
|
|
8147
8152
|
<h1>${esc(state.title ?? id)}</h1>${state.shows ? `<p class="sub">${esc(state.shows)}</p>` : ""}
|
|
8148
8153
|
${stops.length ? `<h2>Where it can stop</h2>${stops.join("")}` : ""}
|
|
8149
|
-
<h2>Rules</h2>${rules.map((r) => ruleCard(r,
|
|
8154
|
+
<h2>Rules</h2>${rules.map((r) => ruleCard(r, journey.id, m)).join("") || '<p class="muted">This step has no rules yet.</p>'}`;
|
|
8150
8155
|
}
|
|
8151
8156
|
function ruleBody(rule, m, from) {
|
|
8152
|
-
const others = rule.used_by.filter((id) => id !== from).map((id) => m.
|
|
8153
|
-
const used = others.length ? `<ul class="list">${others.map((f) => `<li><a href="/
|
|
8157
|
+
const others = rule.used_by.filter((id) => id !== from).map((id) => m.journeyById.get(id)).filter(Boolean);
|
|
8158
|
+
const used = others.length ? `<ul class="list">${others.map((f) => `<li><a href="/journeys/${encodeURIComponent(f.id)}">${esc(f.title)}</a> <span class="muted">${esc(f.persona)} \xB7 v${esc(f.version)}</span></li>`).join("")}</ul>` : `<p class="muted">${from ? "No other journey uses this rule." : "No journey uses this rule."}</p>`;
|
|
8154
8159
|
const where = rule.scope === "product" ? '<p class="tag">Product-wide rule</p>' : "";
|
|
8155
|
-
return `<p class="sub">${from && m.
|
|
8160
|
+
return `<p class="sub">${from && m.journeyById.has(from) ? `<a href="/journeys/${encodeURIComponent(from)}">${esc(m.journeyById.get(from).title)}</a>` : '<a href="/">Journeys</a>'}</p>
|
|
8156
8161
|
<h1>${esc(rule.id)} \xB7 ${esc(rule.title)}</h1>${where}
|
|
8157
8162
|
<div class="card"><h3>Statement</h3><p>${esc(rule.statement)}</p><h3>In plain words</h3><p>${esc(plainOf(rule))}</p></div>
|
|
8158
8163
|
<h2>Scenarios</h2>${scenarioList(rule, m)}
|
|
8159
|
-
<h2>${from ? "Other
|
|
8164
|
+
<h2>${from ? "Other journeys that use it" : "Journeys that use it"}</h2>${used}`;
|
|
8160
8165
|
}
|
|
8161
8166
|
function productWide(m) {
|
|
8162
8167
|
const sections = m.capabilities.map((c) => ({ c, rules: c.requirements.filter((r) => r.scope === "product") })).filter((s) => s.rules.length).map(
|
|
8163
8168
|
(s) => `<h2>${esc(s.c.title)}</h2><ul class="list">${s.rules.map((r) => `<li>${ruleLink(r)}</li>`).join("")}</ul>`
|
|
8164
8169
|
);
|
|
8165
|
-
return `<h1>Product-wide rules</h1><p class="sub">Rules that hold across every
|
|
8170
|
+
return `<h1>Product-wide rules</h1><p class="sub">Rules that hold across every journey.</p>${sections.join("") || '<p class="muted">No product-wide rules yet.</p>'}`;
|
|
8166
8171
|
}
|
|
8167
|
-
function
|
|
8168
|
-
const
|
|
8169
|
-
if (!
|
|
8170
|
-
if (kind === "compare" && state === void 0) return compareRoute(
|
|
8171
|
-
if (kind === void 0) return { title:
|
|
8172
|
-
if (kind !== "steps" || !isStep(
|
|
8173
|
-
return { title:
|
|
8172
|
+
function journeyRoute(m, [id, kind, state], query) {
|
|
8173
|
+
const journey = m.journeyById.get(id);
|
|
8174
|
+
if (!journey) return void 0;
|
|
8175
|
+
if (kind === "compare" && state === void 0) return compareRoute(journey, m, query, TOOLS);
|
|
8176
|
+
if (kind === void 0) return { title: journey.title, body: journeyBody(journey, m) };
|
|
8177
|
+
if (kind !== "steps" || !isStep(journey.states[state])) return void 0;
|
|
8178
|
+
return { title: journey.states[state].title ?? state, body: stepBody(journey, state, m) };
|
|
8174
8179
|
}
|
|
8175
8180
|
function ruleRoute(m, [id], query) {
|
|
8176
8181
|
const rule = m.rules.get(id);
|
|
8177
8182
|
return rule && { title: rule.title, body: ruleBody(rule, m, query.get("from")) };
|
|
8178
8183
|
}
|
|
8179
|
-
function route(m, parts, query) {
|
|
8180
|
-
if (parts.length === 0) return { title: "
|
|
8184
|
+
function route(m, parts, query, origin) {
|
|
8185
|
+
if (parts.length === 0) return { title: "Journeys", body: dashboard(m, query, origin) };
|
|
8181
8186
|
if (!Object.hasOwn(ROUTES, parts[0])) return void 0;
|
|
8182
8187
|
return ROUTES[parts[0]](m, parts.slice(1), query);
|
|
8183
8188
|
}
|
|
8184
8189
|
function notFound(findings) {
|
|
8185
|
-
const body = '<h1>Not found</h1><p class="sub">There is no such page. <a href="/">Back to the
|
|
8190
|
+
const body = '<h1>Not found</h1><p class="sub">There is no such page. <a href="/">Back to the journeys</a>.</p>';
|
|
8186
8191
|
return { status: 404, html: layout("Not found", body, findings) };
|
|
8187
8192
|
}
|
|
8188
8193
|
function renderPage(mapper, pathname, files, findings, evidence2 = []) {
|
|
@@ -8199,7 +8204,13 @@ function renderPage(mapper, pathname, files, findings, evidence2 = []) {
|
|
|
8199
8204
|
)
|
|
8200
8205
|
};
|
|
8201
8206
|
const parts = url2.pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
8202
|
-
const
|
|
8207
|
+
const origin = url2.origin === "http://studio" ? void 0 : url2.origin;
|
|
8208
|
+
const found = route(
|
|
8209
|
+
versionModel(indexModel(model, evidence2), files),
|
|
8210
|
+
parts,
|
|
8211
|
+
url2.searchParams,
|
|
8212
|
+
origin
|
|
8213
|
+
);
|
|
8203
8214
|
if (!found) return notFound(shown);
|
|
8204
8215
|
return { status: 200, html: layout(found.title, found.body, shown) };
|
|
8205
8216
|
}
|
|
@@ -8208,7 +8219,7 @@ var init_pages = __esm({
|
|
|
8208
8219
|
"src/studio/pages.js"() {
|
|
8209
8220
|
init_evidence();
|
|
8210
8221
|
init_versionPages();
|
|
8211
|
-
STEP_TYPES = ["screen", "outside", "
|
|
8222
|
+
STEP_TYPES = ["screen", "outside", "subjourney"];
|
|
8212
8223
|
CSS = `
|
|
8213
8224
|
:root{--bg:#fff;--fg:#1b1b1f;--muted:#5f6368;--line:#d9dbe0;--card:#f6f7f9;--accent:#2f5bd8;--stop:#b3261e;--stopbg:#fdf0ef;--warn:#8a5a00;--warnbg:#fff6e0;--ok:#1e7b34}
|
|
8214
8225
|
@media (prefers-color-scheme: dark){:root{--bg:#16171a;--fg:#e8e9ed;--muted:#9aa0a8;--line:#34363c;--card:#1f2125;--accent:#8ab0ff;--stop:#ff9a92;--stopbg:#2b1b1a;--warn:#f0c36a;--warnbg:#2a2415;--ok:#6fcf86}}
|
|
@@ -8243,7 +8254,7 @@ pre{background:var(--card);border:1px solid var(--line);border-radius:8px;paddin
|
|
|
8243
8254
|
ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
8244
8255
|
TOOLS = { esc, plainOf };
|
|
8245
8256
|
ROUTES = {
|
|
8246
|
-
|
|
8257
|
+
journeys: (m, rest, query) => [1, 2, 3].includes(rest.length) ? journeyRoute(m, rest, query) : void 0,
|
|
8247
8258
|
rules: (m, rest, query) => rest.length === 1 ? ruleRoute(m, rest, query) : void 0,
|
|
8248
8259
|
"product-wide": (m, rest) => rest.length === 0 ? { title: "Product-wide rules", body: productWide(m) } : void 0
|
|
8249
8260
|
};
|
|
@@ -24211,8 +24222,8 @@ function finalize(ctx, schema) {
|
|
|
24211
24222
|
compactTypeUnion(entry[1].def ?? entry[1].schema);
|
|
24212
24223
|
}
|
|
24213
24224
|
}
|
|
24214
|
-
for (const
|
|
24215
|
-
|
|
24225
|
+
for (const rewrite2 of ctx.deferred)
|
|
24226
|
+
rewrite2();
|
|
24216
24227
|
if (ctx.intersections.length) {
|
|
24217
24228
|
const carriers = /* @__PURE__ */ new Map();
|
|
24218
24229
|
for (const seen of ctx.seen.values()) {
|
|
@@ -44271,13 +44282,13 @@ var mcp_exports = {};
|
|
|
44271
44282
|
__export(mcp_exports, {
|
|
44272
44283
|
mcpHandler: () => mcpHandler
|
|
44273
44284
|
});
|
|
44274
|
-
import { existsSync as
|
|
44275
|
-
import { dirname as dirname3, join as
|
|
44285
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
44286
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
44276
44287
|
import { Readable } from "node:stream";
|
|
44277
44288
|
function specPath(path) {
|
|
44278
44289
|
const [dir, name, ...rest] = path.split("/");
|
|
44279
44290
|
const ok = FOLDERS2.includes(dir) && name && rest.length === 0 && !path.includes("..") && !path.includes("\\") && name.endsWith(EXTENSION[dir]);
|
|
44280
|
-
return ok ? null : `"${path}" is not a spec file. Use capabilities/<NAME>.yaml,
|
|
44291
|
+
return ok ? null : `"${path}" is not a spec file. Use capabilities/<NAME>.yaml, journeys/<name>.yaml or scenarios/<ID>.feature.`;
|
|
44281
44292
|
}
|
|
44282
44293
|
function check2(mapper, files) {
|
|
44283
44294
|
const findings = mapper.v2Check(files);
|
|
@@ -44298,8 +44309,8 @@ function impactLines(effect) {
|
|
|
44298
44309
|
const moved = effect.affected.map((id) => `${id} (now version ${effect.moved[id]}, not built)`);
|
|
44299
44310
|
return `
|
|
44300
44311
|
Rule changed: ${effect.rules.join(", ")}.
|
|
44301
|
-
Affected
|
|
44302
|
-
Not affected
|
|
44312
|
+
Affected journeys: ${moved.join(", ") || "none"}.
|
|
44313
|
+
Not affected journeys: ${effect.unaffected.join(", ") || "none"}.`;
|
|
44303
44314
|
}
|
|
44304
44315
|
function buildServer(root, mapper) {
|
|
44305
44316
|
const server = new McpServer(
|
|
@@ -44310,7 +44321,7 @@ function buildServer(root, mapper) {
|
|
|
44310
44321
|
server.registerTool(
|
|
44311
44322
|
"vds_index",
|
|
44312
44323
|
{
|
|
44313
|
-
description: "The workspace index: every capability, rule,
|
|
44324
|
+
description: "The workspace index: every capability, rule, journey and scenario with its id. Call this first.",
|
|
44314
44325
|
inputSchema: external_exports.object({}),
|
|
44315
44326
|
annotations: { readOnlyHint: true }
|
|
44316
44327
|
},
|
|
@@ -44319,19 +44330,19 @@ function buildServer(root, mapper) {
|
|
|
44319
44330
|
server.registerTool(
|
|
44320
44331
|
"vds_guide",
|
|
44321
44332
|
{
|
|
44322
|
-
description: "The text of VDS.md: how rules,
|
|
44333
|
+
description: "The text of VDS.md: how rules, journeys and scenarios are written in this workspace.",
|
|
44323
44334
|
inputSchema: external_exports.object({}),
|
|
44324
44335
|
annotations: { readOnlyHint: true }
|
|
44325
44336
|
},
|
|
44326
44337
|
async () => {
|
|
44327
|
-
const file2 =
|
|
44328
|
-
return
|
|
44338
|
+
const file2 = join8(root, "VDS.md");
|
|
44339
|
+
return existsSync5(file2) ? say(readFileSync9(file2, "utf8")) : refuse("This workspace has no VDS.md.");
|
|
44329
44340
|
}
|
|
44330
44341
|
);
|
|
44331
44342
|
server.registerTool(
|
|
44332
44343
|
"vds_read",
|
|
44333
44344
|
{
|
|
44334
|
-
description: "Read one spec file: capabilities/<NAME>.yaml,
|
|
44345
|
+
description: "Read one spec file: capabilities/<NAME>.yaml, journeys/<name>.yaml or scenarios/<ID>.feature.",
|
|
44335
44346
|
inputSchema: external_exports.object({ path: pathArg }),
|
|
44336
44347
|
annotations: { readOnlyHint: true }
|
|
44337
44348
|
},
|
|
@@ -44393,22 +44404,22 @@ function buildServer(root, mapper) {
|
|
|
44393
44404
|
delete after[path];
|
|
44394
44405
|
const refused = newProblems(mapper, before, after);
|
|
44395
44406
|
if (refused) return refused;
|
|
44396
|
-
rmSync(
|
|
44407
|
+
rmSync(join8(root, path));
|
|
44397
44408
|
return say(`Deleted ${path}.`);
|
|
44398
44409
|
}
|
|
44399
44410
|
);
|
|
44400
44411
|
server.registerTool(
|
|
44401
44412
|
"vds_mark_built",
|
|
44402
44413
|
{
|
|
44403
|
-
description: 'Mark a
|
|
44404
|
-
inputSchema: external_exports.object({
|
|
44414
|
+
description: 'Mark a journey built at its current version, recording who and when. Use the journey id, like "onboarding".',
|
|
44415
|
+
inputSchema: external_exports.object({ journey: external_exports.string().describe("The journey id.") }),
|
|
44405
44416
|
annotations: { destructiveHint: true }
|
|
44406
44417
|
},
|
|
44407
|
-
async ({
|
|
44408
|
-
const writes = builtWrites(readWorkspace(root),
|
|
44409
|
-
if (!writes) return refuse(`No
|
|
44418
|
+
async ({ journey }) => {
|
|
44419
|
+
const writes = builtWrites(readWorkspace(root), journey);
|
|
44420
|
+
if (!writes) return refuse(`No journey ${journey}.`);
|
|
44410
44421
|
writeAll(root, writes);
|
|
44411
|
-
return say(`${
|
|
44422
|
+
return say(`${journey} is built at its current version.`);
|
|
44412
44423
|
}
|
|
44413
44424
|
);
|
|
44414
44425
|
return server;
|
|
@@ -44454,8 +44465,8 @@ var init_mcp = __esm({
|
|
|
44454
44465
|
init_built();
|
|
44455
44466
|
init_versions();
|
|
44456
44467
|
init_workspace();
|
|
44457
|
-
EXTENSION = { capabilities: ".yaml",
|
|
44458
|
-
INSTRUCTIONS = "VDS workspace on this machine. Call vds_index first to see every rule,
|
|
44468
|
+
EXTENSION = { capabilities: ".yaml", journeys: ".yaml", scenarios: ".feature" };
|
|
44469
|
+
INSTRUCTIONS = "VDS workspace on this machine. Call vds_index first to see every rule, journey and scenario, and vds_guide for how to write them. Change files with vds_write and vds_delete: the checker runs on the whole result and refuses a change that adds a problem, naming each one. Fix what it names and write again.";
|
|
44459
44470
|
say = (text) => ({ content: [{ type: "text", text }] });
|
|
44460
44471
|
refuse = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
44461
44472
|
findingKey = (f) => `${f.path}
|
|
@@ -44468,7 +44479,7 @@ ${f.message}`;
|
|
|
44468
44479
|
init_node();
|
|
44469
44480
|
init_built();
|
|
44470
44481
|
import { spawn } from "node:child_process";
|
|
44471
|
-
import { existsSync as
|
|
44482
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10 } from "node:fs";
|
|
44472
44483
|
import { resolve } from "node:path";
|
|
44473
44484
|
|
|
44474
44485
|
// src/changelog.js
|
|
@@ -44476,7 +44487,7 @@ import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
|
44476
44487
|
|
|
44477
44488
|
// src/version.js
|
|
44478
44489
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
44479
|
-
var baked = false ? null : "0.0.
|
|
44490
|
+
var baked = false ? null : "0.0.8";
|
|
44480
44491
|
var VERSION = process.env.VDS_VERSION ?? baked ?? JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
44481
44492
|
function compare(a, b) {
|
|
44482
44493
|
const [x, y] = [a, b].map((v) => v.split("-")[0].split(".").map(Number));
|
|
@@ -44536,21 +44547,21 @@ import { join as join3 } from "node:path";
|
|
|
44536
44547
|
// src/guide.js
|
|
44537
44548
|
var GUIDE = `# Working in this VDS workspace
|
|
44538
44549
|
|
|
44539
|
-
This folder holds the product's requirements,
|
|
44550
|
+
This folder holds the product's requirements, journeys and scenarios as plain files.
|
|
44540
44551
|
Read and write them with the vds command.
|
|
44541
44552
|
|
|
44542
44553
|
## Layout
|
|
44543
44554
|
|
|
44544
44555
|
- capabilities/<CODE>.yaml: one capability and its requirements (ids like ONB-01).
|
|
44545
|
-
-
|
|
44556
|
+
- journeys/<id>.yaml: a journey made of states; each state points at requirement ids.
|
|
44546
44557
|
- scenarios/<REQ-ID>.feature: Given, When, Then scenarios for one requirement.
|
|
44547
44558
|
|
|
44548
44559
|
## How to work
|
|
44549
44560
|
|
|
44550
|
-
1. Run \`vds index\` first. It prints one line per
|
|
44561
|
+
1. Run \`vds index\` first. It prints one line per journey and requirement, so you know what exists.
|
|
44551
44562
|
2. Open one file at a time. Read only the file you are changing.
|
|
44552
44563
|
3. Write requirements in EARS: "When <trigger>, the system SHALL <response>." The system is always the subject.
|
|
44553
|
-
4. Point
|
|
44564
|
+
4. Point journeys at requirement ids. A state lists the ids it needs under \`requirements:\`.
|
|
44554
44565
|
5. Run \`vds check\` before you finish. It prints "valid" when nothing is wrong; fix every problem it lists.
|
|
44555
44566
|
`;
|
|
44556
44567
|
var markerFor = (version2) => `<!-- vds ${version2} -->`;
|
|
@@ -44560,8 +44571,8 @@ ${GUIDE}`;
|
|
|
44560
44571
|
}
|
|
44561
44572
|
|
|
44562
44573
|
// src/init.js
|
|
44563
|
-
var FOLDERS = ["capabilities", "
|
|
44564
|
-
var FIRST_PORT = 4600;
|
|
44574
|
+
var FOLDERS = ["capabilities", "journeys", "scenarios"];
|
|
44575
|
+
var FIRST_PORT = Number(process.env.VDS_FIRST_PORT) || 4600;
|
|
44565
44576
|
async function freePort() {
|
|
44566
44577
|
for (let port = FIRST_PORT; ; port++) {
|
|
44567
44578
|
const free = await new Promise((resolve2) => {
|
|
@@ -44589,12 +44600,75 @@ async function init(root) {
|
|
|
44589
44600
|
|
|
44590
44601
|
// src/update.js
|
|
44591
44602
|
import { spawnSync } from "node:child_process";
|
|
44592
|
-
import { existsSync as
|
|
44593
|
-
import { dirname as dirname2, join as
|
|
44603
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
44604
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
44594
44605
|
import { fileURLToPath } from "node:url";
|
|
44595
44606
|
|
|
44596
44607
|
// src/migrations.js
|
|
44597
|
-
|
|
44608
|
+
import {
|
|
44609
|
+
existsSync as existsSync3,
|
|
44610
|
+
mkdirSync as mkdirSync4,
|
|
44611
|
+
readdirSync as readdirSync2,
|
|
44612
|
+
readFileSync as readFileSync5,
|
|
44613
|
+
rmdirSync,
|
|
44614
|
+
writeFileSync as writeFileSync4,
|
|
44615
|
+
renameSync
|
|
44616
|
+
} from "node:fs";
|
|
44617
|
+
import { join as join4 } from "node:path";
|
|
44618
|
+
function journeyText(text) {
|
|
44619
|
+
const hasKind = /^kind:/m.test(text);
|
|
44620
|
+
return text.split("\n").map((line) => {
|
|
44621
|
+
const top = line.match(/^flow:(.*)$/);
|
|
44622
|
+
if (top) return hasKind ? `journey:${top[1]}` : `journey:${top[1]}
|
|
44623
|
+
kind: journey`;
|
|
44624
|
+
return line.replace(/\bnext_flow(?=\s*:)/g, "next_journey").replace(/(\btype:\s*)subflow\b/g, "$1subjourney");
|
|
44625
|
+
}).join("\n");
|
|
44626
|
+
}
|
|
44627
|
+
function rewrite(file2) {
|
|
44628
|
+
const before = readFileSync5(file2, "utf8");
|
|
44629
|
+
const after = journeyText(before);
|
|
44630
|
+
if (after === before) return false;
|
|
44631
|
+
writeFileSync4(file2, after);
|
|
44632
|
+
return true;
|
|
44633
|
+
}
|
|
44634
|
+
var yamlFiles = (dir) => existsSync3(dir) ? readdirSync2(dir).filter((n) => n.endsWith(".yaml")) : [];
|
|
44635
|
+
function flowsToJourneys(root) {
|
|
44636
|
+
const changed = [];
|
|
44637
|
+
const old = join4(root, "flows");
|
|
44638
|
+
const target = join4(root, "journeys");
|
|
44639
|
+
const names = yamlFiles(old);
|
|
44640
|
+
if (names.length > 0) mkdirSync4(target, { recursive: true });
|
|
44641
|
+
for (const name of names) {
|
|
44642
|
+
if (existsSync3(join4(target, name)))
|
|
44643
|
+
throw new Error(`journeys/${name} already exists; flows/${name} was left in place`);
|
|
44644
|
+
renameSync(join4(old, name), join4(target, name));
|
|
44645
|
+
rewrite(join4(target, name));
|
|
44646
|
+
changed.push(`flows/${name} -> journeys/${name}`);
|
|
44647
|
+
}
|
|
44648
|
+
if (existsSync3(old) && readdirSync2(old).length === 0) rmdirSync(old);
|
|
44649
|
+
for (const name of yamlFiles(target)) {
|
|
44650
|
+
if (!changed.some((c) => c.endsWith(`journeys/${name}`)) && rewrite(join4(target, name)))
|
|
44651
|
+
changed.push(`journeys/${name}`);
|
|
44652
|
+
}
|
|
44653
|
+
return changed;
|
|
44654
|
+
}
|
|
44655
|
+
function rewriteVersions(root) {
|
|
44656
|
+
const versions = join4(root, "versions");
|
|
44657
|
+
const changed = [];
|
|
44658
|
+
for (const id of existsSync3(versions) ? readdirSync2(versions) : []) {
|
|
44659
|
+
for (const name of yamlFiles(join4(versions, id))) {
|
|
44660
|
+
if (rewrite(join4(versions, id, name))) changed.push(`versions/${id}/${name}`);
|
|
44661
|
+
}
|
|
44662
|
+
}
|
|
44663
|
+
return changed;
|
|
44664
|
+
}
|
|
44665
|
+
var MIGRATIONS = [
|
|
44666
|
+
{
|
|
44667
|
+
version: "0.0.8",
|
|
44668
|
+
describe: "flows became journeys: flows/ moves to journeys/, with kind: journey",
|
|
44669
|
+
run: (root) => [...flowsToJourneys(root), ...rewriteVersions(root)]
|
|
44670
|
+
}
|
|
44671
|
+
];
|
|
44598
44672
|
async function loadMigrations() {
|
|
44599
44673
|
if (!process.env.VDS_MIGRATIONS) return MIGRATIONS;
|
|
44600
44674
|
return (await import(process.env.VDS_MIGRATIONS)).default;
|
|
@@ -44626,28 +44700,28 @@ function installKind() {
|
|
|
44626
44700
|
if (file2.includes("/_npx/")) return { kind: "npx" };
|
|
44627
44701
|
const at = file2.indexOf("/node_modules/");
|
|
44628
44702
|
const root = at > 0 ? file2.slice(0, at) : null;
|
|
44629
|
-
if (root &&
|
|
44703
|
+
if (root && existsSync4(join5(root, "package.json"))) return { kind: "project", root };
|
|
44630
44704
|
return { kind: "global" };
|
|
44631
44705
|
}
|
|
44632
44706
|
function installPlan(version2) {
|
|
44633
44707
|
const spec = `@teurons/vds@${version2}`;
|
|
44634
44708
|
const found = installKind();
|
|
44635
44709
|
if (found.kind === "project") {
|
|
44636
|
-
const has = (f) =>
|
|
44710
|
+
const has = (f) => existsSync4(join5(found.root, f));
|
|
44637
44711
|
const cmd = has("pnpm-lock.yaml") ? `pnpm add -D ${spec}` : has("yarn.lock") ? `yarn add -D ${spec}` : `npm install -D ${spec}`;
|
|
44638
|
-
return { ...found, cmd, bin:
|
|
44712
|
+
return { ...found, cmd, bin: join5(found.root, "node_modules", ".bin", "vds") };
|
|
44639
44713
|
}
|
|
44640
44714
|
return { ...found, cmd: `npm install -g ${spec}`, bin: "vds" };
|
|
44641
44715
|
}
|
|
44642
44716
|
function refreshGuide(folder) {
|
|
44643
|
-
const path =
|
|
44644
|
-
if (!
|
|
44645
|
-
const current =
|
|
44717
|
+
const path = join5(folder, "VDS.md");
|
|
44718
|
+
if (!existsSync4(path)) return false;
|
|
44719
|
+
const current = readFileSync6(path, "utf8");
|
|
44646
44720
|
const marked = current.match(MARKER)?.[1];
|
|
44647
44721
|
if (marked && compare(marked, VERSION) >= 0) return false;
|
|
44648
44722
|
const next = guideText();
|
|
44649
44723
|
if (next === current) return false;
|
|
44650
|
-
|
|
44724
|
+
writeFileSync5(path, next);
|
|
44651
44725
|
return true;
|
|
44652
44726
|
}
|
|
44653
44727
|
function runShell(cmd, version2, cwd) {
|
|
@@ -44659,8 +44733,8 @@ function runShell(cmd, version2, cwd) {
|
|
|
44659
44733
|
});
|
|
44660
44734
|
}
|
|
44661
44735
|
function guideVersion(folder) {
|
|
44662
|
-
const path =
|
|
44663
|
-
return
|
|
44736
|
+
const path = join5(folder, "VDS.md");
|
|
44737
|
+
return existsSync4(path) ? readFileSync6(path, "utf8").match(MARKER)?.[1] : void 0;
|
|
44664
44738
|
}
|
|
44665
44739
|
async function afterUpdate(folder, old) {
|
|
44666
44740
|
if (old) {
|
|
@@ -44715,8 +44789,8 @@ async function update(folder, { guideOnly = false, after } = {}) {
|
|
|
44715
44789
|
// src/bin.js
|
|
44716
44790
|
init_workspace();
|
|
44717
44791
|
function defaultFolder() {
|
|
44718
|
-
const isWorkspace = [...FOLDERS2, "VDS.md"].some((name) =>
|
|
44719
|
-
return !isWorkspace &&
|
|
44792
|
+
const isWorkspace = [...FOLDERS2, "VDS.md"].some((name) => existsSync6(name));
|
|
44793
|
+
return !isWorkspace && existsSync6("vds-workspace") ? "vds-workspace" : ".";
|
|
44720
44794
|
}
|
|
44721
44795
|
function openBrowser(url2) {
|
|
44722
44796
|
if (process.env.VDS_NO_OPEN === "1") return;
|
|
@@ -44758,7 +44832,7 @@ function evidence(args) {
|
|
|
44758
44832
|
const [report, folder] = args.filter((_, i) => at < 0 || i !== at && i !== at + 1);
|
|
44759
44833
|
if (!report) throw new Error("usage: vds evidence <report-file> [folder] [--env <name>]");
|
|
44760
44834
|
const root = resolve(folder ?? defaultFolder());
|
|
44761
|
-
const r = recordEvidence({ reportText:
|
|
44835
|
+
const r = recordEvidence({ reportText: readFileSync10(resolve(report), "utf8"), root, env });
|
|
44762
44836
|
const s = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
44763
44837
|
console.log(
|
|
44764
44838
|
`${s(r.recorded, "scenario", "scenarios")} recorded in ${r.file}; ${s(r.unmatched, "test", "tests")} matched no scenario`
|
|
@@ -44771,15 +44845,15 @@ async function printIndex(folder) {
|
|
|
44771
44845
|
process.stdout.write(mapper.v2Index(readWorkspace(resolve(folder))));
|
|
44772
44846
|
return 0;
|
|
44773
44847
|
}
|
|
44774
|
-
function
|
|
44848
|
+
function markJourneyBuilt(journeyId, folder) {
|
|
44775
44849
|
const root = resolve(folder);
|
|
44776
|
-
const writes =
|
|
44850
|
+
const writes = journeyId && builtWrites(readWorkspace(root), journeyId);
|
|
44777
44851
|
if (!writes) {
|
|
44778
|
-
console.error(`No
|
|
44852
|
+
console.error(`No journey ${journeyId ?? ""}. Usage: vds built <journey-id> [folder]`);
|
|
44779
44853
|
return 1;
|
|
44780
44854
|
}
|
|
44781
44855
|
writeAll(root, writes);
|
|
44782
|
-
console.log(`${
|
|
44856
|
+
console.log(`${journeyId} is built at its current version.`);
|
|
44783
44857
|
return 0;
|
|
44784
44858
|
}
|
|
44785
44859
|
function changelog(args) {
|
|
@@ -44794,7 +44868,7 @@ var BY_FOLDER = { check: check3, index: printIndex };
|
|
|
44794
44868
|
var BY_ARGS = {
|
|
44795
44869
|
evidence,
|
|
44796
44870
|
changelog,
|
|
44797
|
-
built: ([
|
|
44871
|
+
built: ([journey, folder]) => markJourneyBuilt(journey, folder ?? defaultFolder())
|
|
44798
44872
|
};
|
|
44799
44873
|
function updateFlags(argv) {
|
|
44800
44874
|
const afterAt = argv.indexOf("--after");
|
|
@@ -44816,7 +44890,7 @@ async function main(argv) {
|
|
|
44816
44890
|
if (Object.hasOwn(BY_FOLDER, command)) return BY_FOLDER[command](folder);
|
|
44817
44891
|
if (Object.hasOwn(BY_ARGS, command)) return BY_ARGS[command](rest);
|
|
44818
44892
|
console.error(
|
|
44819
|
-
"usage: vds init [folder] | vds check [folder] | vds index [folder] | vds built <
|
|
44893
|
+
"usage: vds init [folder] | vds check [folder] | vds index [folder] | vds built <journey> [folder] | vds changelog [--since x.y.z] | vds update [folder] | vds run local [folder]"
|
|
44820
44894
|
);
|
|
44821
44895
|
return 2;
|
|
44822
44896
|
}
|
package/dist/vds_mapper.wasm
CHANGED
|
Binary file
|