@cosmicdrift/kumiko-framework 0.146.0 → 0.146.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.146.0",
3
+ "version": "0.146.2",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -189,7 +189,7 @@
189
189
  "zod": "^4.4.3"
190
190
  },
191
191
  "devDependencies": {
192
- "@cosmicdrift/kumiko-dispatcher-live": "0.146.0",
192
+ "@cosmicdrift/kumiko-dispatcher-live": "0.146.2",
193
193
  "bun-types": "^1.3.13",
194
194
  "pino-pretty": "^13.1.3"
195
195
  },
@@ -2,7 +2,7 @@ import type { Context } from "hono";
2
2
  import { Hono } from "hono";
3
3
  import { deleteCookie, setCookie } from "hono/cookie";
4
4
  import { z } from "zod";
5
- import { stripForbiddenMembershipRoles } from "../engine/membership-roles";
5
+ import { buildSessionRoles } from "../engine/membership-roles";
6
6
  import { createSystemUser } from "../engine/system-user";
7
7
  import { type SessionUser, SYSTEM_TENANT_ID, type TenantId } from "../engine/types";
8
8
  import { NotFoundError } from "../errors";
@@ -870,13 +870,12 @@ export function createAuthRoutes(
870
870
  // tenant A accidentally surviving into tenant B's session). The
871
871
  // resolver runs each feature's r.authClaims() hook under the new
872
872
  // TenantDb scope.
873
- // Strip reserved roles from the membership portion only — globalRoles
873
+ // buildSessionRoles calls stripForbiddenMembershipRoles internally and
874
+ // strips reserved roles from the membership side only — globalRoles
874
875
  // (where SystemAdmin legitimately lives) is never filtered. Backstop for a
875
876
  // membership role that a projection rebuild resurrected past command-time
876
877
  // validation (see engine/membership-roles).
877
- const mergedRoles = Array.from(
878
- new Set([...globalRoles, ...stripForbiddenMembershipRoles(membership.roles)]),
879
- );
878
+ const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
880
879
  const targetSession: SessionUser = {
881
880
  id: user.id,
882
881
  tenantId: targetTenantId,
@@ -0,0 +1,34 @@
1
+ // Regression test for #255-class drift: unmanagedTables must appear in
2
+ // enumerateFeatureTableSources so setupTestStack's auto-push and
3
+ // collectTableMetas see the exact same table set. Before this fix,
4
+ // unmanagedTables were only handled by collectTableMetas directly, so any
5
+ // app relying on setupTestStack's ephemeral DB (Playwright/e2e) never got
6
+ // its unmanaged tables created — e.g. the bundled "sessions" feature's
7
+ // read_user_sessions, breaking every login in an ephemeral test stack.
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { defineFeature } from "../../engine/define-feature";
11
+ import { defineUnmanagedTable } from "../entity-table-meta";
12
+ import { enumerateFeatureTableSources } from "../feature-table-sources";
13
+
14
+ const probeMeta = defineUnmanagedTable({
15
+ tableName: "ftst_probe",
16
+ columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
17
+ });
18
+
19
+ describe("enumerateFeatureTableSources — unmanagedTables", () => {
20
+ test("includes a feature's unmanagedTable as a table source", () => {
21
+ const feature = defineFeature("probe", (r) => {
22
+ r.unmanagedTable(probeMeta, { reason: "direct-write store" });
23
+ });
24
+ const sources = enumerateFeatureTableSources(feature);
25
+ const entry = sources.find((s) => s.origin.includes("ftst_probe"));
26
+ expect(entry).toBeDefined();
27
+ expect(entry?.table).toBe(probeMeta);
28
+ });
29
+
30
+ test("a feature with no unmanagedTable contributes nothing extra", () => {
31
+ const feature = defineFeature("plain", () => {});
32
+ expect(enumerateFeatureTableSources(feature)).toEqual([]);
33
+ });
34
+ });
@@ -1,7 +1,8 @@
1
1
  // Single enumeration of every table-bearing registration on a feature
2
- // (r.projection, r.multiStreamProjection with table, r.rawTable). Consumed
3
- // by BOTH the setupTestStack auto-push and collectTableMetas — one list, so
4
- // test-DB-push and `kumiko schema generate` cannot drift apart again (#255).
2
+ // (r.projection, r.multiStreamProjection with table, r.rawTable,
3
+ // r.unmanagedTable). Consumed by BOTH the setupTestStack auto-push and
4
+ // collectTableMetas — one list, so test-DB-push and `kumiko schema generate`
5
+ // cannot drift apart again (#255).
5
6
  // A new table-bearing registrar must be added HERE, not in the consumers.
6
7
 
7
8
  import type { FeatureDefinition } from "../engine/types";
@@ -31,5 +32,11 @@ export function enumerateFeatureTableSources(
31
32
  for (const [name, raw] of Object.entries(feature.rawTables)) {
32
33
  sources.push({ table: raw.table, origin: `rawTable "${name}" (${feature.name})` });
33
34
  }
35
+ for (const entry of Object.values(feature.unmanagedTables)) {
36
+ sources.push({
37
+ table: entry.meta,
38
+ origin: `unmanagedTable "${entry.name}" (${feature.name})`,
39
+ });
40
+ }
34
41
  return sources;
35
42
  }
@@ -103,14 +103,14 @@ export const myHandler = defineWriteHandler({
103
103
  `;
104
104
 
105
105
  const alreadyPipelineContent = `\
106
- import { access, defineWriteHandler, pipeline } from "@cosmicdrift/kumiko-framework/engine";
106
+ import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
107
107
  import { z } from "zod";
108
108
 
109
109
  export const myHandler = defineWriteHandler({
110
110
  name: "test:pipeline",
111
111
  schema: z.object({}),
112
112
  access: { roles: access.authenticated },
113
- perform: pipeline(({ event, r }) => [
113
+ perform: stepsPipeline(({ event, r }) => [
114
114
  r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })),
115
115
  ]),
116
116
  });
@@ -173,14 +173,14 @@ describe("analyzeFile", () => {
173
173
 
174
174
  describe("convertFile", () => {
175
175
  describe("static return handler", () => {
176
- it("converts handler to perform: pipeline(...)", async () => {
176
+ it("converts handler to perform: stepsPipeline(...)", async () => {
177
177
  const p = writeFixture("static-convert.write.ts", staticReturnContent);
178
178
  const result = await convertFile(p);
179
179
  expect(result.status).toBe("converted");
180
180
  const content = readFileSync(p, "utf8");
181
- expect(content).toContain("perform: pipeline");
181
+ expect(content).toContain("perform: stepsPipeline");
182
182
  expect(content).toContain("r.step.return");
183
- expect(content).toContain("import { access, defineWriteHandler, pipeline }");
183
+ expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
184
184
  expect(content).not.toContain("handler:");
185
185
  });
186
186
 
@@ -198,7 +198,7 @@ export const h = defineWriteHandler({
198
198
  const result = await convertFile(p);
199
199
  expect(result.status).toBe("converted");
200
200
  const converted = readFileSync(p, "utf8");
201
- expect(converted).toContain("pipeline<typeof MySchema, unknown>");
201
+ expect(converted).toContain("stepsPipeline<typeof MySchema, unknown>");
202
202
  });
203
203
  });
204
204
 
@@ -271,12 +271,12 @@ export const h = defineWriteHandler({
271
271
  const result = await convertFile(p);
272
272
  expect(result.status).toBe("converted");
273
273
  const content = readFileSync(p, "utf8");
274
- expect(content).toContain("import { access, defineWriteHandler, pipeline }");
274
+ expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
275
275
  });
276
276
 
277
277
  it("does not duplicate pipeline import when already present", async () => {
278
278
  const content = `\
279
- import { access, defineWriteHandler, pipeline } from "@cosmicdrift/kumiko-framework/engine";
279
+ import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
280
280
  import { z } from "zod";
281
281
 
282
282
  export const h = defineWriteHandler({
@@ -288,7 +288,9 @@ export const h = defineWriteHandler({
288
288
  const result = await convertFile(p);
289
289
  expect(result.status).toBe("converted");
290
290
  const final = readFileSync(p, "utf8").split("\n");
291
- const pipelineImports = final.filter((l) => l.includes("import") && l.includes("pipeline"));
291
+ const pipelineImports = final.filter(
292
+ (l) => l.includes("import") && l.includes("stepsPipeline"),
293
+ );
292
294
  // Only one import line should contain "pipeline"
293
295
  expect(pipelineImports.length).toBe(1);
294
296
  });
@@ -513,14 +515,14 @@ describe("generatePerformBlock", () => {
513
515
  it("generates pipeline block for static return", () => {
514
516
  const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
515
517
  const block = generatePerformBlock(analysis, "", " ");
516
- expect(block).toContain("perform: pipeline(");
518
+ expect(block).toContain("perform: stepsPipeline(");
517
519
  expect(block).toContain("r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })");
518
520
  });
519
521
 
520
522
  it("generates pipeline block with schema type parameter", () => {
521
523
  const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
522
524
  const block = generatePerformBlock(analysis, "typeof InvoiceSchema", " ");
523
- expect(block).toContain("pipeline<typeof InvoiceSchema, unknown>");
525
+ expect(block).toContain("stepsPipeline<typeof InvoiceSchema, unknown>");
524
526
  });
525
527
 
526
528
  it("returns null for non-convertible analysis", () => {
@@ -1,4 +1,4 @@
1
- // Pipeline-engine unit tests — defineWriteHandler({ perform: pipeline(...) })
1
+ // Pipeline-engine unit tests — defineWriteHandler({ perform: stepsPipeline(...) })
2
2
  // boundary, run-pipeline runner contract, defineStep registry guards.
3
3
  // Sub-step-builders (branch, forEach) live in pipeline-sub-pipelines.test.ts;
4
4
  // boot-validator in validate-projection-allowlist.test.ts.
@@ -9,7 +9,7 @@ import { z } from "zod";
9
9
  import { TestUsers } from "../../stack";
10
10
  import { defineWriteHandler } from "../define-handler";
11
11
  import { defineStep } from "../define-step";
12
- import { pipeline } from "../pipeline";
12
+ import { stepsPipeline } from "../pipeline";
13
13
  import type { WriteEvent } from "../types/handlers";
14
14
  import { buildMinimalCtx } from "./_pipeline-test-utils";
15
15
 
@@ -19,7 +19,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
19
19
  name: "demo:noop",
20
20
  schema: z.object({ greeting: z.string() }),
21
21
  access: { roles: ["User"] },
22
- perform: pipeline<{ greeting: string }, { echoed: string }>(({ event, r }) => [
22
+ perform: stepsPipeline<{ greeting: string }, { echoed: string }>(({ event, r }) => [
23
23
  r.step.return(() => ({
24
24
  isSuccess: true as const,
25
25
  data: { echoed: event.payload.greeting },
@@ -49,7 +49,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
49
49
  name: "demo:static",
50
50
  schema: z.object({}),
51
51
  access: { roles: ["User"] },
52
- perform: pipeline<Record<string, never>, { ok: boolean }>(({ r }) => [
52
+ perform: stepsPipeline<Record<string, never>, { ok: boolean }>(({ r }) => [
53
53
  r.step.return({ isSuccess: true as const, data: { ok: true } }),
54
54
  ]),
55
55
  });
@@ -89,7 +89,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
89
89
  name: "demo:no-return",
90
90
  schema: z.object({}),
91
91
  access: { roles: ["User"] },
92
- perform: pipeline<Record<string, never>, never>(() => []),
92
+ perform: stepsPipeline<Record<string, never>, never>(() => []),
93
93
  });
94
94
 
95
95
  await expect(
@@ -105,7 +105,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
105
105
  name: "demo:thread",
106
106
  schema: z.object({ base: z.number() }),
107
107
  access: { roles: ["User"] },
108
- perform: pipeline<{ base: number }, { sum: number }>(({ event, r }) => [
108
+ perform: stepsPipeline<{ base: number }, { sum: number }>(({ event, r }) => [
109
109
  r.step.compute("offset", () => 10),
110
110
  r.step.compute("doubledBase", () => event.payload.base * 2),
111
111
  r.step.return(({ steps }) => ({
@@ -133,7 +133,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
133
133
  name: "demo:fresh",
134
134
  schema: z.object({}),
135
135
  access: { roles: ["User"] },
136
- perform: pipeline<Record<string, never>, { tick: number }>(({ r }) => [
136
+ perform: stepsPipeline<Record<string, never>, { tick: number }>(({ r }) => [
137
137
  r.step.compute("tick", () => ++counter),
138
138
  r.step.return(({ steps }) => ({
139
139
  isSuccess: true as const,
@@ -161,7 +161,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
161
161
  name: "demo:unknown-kind",
162
162
  schema: z.object({}),
163
163
  access: { roles: ["User"] },
164
- perform: pipeline<Record<string, never>, never>(() => [
164
+ perform: stepsPipeline<Record<string, never>, never>(() => [
165
165
  // Hand-crafted instance with a kind that's never been registered —
166
166
  // simulates a typo in a future step-builder factory.
167
167
  { kind: "this-step-does-not-exist", args: {} },
@@ -190,7 +190,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
190
190
  schema: z.object({}),
191
191
  access: { roles: ["User"] },
192
192
  handler: async () => ({ isSuccess: true as const, data: {} }),
193
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
193
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
194
194
  r.step.return({ isSuccess: true as const, data: { ok: true } }),
195
195
  ]),
196
196
  } as unknown as Parameters<typeof defineWriteHandler>[0];
@@ -60,7 +60,7 @@ import {
60
60
  import { defineFeature } from "../define-feature";
61
61
  import { defineWriteHandler } from "../define-handler";
62
62
  import { createEntity, createTextField } from "../factories";
63
- import { pipeline } from "../pipeline";
63
+ import { stepsPipeline } from "../pipeline";
64
64
 
65
65
  const echoSchema = z.object({ greeting: z.string() });
66
66
 
@@ -70,7 +70,7 @@ const echoHandler = defineWriteHandler({
70
70
  name: "echo",
71
71
  schema: echoSchema,
72
72
  access: { roles: ["Admin"] },
73
- perform: pipeline<z.infer<typeof echoSchema>, { echoed: string; from: string }>(
73
+ perform: stepsPipeline<z.infer<typeof echoSchema>, { echoed: string; from: string }>(
74
74
  ({ event, r }) => [
75
75
  r.step.return(() => ({
76
76
  isSuccess: true as const,
@@ -90,7 +90,7 @@ const explodeHandler = defineWriteHandler({
90
90
  name: "explode",
91
91
  schema: explodeSchema,
92
92
  access: { roles: ["Admin"] },
93
- perform: pipeline<z.infer<typeof explodeSchema>, never>(({ r }) => [
93
+ perform: stepsPipeline<z.infer<typeof explodeSchema>, never>(({ r }) => [
94
94
  r.step.return(() => {
95
95
  throw new Error("boom");
96
96
  }),
@@ -106,7 +106,7 @@ const compoundHandler = defineWriteHandler({
106
106
  name: "compound",
107
107
  schema: compoundSchema,
108
108
  access: { roles: ["Admin"] },
109
- perform: pipeline<z.infer<typeof compoundSchema>, { sum: number; userId: string }>(
109
+ perform: stepsPipeline<z.infer<typeof compoundSchema>, { sum: number; userId: string }>(
110
110
  ({ event, r }) => [
111
111
  r.step.compute("offset", () => 100),
112
112
  r.step.compute("doubledBase", () => event.payload.base * 2),
@@ -147,7 +147,7 @@ const logHandler = defineWriteHandler({
147
147
  schema: logSchema,
148
148
  access: { roles: ["Admin"] },
149
149
  // Outer-`event`-capture pattern — see compoundHandler above for the why.
150
- perform: pipeline<z.infer<typeof logSchema>, { correlationId: string }>(({ event, r }) => [
150
+ perform: stepsPipeline<z.infer<typeof logSchema>, { correlationId: string }>(({ event, r }) => [
151
151
  r.step.unsafeProjectionUpsert({
152
152
  table: pipelineDemoLogTable,
153
153
  on: ["correlationId"],
@@ -182,7 +182,7 @@ const widgetCreateHandler = defineWriteHandler({
182
182
  name: "widget:create",
183
183
  schema: widgetSchema,
184
184
  access: { roles: ["Admin"] },
185
- perform: pipeline<z.infer<typeof widgetSchema>, { id: string }>(({ event, r }) => [
185
+ perform: stepsPipeline<z.infer<typeof widgetSchema>, { id: string }>(({ event, r }) => [
186
186
  r.step.aggregate.create("widget", {
187
187
  executor: widgetExecutor,
188
188
  data: () => ({ label: event.payload.label }),
@@ -202,7 +202,7 @@ const annotateHandler = defineWriteHandler({
202
202
  name: "widget:annotate",
203
203
  schema: annotateSchema,
204
204
  access: { roles: ["Admin"] },
205
- perform: pipeline<z.infer<typeof annotateSchema>, { id: string }>(({ event, r }) => [
205
+ perform: stepsPipeline<z.infer<typeof annotateSchema>, { id: string }>(({ event, r }) => [
206
206
  r.step.aggregate.create("widget", {
207
207
  executor: widgetExecutor,
208
208
  data: () => ({ label: event.payload.label }),
@@ -230,7 +230,7 @@ const widgetUpdateHandler = defineWriteHandler({
230
230
  name: "widget:update",
231
231
  schema: widgetUpdateSchema,
232
232
  access: { roles: ["Admin"] },
233
- perform: pipeline<z.infer<typeof widgetUpdateSchema>, { id: string }>(({ event, r }) => [
233
+ perform: stepsPipeline<z.infer<typeof widgetUpdateSchema>, { id: string }>(({ event, r }) => [
234
234
  r.step.aggregate.update("widget", {
235
235
  executor: widgetExecutor,
236
236
  id: () => event.payload.id,
@@ -254,7 +254,7 @@ const widgetBrokenHandler = defineWriteHandler({
254
254
  name: "widget:create-broken",
255
255
  schema: z.object({}),
256
256
  access: { roles: ["Admin"] },
257
- perform: pipeline<Record<string, never>, { id: string }>(({ r }) => [
257
+ perform: stepsPipeline<Record<string, never>, { id: string }>(({ r }) => [
258
258
  r.step.aggregate.create("widget", {
259
259
  executor: widgetExecutor,
260
260
  // Intentionally omits the `label` field that the entity declares
@@ -278,7 +278,7 @@ const widgetCreateThenThrowHandler = defineWriteHandler({
278
278
  name: "widget:create-then-throw",
279
279
  schema: z.object({ label: z.string() }),
280
280
  access: { roles: ["Admin"] },
281
- perform: pipeline<{ label: string }, never>(({ event, r }) => [
281
+ perform: stepsPipeline<{ label: string }, never>(({ event, r }) => [
282
282
  r.step.aggregate.create("widget", {
283
283
  executor: widgetExecutor,
284
284
  data: () => ({ label: event.payload.label }),
@@ -303,7 +303,7 @@ const forEachThenThrowHandler = defineWriteHandler({
303
303
  name: "widget:foreach-then-throw",
304
304
  schema: forEachThenThrowSchema,
305
305
  access: { roles: ["Admin"] },
306
- perform: pipeline<{ labels: string[] }, never>(({ event, r }) => [
306
+ perform: stepsPipeline<{ labels: string[] }, never>(({ event, r }) => [
307
307
  r.step.forEach({
308
308
  over: () => event.payload.labels,
309
309
  as: "label",
@@ -337,7 +337,7 @@ const lookupHandler = defineWriteHandler({
337
337
  name: "widget:lookup",
338
338
  schema: lookupSchema,
339
339
  access: { roles: ["Admin"] },
340
- perform: pipeline<z.infer<typeof lookupSchema>, { found: boolean; label: string | null }>(
340
+ perform: stepsPipeline<z.infer<typeof lookupSchema>, { found: boolean; label: string | null }>(
341
341
  ({ event, r }) => [
342
342
  r.step.read.findOne("widget", {
343
343
  table: widgetTable,
@@ -358,7 +358,7 @@ const listAllHandler = defineWriteHandler({
358
358
  name: "widget:list",
359
359
  schema: z.object({}),
360
360
  access: { roles: ["Admin"] },
361
- perform: pipeline<Record<string, never>, { count: number }>(({ r }) => [
361
+ perform: stepsPipeline<Record<string, never>, { count: number }>(({ r }) => [
362
362
  r.step.read.findMany("widgets", { table: widgetTable }),
363
363
  r.step.return(({ steps }) => ({
364
364
  isSuccess: true as const,
@@ -374,7 +374,7 @@ const listLimitedHandler = defineWriteHandler({
374
374
  name: "widget:list-one",
375
375
  schema: z.object({}),
376
376
  access: { roles: ["Admin"] },
377
- perform: pipeline<Record<string, never>, { count: number }>(({ r }) => [
377
+ perform: stepsPipeline<Record<string, never>, { count: number }>(({ r }) => [
378
378
  r.step.read.findMany("widgets", { table: widgetTable, limit: 1 }),
379
379
  r.step.return(({ steps }) => ({
380
380
  isSuccess: true as const,
@@ -388,7 +388,7 @@ const purgeLogHandler = defineWriteHandler({
388
388
  name: "log:purge",
389
389
  schema: purgeLogSchema,
390
390
  access: { roles: ["Admin"] },
391
- perform: pipeline<z.infer<typeof purgeLogSchema>, { ok: true }>(({ event, r }) => [
391
+ perform: stepsPipeline<z.infer<typeof purgeLogSchema>, { ok: true }>(({ event, r }) => [
392
392
  r.step.unsafeProjectionDelete({
393
393
  table: pipelineDemoLogTable,
394
394
  where: () => ({ correlationId: event.payload.correlationId }),
@@ -403,7 +403,7 @@ const conditionalLogHandler = defineWriteHandler({
403
403
  name: "log:conditional",
404
404
  schema: conditionalLogSchema,
405
405
  access: { roles: ["Admin"] },
406
- perform: pipeline<z.infer<typeof conditionalLogSchema>, { ok: true }>(({ event, r }) => [
406
+ perform: stepsPipeline<z.infer<typeof conditionalLogSchema>, { ok: true }>(({ event, r }) => [
407
407
  r.step.branch({
408
408
  if: () => event.payload.message.length > 0,
409
409
  onTrue: [
@@ -428,7 +428,7 @@ const bulkLogHandler = defineWriteHandler({
428
428
  name: "log:bulk",
429
429
  schema: bulkLogSchema,
430
430
  access: { roles: ["Admin"] },
431
- perform: pipeline<z.infer<typeof bulkLogSchema>, { count: number }>(({ event, r }) => [
431
+ perform: stepsPipeline<z.infer<typeof bulkLogSchema>, { count: number }>(({ event, r }) => [
432
432
  r.step.forEach({
433
433
  over: () => event.payload.correlationIds,
434
434
  as: "correlationId",
@@ -476,7 +476,7 @@ const demoPipelineFeature = defineFeature("demoPipeline", (r) => {
476
476
  let stack: TestStack;
477
477
  const admin = TestUsers.admin;
478
478
 
479
- describe("defineWriteHandler({ perform: pipeline(...) }) — real dispatcher path", () => {
479
+ describe("defineWriteHandler({ perform: stepsPipeline(...) }) — real dispatcher path", () => {
480
480
  beforeAll(async () => {
481
481
  stack = await setupTestStack({ features: [demoPipelineFeature] });
482
482
  // Push the read-side-projection table — not registered as an entity,
@@ -20,7 +20,7 @@ import { createRecordingProvider, type RecordingProvider } from "../../testing";
20
20
  import { defineFeature } from "../define-feature";
21
21
  import { defineWriteHandler } from "../define-handler";
22
22
  import { createEntity, createTextField } from "../factories";
23
- import { pipeline } from "../pipeline";
23
+ import { stepsPipeline } from "../pipeline";
24
24
 
25
25
  // Handler whose step bodies emit a span + a metric. The recording
26
26
  // provider afterwards lets us assert both landed.
@@ -30,7 +30,7 @@ const observedHandler = defineWriteHandler({
30
30
  name: "observed",
31
31
  schema: observedSchema,
32
32
  access: { roles: ["Admin"] },
33
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
33
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
34
34
  r.step.compute("traced", (ctx) => {
35
35
  // Span emitted from inside a step. End it explicitly because
36
36
  // step.run is sync-or-async-but-not-otel-instrumented; the
@@ -1,6 +1,6 @@
1
1
  // Pipeline-engine performance smoke-test.
2
2
  //
3
- // Compares the M.1 pipeline-form handler ({ perform: pipeline(...) })
3
+ // Compares the M.1 pipeline-form handler ({ perform: stepsPipeline(...) })
4
4
  // against the equivalent free-form handler ({ handler: async (...) })
5
5
  // over N identical writes against the real Postgres stack. The point
6
6
  // is NOT to optimise — the pipeline-form is a thin wrapper, not a
@@ -28,7 +28,7 @@ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } fr
28
28
  import { defineFeature } from "../define-feature";
29
29
  import { defineWriteHandler } from "../define-handler";
30
30
  import { createEntity, createNumberField, createTextField } from "../factories";
31
- import { pipeline } from "../pipeline";
31
+ import { stepsPipeline } from "../pipeline";
32
32
 
33
33
  // Same logical operation in both handler-forms: read input, return a
34
34
  // trivial transformed payload. No DB-write — the goal is to compare
@@ -41,7 +41,7 @@ const trivialPipeline = defineWriteHandler({
41
41
  name: "trivial:pipeline",
42
42
  schema: trivialSchema,
43
43
  access: { roles: ["Admin"] },
44
- perform: pipeline<{ n: number }, { doubled: number }>(({ event, r }) => [
44
+ perform: stepsPipeline<{ n: number }, { doubled: number }>(({ event, r }) => [
45
45
  r.step.return(() => ({
46
46
  isSuccess: true as const,
47
47
  data: { doubled: event.payload.n * 2 },
@@ -9,7 +9,7 @@ import { describe, expect, it } from "bun:test";
9
9
  import { z } from "zod";
10
10
  import { TestUsers } from "../../stack";
11
11
  import { defineWriteHandler } from "../define-handler";
12
- import { pipeline } from "../pipeline";
12
+ import { stepsPipeline } from "../pipeline";
13
13
  import { buildBranchStep } from "../steps/branch";
14
14
  import { buildForEachStep } from "../steps/for-each";
15
15
  import { buildReturnStep } from "../steps/return";
@@ -21,7 +21,7 @@ describe("r.step.branch", () => {
21
21
  name: "demo:branch-then",
22
22
  schema: z.object({}),
23
23
  access: { roles: ["User"] },
24
- perform: pipeline<Record<string, never>, { value: number }>(({ r }) => [
24
+ perform: stepsPipeline<Record<string, never>, { value: number }>(({ r }) => [
25
25
  r.step.branch({
26
26
  if: () => true,
27
27
  onTrue: [r.step.compute("inThen", () => 42)],
@@ -47,7 +47,7 @@ describe("r.step.branch", () => {
47
47
  name: "demo:branch-else",
48
48
  schema: z.object({}),
49
49
  access: { roles: ["User"] },
50
- perform: pipeline<Record<string, never>, { value: number }>(({ r }) => [
50
+ perform: stepsPipeline<Record<string, never>, { value: number }>(({ r }) => [
51
51
  r.step.branch({
52
52
  if: () => false,
53
53
  onTrue: [r.step.compute("inThen", () => 42)],
@@ -72,7 +72,7 @@ describe("r.step.branch", () => {
72
72
  name: "demo:branch-noop",
73
73
  schema: z.object({}),
74
74
  access: { roles: ["User"] },
75
- perform: pipeline<Record<string, never>, { ran: boolean }>(({ r }) => [
75
+ perform: stepsPipeline<Record<string, never>, { ran: boolean }>(({ r }) => [
76
76
  r.step.branch({
77
77
  if: () => false,
78
78
  onTrue: [r.step.compute("ran", () => true)],
@@ -122,7 +122,7 @@ describe("r.step.forEach", () => {
122
122
  name: "demo:foreach",
123
123
  schema: z.object({}),
124
124
  access: { roles: ["User"] },
125
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
125
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
126
126
  r.step.forEach({
127
127
  over: () => [10, 20, 30],
128
128
  as: "n",
@@ -153,7 +153,7 @@ describe("r.step.forEach", () => {
153
153
  name: "demo:foreach-cleanup",
154
154
  schema: z.object({}),
155
155
  access: { roles: ["User"] },
156
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
156
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
157
157
  r.step.forEach({
158
158
  over: () => [1],
159
159
  as: "tmp",
@@ -180,7 +180,7 @@ describe("r.step.forEach", () => {
180
180
  name: "demo:foreach-empty",
181
181
  schema: z.object({}),
182
182
  access: { roles: ["User"] },
183
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
183
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
184
184
  r.step.forEach({
185
185
  over: () => [],
186
186
  as: "n",
@@ -233,7 +233,7 @@ describe("r.step.forEach", () => {
233
233
  name: "demo:foreach-throws",
234
234
  schema: z.object({}),
235
235
  access: { roles: ["User"] },
236
- perform: pipeline<Record<string, never>, never>(({ r }) => [
236
+ perform: stepsPipeline<Record<string, never>, never>(({ r }) => [
237
237
  r.step.forEach({
238
238
  over: () => [1, 2, 3],
239
239
  as: "item",
@@ -266,7 +266,7 @@ describe("r.step.forEach", () => {
266
266
  name: "demo:foreach-bad-over",
267
267
  schema: z.object({}),
268
268
  access: { roles: ["User"] },
269
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
269
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
270
270
  r.step.forEach({
271
271
  // Cast: TypeScript would normally catch this; the test exercises
272
272
  // the runtime guard for hand-crafted / dynamically built cases.
@@ -11,7 +11,7 @@ import { defineFeature } from "../define-feature";
11
11
  import { defineWriteHandler } from "../define-handler";
12
12
  import { defineStep } from "../define-step";
13
13
  import { createEntity, createTextField } from "../factories";
14
- import { pipeline } from "../pipeline";
14
+ import { stepsPipeline } from "../pipeline";
15
15
  import { validateProjectionAllowlist } from "../validate-projection-allowlist";
16
16
 
17
17
  describe("validateProjectionAllowlist", () => {
@@ -28,7 +28,7 @@ describe("validateProjectionAllowlist", () => {
28
28
  name: "log",
29
29
  schema: z.object({ msg: z.string() }),
30
30
  access: { roles: ["User"] },
31
- perform: pipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
31
+ perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
32
32
  r.step.unsafeProjectionUpsert({
33
33
  table: demoLogTable,
34
34
  on: ["id"],
@@ -70,7 +70,7 @@ describe("validateProjectionAllowlist", () => {
70
70
  name: "sneaky",
71
71
  schema: z.object({}),
72
72
  access: { roles: ["User"] },
73
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
73
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
74
74
  r.step.unsafeProjectionUpsert({
75
75
  table: widgetsTable,
76
76
  on: ["id"],
@@ -113,7 +113,7 @@ describe("validateProjectionAllowlist", () => {
113
113
  name: "sneaky-delete",
114
114
  schema: z.object({}),
115
115
  access: { roles: ["User"] },
116
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
116
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
117
117
  r.step.unsafeProjectionDelete({
118
118
  table: widgetsTable,
119
119
  where: () => ({ id: "anything" }),
@@ -136,7 +136,7 @@ describe("validateProjectionAllowlist", () => {
136
136
  name: "purge",
137
137
  schema: z.object({}),
138
138
  access: { roles: ["User"] },
139
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
139
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
140
140
  r.step.unsafeProjectionDelete({
141
141
  table: demoLogTable,
142
142
  where: () => ({ id: "anything" }),
@@ -162,7 +162,7 @@ describe("validateProjectionAllowlist", () => {
162
162
  name: "branchedWrite",
163
163
  schema: z.object({}),
164
164
  access: { roles: ["User"] },
165
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
165
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
166
166
  r.step.branch({
167
167
  if: () => true,
168
168
  onTrue: [
@@ -195,7 +195,7 @@ describe("validateProjectionAllowlist", () => {
195
195
  name: "nestedWrite",
196
196
  schema: z.object({}),
197
197
  access: { roles: ["User"] },
198
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
198
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
199
199
  r.step.forEach({
200
200
  over: () => [],
201
201
  as: "item",
@@ -230,7 +230,7 @@ describe("validateProjectionAllowlist", () => {
230
230
  name: "loopedWrite",
231
231
  schema: z.object({}),
232
232
  access: { roles: ["User"] },
233
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
233
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
234
234
  r.step.forEach({
235
235
  over: () => [],
236
236
  as: "x",
@@ -267,7 +267,7 @@ describe("validateProjectionAllowlist", () => {
267
267
  name: "futureBuilder",
268
268
  schema: z.object({}),
269
269
  access: { roles: ["User"] },
270
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
270
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
271
271
  // Hand-crafted StepInstance simulating a future builder
272
272
  // whose kind isn't registered via defineStep yet.
273
273
  {
@@ -316,7 +316,7 @@ describe("validateProjectionAllowlist", () => {
316
316
  name: "futureBuilder",
317
317
  schema: z.object({}),
318
318
  access: { roles: ["User"] },
319
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
319
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
320
320
  {
321
321
  kind: futureKind,
322
322
  args: {
@@ -380,7 +380,7 @@ describe("validateProjectionAllowlist", () => {
380
380
  name: "sneak",
381
381
  schema: z.object({}),
382
382
  access: { roles: ["Admin"] },
383
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
383
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
384
384
  r.step.webhook.send({
385
385
  url: "https://hooks.example/sneak",
386
386
  mode: "deferred",
@@ -404,7 +404,7 @@ describe("validateProjectionAllowlist", () => {
404
404
  name: "sneak",
405
405
  schema: z.object({}),
406
406
  access: { roles: ["Admin"] },
407
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
407
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
408
408
  r.step.mail.send({
409
409
  to: "x@y.com",
410
410
  subject: "hi",
@@ -428,7 +428,7 @@ describe("validateProjectionAllowlist", () => {
428
428
  name: "sneak",
429
429
  schema: z.object({}),
430
430
  access: { roles: ["Admin"] },
431
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
431
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
432
432
  r.step.callFeature("subResult", {
433
433
  handler: "other:write:do",
434
434
  payload: () => ({}),
@@ -451,7 +451,7 @@ describe("validateProjectionAllowlist", () => {
451
451
  name: "ok",
452
452
  schema: z.object({}),
453
453
  access: { roles: ["Admin"] },
454
- perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
454
+ perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
455
455
  r.step.webhook.send({
456
456
  url: "https://hooks.example/ok",
457
457
  mode: "deferred",
@@ -473,7 +473,7 @@ describe("validateProjectionAllowlist", () => {
473
473
  name: "log",
474
474
  schema: z.object({ msg: z.string() }),
475
475
  access: { roles: ["User"] },
476
- perform: pipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
476
+ perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
477
477
  r.step.unsafeProjectionUpsert({
478
478
  table: demoLogTable,
479
479
  on: ["id"],
@@ -370,7 +370,7 @@ export function generatePerformBlock(
370
370
  const pipelineType = schemaType ? `<${schemaType}, unknown>` : "";
371
371
 
372
372
  return [
373
- `perform: pipeline${pipelineType}(({ event, r }) => [`,
373
+ `perform: stepsPipeline${pipelineType}(({ event, r }) => [`,
374
374
  stepsStr,
375
375
  ` ${stepIndent}]),`,
376
376
  ].join("\n");
@@ -513,7 +513,7 @@ function contentHasPipelineImport(content: string): boolean {
513
513
  const importLine = content
514
514
  .split("\n")
515
515
  .find((l) => l.includes("import") && l.includes("@cosmicdrift/kumiko-framework/engine"));
516
- return !!importLine && importLine.includes("pipeline");
516
+ return !!importLine && importLine.includes("stepsPipeline");
517
517
  }
518
518
 
519
519
  function ensurePipelineImport(content: string): string | null {
@@ -525,7 +525,7 @@ function ensurePipelineImport(content: string): string | null {
525
525
  const match = content.match(importRegex);
526
526
  if (match) {
527
527
  const existingImports = (match[1] as string).trim();
528
- const newImports = existingImports ? `${existingImports}, pipeline` : "pipeline";
528
+ const newImports = existingImports ? `${existingImports}, stepsPipeline` : "stepsPipeline";
529
529
  return content.replace(
530
530
  importRegex,
531
531
  `import { ${newImports} } from "@cosmicdrift/kumiko-framework/engine"`,
@@ -611,7 +611,7 @@ export async function convertFile(
611
611
  if (importResult) {
612
612
  content = importResult;
613
613
  } else if (options.verbose) {
614
- console.log(` ~ ${filePath}: pipeline import not needed or already present`);
614
+ console.log(` ~ ${filePath}: stepsPipeline import not needed or already present`);
615
615
  }
616
616
  }
617
617
 
@@ -621,7 +621,7 @@ export async function convertFile(
621
621
 
622
622
  const status = hadChanges ? "converted" : "skipped";
623
623
  const reason = hadChanges
624
- ? "handler replaced with perform: pipeline(...)"
624
+ ? "handler replaced with perform: stepsPipeline(...)"
625
625
  : "no convertible handler";
626
626
  return { filePath, status, reason };
627
627
  } catch (err) {
@@ -23,7 +23,7 @@ import type { PipelineDef } from "./types/step";
23
23
  // visible) and collapse `keyof TMap` to `never`. See the spike-findings
24
24
  // memory for the empirical proof.
25
25
  //
26
- // Two authoring forms — `handler` (free-form) or `perform: pipeline(...)`
26
+ // Two authoring forms — `handler` (free-form) or `perform: stepsPipeline(...)`
27
27
  // (step-pipeline). A `perform` is compiled to a handler-function at
28
28
  // definition time; the dispatcher only ever sees `handler`.
29
29
 
@@ -126,7 +126,7 @@ export function defineWriteHandler<
126
126
  throw new Error(
127
127
  `defineWriteHandler("${def.name}"): both \`handler\` and \`perform\` are set. ` +
128
128
  `Pick one — \`handler\` for the free-form async function, ` +
129
- `\`perform: pipeline(...)\` for the step-pipeline form. ` +
129
+ `\`perform: stepsPipeline(...)\` for the step-pipeline form. ` +
130
130
  `(See step-vocabulary.md for which form fits.)`,
131
131
  );
132
132
  }
@@ -57,7 +57,7 @@ export type WorkflowInput<TPayload = unknown, TData = unknown> = {
57
57
  * defineWorkflow({
58
58
  * name: "user-onboarding",
59
59
  * trigger: { kind: "event", eventType: "user.signed-up" },
60
- * steps: pipeline(({ event, r }) => [
60
+ * steps: stepsPipeline(({ event, r }) => [
61
61
  * r.step.mail.send({ to: () => event.payload.email, subject: "Welcome!", body: "..." }),
62
62
  * r.step.wait({ for: "P1D" }),
63
63
  * r.step.read.findOne("user", { table: userTable, where: ... }),
@@ -179,18 +179,20 @@ export {
179
179
  filterReadFields,
180
180
  } from "./field-access";
181
181
  // findForbiddenMembershipRole/isForbiddenMembershipRole/
182
- // stripForbiddenMembershipRoles are Public API for host apps that build
183
- // their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES itself stays
184
- // internal (637/3) — exporting the raw Set would make its representation a
185
- // semver promise; the predicate functions are the intended surface.
182
+ // stripForbiddenMembershipRoles/buildSessionRoles are Public API for host
183
+ // apps that build their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
184
+ // itself stays internal (637/3) — exporting the raw Set would make its
185
+ // representation a semver promise; the predicate functions are the intended
186
+ // surface.
186
187
  export {
188
+ buildSessionRoles,
187
189
  findForbiddenMembershipRole,
188
190
  isForbiddenMembershipRole,
189
191
  stripForbiddenMembershipRoles,
190
192
  } from "./membership-roles";
191
193
  export type { OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule } from "./ownership";
192
194
  export { from } from "./ownership";
193
- export { buildPipelineSteps, pipeline } from "./pipeline";
195
+ export { buildPipelineSteps, stepsPipeline } from "./pipeline";
194
196
  export { defineApply, defineMspApply, setFields } from "./projection-helpers";
195
197
  export type { BuiltinQnType, ParsedQn, QnType } from "./qualified-name";
196
198
  export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name";
@@ -30,3 +30,16 @@ export function findForbiddenMembershipRole(roles: readonly string[]): string |
30
30
  export function stripForbiddenMembershipRoles(roles: readonly string[]): readonly string[] {
31
31
  return roles.filter((role) => !isForbiddenMembershipRole(role));
32
32
  }
33
+
34
+ // Single mint path for session roles: merges globalRoles with the stripped
35
+ // membership portion and dedupes. Every place that builds a session's roles
36
+ // from a membership should go through this instead of repeating
37
+ // `new Set([...globalRoles, ...stripForbiddenMembershipRoles(x)])` — a new
38
+ // mint site that forgets the strip is exactly the bug class this guards
39
+ // against.
40
+ export function buildSessionRoles(
41
+ globalRoles: readonly string[],
42
+ membershipRoles: readonly string[],
43
+ ): readonly string[] {
44
+ return Array.from(new Set([...globalRoles, ...stripForbiddenMembershipRoles(membershipRoles)]));
45
+ }
@@ -1,4 +1,4 @@
1
- // pipeline() — public factory used in defineWriteHandler({ perform: pipeline(...) }).
1
+ // stepsPipeline() — public factory used in defineWriteHandler({ perform: stepsPipeline(...) }).
2
2
  //
3
3
  // The closure receives { event, r } and returns the immutable list of
4
4
  // step instances. `r` is the StepBuilder singleton; new tier-1 steps
@@ -8,15 +8,10 @@
8
8
  // the resolver-side PipelineCtx (run-pipeline.ts). Resolvers that need
9
9
  // prior step results destructure them from the resolver's ctx.
10
10
  //
11
- // Naming note (Followup #1): the public `pipeline()` helper shares its
12
- // name with the internal `packages/framework/src/pipeline/` directory
13
- // (dispatcher, lifecycle, outbox-poller). No user-visible collision
14
- // the internal directory isn't an export — but maintainer repo-wide
15
- // grep for `pipeline` returns mixed results. If a rename ever lands,
16
- // candidates are `r.steps([...])` for the public API or
17
- // `engine-pipeline/` / `runtime/` for the internal directory.
18
- // Decision-cost grows with each new caller; the rename window narrows
19
- // after the first external consumer.
11
+ // Renamed from `pipeline()` (Followup #1) it shared its name with the
12
+ // internal `packages/framework/src/pipeline/` directory (dispatcher,
13
+ // lifecycle, outbox-poller), which made repo-wide grep for `pipeline`
14
+ // return mixed results.
20
15
 
21
16
  import { buildAggregateAppendEventStep } from "./steps/aggregate-append-event";
22
17
  import { buildAggregateCreateStep } from "./steps/aggregate-create";
@@ -69,7 +64,7 @@ const stepBuilder: StepBuilder = {
69
64
  },
70
65
  };
71
66
 
72
- export function pipeline<TPayload = unknown, TData = unknown>(
67
+ export function stepsPipeline<TPayload = unknown, TData = unknown>(
73
68
  closure: (ctx: PipelineBuildCtx<TPayload>) => readonly StepInstance[],
74
69
  ): PipelineDef<TPayload, TData> {
75
70
  return {
@@ -64,7 +64,7 @@ export async function runPipeline<TPayload, TData, TMap extends object = KumikoE
64
64
  if (outcome.kind === "return") {
65
65
  // RETURN_RESULT_KEY is only produced by r.step.return, whose run()
66
66
  // returns WriteResult<unknown>. The pipeline's generic TData is
67
- // bound at build time (defineWriteHandler ↔ pipeline<P, D>(...));
67
+ // bound at build time (defineWriteHandler ↔ stepsPipeline<P, D>(...));
68
68
  // matching the runtime value to that compile-time type is the
69
69
  // contract user-side. Cast crosses that boundary.
70
70
  return outcome.result as WriteResult<TData>;
@@ -796,7 +796,7 @@ export type WriteHandlerDef = {
796
796
  readonly access?: AccessRule;
797
797
  readonly unsafeSkipTransitionGuard?: boolean;
798
798
  readonly rateLimit?: RateLimitOption;
799
- // Set when the author wrote a `perform: pipeline(...)` block. Boot-
799
+ // Set when the author wrote a `perform: stepsPipeline(...)` block. Boot-
800
800
  // validators (projection-allowlist) and Designer/AI tooling read this
801
801
  // to inspect the step list. Absent on free-form handlers.
802
802
  // Inline-import is intentional: step.ts imports HandlerContext from
@@ -126,7 +126,7 @@ export type StepInstance = {
126
126
  };
127
127
 
128
128
  /**
129
- * What `pipeline(closure)` returns. Carries the closure (instead of an
129
+ * What `stepsPipeline(closure)` returns. Carries the closure (instead of an
130
130
  * eagerly-built array) so each handler-call sees a fresh event ref and
131
131
  * the `r` step-builder is resolved at runtime, not at module-load time.
132
132
  *
@@ -140,7 +140,7 @@ export type StepInstance = {
140
140
  * `noUnusedVariables` marker, not user-facing). _TData is NOT inferred
141
141
  * from the closure body (r.step.return has its own per-call TData), so
142
142
  * callers must spell it explicitly:
143
- * `pipeline<{ greeting: string }, { echoed: string }>(...)`
143
+ * `stepsPipeline<{ greeting: string }, { echoed: string }>(...)`
144
144
  * Better DX is a known follow-up — see step-vocabulary.md M.1-Followups.
145
145
  */
146
146
  export type PipelineDef<TPayload = unknown, _TData = unknown> = {
@@ -149,7 +149,7 @@ export type PipelineDef<TPayload = unknown, _TData = unknown> = {
149
149
  };
150
150
 
151
151
  /**
152
- * Argument bundle passed to the closure inside `pipeline(closure)`.
152
+ * Argument bundle passed to the closure inside `stepsPipeline(closure)`.
153
153
  * Build-time only — no `steps`, no `scope`, no `db`: at build time no
154
154
  * step has run yet.
155
155
  *
@@ -314,7 +314,7 @@ export type StepNamespace = {
314
314
  },
315
315
  ) => StepInstance;
316
316
  // --- Tier-3 / Workflow-only steps ---
317
- // Only available inside defineWorkflow ({ steps: pipeline(...) }).
317
+ // Only available inside defineWorkflow ({ steps: stepsPipeline(...) }).
318
318
  // Runtime guard: throws when used inside sync defineWriteHandler.
319
319
  readonly wait: (args: { readonly for: StepResolver<string> }) => StepInstance;
320
320
  readonly waitForEvent: (args: {
@@ -13,7 +13,7 @@
13
13
  // don't run at build time, so dummy event-payloads are fine.
14
14
  //
15
15
  // CLOSURE-BODY CONTRACT: the pipeline-closure body (the function passed
16
- // to `pipeline(...)`) must produce its step-list deterministically based
16
+ // to `stepsPipeline(...)`) must produce its step-list deterministically based
17
17
  // on the step-builder `r` alone. Reading `event.payload` outside of
18
18
  // resolvers (i.e. at the top of the closure, not inside a step's `row:`
19
19
  // or `data:` callback) is forbidden — at boot the dummy payload is `{}`