@ggui-ai/negotiator 0.2.0-alpha.3 → 0.2.0-alpha.4

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 (43) hide show
  1. package/dist/ensure-conforming-contract.d.ts +70 -0
  2. package/dist/ensure-conforming-contract.d.ts.map +1 -0
  3. package/dist/ensure-conforming-contract.js +115 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/normalize-draft.d.ts +33 -0
  8. package/dist/normalize-draft.d.ts.map +1 -0
  9. package/dist/normalize-draft.js +143 -0
  10. package/dist/preserve-seed-surfaces.d.ts +40 -0
  11. package/dist/preserve-seed-surfaces.d.ts.map +1 -0
  12. package/dist/preserve-seed-surfaces.js +57 -0
  13. package/dist/synth-bench/cli-llm.d.ts +20 -0
  14. package/dist/synth-bench/cli-llm.d.ts.map +1 -0
  15. package/dist/synth-bench/cli-llm.js +97 -0
  16. package/dist/synth-bench/corpus.d.ts +52 -0
  17. package/dist/synth-bench/corpus.d.ts.map +1 -1
  18. package/dist/synth-bench/corpus.js +306 -5
  19. package/dist/synth-bench/round-trip-score.d.ts +87 -0
  20. package/dist/synth-bench/round-trip-score.d.ts.map +1 -0
  21. package/dist/synth-bench/round-trip-score.js +105 -0
  22. package/dist/synth-bench/run-bench-cli.js +6 -82
  23. package/dist/synth-bench/run-repair-bench-cli.d.ts +3 -0
  24. package/dist/synth-bench/run-repair-bench-cli.d.ts.map +1 -0
  25. package/dist/synth-bench/run-repair-bench-cli.js +86 -0
  26. package/dist/synth-bench/run-repair-bench.d.ts +94 -0
  27. package/dist/synth-bench/run-repair-bench.d.ts.map +1 -0
  28. package/dist/synth-bench/run-repair-bench.js +172 -0
  29. package/dist/synthesize-contract.d.ts +38 -6
  30. package/dist/synthesize-contract.d.ts.map +1 -1
  31. package/dist/synthesize-contract.js +246 -32
  32. package/package.json +5 -4
  33. package/src/ensure-conforming-contract.ts +175 -0
  34. package/src/index.ts +2 -0
  35. package/src/normalize-draft.ts +156 -0
  36. package/src/preserve-seed-surfaces.ts +61 -0
  37. package/src/synth-bench/cli-llm.ts +140 -0
  38. package/src/synth-bench/corpus.ts +335 -5
  39. package/src/synth-bench/round-trip-score.ts +169 -0
  40. package/src/synth-bench/run-bench-cli.ts +13 -115
  41. package/src/synth-bench/run-repair-bench-cli.ts +119 -0
  42. package/src/synth-bench/run-repair-bench.ts +266 -0
  43. package/src/synthesize-contract.ts +299 -37
@@ -19,10 +19,32 @@
19
19
  * - structure-bench.test.ts (deterministic validator-only check)
20
20
  */
21
21
  import type { GadgetDescriptor } from '@ggui-ai/protocol';
