@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 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.7 (2026-11-02)
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(flow) {
7514
- return new Set(Object.values(flow.states ?? {}).flatMap(stateRules));
7513
+ function ruleIdsOf(journey) {
7514
+ return new Set(Object.values(journey.states ?? {}).flatMap(stateRules));
7515
7515
  }
7516
- function flowsOf(files) {
7517
- return Object.keys(files).filter((p) => p.startsWith("flows/") && p.endsWith(".yaml")).sort().map((path) => {
7518
- const flow = (0, import_yaml.parse)(files[path]) ?? {};
7519
- return { path, id: flow.flow ?? path.slice(6, -5), flow };
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(flow) {
7523
- return !flow.built || flow.built.version === flow.version;
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 flow = doc.toJS();
7532
+ const journey = doc.toJS();
7533
7533
  const writes = {};
7534
- if (isBuilt(flow)) {
7535
- writes.old = { version: flow.version, text };
7536
- doc.set("version", flow.version + 1);
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, flow } of flowsOf(after)) {
7551
- const used = ruleIdsOf(flow);
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, flowId, by, at = today()) {
7570
- const found = flowsOf(files).find((f) => f.id === flowId);
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.flow.version, by, at }));
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, flowId) {
7577
- const kept = Object.keys(files).map((p) => p.match(new RegExp(`^versions/${flowId}/v(\\d+)\\.yaml$`))).filter(Boolean).map((m) => ({ version: Number(m[1]), text: files[m[0]] }));
7578
- const current = flowsOf(files).find((f) => f.id === flowId);
7579
- if (current) kept.push({ version: current.flow.version, text: files[current.path] });
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 compareFlows(aText, bText) {
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, flowId) {
7627
- return markBuilt(files, flowId, whoIsBuilding());
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 readdirSync2, readFileSync as readFileSync6 } from "node:fs";
7739
- import { join as join5 } from "node:path";
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 = readdirSync2(join5(root, dir));
7745
+ names = readdirSync3(join6(root, dir));
7746
7746
  } catch {
7747
7747
  continue;
7748
7748
  }
7749
- for (const name of names) files[`${dir}/${name}`] = readFileSync6(join5(root, dir, name), "utf8");
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 flows = [];
7755
+ let journeys = [];
7756
7756
  try {
7757
- flows = readdirSync2(join5(root, "versions"));
7757
+ journeys = readdirSync3(join6(root, "versions"));
7758
7758
  } catch {
7759
7759
  return files;
7760
7760
  }
7761
- for (const id of flows) {
7761
+ for (const id of journeys) {
7762
7762
  let names = [];
7763
7763
  try {
7764
- names = readdirSync2(join5(root, "versions", id));
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}`] = readFileSync6(join5(root, "versions", id, n), "utf8");
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", "flows", "scenarios"];
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 readFileSync7, watch } from "node:fs";
7786
+ import { readFileSync as readFileSync8, watch } from "node:fs";
7787
7787
  import { createServer as createServer2 } from "node:http";
7788
- import { join as join6 } from "node:path";
7788
+ import { join as join7 } from "node:path";
7789
7789
  function portFor(root) {
7790
7790
  try {
7791
- const { port } = JSON.parse(readFileSync7(join6(root, ".vds.json"), "utf8"));
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 { status, html } = render(pathname, files, check4(files), loadEvidence(root));
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(flows, files) {
7889
- const info = flows.map((flow) => {
7890
- const raw = (0, import_yaml2.parse)(files[`flows/${flow.id}.yaml`] ?? "") ?? {};
7891
- const versions = versionsOf(files, flow.id).map((v) => v.version);
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
- ...flow,
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(flow, m, { esc: esc3, plainOf: plainOf2 }) {
7900
- if (!flow.vinfo?.unbuilt) return "";
7900
+ function changedBadge(journey, m, { esc: esc3, plainOf: plainOf2 }) {
7901
+ if (!journey.vinfo?.unbuilt) return "";
7901
7902
  const rules = [
7902
7903
  ...new Set(
7903
- flow.vinfo.changes.filter((c) => c.version > (flow.vinfo.built?.version ?? 0)).map((c) => c.rule)
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(flow.version)} is not built yet${lines.length ? `: ${lines.map(esc3).join("; ")}` : ""}</div>`;
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(flow) {
7910
- const { versions, built } = flow.vinfo;
7911
- const current = flow.version;
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(flow) {
7916
- if (!flow.vinfo || flow.vinfo.versions.length < 2) return "";
7917
- const [a, b] = defaultPair(flow);
7918
- return ` \xB7 <a href="/flows/${encodeURIComponent(flow.id)}/compare?a=${a}&amp;b=${b}">Compare versions</a>`;
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}&amp;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(flow, m, query, tools) {
7933
- if (!flow.vinfo || flow.vinfo.versions.length < 2) return void 0;
7934
- const [a0, b0] = defaultPair(flow);
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, flow.id);
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 = compareFlows(va.text, vb.text);
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="/flows/${encodeURIComponent(flow.id)}">${esc3(flow.title)}</a></p>
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: `${flow.title}: versions`, body };
7947
+ return { title: `${journey.title}: versions`, body };
7947
7948
  }
7948
7949
  function versionModel(m, files) {
7949
- const flows = withVersionInfo(m.flows, files);
7950
- return { ...m, files, flows, flowById: new Map(flows.map((f) => [f.id, f])) };
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="/">Flows</a><a href="/product-wide">Product-wide rules</a></header>${banner}
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
- flowById: new Map(model.flows.map((f) => [f.id, f]))
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(flow) {
8001
- return Object.values(flow.states).filter(isStep).length;
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(flow, m) {
8009
- return Object.values(flow.states).filter((s) => isStep(s) && isTicked(s, m)).length;
8009
+ function walkedCount(journey, m) {
8010
+ return Object.values(journey.states).filter((s) => isStep(s) && isTicked(s, m)).length;
8010
8011
  }
8011
- function walkedText(flow, m) {
8012
- return `${walkedCount(flow, m)} of ${stepCount(flow)} steps walked`;
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(flow) {
8025
+ function walk(journey) {
8025
8026
  const order = [];
8026
8027
  const seen = /* @__PURE__ */ new Set();
8027
8028
  const visit2 = (id) => {
8028
- const state = flow.states[id];
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(flow.start_at);
8036
+ visit2(journey.start_at);
8036
8037
  return order;
8037
8038
  }
8038
- function ruleLink(rule, flowId) {
8039
- const query = flowId ? `?from=${encodeURIComponent(flowId)}` : "";
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 flowCard(flow, m) {
8043
- return `<li><a href="/flows/${encodeURIComponent(flow.id)}"><b>${esc(flow.title)}</b></a>
8044
- <div class="muted">${esc(flow.persona)} \xB7 v${esc(flow.version)} \xB7 ${walkedText(flow, m)}</div>${changedBadge(flow, m, TOOLS)}</li>`;
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 flowsByCapability(m) {
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, flows: m.flows.filter((f) => f.requirements.some((id) => mine.has(id))) };
8050
- }).filter((s) => s.flows.length);
8051
- const bare = m.flows.filter((f) => f.requirements.length === 0);
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.flows.map((f) => flowCard(f, m)).join("")}</ul>`
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) => flowCard(f, m)).join("")}</ul>`
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
- return `<h1>No flows yet</h1>
8063
- <p class="sub">Connect an AI app to this studio, then ask it to write the first flow through MCP.</p>
8062
+ function emptyWorkspace(origin) {
8063
+ const address = origin ? `${esc(origin)}/mcp` : "http://localhost:&lt;port&gt;/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, which is this server's /mcp:</p>
8066
- <pre>http://localhost:&lt;port&gt;/mcp</pre>
8067
- <p class="muted">Use the port this server is listening on. Then ask the app to write the first flow: who it is for, the steps they take, and the rules on each step.</p></div>`;
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.flows.length === 0) return emptyWorkspace();
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 flows</a>' : '<a href="/?group=capability">Group by capability</a>';
8073
- const list = grouped ? flowsByCapability(m) : `<ul class="list">${m.flows.map((f) => flowCard(f, m)).join("")}</ul>`;
8074
- return `<h1>Flows</h1><p class="sub">${switcher} \xB7 <a href="/product-wide">Product-wide rules</a></p>${list}`;
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, flow, m) {
8077
- const stop = flow.states[id];
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 && flow.states[stop.next] ? ` Back to ${flow.states[stop.next].title ?? 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, flow, numbers) {
8084
+ function choiceBox(state, journey, numbers) {
8083
8085
  const lines = (state.choices ?? []).map((c) => {
8084
- const target = flow.states[c.next];
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, flow, m, numbers) {
8091
- const state = flow.states[id];
8092
- const href = `/flows/${encodeURIComponent(flow.id)}/steps/${encodeURIComponent(id)}`;
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.catch ?? []).filter((s) => flow.states[s]).map((s) => stopBox(s, flow, m));
8096
- const notes = stateRules2(state).map((r) => m.rules.get(r)).filter(Boolean).map((r) => `<li>${ruleLink(r, flow.id)}</li>`);
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 flowBody(flow, m) {
8100
- const order = walk(flow);
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(flow.states[id])) numbers.set(id, numbers.size + 1);
8103
- const rows = order.filter((id) => flow.states[id].type !== "stop").map((id) => {
8104
- const state = flow.states[id];
8105
- if (isStep(state)) return stepRow(id, flow, m, numbers);
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, flow, numbers)}</div><div></div></div>`;
8108
- const flowOut = state.next_flow ? ` \xB7 next: <a href="/flows/${esc(state.next_flow)}">${esc(state.next_flow)}</a>` : "";
8109
- return `<div class="row"><div><span class="end">${esc(state.title ?? "Done")}${flowOut}</span></div><div></div></div>`;
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(flow.states).filter(
8113
- (id) => flow.states[id].type === "stop" && !order.some((o) => (flow.states[o].catch ?? []).includes(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, flow, m)).join("")}` : "";
8116
- const unreached = Object.keys(flow.states).filter(
8117
- (id) => isStep(flow.states[id]) && !reached.has(id)
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(flow.states[id].title ?? id)).join(", ")}.</p>` : "";
8120
- const checklist = flow.requirements.map((id) => m.rules.get(id)).filter(Boolean).map(
8121
- (r) => `<li>${ruleLink(r, flow.id)} <span class="tag">${r.scenarios.length} ${r.scenarios.length === 1 ? "scenario" : "scenarios"} \xB7 ${runSummary(r, m)}</span></li>`
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(flow.title)}</h1><p class="sub">${esc(flow.persona)} \xB7 v${esc(flow.version)} \xB7 ${walkedText(flow, m)}${compareLink(flow)}</p>${changedBadge(flow, m, TOOLS)}
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 flow has no rules yet.</li>'}</ul>`;
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, flowId, m) {
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, flowId)}</p>${scenarioList(rule, m)}</div>`;
8142
+ <p>${esc(rule.statement)}</p><p class="muted">${ruleLink(rule, journeyId)}</p>${scenarioList(rule, m)}</div>`;
8141
8143
  }
8142
- function stepBody(flow, id, m) {
8143
- const state = flow.states[id];
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.catch ?? []).filter((s) => flow.states[s]).map((s) => stopBox(s, flow, m));
8146
- return `<p class="sub"><a href="/flows/${encodeURIComponent(flow.id)}">${esc(flow.title)}</a></p>
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, flow.id, m)).join("") || '<p class="muted">This step has no rules yet.</p>'}`;
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.flowById.get(id)).filter(Boolean);
8153
- const used = others.length ? `<ul class="list">${others.map((f) => `<li><a href="/flows/${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 flow uses this rule." : "No flow uses this rule."}</p>`;
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.flowById.has(from) ? `<a href="/flows/${encodeURIComponent(from)}">${esc(m.flowById.get(from).title)}</a>` : '<a href="/">Flows</a>'}</p>
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 flows that use it" : "Flows that use it"}</h2>${used}`;
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 flow.</p>${sections.join("") || '<p class="muted">No product-wide rules yet.</p>'}`;
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 flowRoute(m, [id, kind, state], query) {
8168
- const flow = m.flowById.get(id);
8169
- if (!flow) return void 0;
8170
- if (kind === "compare" && state === void 0) return compareRoute(flow, m, query, TOOLS);
8171
- if (kind === void 0) return { title: flow.title, body: flowBody(flow, m) };
8172
- if (kind !== "steps" || !isStep(flow.states[state])) return void 0;
8173
- return { title: flow.states[state].title ?? state, body: stepBody(flow, state, m) };
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: "Flows", body: dashboard(m, query) };
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 flows</a>.</p>';
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 found = route(versionModel(indexModel(model, evidence2), files), parts, url2.searchParams);
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", "subflow"];
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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
8244
8255
  TOOLS = { esc, plainOf };
8245
8256
  ROUTES = {
8246
- flows: (m, rest, query) => [1, 2, 3].includes(rest.length) ? flowRoute(m, rest, query) : void 0,
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 rewrite of ctx.deferred)
24215
- rewrite();
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 existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
44275
- import { dirname as dirname3, join as join7 } from "node:path";
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, flows/<name>.yaml or scenarios/<ID>.feature.`;
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 flows: ${moved.join(", ") || "none"}.
44302
- Not affected flows: ${effect.unaffected.join(", ") || "none"}.`;
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, flow and scenario with its id. Call this first.",
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, flows and scenarios are written in this workspace.",
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 = join7(root, "VDS.md");
44328
- return existsSync4(file2) ? say(readFileSync8(file2, "utf8")) : refuse("This workspace has no VDS.md.");
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, flows/<name>.yaml or scenarios/<ID>.feature.",
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(join7(root, path));
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 flow built at its current version, recording who and when. Use the flow id, like "onboarding".',
44404
- inputSchema: external_exports.object({ flow: external_exports.string().describe("The flow id.") }),
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 ({ flow }) => {
44408
- const writes = builtWrites(readWorkspace(root), flow);
44409
- if (!writes) return refuse(`No flow ${flow}.`);
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(`${flow} is built at its current version.`);
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", flows: ".yaml", scenarios: ".feature" };
44458
- INSTRUCTIONS = "VDS workspace on this machine. Call vds_index first to see every rule, flow 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.";
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 existsSync5, readFileSync as readFileSync9 } from "node:fs";
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.7";
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, flows and scenarios as plain files.
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
- - flows/<id>.yaml: a journey made of states; each state points at requirement ids.
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 flow and requirement, so you know what exists.
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 flows at requirement ids. A state lists the ids it needs under \`requirements:\`.
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", "flows", "scenarios"];
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 existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
44593
- import { dirname as dirname2, join as join4 } from "node:path";
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
- var MIGRATIONS = [];
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 && existsSync3(join4(root, "package.json"))) return { kind: "project", 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) => existsSync3(join4(found.root, 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: join4(found.root, "node_modules", ".bin", "vds") };
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 = join4(folder, "VDS.md");
44644
- if (!existsSync3(path)) return false;
44645
- const current = readFileSync5(path, "utf8");
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
- writeFileSync4(path, next);
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 = join4(folder, "VDS.md");
44663
- return existsSync3(path) ? readFileSync5(path, "utf8").match(MARKER)?.[1] : void 0;
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) => existsSync5(name));
44719
- return !isWorkspace && existsSync5("vds-workspace") ? "vds-workspace" : ".";
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: readFileSync9(resolve(report), "utf8"), root, env });
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 markFlowBuilt(flowId, folder) {
44848
+ function markJourneyBuilt(journeyId, folder) {
44775
44849
  const root = resolve(folder);
44776
- const writes = flowId && builtWrites(readWorkspace(root), flowId);
44850
+ const writes = journeyId && builtWrites(readWorkspace(root), journeyId);
44777
44851
  if (!writes) {
44778
- console.error(`No flow ${flowId ?? ""}. Usage: vds built <flow-id> [folder]`);
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(`${flowId} is built at its current version.`);
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: ([flow, folder]) => markFlowBuilt(flow, folder ?? defaultFolder())
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 <flow> [folder] | vds changelog [--since x.y.z] | vds update [folder] | vds run local [folder]"
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
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teurons/vds",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "The vds command: check, index and run a VDS workspace on your machine.",
5
5
  "type": "module",
6
6
  "bin": {