@pipefy/pipefy-process-coder 0.1.0 → 0.1.1

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.
Files changed (159) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
  19. package/AGENTS.md +465 -456
  20. package/README.md +825 -807
  21. package/bin/pipe.js +25 -4
  22. package/dist/apply/adopt.js +128 -0
  23. package/dist/apply/agentops.js +42 -0
  24. package/dist/apply/compile.js +722 -0
  25. package/dist/apply/execute.js +277 -0
  26. package/dist/apply/flowops.js +293 -0
  27. package/dist/apply/idmap.js +233 -0
  28. package/dist/apply/mutations.js +704 -0
  29. package/dist/apply/registry.js +348 -0
  30. package/dist/apply/types.js +18 -0
  31. package/dist/cli/args.js +79 -0
  32. package/dist/cli.js +187 -0
  33. package/dist/codec/flow.js +164 -0
  34. package/dist/codec/pack.js +88 -0
  35. package/dist/codec/roundtrip.js +86 -0
  36. package/dist/codec/unpack.js +181 -0
  37. package/dist/commands/agents.js +145 -0
  38. package/dist/commands/apply.js +982 -0
  39. package/dist/commands/context.js +79 -0
  40. package/dist/commands/create.js +63 -0
  41. package/dist/commands/diff.js +294 -0
  42. package/dist/commands/flows.js +348 -0
  43. package/dist/commands/misc.js +528 -0
  44. package/dist/commands/plan.js +301 -0
  45. package/dist/commands/pull.js +511 -0
  46. package/dist/commands/runs.js +74 -0
  47. package/dist/commands/skills.js +131 -0
  48. package/dist/commands/verify.js +205 -0
  49. package/dist/config.js +125 -0
  50. package/dist/diff/agents.js +80 -0
  51. package/dist/diff/diff.js +983 -0
  52. package/dist/diff/flow.js +258 -0
  53. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  54. package/dist/diff/render.js +90 -0
  55. package/dist/model/payload.js +84 -0
  56. package/dist/model/tree.js +7 -0
  57. package/dist/model/volatile.js +54 -0
  58. package/dist/pipefy/agents.js +122 -0
  59. package/dist/pipefy/automations.js +155 -0
  60. package/dist/pipefy/capability.js +84 -0
  61. package/dist/pipefy/client.js +204 -0
  62. package/dist/pipefy/discovery.js +148 -0
  63. package/dist/pipefy/internal.js +315 -0
  64. package/dist/pipefy/ipaas.js +269 -0
  65. package/dist/pipefy/reconstruct.js +674 -0
  66. package/dist/pipefy/reference.js +172 -0
  67. package/dist/pipefy/snapshot.js +185 -0
  68. package/dist/pipefy/toolkit.js +158 -0
  69. package/dist/report/integrations.js +165 -0
  70. package/dist/report/run.js +355 -0
  71. package/dist/util/fsx.js +50 -0
  72. package/dist/util/git.js +25 -0
  73. package/dist/util/json.js +56 -0
  74. package/dist/util/log.js +69 -0
  75. package/dist/util/pool.js +42 -0
  76. package/dist/util/slug.js +23 -0
  77. package/dist/util/tui.js +287 -0
  78. package/dist/validate/index.js +86 -0
  79. package/dist/validate/integrity.js +370 -0
  80. package/dist/validate/reference.js +120 -0
  81. package/dist/validate/schema.js +278 -0
  82. package/dist/workspace/agents.js +245 -0
  83. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  84. package/dist/workspace/flows.js +149 -0
  85. package/dist/workspace/layout.js +146 -0
  86. package/dist/workspace/lock.js +83 -0
  87. package/dist/workspace/read.js +168 -0
  88. package/dist/workspace/reference.js +24 -0
  89. package/dist/workspace/stamp.js +234 -0
  90. package/dist/workspace/write.js +194 -0
  91. package/package.json +37 -35
  92. package/src/apply/adopt.ts +0 -150
  93. package/src/apply/agentops.ts +0 -71
  94. package/src/apply/compile.ts +0 -875
  95. package/src/apply/execute.ts +0 -336
  96. package/src/apply/flowops.ts +0 -399
  97. package/src/apply/idmap.ts +0 -241
  98. package/src/apply/mutations.ts +0 -955
  99. package/src/apply/registry.ts +0 -430
  100. package/src/apply/types.ts +0 -134
  101. package/src/cli/args.ts +0 -88
  102. package/src/cli.ts +0 -211
  103. package/src/codec/flow.ts +0 -199
  104. package/src/codec/pack.ts +0 -103
  105. package/src/codec/roundtrip.ts +0 -94
  106. package/src/codec/unpack.ts +0 -198
  107. package/src/commands/agents.ts +0 -184
  108. package/src/commands/apply.ts +0 -1144
  109. package/src/commands/context.ts +0 -119
  110. package/src/commands/create.ts +0 -79
  111. package/src/commands/diff.ts +0 -314
  112. package/src/commands/flows.ts +0 -414
  113. package/src/commands/misc.ts +0 -644
  114. package/src/commands/plan.ts +0 -331
  115. package/src/commands/pull.ts +0 -567
  116. package/src/commands/runs.ts +0 -83
  117. package/src/commands/skills.ts +0 -137
  118. package/src/commands/verify.ts +0 -253
  119. package/src/config.ts +0 -168
  120. package/src/diff/agents.ts +0 -122
  121. package/src/diff/diff.ts +0 -1130
  122. package/src/diff/flow.ts +0 -318
  123. package/src/diff/render.ts +0 -101
  124. package/src/model/payload.ts +0 -154
  125. package/src/model/tree.ts +0 -99
  126. package/src/model/volatile.ts +0 -55
  127. package/src/pipefy/agents.ts +0 -165
  128. package/src/pipefy/automations.ts +0 -219
  129. package/src/pipefy/capability.ts +0 -119
  130. package/src/pipefy/client.ts +0 -267
  131. package/src/pipefy/discovery.ts +0 -209
  132. package/src/pipefy/internal.ts +0 -380
  133. package/src/pipefy/ipaas.ts +0 -365
  134. package/src/pipefy/reconstruct.ts +0 -775
  135. package/src/pipefy/reference.ts +0 -251
  136. package/src/pipefy/snapshot.ts +0 -245
  137. package/src/pipefy/toolkit.ts +0 -200
  138. package/src/report/integrations.ts +0 -231
  139. package/src/report/run.ts +0 -475
  140. package/src/util/fsx.ts +0 -45
  141. package/src/util/git.ts +0 -32
  142. package/src/util/json.ts +0 -55
  143. package/src/util/log.ts +0 -76
  144. package/src/util/pool.ts +0 -48
  145. package/src/util/slug.ts +0 -26
  146. package/src/util/tui.ts +0 -335
  147. package/src/validate/index.ts +0 -123
  148. package/src/validate/integrity.ts +0 -387
  149. package/src/validate/reference.ts +0 -136
  150. package/src/validate/schema.ts +0 -328
  151. package/src/workspace/agents.ts +0 -290
  152. package/src/workspace/flows.ts +0 -191
  153. package/src/workspace/layout.ts +0 -165
  154. package/src/workspace/lock.ts +0 -148
  155. package/src/workspace/read.ts +0 -165
  156. package/src/workspace/reference.ts +0 -24
  157. package/src/workspace/stamp.ts +0 -301
  158. package/src/workspace/write.ts +0 -225
  159. /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