22
+ import type { RoundTripExpectation } from './round-trip-score.js';
22
23
  export interface BenchEntry {
23
24
  readonly id: string;
24
25
  readonly intent: string;
25
26
  readonly expected: BenchExpectation;
27
+ /**
28
+ * Repair-path seed. When set, the entry exercises the FORGIVING
29
+ * handshake flow rather than synthesize-from-intent: the runner feeds
30
+ * this (possibly malformed / suboptimal) agent draft to
31
+ * `ensureConformingContract`, which lints it → returns verbatim if
32
+ * clean (origin `agent`) or repairs-in-place if not (origin `synth`).
33
+ * Mirrors what a real agent submits on `ggui_handshake.blueprintDraft`.
34
+ * Typed `unknown` because the draft is untrusted — it may not be a
35
+ * valid `DataContract` (that is the whole point of repairing it).
36
+ *
37
+ * Entries WITHOUT a draft run the classic synthesize-from-intent path.
38
+ */
39
+ readonly draft?: unknown;
40
+ /**
41
+ * Round-trip quality expectation — what the agent intends to do with
42
+ * the AGREED contract next turn (seed props on `ggui_render`, consume
43
+ * gestures). The round-trip scorer asserts the produced contract
44
+ * actually supports it. Only meaningful alongside a `draft` (the
45
+ * repair corpus); leave absent for shape-only entries.
46
+ */
47
+ readonly roundTrip?: RoundTripExpectation;
26
48
  /**
27
49
  * Per-entry registered gadget catalog. When set, the runner
28
50
  * forwards this to `synthesizeContract`'s `appGadgets` option so
@@ -103,4 +125,34 @@ export interface BenchExpectation {
103
125
  */
104
126
  export declare function contractShape(expected: BenchExpectation): string;
105
127
  export declare const BENCH_CORPUS: readonly BenchEntry[];
128
+ /**
129
+ * Repair-path corpus — the round-trip QUALITY bench.
130
+ *
131
+ * Where {@link BENCH_CORPUS} asks "does synth emit the right SHAPE from
132
+ * an intent?", this corpus asks the contract-quality question: "when an
133
+ * agent submits a draft to `ggui_handshake`, does the negotiator's
134
+ * repair produce a contract that is round-trip-USABLE?" Each entry
135
+ * carries a real-shaped agent `draft` (run through the production
136
+ * `ensureConformingContract` create-path) plus a `roundTrip`
137
+ * expectation describing what the agent does next turn (seed props,
138
+ * consume gestures). The new round-trip scorer mirrors the render +
139
+ * consume wire gates, so a "valid-but-broken" repair fails here even
140
+ * though `lintContract` and the shape scorer pass it.
141
+ *
142
+ * The four entries map a 2×2 of (repair-fired?) × (round-trip-survives?):
143
+ * - `repair-todo-seed` — repair FIRES and BREAKS round-trip (RED today;
144
+ * the contract-quality falsifier — synth reshapes
145
+ * the seedable `todos` collection propsSpec →
146
+ * contextSpec, deleting the only agent seed channel).
147
+ * - `repair-todo-clean` — no repair needed (origin `agent`, fast-path);
148
+ * round-trip survives. Pins the no-false-positive case.
149
+ * - `repair-form-nextstep` — repair FIRES (dangling nextStep) and PRESERVES the
150
+ * action round-trip. A "good repair" of a non-seed error.
151
+ * - `repair-weather-seed` — repair FIRES (stray wrapper key) and PRESERVES the
152
+ * props round-trip. A "good repair" that keeps propsSpec.
153
+ *
154
+ * Used by run-repair-bench(-cli).ts (live LLM probe) and
155
+ * round-trip-score.test.ts (deterministic scorer pinning).
156
+ */
157
+ export declare const REPAIR_CORPUS: readonly BenchEntry[];
106
158
  //# sourceMappingURL=corpus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../src/synth-bench/corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;+DAG2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;;2DAIuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C;;;0EAGsE;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C;;;;;;;;;OASG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtD;;;;;sEAKkE;IAClE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C;;qDAEiD;IACjD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAOhE;AAoDD,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EA+7B7C,CAAC"}
1
+ {"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../src/synth-bench/corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC;IAC1C;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;+DAG2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;;2DAIuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C;;;0EAGsE;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C;;;;;;;;;OASG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtD;;;;;sEAKkE;IAClE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C;;qDAEiD;IACjD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAOhE;AAoDD,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAs8B7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EA8Q9C,CAAC"}
@@ -325,12 +325,19 @@ export const BENCH_CORPUS = [
325
325
  intent: 'an agent-backed todo list that persists across sessions',
326
326
  expected: {
327
327
  hasActionSpec: true,
328
- hasContextSpec: true,
328
+ hasContextSpec: false,
329
329
  hasStreamSpec: false,
330
- hasProps: false,
331
- actionNames: ['addTodo', 'deleteTodo', 'add', 'delete', 'create', 'remove'],
332
- contextSlots: ['todos', 'items'],
333
- notes: 'agent-backed = explicit persistence = each add/delete IS a discrete event the agent must witness.',
330
+ hasProps: true,
331
+ actionNames: [
332
+ 'addTodo',
333
+ 'deleteTodo',
334
+ 'toggle',
335
+ 'add',
336
+ 'delete',
337
+ 'create',
338
+ 'remove',
339
+ ],
340
+ notes: 'agent-backed/persisted = the agent OWNS the items → todos seed on propsSpec (refreshed via ggui_update); add/delete/toggle are discrete events on actionSpec. contextSpec has no agent-push channel, so an agent-owned persisted list there cannot round-trip — this is the round-trip-correct shape, aligned with list-message-thread / list-file-browser (both props-bearing agent-supplied collections).',
334
341
  },
335
342
  },
336
343
  {
@@ -992,3 +999,297 @@ export const BENCH_CORPUS = [
992
999
  appGadgets: [CHART_DESCRIPTOR],
993
1000
  },
994
1001
  ];
1002
+ /**
1003
+ * Repair-path corpus — the round-trip QUALITY bench.
1004
+ *
1005
+ * Where {@link BENCH_CORPUS} asks "does synth emit the right SHAPE from
1006
+ * an intent?", this corpus asks the contract-quality question: "when an
1007
+ * agent submits a draft to `ggui_handshake`, does the negotiator's
1008
+ * repair produce a contract that is round-trip-USABLE?" Each entry
1009
+ * carries a real-shaped agent `draft` (run through the production
1010
+ * `ensureConformingContract` create-path) plus a `roundTrip`
1011
+ * expectation describing what the agent does next turn (seed props,
1012
+ * consume gestures). The new round-trip scorer mirrors the render +
1013
+ * consume wire gates, so a "valid-but-broken" repair fails here even
1014
+ * though `lintContract` and the shape scorer pass it.
1015
+ *
1016
+ * The four entries map a 2×2 of (repair-fired?) × (round-trip-survives?):
1017
+ * - `repair-todo-seed` — repair FIRES and BREAKS round-trip (RED today;
1018
+ * the contract-quality falsifier — synth reshapes
1019
+ * the seedable `todos` collection propsSpec →
1020
+ * contextSpec, deleting the only agent seed channel).
1021
+ * - `repair-todo-clean` — no repair needed (origin `agent`, fast-path);
1022
+ * round-trip survives. Pins the no-false-positive case.
1023
+ * - `repair-form-nextstep` — repair FIRES (dangling nextStep) and PRESERVES the
1024
+ * action round-trip. A "good repair" of a non-seed error.
1025
+ * - `repair-weather-seed` — repair FIRES (stray wrapper key) and PRESERVES the
1026
+ * props round-trip. A "good repair" that keeps propsSpec.
1027
+ *
1028
+ * Used by run-repair-bench(-cli).ts (live LLM probe) and
1029
+ * round-trip-score.test.ts (deterministic scorer pinning).
1030
+ */
1031
+ export const REPAIR_CORPUS = [
1032
+ {
1033
+ // THE falsifier. The agent reaches for a JSON-Schema reflex — a
1034
+ // collection on propsSpec plus a wrapper-level `required` names-array
1035
+ // (illegal: required-ness is a per-prop boolean; the wrapper only
1036
+ // takes `description` + `properties`). That single
1037
+ // CTR_SHAPE_UNRECOGNIZED_KEYS bounces the draft off the fast path
1038
+ // into repair. The MINIMAL correct repair is "drop the stray
1039
+ // `required` key — `todos` is render-time seed data, it stays on
1040
+ // propsSpec." But synth's "a mutable collection is ALWAYS contextSpec"
1041
+ // heuristic instead reshapes propsSpec → contextSpec, producing a
1042
+ // VALID contract with no propsSpec — which the accept-path then drops
1043
+ // the agent's `props: { todos }` against. RED on BOTH the shape scorer
1044
+ // (has-props-mismatch) and the round-trip scorer (props-no-home) until
1045
+ // the negotiator learns seed data stays on propsSpec.
1046
+ id: 'repair-todo-seed',
1047
+ intent: 'show my todos, let me check off completed ones and add new items',
1048
+ draft: {
1049
+ propsSpec: {
1050
+ description: "The user's todo items",
1051
+ // ⚠️ FATAL: `required` is not a legal key on the propsSpec wrapper.
1052
+ required: ['todos'],
1053
+ properties: {
1054
+ todos: {
1055
+ required: true,
1056
+ schema: {
1057
+ type: 'array',
1058
+ items: {
1059
+ type: 'object',
1060
+ properties: {
1061
+ id: { type: 'string' },
1062
+ text: { type: 'string' },
1063
+ completed: { type: 'boolean' },
1064
+ },
1065
+ required: ['id', 'text', 'completed'],
1066
+ },
1067
+ },
1068
+ },
1069
+ },
1070
+ },
1071
+ actionSpec: {
1072
+ toggleTodo: {
1073
+ label: 'Toggle todo',
1074
+ schema: {
1075
+ type: 'object',
1076
+ properties: { id: { type: 'string' } },
1077
+ required: ['id'],
1078
+ },
1079
+ nextStep: 'todo_toggle',
1080
+ },
1081
+ addTodo: {
1082
+ label: 'Add todo',
1083
+ schema: {
1084
+ type: 'object',
1085
+ properties: { text: { type: 'string' } },
1086
+ required: ['text'],
1087
+ },
1088
+ nextStep: 'todo_add',
1089
+ },
1090
+ },
1091
+ agentCapabilities: {
1092
+ tools: {
1093
+ todo_toggle: {
1094
+ description: 'Flip a todo done/undone',
1095
+ inputSchema: {
1096
+ type: 'object',
1097
+ properties: { id: { type: 'string' } },
1098
+ required: ['id'],
1099
+ },
1100
+ },
1101
+ todo_add: {
1102
+ description: 'Append a new todo',
1103
+ inputSchema: {
1104
+ type: 'object',
1105
+ properties: { text: { type: 'string' } },
1106
+ required: ['text'],
1107
+ },
1108
+ },
1109
+ },
1110
+ },
1111
+ },
1112
+ expected: {
1113
+ // The GOOD outcome: todos stays seedable on propsSpec, toggle/add
1114
+ // stay as actions, NO contextSpec reshape.
1115
+ hasActionSpec: true,
1116
+ hasContextSpec: false,
1117
+ hasStreamSpec: false,
1118
+ hasProps: true,
1119
+ actionNames: ['toggle', 'add'],
1120
+ notes: 'contract-quality falsifier: synth currently reshapes the seedable todos collection propsSpec → contextSpec, breaking the agent seed round-trip. Expected GREEN once the negotiator preserves render-time seed data on propsSpec.',
1121
+ },
1122
+ roundTrip: {
1123
+ renderProps: {
1124
+ todos: [{ id: 't1', text: 'Buy milk', completed: false }],
1125
+ },
1126
+ consumableActions: ['toggleTodo', 'addTodo'],
1127
+ },
1128
+ },
1129
+ {
1130
+ // The fast-path / no-false-positive case. Same intent, but the agent
1131
+ // submitted a CLEAN, round-trip-correct draft (todos on propsSpec, no
1132
+ // stray key, resolvable nextStep cross-refs). ensureConformingContract
1133
+ // returns it VERBATIM (origin `agent`, no LLM call). The round-trip
1134
+ // scorer MUST pass — if it flags this, the scorer is over-eager.
1135
+ id: 'repair-todo-clean',
1136
+ intent: 'show my todos, let me check off completed ones and add new items',
1137
+ draft: {
1138
+ propsSpec: {
1139
+ description: "The user's todo items",
1140
+ properties: {
1141
+ todos: {
1142
+ required: true,
1143
+ schema: {
1144
+ type: 'array',
1145
+ items: {
1146
+ type: 'object',
1147
+ properties: {
1148
+ id: { type: 'string' },
1149
+ text: { type: 'string' },
1150
+ completed: { type: 'boolean' },
1151
+ },
1152
+ required: ['id', 'text', 'completed'],
1153
+ },
1154
+ },
1155
+ },
1156
+ },
1157
+ },
1158
+ actionSpec: {
1159
+ toggleTodo: {
1160
+ label: 'Toggle todo',
1161
+ schema: {
1162
+ type: 'object',
1163
+ properties: { id: { type: 'string' } },
1164
+ required: ['id'],
1165
+ },
1166
+ nextStep: 'todo_toggle',
1167
+ },
1168
+ addTodo: {
1169
+ label: 'Add todo',
1170
+ schema: {
1171
+ type: 'object',
1172
+ properties: { text: { type: 'string' } },
1173
+ required: ['text'],
1174
+ },
1175
+ nextStep: 'todo_add',
1176
+ },
1177
+ },
1178
+ agentCapabilities: {
1179
+ tools: {
1180
+ todo_toggle: {
1181
+ description: 'Flip a todo done/undone',
1182
+ inputSchema: {
1183
+ type: 'object',
1184
+ properties: { id: { type: 'string' } },
1185
+ required: ['id'],
1186
+ },
1187
+ },
1188
+ todo_add: {
1189
+ description: 'Append a new todo',
1190
+ inputSchema: {
1191
+ type: 'object',
1192
+ properties: { text: { type: 'string' } },
1193
+ required: ['text'],
1194
+ },
1195
+ },
1196
+ },
1197
+ },
1198
+ },
1199
+ expected: {
1200
+ hasActionSpec: true,
1201
+ hasContextSpec: false,
1202
+ hasStreamSpec: false,
1203
+ hasProps: true,
1204
+ actionNames: ['toggle', 'add'],
1205
+ notes: 'clean draft — must pass the fast path verbatim (origin agent) and the round-trip scorer. Guards against the scorer false-positiving on a good contract.',
1206
+ },
1207
+ roundTrip: {
1208
+ renderProps: {
1209
+ todos: [{ id: 't1', text: 'Buy milk', completed: false }],
1210
+ },
1211
+ consumableActions: ['toggleTodo', 'addTodo'],
1212
+ },
1213
+ },
1214
+ {
1215
+ // Good repair of a NON-seed error. A contact form: fields are
1216
+ // client-originated draft state (contextSpec), submit is an action.
1217
+ // The draft's submit.nextStep points at a tool that isn't declared
1218
+ // (CTR_REF_NEXT_STEP dangling) → repair fires. The fix (add the tool
1219
+ // or drop the nextStep) has no reason to touch the `submit` action, so
1220
+ // the gesture round-trip survives. No seed props.
1221
+ id: 'repair-form-nextstep',
1222
+ intent: 'a contact form with name, email and message that sends to my backend',
1223
+ draft: {
1224
+ contextSpec: {
1225
+ name: { schema: { type: 'string' }, default: '' },
1226
+ email: { schema: { type: 'string' }, default: '' },
1227
+ message: { schema: { type: 'string' }, default: '' },
1228
+ },
1229
+ actionSpec: {
1230
+ submit: {
1231
+ label: 'Send message',
1232
+ schema: {
1233
+ type: 'object',
1234
+ properties: {
1235
+ name: { type: 'string' },
1236
+ email: { type: 'string' },
1237
+ message: { type: 'string' },
1238
+ },
1239
+ required: ['name', 'email', 'message'],
1240
+ },
1241
+ // ⚠️ dangling: no `send_message` tool declared below.
1242
+ nextStep: 'send_message',
1243
+ },
1244
+ },
1245
+ },
1246
+ expected: {
1247
+ hasActionSpec: true,
1248
+ hasContextSpec: true,
1249
+ hasStreamSpec: false,
1250
+ hasProps: false,
1251
+ actionNames: ['submit', 'send'],
1252
+ contextSlots: ['name', 'email', 'message'],
1253
+ notes: 'good repair of a dangling nextStep — the submit gesture must survive so the form round-trips.',
1254
+ },
1255
+ roundTrip: {
1256
+ consumableActions: ['submit'],
1257
+ },
1258
+ },
1259
+ {
1260
+ // Good repair that must KEEP propsSpec. A weather card is the
1261
+ // canonical props-only display case; the agent seeds city/temp/
1262
+ // conditions. The draft carries a stray `additionalProperties` key on
1263
+ // the propsSpec wrapper (illegal — only description+properties allowed)
1264
+ // → CTR_SHAPE_UNRECOGNIZED_KEYS → repair fires. The fix drops the stray
1265
+ // key; propsSpec (and the seed round-trip) must survive.
1266
+ id: 'repair-weather-seed',
1267
+ intent: 'show a weather card for a city with temperature and conditions',
1268
+ draft: {
1269
+ propsSpec: {
1270
+ description: 'Current weather',
1271
+ // ⚠️ FATAL: not a legal key on the propsSpec wrapper.
1272
+ additionalProperties: false,
1273
+ properties: {
1274
+ city: { schema: { type: 'string' }, required: true },
1275
+ temp: { schema: { type: 'number' }, required: true },
1276
+ conditions: { schema: { type: 'string' }, required: true },
1277
+ },
1278
+ },
1279
+ },
1280
+ expected: {
1281
+ hasActionSpec: false,
1282
+ hasContextSpec: false,
1283
+ hasStreamSpec: false,
1284
+ hasProps: true,
1285
+ notes: 'good repair of a stray wrapper key — propsSpec must survive so the seed round-trips.',
1286
+ },
1287
+ roundTrip: {
1288
+ renderProps: {
1289
+ city: 'San Francisco',
1290
+ temp: 62,
1291
+ conditions: 'Foggy',
1292
+ },
1293
+ },
1294
+ },
1295
+ ];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Round-trip quality scorer.
3
+ *
4
+ * The shape scorer ({@link scoreSynthesizedContract}) answers "did the
5
+ * negotiator emit the right SPECS?" — a validity floor. This scorer
6
+ * answers the harder question the contract-quality frontier cares
7
+ * about: "is the produced contract actually USABLE end-to-end?" — i.e.
8
+ * after handshake agrees on it, can the agent `ggui_render` its seed
9
+ * data, and `ggui_consume` its gestures, without the wire rejecting or
10
+ * silently dropping anything?
11
+ *
12
+ * Why a structural scorer can't see this: a contract that reshapes a
13
+ * seedable collection from `propsSpec` to `contextSpec` is STRUCTURALLY
14
+ * VALID (passes `lintContract`) and PASSES the shape scorer (it has a
15
+ * contextSpec). Yet it is round-trip-BROKEN: `propsSpec` is the only
16
+ * agent→client seed channel (and the only `ggui_update` target);
17
+ * `contextSpec` is client→agent observed state with NO runtime seed
18
+ * path (`contextSpec.default` is `useState` scaffold, not wire data).
19
+ * So the agent's `ggui_render({props:{todos:[…]}})` hits a contract
20
+ * with no `propsSpec` — the accept-path silently drops the props and
21
+ * the UI renders empty; the override-path hard-throws.
22
+ *
23
+ * This scorer mirrors that exact render-handler gate deterministically
24
+ * (no LLM, no iframe), so the harness catches the "valid-but-broken"
25
+ * class the shape scorer is blind to. The gate it mirrors lives at
26
+ * `mcp-server-handlers/src/renders/render.ts:1218-1269` (props-vs-
27
+ * propsSpec) and the consume action-declaration requirement at
28
+ * `consume.ts` / `submit-action.ts` (every gesture intent MUST be a
29
+ * declared `actionSpec` key).
30
+ */
31
+ import type { DataContract } from '@ggui-ai/protocol';
32
+ /**
33
+ * What the agent intends to do with the agreed contract on the NEXT
34
+ * turn — the round-trip the produced contract must support. Authored
35
+ * per repair-corpus entry from the agent's original draft + intent.
36
+ */
37
+ export interface RoundTripExpectation {
38
+ /**
39
+ * The props the agent intends to pass on `ggui_render` once the
40
+ * contract is agreed — its render-time SEED data. Every key here MUST
41
+ * land in `contract.propsSpec.properties[key]` (the ONLY agent→client
42
+ * seed channel — `contextSpec` has no runtime seed path), and the
43
+ * value MUST satisfy that prop's schema. Mirrors the render handler's
44
+ * props-vs-propsSpec gate. Omit / leave empty for contracts with no
45
+ * agent-seeded data (pure forms, broadcasts, counters).
46
+ */
47
+ readonly renderProps?: Record<string, unknown>;
48
+ /**
49
+ * Gesture intents the agent expects to consume after render (e.g.
50
+ * `toggleTodo`, `submit`). Each MUST be a declared `actionSpec[*]`
51
+ * key, else the gesture is structurally unconsumable — the iframe has
52
+ * no declared intent to dispatch and `ggui_consume` never wakes.
53
+ */
54
+ readonly consumableActions?: readonly string[];
55
+ }
56
+ export type RoundTripFailureKind =
57
+ /** Repair bailed to an empty `{}` contract — nothing round-trips. */
58
+ 'contract-empty'
59
+ /**
60
+ * The agent would seed props but the produced contract declares NO
61
+ * `propsSpec` — the accept-path drops them / the override-path throws.
62
+ * The canonical reshape regression (propsSpec → contextSpec).
63
+ */
64
+ | 'props-no-home'
65
+ /** A seed-prop key has no matching `propsSpec.properties` entry. */
66
+ | 'props-key-unhomed'
67
+ /** Seed props fail `validatePropsData` against the propsSpec (the
68
+ * real wire validator — wrong type, missing required, etc.). */
69
+ | 'props-rejected'
70
+ /** A consumable gesture intent is absent from `actionSpec`. */
71
+ | 'action-undeclared';
72
+ export interface RoundTripFailure {
73
+ readonly kind: RoundTripFailureKind;
74
+ readonly hint: string;
75
+ }
76
+ export interface RoundTripScore {
77
+ readonly pass: boolean;
78
+ readonly failures: readonly RoundTripFailure[];
79
+ }
80
+ /**
81
+ * Score whether a produced contract supports the agent's intended
82
+ * round-trip. Pure / deterministic — mirrors the render + consume wire
83
+ * gates so a "valid-but-round-trip-broken" contract fails here even
84
+ * though `lintContract` and {@link scoreSynthesizedContract} pass it.
85
+ */
86
+ export declare function scoreContractRoundTrip(contract: DataContract, rt: RoundTripExpectation): RoundTripScore;
87
+ //# sourceMappingURL=round-trip-score.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"round-trip-score.d.ts","sourceRoot":"","sources":["../../src/synth-bench/round-trip-score.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AAED,MAAM,MAAM,oBAAoB;AAC9B,qEAAqE;AACnE,gBAAgB;AAClB;;;;GAIG;GACD,eAAe;AACjB,oEAAoE;GAClE,mBAAmB;AACrB;iEACiE;GAC/D,gBAAgB;AAClB,+DAA+D;GAC7D,mBAAmB,CAAC;AAExB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAmBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,YAAY,EACtB,EAAE,EAAE,oBAAoB,GACvB,cAAc,CAwDhB"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Round-trip quality scorer.
3
+ *
4
+ * The shape scorer ({@link scoreSynthesizedContract}) answers "did the
5
+ * negotiator emit the right SPECS?" — a validity floor. This scorer
6
+ * answers the harder question the contract-quality frontier cares
7
+ * about: "is the produced contract actually USABLE end-to-end?" — i.e.
8
+ * after handshake agrees on it, can the agent `ggui_render` its seed
9
+ * data, and `ggui_consume` its gestures, without the wire rejecting or
10
+ * silently dropping anything?
11
+ *
12
+ * Why a structural scorer can't see this: a contract that reshapes a
13
+ * seedable collection from `propsSpec` to `contextSpec` is STRUCTURALLY
14
+ * VALID (passes `lintContract`) and PASSES the shape scorer (it has a
15
+ * contextSpec). Yet it is round-trip-BROKEN: `propsSpec` is the only
16
+ * agent→client seed channel (and the only `ggui_update` target);
17
+ * `contextSpec` is client→agent observed state with NO runtime seed
18
+ * path (`contextSpec.default` is `useState` scaffold, not wire data).
19
+ * So the agent's `ggui_render({props:{todos:[…]}})` hits a contract
20
+ * with no `propsSpec` — the accept-path silently drops the props and
21
+ * the UI renders empty; the override-path hard-throws.
22
+ *
23
+ * This scorer mirrors that exact render-handler gate deterministically
24
+ * (no LLM, no iframe), so the harness catches the "valid-but-broken"
25
+ * class the shape scorer is blind to. The gate it mirrors lives at
26
+ * `mcp-server-handlers/src/renders/render.ts:1218-1269` (props-vs-
27
+ * propsSpec) and the consume action-declaration requirement at
28
+ * `consume.ts` / `submit-action.ts` (every gesture intent MUST be a
29
+ * declared `actionSpec` key).
30
+ */
31
+ import { validatePropsData } from '@ggui-ai/protocol';
32
+ /**
33
+ * True when a contract declares none of the six spec surfaces — the
34
+ * `EMPTY_CONTRACT` (`{}`) that `ensureConformingContract` returns when a
35
+ * draft is unrepairable. Such a contract is structurally valid but
36
+ * carries no wire at all.
37
+ */
38
+ function isEmptyContract(contract) {
39
+ return (contract.propsSpec === undefined &&
40
+ contract.actionSpec === undefined &&
41
+ contract.streamSpec === undefined &&
42
+ contract.contextSpec === undefined &&
43
+ contract.agentCapabilities === undefined &&
44
+ contract.clientCapabilities === undefined);
45
+ }
46
+ /**
47
+ * Score whether a produced contract supports the agent's intended
48
+ * round-trip. Pure / deterministic — mirrors the render + consume wire
49
+ * gates so a "valid-but-round-trip-broken" contract fails here even
50
+ * though `lintContract` and {@link scoreSynthesizedContract} pass it.
51
+ */
52
+ export function scoreContractRoundTrip(contract, rt) {
53
+ const failures = [];
54
+ const renderProps = rt.renderProps ?? {};
55
+ const seedKeys = Object.keys(renderProps);
56
+ const consumable = rt.consumableActions ?? [];
57
+ // An empty contract round-trips nothing it was asked to carry.
58
+ if (isEmptyContract(contract) && (seedKeys.length > 0 || consumable.length > 0)) {
59
+ failures.push({
60
+ kind: 'contract-empty',
61
+ hint: `repair produced an empty contract ({}); the agent's seed data {${seedKeys.join(', ')}} and gestures [${consumable.join(', ')}] have no wire — the draft was unrepairable.`,
62
+ });
63
+ }
64
+ // Props acceptance — mirrors render.ts:1218-1269 (the props-vs-propsSpec gate).
65
+ if (seedKeys.length > 0) {
66
+ const propsSpec = contract.propsSpec;
67
+ if (propsSpec === undefined) {
68
+ failures.push({
69
+ kind: 'props-no-home',
70
+ hint: `agent would seed props {${seedKeys.join(', ')}} on ggui_render, but the produced contract declares NO propsSpec. The accept-path silently drops them (render.ts:1233-1253) and the override-path throws (render.ts:1254-1268); the UI renders empty. A seedable collection was likely reshaped to contextSpec, which has no agent→client seed channel.`,
71
+ });
72
+ }
73
+ else {
74
+ const properties = propsSpec.properties ?? {};
75
+ const unhomed = seedKeys.filter((k) => properties[k] === undefined);
76
+ if (unhomed.length > 0) {
77
+ failures.push({
78
+ kind: 'props-key-unhomed',
79
+ hint: `seed-prop keys {${unhomed.join(', ')}} have no propsSpec.properties entry — ggui_render would reject them as undeclared props.`,
80
+ });
81
+ }
82
+ // The real wire validator the render handler runs (Branch A).
83
+ const result = validatePropsData(renderProps, propsSpec);
84
+ if (!result.valid) {
85
+ failures.push({
86
+ kind: 'props-rejected',
87
+ hint: `validatePropsData rejected the agent's seed props against the produced propsSpec: ${result.violations
88
+ .map((v) => `${v.field}: ${v.message}`)
89
+ .join('; ')}`,
90
+ });
91
+ }
92
+ }
93
+ }
94
+ // Consumable gestures — every intent the agent dispatches MUST be a
95
+ // declared actionSpec key, else the gesture is structurally undefined.
96
+ for (const action of consumable) {
97
+ if (contract.actionSpec?.[action] === undefined) {
98
+ failures.push({
99
+ kind: 'action-undeclared',
100
+ hint: `agent expects to consume gesture '${action}', but the produced contract's actionSpec has no '${action}' entry — the iframe has no declared intent to dispatch and ggui_consume never wakes.`,
101
+ });
102
+ }
103
+ }
104
+ return { pass: failures.length === 0, failures };
105
+ }