@enricai/barnacle 1.3.0 → 1.4.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 (137) hide show
  1. package/README.md +35 -1
  2. package/dist/api/errors.d.ts.map +1 -1
  3. package/dist/api/errors.js.map +1 -1
  4. package/dist/api/helpers/envelope.js.map +1 -1
  5. package/dist/api/helpers/reply.js.map +1 -1
  6. package/dist/api/plugins/auth.d.ts.map +1 -1
  7. package/dist/api/plugins/auth.js.map +1 -1
  8. package/dist/api/plugins/error-handler.d.ts.map +1 -1
  9. package/dist/api/plugins/error-handler.js.map +1 -1
  10. package/dist/api/plugins/request-context.d.ts.map +1 -1
  11. package/dist/api/plugins/request-context.js.map +1 -1
  12. package/dist/api/routes/health.js.map +1 -1
  13. package/dist/api/routes/plugins-introspection.js.map +1 -1
  14. package/dist/api/schemas/common.d.ts +12 -12
  15. package/dist/api/schemas/common.d.ts.map +1 -1
  16. package/dist/api/schemas/common.js.map +1 -1
  17. package/dist/cache/keyed-ttl-cache.js.map +1 -1
  18. package/dist/cache/response-cache.js.map +1 -1
  19. package/dist/config.js.map +1 -1
  20. package/dist/lib/applicant-payload.d.ts +3 -3
  21. package/dist/lib/bedrock.js.map +1 -1
  22. package/dist/lib/case-insensitive-headers.js.map +1 -1
  23. package/dist/lib/chromium-client-hints.js.map +1 -1
  24. package/dist/lib/cookie-value.js.map +1 -1
  25. package/dist/lib/datadog.js.map +1 -1
  26. package/dist/lib/dd-metrics.js.map +1 -1
  27. package/dist/lib/dispatch-metrics.d.ts.map +1 -1
  28. package/dist/lib/dispatch-metrics.js.map +1 -1
  29. package/dist/lib/env.js.map +1 -1
  30. package/dist/lib/errors.js.map +1 -1
  31. package/dist/lib/http.js.map +1 -1
  32. package/dist/lib/llm/anthropic-client.js.map +1 -1
  33. package/dist/lib/llm/judge.js.map +1 -1
  34. package/dist/lib/llm/judges/error-messages.js.map +1 -1
  35. package/dist/lib/llm/judges/invalid-fields.js.map +1 -1
  36. package/dist/lib/llm/judges/modal-priority.js.map +1 -1
  37. package/dist/lib/llm/judges/select-option.js.map +1 -1
  38. package/dist/lib/llm/judges/verify-submit.js.map +1 -1
  39. package/dist/lib/llm/schemas.d.ts +7 -7
  40. package/dist/lib/llm/schemas.js.map +1 -1
  41. package/dist/lib/logging.js.map +1 -1
  42. package/dist/lib/multipart.js.map +1 -1
  43. package/dist/lib/option-matcher.js.map +1 -1
  44. package/dist/lib/phone.js.map +1 -1
  45. package/dist/lib/random.js.map +1 -1
  46. package/dist/lib/statsd.js.map +1 -1
  47. package/dist/lib/telemetry/call-capture.d.ts +6 -6
  48. package/dist/lib/telemetry/call-capture.js.map +1 -1
  49. package/dist/lib/telemetry/run-state.js.map +1 -1
  50. package/dist/lib/telemetry/s3-sink.js.map +1 -1
  51. package/dist/lib/telemetry/submission-capture.js.map +1 -1
  52. package/dist/lib/telemetry/telemetry-paths.js.map +1 -1
  53. package/dist/lib/tracking-click.d.ts.map +1 -1
  54. package/dist/lib/tracking-click.js +5 -1
  55. package/dist/lib/tracking-click.js.map +1 -1
  56. package/dist/lib/url-capture.js.map +1 -1
  57. package/dist/lib/us-states.js.map +1 -1
  58. package/dist/lib/zod-multipart.js.map +1 -1
  59. package/dist/plugins/config-plugin.d.ts +1 -0
  60. package/dist/plugins/config-plugin.d.ts.map +1 -1
  61. package/dist/plugins/config-plugin.js +4 -0
  62. package/dist/plugins/config-plugin.js.map +1 -1
  63. package/dist/plugins/discover.d.ts.map +1 -1
  64. package/dist/plugins/discover.js +1 -0
  65. package/dist/plugins/discover.js.map +1 -1
  66. package/dist/plugins/json-schema-to-zod.d.ts.map +1 -1
  67. package/dist/plugins/json-schema-to-zod.js.map +1 -1
  68. package/dist/plugins/loader.d.ts.map +1 -1
  69. package/dist/plugins/loader.js +12 -2
  70. package/dist/plugins/loader.js.map +1 -1
  71. package/dist/recon/load-vocabulary.d.ts +24 -0
  72. package/dist/recon/load-vocabulary.d.ts.map +1 -0
  73. package/dist/recon/load-vocabulary.js +71 -0
  74. package/dist/recon/load-vocabulary.js.map +1 -0
  75. package/dist/recon/vocabulary.d.ts +62 -0
  76. package/dist/recon/vocabulary.d.ts.map +1 -0
  77. package/dist/recon/vocabulary.js +34 -0
  78. package/dist/recon/vocabulary.js.map +1 -0
  79. package/dist/scraper/behavioral-signals.js.map +1 -1
  80. package/dist/scraper/errors.d.ts.map +1 -1
  81. package/dist/scraper/errors.js.map +1 -1
  82. package/dist/scraper/fixtures.js.map +1 -1
  83. package/dist/scraper/flow-runner.js.map +1 -1
  84. package/dist/scraper/graphql-client.js.map +1 -1
  85. package/dist/scraper/http-client.js.map +1 -1
  86. package/dist/scraper/http-status-classifier.js.map +1 -1
  87. package/dist/scraper/metrics.js.map +1 -1
  88. package/dist/scraper/navigate.js.map +1 -1
  89. package/dist/scraper/oracle-sentinels.js.map +1 -1
  90. package/dist/scraper/parse-json-response.js.map +1 -1
  91. package/dist/scraper/pool.d.ts +2 -3
  92. package/dist/scraper/pool.d.ts.map +1 -1
  93. package/dist/scraper/pool.js.map +1 -1
  94. package/dist/scraper/rate-limited-json-client.js.map +1 -1
  95. package/dist/scraper/raw-fetch.js.map +1 -1
  96. package/dist/scraper/require-response-field.js.map +1 -1
  97. package/dist/scraper/retry.js.map +1 -1
  98. package/dist/scraper/session-browserbase.d.ts +6 -10
  99. package/dist/scraper/session-browserbase.d.ts.map +1 -1
  100. package/dist/scraper/session-browserbase.js +7 -6
  101. package/dist/scraper/session-browserbase.js.map +1 -1
  102. package/dist/scraper/session-shared.d.ts +16 -0
  103. package/dist/scraper/session-shared.d.ts.map +1 -1
  104. package/dist/scraper/session-shared.js.map +1 -1
  105. package/dist/scraper/session-steel.js.map +1 -1
  106. package/dist/scraper/session-warmup.js.map +1 -1
  107. package/dist/scraper/session.d.ts +6 -6
  108. package/dist/scraper/session.d.ts.map +1 -1
  109. package/dist/scraper/session.js +7 -1
  110. package/dist/scraper/session.js.map +1 -1
  111. package/dist/scraper/stagehand-guard.d.ts.map +1 -1
  112. package/dist/scraper/stagehand-guard.js.map +1 -1
  113. package/dist/scraper/throttle.js.map +1 -1
  114. package/dist/scripts/judge-llm-batch.js.map +1 -1
  115. package/dist/scripts/llm-heal.js.map +1 -1
  116. package/dist/scripts/migrate-telemetry-dir-names.js.map +1 -1
  117. package/dist/scripts/recon-browser.js.map +1 -1
  118. package/dist/scripts/recon-generate.d.ts +26 -1
  119. package/dist/scripts/recon-generate.d.ts.map +1 -1
  120. package/dist/scripts/recon-generate.js +159 -56
  121. package/dist/scripts/recon-generate.js.map +1 -1
  122. package/dist/scripts/recon-heal.js.map +1 -1
  123. package/dist/scripts/recon-shared.js.map +1 -1
  124. package/dist/server.js.map +1 -1
  125. package/dist/site-plugin.d.ts +7 -0
  126. package/dist/site-plugin.d.ts.map +1 -1
  127. package/dist/testing/batch-email-confirmation.js.map +1 -1
  128. package/dist/testing/batch-report.js.map +1 -1
  129. package/dist/testing/integration-runner.js.map +1 -1
  130. package/dist/testing/mock-fetch-response.js.map +1 -1
  131. package/dist/testing/persona-fixture.d.ts.map +1 -1
  132. package/dist/testing/resume-fixture.js.map +1 -1
  133. package/dist/testmail/client.js.map +1 -1
  134. package/dist/testmail/errors.d.ts.map +1 -1
  135. package/dist/testmail/errors.js.map +1 -1
  136. package/dist/types/dispatch-metrics.d.ts +2 -2
  137. package/package.json +30 -35
