From dd8a501598792aa218030213037b02de8f36ccd6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:46:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(dogfood):=20ADR-0060=20D5=20=E2=80=94=20fl?= =?UTF-8?q?ow=20trigger-type=20conformance=20ledger=20(4th=20surface)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../test/flow-trigger-conformance.ledger.ts | 59 +++++++++++++++++++ .../test/flow-trigger-conformance.test.ts | 47 +++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/dogfood/test/flow-trigger-conformance.ledger.ts create mode 100644 packages/dogfood/test/flow-trigger-conformance.test.ts diff --git a/packages/dogfood/test/flow-trigger-conformance.ledger.ts b/packages/dogfood/test/flow-trigger-conformance.ledger.ts new file mode 100644 index 0000000000..d2d4808903 --- /dev/null +++ b/packages/dogfood/test/flow-trigger-conformance.ledger.ts @@ -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', + }, +]; diff --git a/packages/dogfood/test/flow-trigger-conformance.test.ts b/packages/dogfood/test/flow-trigger-conformance.test.ts new file mode 100644 index 0000000000..325676b2c3 --- /dev/null +++ b/packages/dogfood/test/flow-trigger-conformance.test.ts @@ -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 { + 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(); + 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); + } + }); +});