@walkeros/mcp 4.6.1 → 4.7.0-next-1790245733116

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/index.d.ts CHANGED
@@ -1051,8 +1051,8 @@ declare const FRAME_MANAGE_INPUT_SCHEMA: {
1051
1051
  declare const FRAME_HINT_OPEN_PAGE_OR_GET = "Use action \"page\" with a pageKey (a frame\u2019s source.key) to read a page with marks, or action \"get\" with a frameId.";
1052
1052
  declare const FRAME_HINT_NAMES_ARE_DOCUMENTATION = "Frame names are documentation; the marks inside a frame carry the meaning.";
1053
1053
  declare const FRAME_HINT_NONE_YET = "This project has no frames yet. Frames are drawn in Tag Mode or the app, not through this tool.";
1054
- declare const FRAME_HINT_MARK_SPACE = "Marks are in their frame\u2019s own 0..1 space; a child frame sits inside its parent through placements[].rect.";
1055
- declare const FRAME_HINT_READ_KNOWLEDGE = "Use hub_manage action \"knowledge\" with frameId (and markId) to read what people wrote here. A markId is an id from these marks, and an entity action is addressed by its own id, never by its raw text.";
1054
+ declare const FRAME_HINT_MARK_SPACE = "Tag geometry is fractional: a rect is 0..1 of its frame, and an at is 0..1 of its parent tag\u2019s box, or of the frame for a root tag; a child frame sits inside its parent through placements[].rect.";
1055
+ declare const FRAME_HINT_READ_KNOWLEDGE = "Use hub_manage action \"knowledge\" with frameId (and markId) to read what people wrote here. A markId is a tag id from these marks, and a tag\u2019s threadRef is the id of its thread entry there.";
1056
1056
  declare const FRAME_HINT_NONE_ON_PAGE = "No frames on this page. Check the pageKey against the source.key values from action \"list\".";
1057
1057
  declare const FRAME_HINT_EXTENDS_BASE = "This frame extends another and stores only what it adds; read the base frame (extends) for the rest.";
1058
1058
  declare const FRAME_NOT_FOUND_HINT = "Use action \"list\" or \"page\" to find frame ids.";
package/dist/index.js CHANGED
@@ -2024,7 +2024,7 @@ var HUB_MANAGE_INPUT_SCHEMA = {
2024
2024
  'One frame (frm_...), the named rectangle a note hangs on. Action "knowledge" only. Narrower than pageKey, since a page holds several frames.'
2025
2025
  ),
2026
2026
  markId: z8.string().min(1).max(MAX_MARK_ID_LENGTH).optional().describe(
2027
- 'One mark within "frameId". Action "knowledge" only, and refused without frameId, since a mark id alone addresses nothing. Naming a mark is also what attaches the message bodies.'
2027
+ 'One tag within "frameId", by its tag id (the id of an entry in frame_manage marks.tags). Action "knowledge" only, and refused without frameId, since a mark id alone addresses nothing. Naming a mark is also what attaches the message bodies.'
2028
2028
  )
2029
2029
  };