@@ -19,6 +19,7 @@
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.resolveStepPayloadField = resolveStepPayloadField;
21
21
  exports.inferZodSchemaFromSamples = inferZodSchemaFromSamples;
22
+ exports.selectPayloadAction = selectPayloadAction;
22
23
  exports.loadQuestionPromptKeywords = loadQuestionPromptKeywords;
23
24
  exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
24
25
  exports.emitContractTs = emitContractTs;
@@ -29,6 +30,8 @@ const node_path_1 = require("node:path");
29
30
  const errors_1 = require("../lib/errors");
30
31
  const logging_1 = require("../lib/logging");
31
32
  const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
33
+ const load_vocabulary_1 = require("../recon/load-vocabulary");
34
+ const vocabulary_1 = require("../recon/vocabulary");
32
35
  const recon_shared_1 = require("../scripts/recon-shared");
33
36
  const logger = (0, logging_1.getScriptLogger)("recon-generate");
34
37
  // ── helpers ──────────────────────────────────────────────────────────────────
@@ -39,46 +42,52 @@ function toPascalCase(siteId) {
39
42
  .join("");
40
43
  }
41
44
  /**
42
- * Instruction labels that name a NON-candidate field a reference-contact, an
43
- * employment-history row, the applicant's signature, etc. When any of these
44
- * matches we leave the step literal so the generator never splices the primary
45
- * applicant's PII into a field that must hold someone/something else. Matched
46
- * against the English label in the instruction, never the recon constant value.
47
- */
48
- const PAYLOAD_FIELD_EXCLUSIONS = [
49
- /reference\s*#?\s*\d/i,
50
- /employment history/i,
51
- /\bcompany (name|phone)\b/i,
52
- /\bemployer\b/i,
53
- /signature/i,
54
- /\bfull name\b/i,
55
- /today'?s date/i,
56
- /school|institution|degree|major|education/i,
57
- // Screening-question shapes: "For '<question>' select '<answer>'" and any
58
- // step framed around a 'question'. Here the first quoted string is the
59
- // question label, not a candidate value — a label word inside the question
60
- // (e.g. "...licensed in this state?") must NOT trigger a splice that would
61
- // overwrite the question quote. Candidate-fill steps say "fill in the X
62
- // field with '...'" / "Select '...' from the X dropdown" instead.
63
- /^\s*for\s+'/i,
64
- /\bquestion\b/i,
65
- ];
66
- /**
67
- * Ordered candidate-field label table. Each entry maps an English-label regex
68
- * to the PascalCase payload field the generator should splice. Order matters:
69
- * the first matching row wins, so more-specific labels precede broader ones.
45
+ * The recruiting vocabulary the engine used to hardcode, kept only so consumers
46
+ * who have not yet passed `--vocabulary` keep working through the 1.x line.
47
+ *
48
+ * @deprecated Pass `--vocabulary <specifier>` instead. The engine cannot know
49
+ * what any site's forms mean, and guessing mis-fires on every non-recruiting
50
+ * domain (a cruise site's "Select the departure port from the Country dropdown"
51
+ * became `${payload.Country}`). Slated for deletion in 2.0.0, after which an
52
+ * absent vocabulary on a spliceable flow is a hard error. See `src/recon/vocabulary.ts`.
70
53
  */