@@ -1,55 +0,0 @@
1
- /**
2
- * PLAN.md 9: "Volatile fields will make every diff pure noise." These change
3
- * with no structural edit, so the codec strips them on unpack and the
4
- * comparator ignores them. Without this, `pipe diff` is unusable on day one.
5
- */
6
-
7
- export const VOLATILE_KEYS = new Set([
8
- 'created_at',
9
- 'updated_at',
10
- 'deleted_at',
11
- 'created_by_id',
12
- 'updated_by_id',
13
- 'run_count',
14
- 'cards_count',
15
- 'users_count',
16
- 'opened_cards_count',
17
- 'last_updated_by_card',
18
- 'cloning',
19
- 'sequential_id',
20
- 'clone_from_id',
21
- 'cloned_from_id',
22
- 'cloned_from_phase_uuid',
23
- 'cloned_from_field_uuid',
24
- 'migrated_from_repo_uuid',
25
- ]);
26
-
27
- /**
28
- * Server-owned identity and ownership columns. Kept in the files (Claude needs
29
- * to read them for a field reference to mean anything) but never diffed as
30
- * content, because they are consequences of structure rather than structure.
31
- */
32
- export const SERVER_OWNED_KEYS = new Set([
33
- 'id',
34
- 'repo_id',
35
- 'organization_id',
36
- 'organization_uuid',
37
- 'suid',
38
- 'slug',
39
- ]);
40
-
41
- export const stripVolatile = <T extends Record<string, unknown>>(row: T): T => {
42
- const out: Record<string, unknown> = {};
43
- for (const [k, v] of Object.entries(row)) {
44
- if (VOLATILE_KEYS.has(k)) continue;
45
- out[k] = v;
46
- }
47
- return out as T;
48
- };
49
-
50
- /** The volatile values we drop, kept aside so the UI can still show them. */
51
- export const takeVolatile = (row: Record<string, unknown>): Record<string, unknown> => {
52
- const out: Record<string, unknown> = {};
53
- for (const [k, v] of Object.entries(row)) if (VOLATILE_KEYS.has(k)) out[k] = v;
54
- return out;
55
- };
@@ -1,165 +0,0 @@
1
- import { debug } from '../util/log.ts';
2
- import type { Row } from '../model/payload.ts';
3
- import type { PipefyClient } from './client.ts';
4
-
5
- /**
6
- * AI agents — not in the snapshot payload at all (ROUTE-B-V2.md ask 5), so read
7
- * separately over the public GraphQL API, keyed by the pipe's **uuid**, not its
8
- * numeric id.
9
- *
10
- * `AiAgent.behaviors` answers with full `Automation` rows — a behavior is not a
11
- * new kind of thing, it is an automation with `action_id: "ai_behavior"`. But
12
- * the *snapshot* payload's row for one is a stub: `behavior_uuid` plus a couple
13
- * of field-id lists, no `instruction`, no `actionsAttributes` — the actual
14
- * configuration only exists behind this query. So this is the one source for a
15
- * behavior's real content; the snapshot's copy of it is never used.
16
- *
17
- * The query aliases every field to the snake_case name the rest of this tool
18
- * already uses on disk (matching `automations/*.json`), so nothing downstream
19
- * has to know these rows came from a different endpoint.
20
- */
21
-
22
- export type AgentRow = Row & {
23
- uuid: string;
24
- name: string;
25
- /** Full automation-shaped rows — same content `workspace/agents.ts` files under `behaviors/`. */
26
- behaviors: Row[];
27
- };
28
-
29
- export type AgentsRead = {
30
- agents: AgentRow[];
31
- /** `complete` means this pull read them; `unknown` means it could not. */
32
- status: 'complete' | 'unknown';
33
- note?: string;
34
- };
35
-
36
- type Edge = { node: Row & { behaviors?: Row[] | null } };
37
- type Page = { edges: Edge[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } };
38
-
39
- const QUERY = `
40
- query($repoUuid: ID!, $after: String) {
41
- aiAgents(repoUuid: $repoUuid, first: 50, after: $after) {
42
- edges {
43
- node {
44
- uuid
45
- name
46
- instruction
47
- repoUuid
48
- disabledAt
49
- needReview
50
- dataSourceIds
51
- createdAt
52
- updatedAt
53
- behaviors {
54
- id
55
- uuid
56
- name
57
- active
58
- event_id
59
- action_id
60
- event_params {
61
- to_phase_id
62
- from_phase_id: fromPhaseId
63
- in_phase_id: inPhaseId
64
- trigger_field_ids: triggerFieldIds
65
- kind_of_sla: kindOfSla
66
- trigger_automation_id: triggerAutomationId
67
- }
68
- condition {
69
- id
70
- expressions_structure
71
- expressions { id field_address operation value structure_id }
72
- }
73
- action_params {
74
- ai_behavior_params: aiBehaviorParams {
75
- data_source_ids: dataSourceIds
76
- instruction
77
- referenced_field_ids: referencedFieldIds
78
- provider_id: providerId
79
- system_provider_id: systemProviderId
80
- actions_attributes: actionsAttributes {
81
- id
82
- name
83
- action_type: actionType
84
- reference_id: referenceId
85
- metadata {
86
- destination_phase_id: destinationPhaseId
87
- pipe_id: pipeId
88
- table_id: tableId
89
- email_template_id: emailTemplateId
90
- allow_template_modifications: allowTemplateModifications
91
- mcp_server_id: mcpServerId
92
- tool_name: toolName
93
- title
94
- emails
95
- tool_inputs: toolInputs { name source value field_id: fieldId }
96
- fields_attributes: fieldsAttributes { field_id: fieldId input_mode: inputMode value }
97
- }
98
- }
99
- capabilities_attributes: capabilitiesAttributes { capability_type: capabilityType enabled }
100
- }
101
- }
102
- }
103
- }
104
- }
105
- pageInfo { hasNextPage endCursor }
106
- }
107
- }
108
- `;
109
-
110
- /**
111
- * `event_params` always comes back with every key this query asked for, most
112
- * of them null for any given event — `card_created` genuinely accepts none of
113
- * them. An ordinary automation's `event_params` only ever has the keys its
114
- * own event actually uses, and `pipe validate` checks a present key against
115
- * the event's catalogue regardless of its value, so a null one reads as one
116
- * more thing this event does not accept. Dropped to match.
117
- */
118
- const pruneNulls = (o: Row): Row => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== null && v !== undefined));
119
-
120
- /** A behavior as the query returns it, given the shape `tree.automations` (AutomationNode) needs. */
121
- const normaliseBehavior = (b: Row): Row => ({
122
- ...b,
123
- event_params: pruneNulls((b['event_params'] as Row) ?? {}),
124
- condition: b['condition'] ?? null,
125
- field_maps: [],
126
- distribute_assignments: [],
127
- response_schemas: [],
128
- });
129
-
130
- export const fetchAgents = async (client: PipefyClient, repoUuid: string): Promise<AgentsRead> => {
131
- const queryFields = await client.typeFields('Query');
132
- if (!queryFields?.has('aiAgents')) {
133
- debug('no aiAgents query on this schema');
134
- return { agents: [], status: 'unknown', note: 'agents: no aiAgents query on this endpoint — AI agents cannot be read or checked' };
135
- }
136
-
137
- const agents: AgentRow[] = [];
138
- let after: string | null = null;
139
- for (let page = 0; page < 50; page++) {
140
- const data: { aiAgents: Page | null } | null = await client.tryRaw(QUERY, { repoUuid, after }, 'ai agents');
141
- const result: Page | null | undefined = data?.aiAgents;
142
- if (!result) {
143
- return {
144
- agents: [],
145
- status: 'unknown',
146
- note: 'agents: aiAgents exists but did not answer — AI agent configuration is unverified',
147
- };
148
- }
149
-
150
- for (const { node } of result.edges) {
151
- const { behaviors, ...rest } = node;
152
- agents.push({
153
- ...rest,
154
- uuid: String(rest['uuid']),
155
- name: String(rest['name'] ?? ''),
156
- behaviors: (behaviors ?? []).map(normaliseBehavior),
157
- });
158
- }
159
-
160
- if (!result.pageInfo.hasNextPage) break;
161
- after = result.pageInfo.endCursor;
162
- }
163
-
164
- return { agents, status: 'complete' };
165
- };
@@ -1,219 +0,0 @@
1
- import { debug } from '../util/log.ts';
2
- import type { PipefyClient } from './client.ts';
3
-
4
- /**
5
- * The automation event and action catalogue, read from the **public** API.
6
- *
7
- * Pipefy publishes exactly what each event and each action accepts —
8
- * `automationEvents.acceptedParameters`, `automationActions.acceptedParameters` —
9
- * and which combinations are refused. Without reading it, a wrong parameter name
10
- * is only discovered by sending the mutation, and the answer is the single word
11
- * `is invalid`, from both the public and the internal endpoint. Nothing names the
12
- * parameter, the event, or the action.
13
- *
14
- * The case that motivated this: an automation meaning "when a card lands in
15
- * New Request, move it to Archived" was written as
16
- *
17
- * event_id: 'card_moved', event_params: { inPhaseId: '<phaseId>' }
18
- *
19
- * `inPhaseId` is real — it belongs to `card_inbox_received_email`. `card_moved`
20
- * accepts only `to_phase_id`. Both endpoints answered `is invalid` and the apply
21
- * failed at step one with nothing to act on.
22
- */
23
-
24
- export type AutomationEventSpec = {
25
- id: string;
26
- /** Parameter names this event accepts, in the API's own snake_case. */
27
- acceptedParameters: string[];
28
- /** Actions this event cannot be paired with. */
29
- actionsBlacklist: string[];
30
- };
31
-
32
- export type AutomationActionSpec = {
33
- id: string;
34
- acceptedParameters: string[];
35
- eventsBlacklist: string[];
36
- };
37
-
38
- export type AutomationCatalogue = {
39
- events: Map<string, AutomationEventSpec>;
40
- actions: Map<string, AutomationActionSpec>;
41
- };
42
-
43
- const EVENTS_QUERY = `query AutomationEvents {
44
- automationEvents { id acceptedParameters actionsBlacklist }
45
- }`;
46
-
47
- const ACTIONS_QUERY = `query AutomationActions($repoId: ID!) {
48
- automationActions(repoId: $repoId) { id acceptedParameters eventsBlacklist }
49
- }`;
50
-
51
- /**
52
- * Read the catalogue for one pipe. Actions are per-repo (a pipe without AI
53
- * enabled offers fewer), events are global.
54
- */
55
- export const readAutomationCatalogue = async (
56
- client: PipefyClient,
57
- repoId: number | string,
58
- ): Promise<AutomationCatalogue> => {
59
- const [ev, ac] = await Promise.all([
60
- client.raw<{ automationEvents: AutomationEventSpec[] }>(EVENTS_QUERY),
61
- client.raw<{ automationActions: AutomationActionSpec[] }>(ACTIONS_QUERY, { repoId: String(repoId) }),
62
- ]);
63
-
64
- const events = new Map<string, AutomationEventSpec>();
65
- for (const e of ev?.automationEvents ?? []) {
66
- events.set(e.id, {
67
- id: e.id,
68
- acceptedParameters: e.acceptedParameters ?? [],
69
- actionsBlacklist: e.actionsBlacklist ?? [],
70
- });
71
- }
72
- const actions = new Map<string, AutomationActionSpec>();
73
- for (const a of ac?.automationActions ?? []) {
74
- actions.set(a.id, {
75
- id: a.id,
76
- acceptedParameters: a.acceptedParameters ?? [],
77
- eventsBlacklist: a.eventsBlacklist ?? [],
78
- });
79
- }
80
- debug(`automation catalogue: ${events.size} events, ${actions.size} actions for repo ${repoId}`);
81
- return { events, actions };
82
- };
83
-
84
- /**
85
- * Parameter names the payload spells differently from the API.
86
- *
87
- * The stored row and the mutation input disagree in both directions, so a check
88
- * against `acceptedParameters` has to normalise first or it reports correct
89
- * automations as broken.
90
- */
91
- const PARAM_ALIASES: Record<string, string> = {
92
- // event params
93
- inPhaseId: 'in_phase_id',
94
- fromPhaseId: 'from_phase_id',
95
- toPhaseId: 'to_phase_id',
96
- triggerFieldIds: 'trigger_field_ids',
97
- triggerAutomationId: 'trigger_automation_id',
98
- kindOfSla: 'kind_of_sla',
99
- // action params
100
- fieldsMapOrder: 'fields_map_order',
101
- cardId: 'card_id',
102
- toPhaseIdAction: 'to_phase_id',
103
- taskParams: 'task_params',
104
- aiParams: 'ai_params',
105
- aiBehaviorParams: 'ai_behavior_params',
106
- slaParams: 'sla_params',
107
- httpMethod: 'http_method',
108
- emailTemplateId: 'email_template_id',
109
- authenticationKey: 'authentication_key',
110
- authenticationValue: 'authentication_value',
111
- authenticationAddTo: 'authentication_add_to',
112
- authenticationType: 'authentication_type',
113
- oauth2Uuid: 'oauth2_uuid',
114
- };
115
-
116
- const canonical = (name: string) => PARAM_ALIASES[name] ?? name;
117
-
118
- /**
119
- * Params the API accepts on every action regardless of the catalogue.
120
- *
121
- * `field_map` carries the values an `update_card_field` or `create_card` writes
122
- * and is not listed in `acceptedParameters` — the catalogue lists
123
- * `fields_map_order` beside it and stops. Flagging `field_map` would refuse every
124
- * automation that writes a field, which is most of them.
125
- */
126
- const ALWAYS_ALLOWED_ACTION_PARAMS = new Set(['field_map', 'schema']);
127
-
128
- export type ParamFinding = {
129
- severity: 'error' | 'warning';
130
- where: 'event' | 'action' | 'pairing';
131
- message: string;
132
- fix?: string;
133
- };
134
-
135
- /**
136
- * Check one automation against the catalogue, before anything is sent.
137
- *
138
- * Returns findings rather than throwing, so the compiler can refuse the step and
139
- * name the parameter — which is the whole point. An unknown event or action is an
140
- * error; a catalogue this pipe does not offer is silence, not a guess.
141
- */
142
- export const checkAutomationParams = (
143
- auto: Record<string, unknown>,
144
- catalogue: AutomationCatalogue,
145
- ): ParamFinding[] => {
146
- const findings: ParamFinding[] = [];
147
- const eventId = String(auto['event_id'] ?? '');
148
- const actionId = String(auto['action_id'] ?? '');
149
-
150
- const event = catalogue.events.get(eventId);
151
- const action = catalogue.actions.get(actionId);
152
-
153
- if (eventId && !event) {
154
- findings.push({
155
- severity: 'error',
156
- where: 'event',
157
- message: `event_id ${JSON.stringify(eventId)} is not an event this endpoint offers`,
158
- fix: `available: ${[...catalogue.events.keys()].join(', ')}`,
159
- });
160
- }
161
- if (actionId && !action) {
162
- findings.push({
163
- severity: 'error',
164
- where: 'action',
165
- message: `action_id ${JSON.stringify(actionId)} is not an action this pipe offers`,
166
- fix: `available: ${[...catalogue.actions.keys()].join(', ')}`,
167
- });
168
- }
169
-
170
- if (event && action) {
171
- if (event.actionsBlacklist.includes(actionId)) {
172
- findings.push({
173
- severity: 'error',
174
- where: 'pairing',
175
- message: `event ${eventId} cannot be paired with action ${actionId}`,
176
- });
177
- } else if (action.eventsBlacklist.includes(eventId)) {
178
- findings.push({
179
- severity: 'error',
180
- where: 'pairing',
181
- message: `action ${actionId} cannot be triggered by event ${eventId}`,
182
- });
183
- }
184
- }
185
-
186
- if (event) {
187
- const accepted = new Set(event.acceptedParameters.map(canonical));
188
- for (const key of Object.keys((auto['event_params'] ?? {}) as Record<string, unknown>)) {
189
- const name = canonical(key);
190
- if (accepted.has(name)) continue;
191
- findings.push({
192
- severity: 'error',
193
- where: 'event',
194
- message: `event ${eventId} does not accept event_params.${key}`,
195
- fix: accepted.size
196
- ? `it accepts: ${[...accepted].join(', ')}`
197
- : `it accepts no parameters — remove event_params.${key}`,
198
- });
199
- }
200
- }
201
-
202
- if (action) {
203
- const accepted = new Set(action.acceptedParameters.map(canonical));
204
- for (const key of Object.keys((auto['action_params'] ?? {}) as Record<string, unknown>)) {
205
- const name = canonical(key);
206
- if (accepted.has(name) || ALWAYS_ALLOWED_ACTION_PARAMS.has(name)) continue;
207
- findings.push({
208
- severity: 'error',
209
- where: 'action',
210
- message: `action ${actionId} does not accept action_params.${key}`,
211
- fix: accepted.size
212
- ? `it accepts: ${[...accepted].join(', ')}`
213
- : `it accepts no parameters — remove action_params.${key}`,
214
- });
215
- }
216
- }
217
-
218
- return findings;
219
- };
@@ -1,119 +0,0 @@
1
- import { debug } from '../util/log.ts';
2
- import type { PipefyClient } from './client.ts';
3
- import type { InternalApi } from './internal.ts';
4
-
5
- /**
6
- * Capability detection. PLAN.md §1.2: `restoreRepoSnapshot` and
7
- * `createRepoDraft` carry the entire safety model but do not exist on every
8
- * endpoint, so the app detects them at startup and degrades explicitly rather
9
- * than failing mid-apply.
10
- */
11
-
12
- export type Capabilities = {
13
- /** Read paths. */
14
- snapshots: boolean;
15
- /** Safety primitives. */
16
- restoreRepoSnapshot: boolean;
17
- /** Which spelling this endpoint has, so the caller does not guess. */
18
- restoreMutation: 'restoreRepoToSnapshot' | 'restoreRepoSnapshot' | null;
19
- /** Whether a restore is limited to the newest snapshot. Measured, not assumed. */
20
- restoreLatestOnly: boolean;
21
- createRepoDraft: boolean;
22
- /** Route B, tracked so `pipe doctor` reports the day it lands. */
23
- importRepoSnapshot: boolean;
24
- /** Public write paths this build depends on. */
25
- mutations: Record<string, boolean>;
26
- /** The internal endpoint — phase jumps and automation details. */
27
- internalApi: 'ok' | 'unavailable' | 'unknown';
28
- checkedAt: string;
29
- };
30
-
31
- /** Mutations the plan compiler emits, checked as a set so gaps are visible up front. */
32
- export const REQUIRED_MUTATIONS = [
33
- 'updatePipe',
34
- 'createPhase',
35
- 'updatePhase',
36
- 'deletePhase',
37
- 'createPhaseField',
38
- 'updatePhaseField',
39
- 'deletePhaseField',
40
- 'createLabel',
41
- 'updateLabel',
42
- 'deleteLabel',
43
- 'createWebhook',
44
- 'updateWebhook',
45
- 'deleteWebhook',
46
- 'createFieldCondition',
47
- 'updateFieldCondition',
48
- 'deleteFieldCondition',
49
- 'setFieldConditionOrder',
50
- 'createAutomation',
51
- 'updateAutomation',
52
- 'deleteAutomation',
53
- 'createPipeRelation',
54
- 'updatePipeRelation',
55
- 'deletePipeRelation',
56
- 'phaseSettings',
57
- 'updateRepoPreferences',
58
- 'archiveField',
59
- 'unarchiveField',
60
- 'moveCardToPhase',
61
- ] as const;
62
-
63
- export const detect = async (client: PipefyClient, internal?: InternalApi): Promise<Capabilities> => {
64
- const mutationFields = await client.typeFields('Mutation');
65
- const pipeFields = await client.typeFields('Pipe');
66
- const has = (n: string) => Boolean(mutationFields?.has(n));
67
-
68
- const mutations: Record<string, boolean> = {};
69
- for (const m of REQUIRED_MUTATIONS) mutations[m] = has(m);
70
-
71
- const caps: Capabilities = {
72
- snapshots: has('createRepoSnapshot') && Boolean(pipeFields?.has('snapshots') || pipeFields?.has('snapshot')),
73
- /**
74
- * Both spellings, because this build was looking for the wrong one.
75
- *
76
- * `restoreRepoSnapshot` does not exist on this endpoint and never did.
77
- * **`restoreRepoToSnapshot`** does — "Restores a Pipe structure to its latest
78
- * uploaded snapshot" — so every apply has printed "no one-command rollback"
79
- * beside a snapshot that could in fact be restored, and `pipe rollback` has
80
- * refused to try. That is the same failure as automations sitting on the
81
- * internal endpoint: a name assumed once and never re-checked.
82
- *
83
- * The capability is still narrower than it sounds, and `restoreLatestOnly`
84
- * below is what says so.
85
- */
86
- restoreRepoSnapshot: has('restoreRepoSnapshot') || has('restoreRepoToSnapshot'),
87
- restoreMutation: has('restoreRepoToSnapshot')
88
- ? 'restoreRepoToSnapshot'
89
- : has('restoreRepoSnapshot')
90
- ? 'restoreRepoSnapshot'
91
- : null,
92
- /**
93
- * A restore only accepts the **latest** uploaded snapshot. Measured: an
94
- * older versionId is refused with "Version_id must reference the latest
95
- * uploaded snapshot of the Pipe".
96
- *
97
- * That is the whole story for rollback on this endpoint, and it is why the
98
- * pre-apply snapshot is usually not restorable: `pipe apply` reads the repo
99
- * back to verify, and that read takes a snapshot of its own, which becomes
100
- * the latest. Recovery stays forward-first.
101
- */
102
- restoreLatestOnly: has('restoreRepoToSnapshot'),
103
- createRepoDraft: has('createRepoDraft'),
104
- importRepoSnapshot: has('importRepoSnapshot') || has('createRepoSnapshotFromPayload'),
105
- mutations,
106
- internalApi: internal?.status ?? 'unknown',
107
- checkedAt: new Date().toISOString(),
108
- };
109
- debug(
110
- `capabilities: snapshots=${caps.snapshots} restore=${caps.restoreRepoSnapshot} ` +
111
- `draft=${caps.createRepoDraft} import=${caps.importRepoSnapshot}`,
112
- );
113
- return caps;
114
- };
115
-
116
- export const missingMutations = (c: Capabilities): string[] =>
117
- Object.entries(c.mutations)
118
- .filter(([, present]) => !present)
119
- .map(([name]) => name);