2030
2030
  var annotations8 = {
@@ -2560,7 +2560,7 @@ import { mcpResult as mcpResult9, mcpError as mcpError9 } from "@walkeros/core";
2560
2560
  var FRAME_ID_PATTERN2 = /^frm_[A-Za-z0-9_-]{21}$/;
2561
2561
  var MAX_PAGE_KEY_CHARS = 1024;
2562
2562
  var TITLE9 = "Frames";
2563
- var FRAME_MANAGE_DESCRIPTION = 'Read the frames of a measurement plan: named rectangles with marks inside them, drawn in Tag Mode or in the app. Actions: list (every frame of the project, without marks), page (the frames of one page at any depth, with marks), get (one frame with its marks). Read-only: frames are drawn and edited in Tag Mode or the app, never here. A frame name is documentation; the marks inside it carry the meaning. A frame that extends another stores only what it adds. Use hub_manage action "knowledge" with a frameId or markId to read what people wrote on a frame. A markId is an id read from the marks of a frame here: mark ids come back literal so they can be passed straight back, while the text around them is wrapped as data. An entity action is an object carrying its id beside the raw attribute text, because the id is the address and the raw text is not.';
2563
+ var FRAME_MANAGE_DESCRIPTION = 'Read the frames of a measurement plan: named rectangles with marks inside them, drawn in Tag Mode or in the app. Actions: list (every frame of the project, without marks), page (the frames of one page at any depth, with marks), get (one frame with its marks). Read-only: frames are drawn and edited in Tag Mode or the app, never here. A frame name is documentation; the marks inside it carry the meaning. A frame that extends another stores only what it adds, so its tags may carry only the fields they change. Marks are { tags, note }: tags is one flat list of tags, each with an id, a kind such as entity, property or action, a name, and a parentId naming the tag it sits under; note is the frame\u2019s own description and thread. Use hub_manage action "knowledge" with a frameId or markId to read what people wrote on a frame. A markId is a tag id: a tag\u2019s id, parentId and threadRef come back literal when they have the shape the app mints, so they can be passed straight back; every other string value in the marks is wrapped as data, and so is an object key that is not a plain identifier.';
2564
2564
  var FRAME_MANAGE_INPUT_SCHEMA = {
2565
2565
  action: z9.enum(["list", "page", "get"]).describe(
2566
2566
  "list the project\u2019s frames, read one page with marks, or read one frame"
@@ -2584,8 +2584,8 @@ var annotations9 = {
2584
2584
  var FRAME_HINT_OPEN_PAGE_OR_GET = 'Use action "page" with a pageKey (a frame\u2019s source.key) to read a page with marks, or action "get" with a frameId.';
2585
2585
  var FRAME_HINT_NAMES_ARE_DOCUMENTATION = "Frame names are documentation; the marks inside a frame carry the meaning.";
2586
2586
  var FRAME_HINT_NONE_YET = "This project has no frames yet. Frames are drawn in Tag Mode or the app, not through this tool.";
2587
- var FRAME_HINT_MARK_SPACE = "Marks are in their frame\u2019s own 0..1 space; a child frame sits inside its parent through placements[].rect.";
2588
- var FRAME_HINT_READ_KNOWLEDGE = 'Use hub_manage action "knowledge" with frameId (and markId) to read what people wrote here. A markId is an id from these marks, and an entity action is addressed by its own id, never by its raw text.';
2587
+ var FRAME_HINT_MARK_SPACE = "Tag geometry is fractional: a rect is 0..1 of its frame, and an at is 0..1 of its parent tag\u2019s box, or of the frame for a root tag; a child frame sits inside its parent through placements[].rect.";
2588
+ var FRAME_HINT_READ_KNOWLEDGE = 'Use hub_manage action "knowledge" with frameId (and markId) to read what people wrote here. A markId is a tag id from these marks, and a tag\u2019s threadRef is the id of its thread entry there.';
2589
2589
  var FRAME_HINT_NONE_ON_PAGE = 'No frames on this page. Check the pageKey against the source.key values from action "list".';
2590
2590
  var FRAME_HINT_EXTENDS_BASE = "This frame extends another and stores only what it adds; read the base frame (extends) for the rest.";
2591
2591
  var FRAME_NOT_FOUND_HINT = 'Use action "list" or "page" to find frame ids.';
@@ -2621,67 +2621,59 @@ function serializeLean(frame) {
2621
2621
  function isRecord2(value) {
2622
2622
  return typeof value === "object" && value !== null && !Array.isArray(value);
2623
2623
  }
2624
- function actionMarkId(entityId, raw) {
2625
- return `${entityId}#action.${raw}`;
2626
- }
2627
- var MARK_ADDRESS_KEYS = /* @__PURE__ */ new Set([
2628
- // The mark id itself, and a component of the data and action addresses.
2629
- "id",
2630
- // The first half of an ambient address, `ambient.<kind>.<key>`, and the
2631
- // discriminator a reader branches on.
2632
- "kind",
2633
- // An entity's pointer at another entity, which is that entity's mark id.
2634
- "link",
2635
- // The thread a note became, which action "note_add" takes as threadId.
2636
- "threadRef"
2624
+ var TAG_ID_PATTERN = /^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*(?:#(?:data\.v?[0-9]+|action\.[0-9]+|context\.[0-9]+|[0-9]+))?$/;
2625
+ var MAX_TAG_ID_CHARS = 200;
2626
+ var THREAD_ID_PATTERN = /^thr_[a-z0-9]{21}$/;
2627
+ var isTagId = (value) => value.length <= MAX_TAG_ID_CHARS && TAG_ID_PATTERN.test(value);
2628
+ var isThreadId = (value) => THREAD_ID_PATTERN.test(value);
2629
+ var TAG_ADDRESSES = /* @__PURE__ */ new Map([
2630
+ // The tag id: the markId `hub_manage` action "knowledge" takes, which the
2631
+ // app joins to the frame id as the anchor key `<frameId>:<markId>`.
2632
+ ["id", isTagId],
2633
+ // The tag this one sits under, which is that tag's id: the only relation
2634
+ // between tags, so a reader joins it to `id` to rebuild the tree, and it is
2635
+ // a markId in its own right.
2636
+ ["parentId", isTagId],
2637
+ // The hub thread this tag's note thread became: the `id` of the thread
2638
+ // entry `hub_manage` action "knowledge" returns for this tag.
2639
+ ["threadRef", isThreadId]
2637
2640
  ]);
2638
- var MARK_ADDRESS_LIST_KEYS = /* @__PURE__ */ new Set([
2639
- // The entities a context band covers, by their mark ids.
2640
- "covers"
2641
- ]);
2642
- var MARK_PAGE_DATA_KEYS = /* @__PURE__ */ new Set(["anchor", "actionAnchors"]);
2643
- function isProseKeyedRecord(key, owner) {
2644
- return key === "data" && typeof owner.kind !== "string";
2645
- }
2646
- function walkMarks(value, inPageData = false) {
2641
+ var NOTE_ADDRESSES = /* @__PURE__ */ new Map(
2642
+ [["threadRef", isThreadId]]
2643
+ );
2644
+ var MARK_KEY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$-]{0,63}$/;
2645
+ var markKey = (key) => MARK_KEY_PATTERN.test(key) ? key : wrapUserData(key);
2646
+ function walkText(value) {
2647
2647
  if (typeof value === "string") return wrapUserData(value);
2648
- if (Array.isArray(value)) {
2649
- return value.map((item) => walkMarks(item, inPageData));
2650
- }
2648
+ if (Array.isArray(value)) return value.map(walkText);
2651
2649
  if (!isRecord2(value)) return value;
2652
- const entityId = typeof value.id === "string" ? value.id : void 0;
2653
2650
  const out = {};
2654
2651
  for (const [key, child] of Object.entries(value)) {
2655
- if (MARK_PAGE_DATA_KEYS.has(key)) {
2656
- out[key] = walkMarks(child, true);
2657
- } else if (!inPageData && isRecord2(child) && isProseKeyedRecord(key, value)) {
2658
- out[key] = proseKeyedPairs(child);
2652
+ out[markKey(key)] = walkText(child);
2653
+ }
2654
+ return out;
2655
+ }
2656
+ function walkMarks(marks) {
2657
+ const out = {};
2658
+ for (const [key, value] of Object.entries(marks)) {
2659
+ if (key === "tags" && Array.isArray(value)) {
2660
+ out[key] = value.map((tag) => walkAddressed(tag, TAG_ADDRESSES));
2661
+ } else if (key === "note") {
2662
+ out[key] = walkAddressed(value, NOTE_ADDRESSES);
2659
2663
  } else {
2660
- out[key] = walkMarkEntry(key, child, entityId, inPageData);
2664
+ out[markKey(key)] = walkText(value);
2661
2665
  }
2662
2666
  }
2663
2667
  return out;
2664
2668
  }
2665
- function proseKeyedPairs(record) {
2666
- return Object.entries(record).map(([key, value]) => ({
2667
- key: wrapUserData(key),
2668
- value: walkMarks(value)
2669
- }));
2670
- }
2671
- function walkMarkEntry(key, child, entityId, inPageData) {
2672
- if (inPageData) return walkMarks(child, true);
2673
- if (MARK_ADDRESS_KEYS.has(key) && typeof child === "string") return child;
2674
- if (MARK_ADDRESS_LIST_KEYS.has(key) && Array.isArray(child)) {
2675
- return child.map(
2676
- (item) => typeof item === "string" ? item : walkMarks(item)
2677
- );
2678
- }
2679
- if (key === "actions" && entityId !== void 0 && Array.isArray(child)) {
2680
- return child.map(
2681
- (raw) => typeof raw === "string" ? { id: actionMarkId(entityId, raw), raw: wrapUserData(raw) } : walkMarks(raw)
2682
- );
2669
+ function walkAddressed(value, addresses) {
2670
+ if (!isRecord2(value)) return walkText(value);
2671
+ const out = {};
2672
+ for (const [key, field] of Object.entries(value)) {
2673
+ const isAddress = addresses.get(key);
2674
+ out[markKey(key)] = isAddress !== void 0 && typeof field === "string" && isAddress(field) ? field : walkText(field);
2683
2675
  }
2684
- return walkMarks(child);
2676
+ return out;
2685
2677
  }
2686
2678
  function serializeFrame(frame) {
2687
2679
  return {
@@ -2833,7 +2825,7 @@ async function feedbackHandlerBody(client, input) {
2833
2825
  const isAnonymous = explicitAnonymous ?? anonymous ?? true;
2834
2826
  await client.submitFeedback(text, {
2835
2827
  anonymous: isAnonymous,
2836
- version: "4.6.1"
2828
+ version: "4.7.0-next-1790245733116"
2837
2829
  });
2838
2830
  return mcpResult10({ ok: true });
2839
2831
  } catch (error) {
@@ -3196,7 +3188,7 @@ var inputSchema11 = {
3196
3188
  // Override the (optional) CLI `step` shape: the simulate handler hard-requires
3197
3189
  // a target step (no all-steps mode), so the registered schema must be honest.
3198
3190
  step: z12.string().describe(
3199
- 'Required. Target step as "type.name" \u2014 e.g. "source.demo", "destination.gtag", "transformer.router".'
3191
+ 'Required. Target step as "type.name", e.g. "source.demo", "collector.default", "destination.gtag", "transformer.router". A collector step runs enrichment and then collector.next, returning every event the destinations would receive (none after a stop).'
3200
3192
  ),
3201
3193
  verbose: z12.boolean().optional().describe("Include full payload per destination (default: false)"),
3202
3194
  ingest: z12.record(z12.string(), z12.unknown()).optional().describe(
@@ -3469,7 +3461,7 @@ var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
3469
3461
  var JSDELIVR_BASE = "https://cdn.jsdelivr.net/npm";
3470
3462
  var WALKEROS_JSON_PATH = "dist/walkerOS.json";
3471
3463
  var CACHE_TTL = 5 * 60 * 1e3;
3472
- var CLIENT_HEADER = "walkeros-mcp/4.6.1";
3464
+ var CLIENT_HEADER = "walkeros-mcp/4.7.0-next-1790245733116";
3473
3465
  function getPackageBaseUrl() {
3474
3466
  return process.env.WALKEROS_APP_URL || void 0;
3475
3467
  }
@@ -4116,7 +4108,7 @@ var spec_default = {
4116
4108
  openapi: "3.1.0",
4117
4109
  info: {
4118
4110
  title: "walkerOS Tag Manager API",
4119
- version: "4.6.1",
4111
+ version: "4.7.0",
4120
4112
  description: "API for managing walkerOS flows, projects, and real-time event observation.",
4121
4113
  contact: {
4122
4114
  name: "elbwalker",
@@ -11059,91 +11051,6 @@ var spec_default = {
11059
11051
  },
11060
11052
  required: ["name", "scope", "expiresInDays"]
11061
11053
  },
11062
- BundleResponse: {
11063
- type: "object",
11064
- properties: {
11065
- bundleId: {
11066
- type: "string"
11067
- },
11068
- cached: {
11069
- type: "boolean"
11070
- }
11071
- },
11072
- required: ["bundleId", "cached"]
11073
- },
11074
- SimulateResponse: {
11075
- type: "object",
11076
- properties: {
11077
- success: {
11078
- type: "boolean"
11079
- },
11080
- result: {
11081
- type: "object",
11082
- properties: {
11083
- step: {
11084
- type: "string",
11085
- enum: ["source", "transformer", "destination"]
11086
- },
11087
- name: {
11088
- type: "string"
11089
- },
11090
- events: {
11091
- type: "array",
11092
- items: {
11093
- type: "object",
11094
- additionalProperties: {}
11095
- }
11096
- },
11097
- calls: {
11098
- type: "array",
11099
- items: {
11100
- type: "object",
11101
- properties: {
11102
- fn: {
11103
- type: "string"
11104
- },
11105
- args: {
11106
- type: "array",
11107
- items: {}
11108
- },
11109
- ts: {
11110
- type: "number"
11111
- }
11112
- },
11113
- required: ["fn", "args", "ts"]
11114
- }
11115
- },
11116
- duration: {
11117
- type: "number"
11118
- }
11119
- },
11120
- required: ["step", "name", "events", "calls", "duration"]
11121
- }
11122
- },
11123
- required: ["success"]
11124
- },
11125
- SimulateRequest: {
11126
- type: "object",
11127
- properties: {
11128
- bundleId: {
11129
- type: "string",
11130
- pattern: "^[a-f0-9]{8,64}$"
11131
- },
11132
- config: {
11133
- type: "object",
11134
- additionalProperties: {}
11135
- },
11136
- event: {
11137
- type: "object",
11138
- additionalProperties: {}
11139
- },
11140
- step: {
11141
- type: "string",
11142
- pattern: "^(source|transformer|destination)\\..+$"
11143
- }
11144
- },
11145
- required: ["bundleId", "config", "event", "step"]
11146
- },
11147
11054
  RegisterRuntimeRequest: {
11148
11055
  type: "object",
11149
11056
  properties: {
@@ -16553,7 +16460,7 @@ var spec_default = {
16553
16460
  post: {
16554
16461
  tags: ["Bundle"],
16555
16462
  summary: "Bundle flow",
16556
- description: "Bundle a flow using CLI. Returns bundleId (content-hash). Use ?output=download to redirect to presigned S3 URL.",
16463
+ description: "Bundle a flow for deploy or download. Requires ?output=download: bearer callers get a 302 to a presigned S3 URL, session callers get the bundle bytes streamed. A POST without output=download returns 400.",
16557
16464
  parameters: [
16558
16465
  {
16559
16466
  schema: {
@@ -16590,40 +16497,23 @@ var spec_default = {
16590
16497
  schema: {
16591
16498
  type: "string",
16592
16499
  enum: ["download"],
16593
- description: 'Set to "download" to redirect to the bundle file'
16500
+ description: 'Required. Must be "download"'
16594
16501
  },
16595
- required: false,
16596
- description: 'Set to "download" to redirect to the bundle file',
16502
+ required: true,
16503
+ description: 'Required. Must be "download"',
16597
16504
  name: "output",
16598
16505
  in: "query"
16599
16506
  }
16600
16507
  ],
16601
16508
  responses: {
16602
16509
  "200": {
16603
- description: "Bundle result",
16604
- content: {
16605
- "application/json": {
16606
- schema: {
16607
- $ref: "#/components/schemas/BundleResponse"
16608
- }
16609
- }
16610
- }
16510
+ description: "Bundle file streamed (session callers)"
16611
16511
  },
16612
16512
  "302": {
16613
- description: "Redirect to presigned bundle URL (when output=download)"
16513
+ description: "Redirect to presigned bundle URL (bearer callers)"
16614
16514
  },
16615
- "401": {
16616
- description: "Unauthorized",
16617
- content: {
16618
- "application/json": {
16619
- schema: {
16620
- $ref: "#/components/schemas/ErrorResponse"
16621
- }
16622
- }
16623
- }
16624
- },
16625
- "404": {
16626
- description: "Not found",
16515
+ "400": {
16516
+ description: "Validation error",
16627
16517
  content: {
16628
16518
  "application/json": {
16629
16519
  schema: {
@@ -16631,56 +16521,6 @@ var spec_default = {
16631
16521
  }
16632
16522
  }
16633
16523
  }
16634
- }
16635
- }
16636
- }
16637
- },
16638
- "/api/projects/{projectId}/flows/{flowId}/simulate": {
16639
- post: {
16640
- tags: ["Simulate"],
16641
- summary: "Simulate a flow step",
16642
- description: "Execute a simulation against a pre-built bundle. Requires bundleId from the bundle endpoint.",
16643
- parameters: [
16644
- {
16645
- schema: {
16646
- type: "string",
16647
- pattern: "^proj_[a-zA-Z0-9_-]+$",
16648
- example: "proj_x7y8z9"
16649
- },
16650
- required: true,
16651
- name: "projectId",
16652
- in: "path"
16653
- },
16654
- {
16655
- schema: {
16656
- type: "string",
16657
- pattern: "^flow_[a-zA-Z0-9_-]+$",
16658
- example: "flow_a1b2c3d4"
16659
- },
16660
- required: true,
16661
- name: "flowId",
16662
- in: "path"
16663
- }
16664
- ],
16665
- requestBody: {
16666
- content: {
16667
- "application/json": {
16668
- schema: {
16669
- $ref: "#/components/schemas/SimulateRequest"
16670
- }
16671
- }
16672
- }
16673
- },
16674
- responses: {
16675
- "200": {
16676
- description: "Simulation result",
16677
- content: {
16678
- "application/json": {
16679
- schema: {
16680
- $ref: "#/components/schemas/SimulateResponse"
16681
- }
16682
- }
16683
- }
16684
16524
  },
16685
16525
  "401": {
16686
16526
  description: "Unauthorized",
@@ -16703,7 +16543,7 @@ var spec_default = {
16703
16543
  }
16704
16544
  },
16705
16545
  "502": {
16706
- description: "Simulation container error",
16546
+ description: "Bundle upstream unavailable",
16707
16547
  content: {
16708
16548
  "application/json": {
16709
16549
  schema: {
@@ -24283,9 +24123,10 @@ function registerAddStepPrompt(server) {
24283
24123
  "",
24284
24124
  "Important:",
24285
24125
  "- Read the walkeros://reference/flow-schema resource to understand connection rules.",
24286
- "- Sources connect to pre-collector transformers via `next`.",
24287
- "- Destinations connect to post-collector transformers via `before`.",
24288
- '- Routing supports string IDs, sequences (`["a", "b"]`), or RouteConfig (`{ match?, next }` for gated single-target or `{ match?, one: [...] }` for first-match dispatch). Use `many` (instead of `one`) when every matching entry should run as an independent parallel flow \u2014 restricted to pre-collector positions. Omit `match` for always-match.',
24126
+ "- Sources connect to transformers via `next` (runs before the collector).",
24127
+ "- The collector connects to transformers via `collector.next` (runs once per event before the destination fan-out, for every destination).",
24128
+ "- Destinations connect to transformers via `before` (runs for that destination only; put per-destination filtering here).",
24129
+ '- Routing uses one grammar in every chain field: string IDs, sequences (`["a", "b"]`, where a member\'s own `next` runs right after it), or RouteConfig (`{ match?, next }` for a gated link, `{ match?, one: [...] }` for first-match dispatch, `{ match?, many: [...] }` to fork each match into its own copy that finishes the rest of the path, `{ match?, stop: true }` to drop). A `stop` in `collector.next` drops the event for all destinations. Omit `match` for always-match.',
24289
24130
  "- A transformer entry can be code-bearing (`code`), package-bearing (`package`), or a **pass-through step** (no `code`, no `package`). Pass-through synthesis is automatic at runtime. Three flavors of pass-through: `before`/`next`-only (named hop reusable across destinations), `cache`-only (dedup, rate-limit), or `mapping`-only (declarative event-to-event transform). A pass-through must declare at least one of `before`, `next`, `cache`, or `mapping`.",
24290
24131
  "- The `mapping` field on a transformer step uses the same `Mapping.Config` shape as destinations, but the semantic is event-to-event (mutates the event in place via `policy`, per-rule `policy`, `mapping[].name` for renames, `mapping[].ignore` to drop). Vendor-payload fields (`data`, `silent`) are no-ops at this position.",
24291
24132
  "- Closed schema: unknown top-level keys on a transformer step are errors (catches typos like `dedup` instead of `cache`). `code` + `package` together is a conflict.",
@@ -24424,7 +24265,7 @@ var SERVER_INSTRUCTIONS = `walkerOS is an open-source, privacy-first event data
24424
24265
 
24425
24266
  ## Architecture: Source \u2192 Collector \u2192 Destination(s)
24426
24267
 
24427
- Every component in a flow is a **step**: sources capture events, transformers process them, destinations deliver them, stores provide shared state. Steps connect via \`next\` (pre-collector) and \`before\` (post-collector) chains.
24268
+ Every component in a flow is a **step**: sources capture events, transformers process them, destinations deliver them, stores provide shared state. Steps connect via chains: \`source.next\` runs before the collector, \`collector.next\` runs once per event before the destination fan-out (all destinations), and \`destination.before\` runs for one destination.
24428
24269
 
24429
24270
  ## Flow Config Structure
24430
24271
 
@@ -24452,11 +24293,11 @@ Every component in a flow is a **step**: sources capture events, transformers pr
24452
24293
  - **Contracts** define event schemas using entity-action keying. Can generate FROM mappings or scaffold mappings FROM contracts.
24453
24294
  - **Variables** ($var, $env, $secret, $code, $store) enable DRY, environment-aware config. \`$var.name\` is a reusable flow variable from the top-level \`variables\` block; whole-string refs preserve native type (object/array/scalar), inline interpolation requires a scalar, deep paths via \`$var.name.deep.path\`. \`$env.NAME[:default]\` is an environment variable resolved at bundle/deploy time (non-secret config only). \`$secret.NAME\` is a managed secret the deploy pipeline injects into the server runner at runtime; credentials, tokens, and private keys must use \`$secret\`, not \`$env\`.
24454
24295
  - **Consent** gates destinations, mapping rules, and individual fields. Privacy-first by design.
24455
- - **Routing** wires steps via \`next\` (pre-collector) and \`before\` (post-collector). A route is a string ID, a sequence (\`["a", "b"]\`), or a RouteConfig (\`{ match?, next }\` or \`{ match?, one: [...] }\`). The \`one\` operator dispatches first-match against an ordered list of branches. Use \`many\` (instead of \`one\`) when every matching entry should run as an independent parallel flow \u2014 restricted to pre-collector positions. The optional \`match\` field is omitted to mean always-match (no wildcard literal).
24296
+ - **Routing** uses one grammar in every chain field (\`source.before\`, \`source.next\`, \`transformer.before\`, \`transformer.next\`, \`collector.next\`, \`destination.before\`, \`destination.next\`). A route is a string ID, a sequence (\`["a", "b"]\`), or a RouteConfig: \`{ match?, next }\`, \`{ match?, one: [...] }\` (first match wins), \`{ match?, many: [...] }\` (every match becomes its own copy of the event with its own id and finishes the rest of the path on its own, never merged), or \`{ match?, stop: true }\` (ends that copy). In a sequence, a member's own \`next\` runs right after it, then the sequence continues. \`match\` reads \`{ ingest, event }\` and each route is evaluated when the event reaches it. A \`stop\` in \`collector.next\` drops the event for every destination; per-destination filtering belongs in \`destination.before\`. An array of only route configs is an implicit \`one\`. The optional \`match\` field is omitted to mean always-match (no wildcard literal).
24456
24297
  - **Pass-through steps** are transformer entries with no \`code\` and no \`package\`. The runtime synthesizes the push for them automatically. Three flavors share this shape: a \`before\` / \`next\`-only step (named hop reusable across destinations), a \`cache\`-only step (dedup, rate-limit), and a \`mapping\`-only step (declarative event-to-event transform). A pass-through must declare at least one of \`before\`, \`next\`, \`cache\`, or \`mapping\`.
24457
24298
  - **Mapping at the transformer position** uses the same \`Mapping.Config\` shape as destinations, but the semantic is event-to-event: \`policy\` and per-rule \`policy\`, \`mapping[].name\` (rename), \`mapping[].ignore\` (drop from chain), \`consent\`, and \`include\` apply. Vendor-payload fields (\`data\`, \`silent\`, \`mapping[].data\`) are no-ops at this position.
24458
24299
  - **Closed-schema rule on transformer entries:** known keys only. Operative keys are \`code\`, \`package\`, \`before\`, \`next\`, \`cache\`, \`mapping\`. Combined with structural keys (\`config\`, \`env\`, \`validate\`, \`disabled\`, \`id\`, etc.). Unknown top-level keys on a transformer step are errors (catches typos like \`dedup: {}\` instead of nested under \`cache\`). \`code\` + \`package\` together is a conflict.
24459
- - **Cache** memoizes step output on a configured store. Set \`cache.stop: true\` to short-circuit the chain on HIT (sources default to stop-on-HIT). Set \`cache.namespace\` to prefix keys; omit it to write keys directly. \`CacheRule.match\` is optional, omit for always-match.
24300
+ - **Cache** memoizes step output on a configured store. Set \`cache.stop: true\` to short-circuit the chain on HIT (sources default to stop-on-HIT); in a source chain or \`collector.next\` that halts the event for all destinations. Set \`cache.namespace\` to prefix keys; omit it to write keys directly. \`CacheRule.match\` is optional, omit for always-match.
24460
24301
 
24461
24302
  ## Simulation Tips
24462
24303