71
- const PAYLOAD_FIELD_TABLE = [
72
- [/\bfirst name\b/i, "FirstName"],
73
- [/\blast name\b/i, "LastName"],
74
- [/\b(e-?mail|email address)\b/i, "Email"],
75
- [/\b(mobile phone|primary phone|phone number|mobile)\b/i, "MobilePhone"],
76
- [/\b(street address|address line 1)\b/i, "AddressLine1"],
77
- [/\bcity\b/i, "City"],
78
- [/\b(state|province|state\/region)\b/i, "State"],
79
- [/\b(zip|postal)\b/i, "PostalCode"],
80
- [/\bcountry\b/i, "Country"],
81
- ];
54
+ const DEPRECATED_BUILTIN_ATS_VOCABULARY = {
55
+ // Naming the subject is what separates "…select the test candidate's state"
56
+ // (fill with the caller's data) from "…the departure port from the Country
57
+ // dropdown" (a search facet that merely says Country).
58
+ subject: /\b(the\s+)?(test\s+)?(candidate|applicant)'?s\b/i,
59
+ exclusions: [
60
+ /reference\s*#?\s*\d/i,
61
+ /employment history/i,
62
+ /\bcompany (name|phone)\b/i,
63
+ /\bemployer\b/i,
64
+ /signature/i,
65
+ /\bfull name\b/i,
66
+ /today'?s date/i,
67
+ /school|institution|degree|major|education/i,
68
+ // Screening-question shapes: "For '<question>' select '<answer>'" and any
69
+ // step framed around a 'question'. Here the first quoted string is the
70
+ // question label, not a candidate value — a label word inside the question
71
+ // (e.g. "...licensed in this state?") must NOT trigger a splice that would
72
+ // overwrite the question quote. Candidate-fill steps say "fill in the X
73
+ // field with '...'" / "Select '...' from the X dropdown" instead.
74
+ /^\s*for\s+'/i,
75
+ /\bquestion\b/i,
76
+ // A secondary phone must not fill the primary MobilePhone field.
77
+ /\bsecondary\b[^.]*\bphone\b/i,
78
+ ],
79
+ table: [
80
+ [/\bfirst name\b/i, "FirstName"],
81
+ [/\blast name\b/i, "LastName"],
82
+ [/\b(e-?mail|email address)\b/i, "Email"],
83
+ [/\b(mobile phone|primary phone|phone number|mobile)\b/i, "MobilePhone"],
84
+ [/\b(street address|address line 1)\b/i, "AddressLine1"],
85
+ [/\bcity\b/i, "City"],
86
+ [/\b(state|province|state\/region)\b/i, "State"],
87
+ [/\b(zip|postal)\b/i, "PostalCode"],
88
+ [/\bcountry\b/i, "Country"],
89
+ ],
90
+ };
82
91
  /**
83
92
  * Decide whether a flow step should splice a runtime `payload.<field>` value in
84
93
  * place of the frozen recon constant baked into its instruction. Exists so
@@ -91,27 +100,29 @@ const PAYLOAD_FIELD_TABLE = [
91
100
  * @param instruction the flow step's plain-English instruction
92
101
  * @param explicit an optional flow-authored `payloadField` override (wins outright)
93
102
  * @param forceNone when true, force a literal step (the `payloadFieldNone` opt-out)
103
+ * @param vocabulary the consumer's domain vocabulary; defaults to the deprecated
104
+ * built-in recruiting table so 1.x consumers who pass nothing keep working
94
105
  * @returns the PascalCase payload field name to splice, or null to keep literal
95
106
  */
