Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/dogfood/test/flow-trigger-conformance.ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0060 D5 extension — Flow Trigger-Type Conformance ledger (the fourth
// instance of the reusable pattern). A `Flow.type` declares HOW a flow is
// triggered; if a declared trigger type has no runtime that fires/runs it, the
// flow is authorable but inert — the flow-shaped #1887, and flows are heavily
// AI-authored. One row per `Flow.type` enum value, each pinned to its runtime
// trigger/executor and a proof; the ratchet fails the build if a new flow type
// is added with no runtime.

import type { ConformanceRow } from '@objectstack/verify';

export const FLOW_TRIGGER_SURFACE: ConformanceRow[] = [
{
id: 'flow-record-change',
summary: 'record_change — fires a flow on a record insert/update/delete',
surface: 'flow.zod.ts:type=record_change',
state: 'enforced',
enforcement: '@objectstack/trigger-record-change subscribes to engine record events and starts the flow',
covers: ['record_change'],
proof: 'packages/triggers/trigger-record-change/src/record-change-trigger.test.ts',
},
{
id: 'flow-api',
summary: 'api — flow invoked via an HTTP/API endpoint',
surface: 'flow.zod.ts:type=api',
state: 'enforced',
enforcement: '@objectstack/trigger-api exposes the flow as an invokable endpoint',
covers: ['api'],
proof: 'packages/triggers/trigger-api/src/api-trigger.test.ts',
},
{
id: 'flow-schedule',
summary: 'schedule — flow run on a cron/interval schedule',
surface: 'flow.zod.ts:type=schedule',
state: 'enforced',
enforcement: '@objectstack/trigger-schedule registers cron/interval timers that start the flow',
covers: ['schedule'],
proof: 'packages/triggers/trigger-schedule/src/schedule-trigger.test.ts',
},
{
id: 'flow-autolaunched',
summary: 'autolaunched — subflow / programmatically-started flow (no event trigger)',
surface: 'flow.zod.ts:type=autolaunched',
state: 'enforced',
enforcement: 'service-automation engine — started by a subflow node or a programmatic startFlow call',
covers: ['autolaunched'],
proof: 'packages/services/service-automation/src/builtin/subflow-node.test.ts',
},
{
id: 'flow-screen',
summary: 'screen — interactive (human-in-the-loop) flow; runs server-side, suspends at screen nodes for UI input',
surface: 'flow.zod.ts:type=screen',
state: 'enforced',
enforcement: 'service-automation builtin screen executor + suspended-run store (server runs; resumes on UI input)',
covers: ['screen'],
proof: 'packages/services/service-automation/src/builtin/screen-nodes.test.ts',
},
];
47 changes: 47 additions & 0 deletions packages/dogfood/test/flow-trigger-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0060 D5 extension — the flow trigger-type ledger is a CHECKED artifact on
// the same reusable checkLedger helper (the FOURTH surface: authz / expression /
// validation-rule / flow-trigger). The ratchet re-discovers every `Flow.type`
// enum value from flow.zod.ts and fails the build if one is unclassified — so a
// new flow trigger type added without a runtime that fires it (a flow that is
// authorable but never runs — the flow-shaped #1887) can't ship silently.

import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { checkLedger } from '@objectstack/verify';
import { FLOW_TRIGGER_SURFACE } from './flow-trigger-conformance.ledger.js';

const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(HERE, '../../..');
const FLOW_ZOD = join(REPO_ROOT, 'packages/spec/src/automation/flow.zod.ts');

/** Re-discover every `Flow.type` enum value (the one tagged `.describe('Flow type')`). */
function discoverFlowTypes(): Set<string> {
const src = readFileSync(FLOW_ZOD, 'utf8');
const m = src.match(/type:\s*z\.enum\(\[([^\]]+)\]\)\s*\.describe\('Flow type'\)/);
if (!m) throw new Error('Flow.type enum not found in flow.zod.ts — discovery is stale');
const found = new Set<string>();
for (const lit of m[1].matchAll(/'([a-z_]+)'/g)) found.add(lit[1]);
return found;
}

describe('ADR-0060 D5 — flow trigger-type conformance ledger', () => {
it('is a sound conformance ledger + ratchet, every flow type has a runtime + proof', () => {
const problems = checkLedger(FLOW_TRIGGER_SURFACE, {
proofRoot: REPO_ROOT,
discover: discoverFlowTypes,
proofRequiredForEnforced: true,
});
expect(problems, problems.join('\n')).toEqual([]);
});

it('sanity: discovery finds the known flow types', () => {
const found = discoverFlowTypes();
for (const t of ['record_change', 'api', 'schedule', 'autolaunched', 'screen']) {
expect(found.has(t), `flow type '${t}' not discovered`).toBe(true);
}
});
});
Loading