@gakim-digital/dexter-bridge 0.12.1 → 0.13.0

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.
@@ -0,0 +1,245 @@
1
+ // Shared task-plan and post-mutation evidence gate for design harnesses.
2
+ // Framer keeps its interaction checks and calls the decision helpers.
3
+ // Webflow uses the ledger; whether to look at a screenshot is the model's call.
4
+
5
+ export function missingEvidenceChecks({
6
+ mutationCalls,
7
+ hasPlan,
8
+ requiredChecks = [],
9
+ unavailable = [],
10
+ completed,
11
+ }) {
12
+ if (mutationCalls === 0) return [];
13
+ const missing = [];
14
+ if (!hasPlan) missing.push('task-plan');
15
+ for (const check of requiredChecks) {
16
+ if (unavailable.includes(check)) continue;
17
+ if (!completed(check)) missing.push(check);
18
+ }
19
+ return missing;
20
+ }
21
+
22
+ export function evidenceStillDue({
23
+ hasPlan,
24
+ mutationCalls,
25
+ reading,
26
+ missing = [],
27
+ }) {
28
+ if (!(hasPlan && mutationCalls > 0 && reading)) return [];
29
+ return missing;
30
+ }
31
+
32
+ function uniqueStrings(values) {
33
+ return [...new Set(
34
+ (Array.isArray(values) ? values : [])
35
+ .filter((value) => typeof value === 'string')
36
+ .map((value) => value.trim())
37
+ .filter(Boolean),
38
+ )];
39
+ }
40
+
41
+ function toolError(code, message) {
42
+ return Object.assign(new Error(message), { code, statusCode: 400 });
43
+ }
44
+
45
+ export function createEvidenceLedger({
46
+ checks,
47
+ domains,
48
+ labels = {},
49
+ messages = {},
50
+ } = {}) {
51
+ const allowedChecks = new Set(checks || []);
52
+ const allowedDomains = new Set(domains || []);
53
+ let operationSequence = 0;
54
+ let lastMutationSequence = 0;
55
+ let lastInspectionSequence = 0;
56
+ let mutationCalls = 0;
57
+ let taskPlan = null;
58
+ const verificationEvidence = new Map();
59
+ let lastMutationAt = null;
60
+ let lastInspectionAt = null;
61
+ const toolCalls = [];
62
+
63
+ const synchronized = () =>
64
+ mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
65
+ const requiredVerification = () => taskPlan?.verification || [];
66
+ const requiredChecks = () => [...new Set(['structural', ...requiredVerification()])];
67
+ const completed = (check) => {
68
+ if (mutationCalls === 0) return true;
69
+ if (check === 'structural') return synchronized();
70
+ return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
71
+ };
72
+ const missing = () => missingEvidenceChecks({
73
+ mutationCalls,
74
+ hasPlan: Boolean(taskPlan),
75
+ requiredChecks: requiredChecks(),
76
+ completed,
77
+ });
78
+
79
+ function markMutation() {
80
+ mutationCalls += 1;
81
+ operationSequence += 1;
82
+ lastMutationSequence = operationSequence;
83
+ lastMutationAt = new Date().toISOString();
84
+ return operationSequence;
85
+ }
86
+
87
+ function markInspection({ verifies = [] } = {}) {
88
+ operationSequence += 1;
89
+ lastInspectionSequence = operationSequence;
90
+ lastInspectionAt = new Date().toISOString();
91
+ verificationEvidence.set('structural', operationSequence);
92
+ for (const check of uniqueStrings(verifies)) {
93
+ if (allowedChecks.has(check) && check !== 'structural') {
94
+ verificationEvidence.set(check, operationSequence);
95
+ }
96
+ }
97
+ return operationSequence;
98
+ }
99
+
100
+ function requireInspection() {
101
+ if (lastInspectionSequence === 0) {
102
+ throw toolError(
103
+ 'WEBFLOW_HARNESS_INSPECTION_REQUIRED',
104
+ messages.inspectionRequired
105
+ || 'Inspect the live Webflow site before declaring the task and verification plan.',
106
+ );
107
+ }
108
+ }
109
+
110
+ function requirePlan() {
111
+ if (!taskPlan) {
112
+ throw toolError(
113
+ 'WEBFLOW_HARNESS_PLAN_REQUIRED',
114
+ messages.planRequired
115
+ || 'Inspect the live Webflow site, then call webflow_plan_task before making a change.',
116
+ );
117
+ }
118
+ }
119
+
120
+ function declarePlan(raw = {}) {
121
+ requireInspection();
122
+ const summary = String(raw.summary || '').trim().slice(0, 1000);
123
+ const declaredDomains = uniqueStrings(raw.domains).filter((domain) => allowedDomains.has(domain));
124
+ const declaredChecks = uniqueStrings(raw.verification).filter((check) => allowedChecks.has(check));
125
+ if (summary.length < 10 || declaredDomains.length === 0) {
126
+ throw toolError(
127
+ 'WEBFLOW_HARNESS_PLAN_INVALID',
128
+ messages.planInvalid
129
+ || 'Describe the understood task and declare at least one task domain.',
130
+ );
131
+ }
132
+ taskPlan = {
133
+ summary,
134
+ domains: uniqueStrings([...(taskPlan?.domains || []), ...declaredDomains]),
135
+ verification: uniqueStrings(['structural', ...(taskPlan?.verification || []), ...declaredChecks]),
136
+ };
137
+ return {
138
+ plan: {
139
+ summary: taskPlan.summary,
140
+ domains: [...taskPlan.domains],
141
+ verification: [...taskPlan.verification],
142
+ },
143
+ beforeFinishing: `After your final change, and before returning, produce: ${requiredChecks().map((check) => labels[check] || check).join('; ')}.`,
144
+ };
145
+ }
146
+
147
+ function noteCall(call) {
148
+ toolCalls.push(call);
149
+ }
150
+
151
+ function evidenceForRead(reading) {
152
+ return evidenceStillDue({
153
+ hasPlan: Boolean(taskPlan),
154
+ mutationCalls,
155
+ reading,
156
+ missing: missing(),
157
+ });
158
+ }
159
+
160
+ function verification() {
161
+ return {
162
+ ownedBy: 'harness',
163
+ required: mutationCalls > 0,
164
+ completed: missing().length === 0,
165
+ structuralCompleted: synchronized(),
166
+ taskPlan: taskPlan
167
+ ? {
168
+ summary: taskPlan.summary,
169
+ domains: [...taskPlan.domains],
170
+ verification: [...taskPlan.verification],
171
+ }
172
+ : null,
173
+ requiredChecks: requiredChecks(),
174
+ completedChecks: requiredChecks().filter(completed),
175
+ missingChecks: missing(),
176
+ lastMutationAt,
177
+ lastInspectionAt,
178
+ };
179
+ }
180
+
181
+ function finalize(completion) {
182
+ if (completion?.status !== 'completed' || mutationCalls === 0) return completion;
183
+ if (verification().completed) return completion;
184
+ const described = missing().map((check) => labels[check] || `post-mutation ${check} verification`);
185
+ const reason = `The harness ended before completing ${described.join(' and ')}.`;
186
+ return {
187
+ ...completion,
188
+ status: 'failed',
189
+ summary: String(completion?.summary || '').trim() || 'The requested changes were applied.',
190
+ checks: [
191
+ ...(Array.isArray(completion?.checks) ? completion.checks : []),
192
+ {
193
+ command: 'Harness-owned final verification',
194
+ status: 'failed',
195
+ output: reason,
196
+ },
197
+ ],
198
+ blockedReason: null,
199
+ };
200
+ }
201
+
202
+ function repairPrompt() {
203
+ const current = verification();
204
+ if (current.completed || mutationCalls === 0) return null;
205
+ const steps = (current.missingChecks || []).map((check) => labels[check] || check);
206
+ return [
207
+ messages.repairLead || 'Continue the same Webflow task from its current site state.',
208
+ 'Do not redo successful work.',
209
+ `Before returning, ${steps.join(' and ')}.`,
210
+ 'Then return the required JSON outcome.',
211
+ ].join(' ');
212
+ }
213
+
214
+ function unblockedFailurePrompt(completion, lastGateFailed) {
215
+ if (completion?.status !== 'failed' || mutationCalls > 0 || lastGateFailed) return null;
216
+ return [
217
+ 'You returned failed without changing the Webflow site, and no tool refused the work.',
218
+ 'Nothing in this run is blocking you and there is no time limit.',
219
+ 'Continue the same task from the current site: plan it, apply the change, verify it, then return the required JSON outcome.',
220
+ 'Return failed or blocked only for a concrete blocker you can name.',
221
+ ].join(' ');
222
+ }
223
+
224
+ return {
225
+ markMutation,
226
+ markInspection,
227
+ requirePlan,
228
+ declarePlan,
229
+ noteCall,
230
+ evidenceForRead,
231
+ verification,
232
+ finalize,
233
+ repairPrompt,
234
+ unblockedFailurePrompt,
235
+ mutationCount: () => mutationCalls,
236
+ plan: () => (taskPlan
237
+ ? {
238
+ summary: taskPlan.summary,
239
+ domains: [...taskPlan.domains],
240
+ verification: [...taskPlan.verification],
241
+ }
242
+ : null),
243
+ calls: () => toolCalls.map((call) => ({ ...call })),
244
+ };
245
+ }