96
- function resolveStepPayloadField(instruction, explicit, forceNone) {
107
+ function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = DEPRECATED_BUILTIN_ATS_VOCABULARY) {
97
108
  if (forceNone)
98
109
  return null;
99
110
  if (explicit)
100
111
  return explicit;
101
- // Nothing to splice unless the instruction actually carries a recon constant:
102
- // a quoted literal, the ${RECON_EMAIL} token, or a dropdown/select phrasing.
103
- const hasSpliceable = /'[^']*'/.test(instruction) ||
104
- /\$\{RECON_EMAIL\}/.test(instruction) ||
105
- /\bdropdown\b/i.test(instruction) ||
106
- /\bselect\b[^.]*\bfrom\b/i.test(instruction);
112
+ // A quoted literal or ${RECON_EMAIL} IS the recon constant this step would
113
+ // replace, so it is spliceable on its own.
114
+ const hasQuotedConstant = /'[^']*'/.test(instruction) || /\$\{RECON_EMAIL\}/.test(instruction);
115
+ // A dropdown step carries no constant to replace, so a label match alone can't
116
+ // tell "select the test candidate's state" (the caller's data) from "select the
117
+ // departure port from the Country dropdown" (a facet that merely says Country).
118
+ // Requiring the subject is what keeps this from mis-firing off-domain.
119
+ const isDropdownStep = /\bdropdown\b/i.test(instruction) || /\bselect\b[^.]*\bfrom\b/i.test(instruction);
120
+ const hasSpliceable = hasQuotedConstant || (isDropdownStep && vocabulary.subject.test(instruction));
107
121
  if (!hasSpliceable)
