Skip to content

Commit dd8a501

Browse files
os-zhuangclaude
andcommitted
feat(dogfood): ADR-0060 D5 — flow trigger-type conformance ledger (4th surface)
The fourth instance of the reusable conformance-ledger pattern, on the same checkLedger helper. A `Flow.type` declares HOW a flow is triggered; a declared trigger type with no runtime that fires it is authorable-but-inert — the flow-shaped #1887, and flows are heavily AI-authored. One row per Flow.type enum value (record_change / api / schedule / autolaunched / screen), all enforced with their runtime trigger/executor + a proof. The ratchet re-discovers the enum from flow.zod.ts and fails the build if a new flow type is added with no runtime. Proves the ADR-0060 pattern extends to a fourth surface as a filled-in table. dogfood-private, no changeset. Full build 75/75, dogfood 137; ratchet verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 95b0e9b commit dd8a501

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0060 D5 extension — Flow Trigger-Type Conformance ledger (the fourth
4+
// instance of the reusable pattern). A `Flow.type` declares HOW a flow is
5+
// triggered; if a declared trigger type has no runtime that fires/runs it, the
6+
// flow is authorable but inert — the flow-shaped #1887, and flows are heavily
7+
// AI-authored. One row per `Flow.type` enum value, each pinned to its runtime
8+
// trigger/executor and a proof; the ratchet fails the build if a new flow type
9+
// is added with no runtime.
10+
11+
import type { ConformanceRow } from '@objectstack/verify';
12+
13+
export const FLOW_TRIGGER_SURFACE: ConformanceRow[] = [
14+
{
15+
id: 'flow-record-change',
16+
summary: 'record_change — fires a flow on a record insert/update/delete',
17+
surface: 'flow.zod.ts:type=record_change',
18+
state: 'enforced',
19+
enforcement: '@objectstack/trigger-record-change subscribes to engine record events and starts the flow',
20+
covers: ['record_change'],
21+
proof: 'packages/triggers/trigger-record-change/src/record-change-trigger.test.ts',
22+
},
23+
{
24+
id: 'flow-api',
25+
summary: 'api — flow invoked via an HTTP/API endpoint',
26+
surface: 'flow.zod.ts:type=api',
27+
state: 'enforced',
28+
enforcement: '@objectstack/trigger-api exposes the flow as an invokable endpoint',
29+
covers: ['api'],
30+
proof: 'packages/triggers/trigger-api/src/api-trigger.test.ts',
31+
},
32+
{
33+
id: 'flow-schedule',
34+
summary: 'schedule — flow run on a cron/interval schedule',
35+
surface: 'flow.zod.ts:type=schedule',
36+
state: 'enforced',
37+
enforcement: '@objectstack/trigger-schedule registers cron/interval timers that start the flow',
38+
covers: ['schedule'],
39+
proof: 'packages/triggers/trigger-schedule/src/schedule-trigger.test.ts',
40+
},
41+
{
42+
id: 'flow-autolaunched',
43+
summary: 'autolaunched — subflow / programmatically-started flow (no event trigger)',
44+
surface: 'flow.zod.ts:type=autolaunched',
45+
state: 'enforced',
46+
enforcement: 'service-automation engine — started by a subflow node or a programmatic startFlow call',
47+
covers: ['autolaunched'],
48+
proof: 'packages/services/service-automation/src/builtin/subflow-node.test.ts',
49+
},
50+
{
51+
id: 'flow-screen',
52+
summary: 'screen — interactive (human-in-the-loop) flow; runs server-side, suspends at screen nodes for UI input',
53+
surface: 'flow.zod.ts:type=screen',
54+
state: 'enforced',
55+
enforcement: 'service-automation builtin screen executor + suspended-run store (server runs; resumes on UI input)',
56+
covers: ['screen'],
57+
proof: 'packages/services/service-automation/src/builtin/screen-nodes.test.ts',
58+
},
59+
];
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0060 D5 extension — the flow trigger-type ledger is a CHECKED artifact on
4+
// the same reusable checkLedger helper (the FOURTH surface: authz / expression /
5+
// validation-rule / flow-trigger). The ratchet re-discovers every `Flow.type`
6+
// enum value from flow.zod.ts and fails the build if one is unclassified — so a
7+
// new flow trigger type added without a runtime that fires it (a flow that is
8+
// authorable but never runs — the flow-shaped #1887) can't ship silently.
9+
10+
import { describe, expect, it } from 'vitest';
11+
import { readFileSync } from 'node:fs';
12+
import { fileURLToPath } from 'node:url';
13+
import { dirname, join } from 'node:path';
14+
import { checkLedger } from '@objectstack/verify';
15+
import { FLOW_TRIGGER_SURFACE } from './flow-trigger-conformance.ledger.js';
16+
17+
const HERE = dirname(fileURLToPath(import.meta.url));
18+
const REPO_ROOT = join(HERE, '../../..');
19+
const FLOW_ZOD = join(REPO_ROOT, 'packages/spec/src/automation/flow.zod.ts');
20+
21+
/** Re-discover every `Flow.type` enum value (the one tagged `.describe('Flow type')`). */
22+
function discoverFlowTypes(): Set<string> {
23+
const src = readFileSync(FLOW_ZOD, 'utf8');
24+
const m = src.match(/type:\s*z\.enum\(\[([^\]]+)\]\)\s*\.describe\('Flow type'\)/);
25+
if (!m) throw new Error('Flow.type enum not found in flow.zod.ts — discovery is stale');
26+
const found = new Set<string>();
27+
for (const lit of m[1].matchAll(/'([a-z_]+)'/g)) found.add(lit[1]);
28+
return found;
29+
}
30+
31+
describe('ADR-0060 D5 — flow trigger-type conformance ledger', () => {
32+
it('is a sound conformance ledger + ratchet, every flow type has a runtime + proof', () => {
33+
const problems = checkLedger(FLOW_TRIGGER_SURFACE, {
34+
proofRoot: REPO_ROOT,
35+
discover: discoverFlowTypes,
36+
proofRequiredForEnforced: true,
37+
});
38+
expect(problems, problems.join('\n')).toEqual([]);
39+
});
40+
41+
it('sanity: discovery finds the known flow types', () => {
42+
const found = discoverFlowTypes();
43+
for (const t of ['record_change', 'api', 'schedule', 'autolaunched', 'screen']) {
44+
expect(found.has(t), `flow type '${t}' not discovered`).toBe(true);
45+
}
46+
});
47+
});

0 commit comments

Comments
 (0)