108
122
  return null;
109
- if (PAYLOAD_FIELD_EXCLUSIONS.some((rx) => rx.test(instruction)))
110
- return null;
111
- // "Secondary Phone Number" must not fill the primary MobilePhone field.
112
- if (/\bsecondary\b/i.test(instruction) && /\bphone\b/i.test(instruction))
123
+ if (vocabulary.exclusions.some((rx) => rx.test(instruction)))
113
124
  return null;
114
- for (const [rx, field] of PAYLOAD_FIELD_TABLE) {
125
+ for (const [rx, field] of vocabulary.table) {
115
126
  if (rx.test(instruction))
116
127
  return field;
117
128
  }
@@ -223,6 +234,54 @@ function endpointKey(url) {
223
234
  return url;
224
235
  }
225
236
  }
237
+ /** Empty or absent response bodies carry no evidence of an endpoint's purpose. */
238
+ function isVoidResponse(body) {
239
+ if (body === null || body === undefined || body === "")
240
+ return true;
241
+ if (typeof body !== "object")
242
+ return false;
243
+ if (Array.isArray(body))
244
+ return body.length === 0;
245
+ return Object.keys(body).length === 0;
246
+ }
247
+ /**
248
+ * Picks the action whose request body should define the payload schema.
249
+ *
250
+ * Defaults to the first action, which is right for a transactional flow: the
251
+ * caller's data goes in with the opening POST and later steps only carry the
252
+ * transaction forward. It is wrong when the flow's real subject is a query the
253
+ * page issues repeatedly — a search or inventory endpoint is re-hit on every
254
+ * filter change, while whatever happened to fire first was incidental (a
255
+ * feature-toggle fetch, a config read). Re-issuing the same endpoint with a
256
+ * different body is the signature of the parameters a caller would want to
257
+ * control, so that wins. Anything less clear-cut falls through to first-action
258
+ * behavior rather than guessing.
259
+ *
260
+ * A transactional flow can re-issue an endpoint too — an applicant record built
261
+ * up across several writes — and lands on those writes for the same reason: the
262
+ * call that merely opened the flow carries none of the caller's fields.
263
+ */
264
+ function selectPayloadAction(steps) {
265
+ const first = steps[0];
266
+ if (!first)
267
+ return null;
268
+ const bodiesByEndpoint = new Map();
269
+ for (const step of steps) {
270
+ const key = endpointKey(step.capture.url);
271
+ const bodies = bodiesByEndpoint.get(key) ?? new Set();
272
+ bodies.add(step.capture.requestPostData ?? "");
273
+ bodiesByEndpoint.set(key, bodies);
274
+ }
275
+ const requeried = steps.filter((step) => {
276
+ const bodies = bodiesByEndpoint.get(endpointKey(step.capture.url));
277
+ if (!bodies || bodies.size < 2)
278
+ return false;
279
+ // An endpoint re-hit with varying bodies but nothing to show for it is
280
+ // chatter — client-side error reporting, beacons — not the flow's subject.
281
+ return !isVoidResponse(step.capture.responseBody);
282
+ });
283
+ return requeried[0] ?? first;
284
+ }
226
285
  function deriveBaseUrl(captures) {
227
286
  for (const c of captures) {
228
287
  try {
@@ -2113,12 +2172,12 @@ function buildManifestInstruction(instruction, field) {
2113
2172
  * omitted; a site that needs it keeps the `.ts` path or wires `spec.httpModule`.
2114
2173
  */
2115
2174
  function emitConfigManifest(opts) {
2116
- const { siteId, displayName, baseUrl, flowSteps } = opts;
2175
+ const { siteId, displayName, baseUrl, flowSteps, vocabulary } = opts;
2117
2176
  const payloadFieldNames = new Set();
2118
2177
  const steps = flowSteps.map((step) => {
2119
2178
  const isObj = typeof step !== "string";
2120
2179
  const instruction = isObj ? step.step : step;
2121
- const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined);
2180
+ const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
2122
2181
  if (field !== null)
2123
2182
  payloadFieldNames.add(field);
2124
2183
  const rewritten = buildManifestInstruction(instruction, field);
@@ -2162,13 +2221,13 @@ function emitConfigManifest(opts) {
2162
2221
  * payload schema (both are driven by this same set).
2163
2222
  */
2164
2223
  function emitBrowserFlowTs(opts) {
2165
- const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false } = opts;
2224
+ const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, vocabulary, } = opts;
2166
2225
  const payloadFieldNames = new Set();
2167
2226
  const hasUploadStep = flowSteps.some((s) => typeof s !== "string" && s.upload === true);
2168
2227
  const stepLiterals = flowSteps.map((step) => {
2169
2228
  const isObj = typeof step !== "string";
2170
2229
  const instruction = isObj ? step.step : step;
2171
- const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined);
2230
+ const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
2172
2231
  if (field !== null)
2173
2232
  payloadFieldNames.add(field);
2174
2233
  const instructionExpr = buildStepInstructionExpr(instruction, field);
@@ -2289,14 +2348,51 @@ export { ${camel}Plugin } from "@/sites/${siteId}/contract";
2289
2348
  `;
2290
2349
  }
2291
2350
  // ── main ──────────────────────────────────────────────────────────────────────
2351
+ /**
2352
+ * Resolves the vocabulary for this run and reports which one is in play.
2353
+ *
2354
+ * The deprecation warning is the whole point of the 1.x window: removing a
2355
+ * built-in default that consumers silently depend on is only safe if the
2356
+ * transition is loud. It fires only when the built-in would actually change the
2357
+ * output — a flow with nothing to splice gets no nag it can't act on.
2358
+ */
2359
+ async function resolveVocabulary(specifier, flowSteps) {
2360
+ if (specifier) {
2361
+ const vocabulary = await (0, load_vocabulary_1.loadReconVocabulary)(specifier, process.cwd());
2362
+ logger.info(`vocabulary: ${specifier === load_vocabulary_1.VOCABULARY_NONE ? "none (splicing disabled)" : `${vocabulary.table.length} row(s) from ${specifier}`}`);
2363
+ return vocabulary;
2364
+ }
2365
+ // Warn only when the built-in table itself changes the outcome. A step with an
2366
+ // explicit payloadField resolves the same under any vocabulary, so nagging
2367
+ // about it would send consumers to fix something that isn't broken.
2368
+ const builtinChangesOutcome = flowSteps.some((step) => {
2369
+ const isObj = typeof step !== "string";
2370
+ const instruction = isObj ? step.step : step;
2371
+ const explicit = isObj ? step.payloadField : undefined;
2372
+ const forceNone = isObj ? step.payloadFieldNone : undefined;
2373
+ const withBuiltin = resolveStepPayloadField(instruction, explicit, forceNone, DEPRECATED_BUILTIN_ATS_VOCABULARY);
2374
+ const withNothing = resolveStepPayloadField(instruction, explicit, forceNone, vocabulary_1.EMPTY_VOCABULARY);
2375
+ return withBuiltin !== withNothing;
2376
+ });
2377
+ if (builtinChangesOutcome) {
2378
+ logger.warn(`DeprecationWarning: no --vocabulary given, falling back to the built-in recruiting table. ` +
2379
+ `It is removed in 2.0.0, after which this is an error. Fix: create a vocabulary module ` +
2380
+ `exporting a ReconVocabulary (see @enricai/barnacle/recon/vocabulary) and pass ` +
2381
+ `--vocabulary ./src/recon/<name>.ts, or --vocabulary none if this site splices no caller data.`);
2382
+ }
2383
+ return DEPRECATED_BUILTIN_ATS_VOCABULARY;
2384
+ }
2292
2385
  async function main() {
2293
2386
  const args = process.argv.slice(2);
2294
2387
  let siteId = "";
2295
2388
  let force = false;
2296
2389
  let emit = "ts";
2390
+ let vocabularySpecifier = "";
2297
2391
  for (let i = 0; i < args.length; i++) {
2298
2392
  if (args[i] === "--site-id" && args[i + 1])
2299
2393
  siteId = args[++i];
2394
+ else if (args[i] === "--vocabulary" && args[i + 1])
2395
+ vocabularySpecifier = args[++i];
2300
2396
  else if (args[i] === "--force")
2301
2397
  force = true;
2302
2398
  else if (args[i] === "--emit" && args[i + 1]) {
@@ -2363,6 +2459,10 @@ async function main() {
2363
2459
  return [];
2364
2460
  }
2365
2461
  })();
2462
+ // Resolved once and threaded down, never captured into a module const: a
2463
+ // module-level const would freeze at import time, which is the bug that makes
2464
+ // RECON_QUESTION_KEYWORDS silently inert for anyone setting it after load.
2465
+ const vocabulary = await resolveVocabulary(vocabularySpecifier, flowSteps);
2366
2466
  const pascal = toPascalCase(siteId);
2367
2467
  const baseUrl = deriveBaseUrl(captures);
2368
2468
  const baseHeaders = deriveRequestHeaders(captures, replays, baseUrl);
@@ -2426,7 +2526,8 @@ async function main() {
2426
2526
  const inputBody = isSubmissionFlow
2427
2527
  ? (() => {
2428
2528
  try {
2429
- return JSON.parse(actionSteps[0].capture.requestPostData ?? "null");
2529
+ const payloadAction = selectPayloadAction(actionSteps);
2530
+ return JSON.parse(payloadAction?.capture.requestPostData ?? "null");
2430
2531
  }
2431
2532
  catch {
2432
2533
  return null;
@@ -2582,6 +2683,7 @@ async function main() {
2582
2683
  displayName: pascal,
2583
2684
  baseUrl,
2584
2685
  flowSteps,
2686
+ vocabulary,
2585
2687
  }));
2586
2688
  logger.info(`wrote ${manifestPath}`);
2587
2689
  logger.info(`done — review ${manifestPath}, fill in response/extract schemas, then load via BARNACLE_PLUGINS or BARNACLE_PLUGINS_CONFIG_DIR (no compile step)`);
@@ -2598,6 +2700,7 @@ async function main() {
2598
2700
  flowSteps,
2599
2701
  isSubmissionFlow,
2600
2702
  hasMultipartStep,
2703
+ vocabulary,
2601
2704
  });
2602
2705
  (0, node_fs_1.writeFileSync)(`${outDir}/contract.ts`, emitContractTs({
2603
2706
  siteId,