@kivimedia/kmhub 2.9.1 → 2.10.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.
Files changed (57) hide show
  1. package/README.md +170 -170
  2. package/bin/kmhub.mjs +896 -896
  3. package/coach-book-output-guard.mjs +760 -760
  4. package/index.mjs +57 -57
  5. package/package.json +56 -56
  6. package/prompts/briefing.md +29 -29
  7. package/prompts/luxury.md +70 -70
  8. package/prompts/play.md +49 -49
  9. package/prompts/run.md +36 -36
  10. package/prompts/setup.md +33 -33
  11. package/prompts/vs-booked.md +46 -46
  12. package/prompts/what-can-you-do.md +40 -40
  13. package/prompts.mjs +110 -110
  14. package/read-only-tools.json +143 -142
  15. package/remote.mjs +929 -929
  16. package/tools/balloon-costing.mjs +80 -80
  17. package/tools/booking-equipment.mjs +110 -110
  18. package/tools/bridges.mjs +54 -54
  19. package/tools/briefing.mjs +91 -91
  20. package/tools/calendar.mjs +170 -170
  21. package/tools/capabilities.mjs +155 -155
  22. package/tools/catalog.mjs +288 -288
  23. package/tools/clubs.mjs +176 -176
  24. package/tools/coach.mjs +771 -771
  25. package/tools/compare.mjs +76 -76
  26. package/tools/core.mjs +244 -244
  27. package/tools/crm.mjs +209 -209
  28. package/tools/dubsado.mjs +137 -137
  29. package/tools/exports.mjs +128 -128
  30. package/tools/fact-review.mjs +125 -125
  31. package/tools/flows.mjs +261 -261
  32. package/tools/forms.mjs +158 -158
  33. package/tools/gols.mjs +134 -134
  34. package/tools/hr.mjs +162 -162
  35. package/tools/knowledge.mjs +125 -125
  36. package/tools/marketing.mjs +396 -396
  37. package/tools/meta.mjs +245 -245
  38. package/tools/military.mjs +244 -244
  39. package/tools/money.mjs +235 -197
  40. package/tools/outreach.mjs +238 -238
  41. package/tools/pending.mjs +122 -122
  42. package/tools/photos.mjs +140 -140
  43. package/tools/plays.mjs +244 -244
  44. package/tools/profile.mjs +118 -118
  45. package/tools/radar.mjs +173 -173
  46. package/tools/recurring-invoices.mjs +149 -149
  47. package/tools/reengage.mjs +434 -434
  48. package/tools/schedules.mjs +55 -55
  49. package/tools/setup.mjs +168 -168
  50. package/tools/sops-bridges.mjs +86 -86
  51. package/tools/sops.mjs +314 -314
  52. package/tools/sourcing.mjs +268 -268
  53. package/tools/strategy.mjs +146 -146
  54. package/tools/studio.mjs +132 -132
  55. package/tools/venueradar.mjs +151 -151
  56. package/tools/voice.mjs +134 -134
  57. package/tools.mjs +407 -407
@@ -1,760 +1,760 @@
1
- const FORBIDDEN_OUTPUT_KEYS = new Set([
2
- 'credential', 'credentials', 'accesstoken', 'refreshtoken', 'apikey',
3
- 'password', 'secret', 'secretvalue', 'clientsecret', 'token',
4
- 'authorization', 'bearer', 'privatekey', 'signature',
5
- 'protectedspec', 'sourcetext', 'chunktext', 'excerpt', 'embedding',
6
- 'vector', 'vectors', 'queryvector', 'clausevector', 'clausevectors',
7
- 'anchor', 'anchors', 'anchorid', 'anchorids', 'semantictext', 'semantictexts',
8
- 'semanticmodel', 'modelinternals',
9
- 'systemprompt', 'providerconfig', 'connectorconfig', 'runtimeadapterid',
10
- 'adaptersecret', 'sourcecode', 'skillfile', 'skillfiles', 'skillpath',
11
- 'sqlquery', 'formula', 'expression', 'when', 'left', 'right',
12
- 'forbiddeneffects', 'sourcedocumentid', 'sourcesha256',
13
- ]);
14
-
15
- const SECRET_VALUE_PATTERNS = [
16
- /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/i,
17
- /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/i,
18
- /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
19
- /\bsk-(?:proj-|svcacct-|ant-)?[A-Za-z0-9_-]{20,}\b/i,
20
- /\bgithub_pat_[A-Za-z0-9_]{40,}\b/,
21
- /\bgh[pousr]_[A-Za-z0-9]{30,}\b/,
22
- /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/i,
23
- /\b(?:sk|rk)_live_[A-Za-z0-9]{16,}\b/i,
24
- /\bAIza[0-9A-Za-z_-]{30,}\b/,
25
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
26
- /\bEA[A-Za-z0-9]{35,}\b/,
27
- /\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|password)\s*[:=]\s*["']?[A-Za-z0-9_+/.=-]{16,}/i,
28
- /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\/[^:\s/]+:[^@\s/]{6,}@/i,
29
- /[?&](?:access_token|refresh_token|api_key|token|signature|sig)=[A-Za-z0-9_+/.=-]{16,}/i,
30
- ];
31
-
32
- export class CoachBookOutputError extends Error {
33
- constructor() {
34
- super('The Fully Booked Coach response failed public-output verification.');
35
- this.name = 'CoachBookOutputError';
36
- }
37
- }
38
-
39
- function normalizedKey(key) {
40
- return key.toLowerCase().replace(/[^a-z0-9]/g, '');
41
- }
42
-
43
- function record(value) {
44
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new CoachBookOutputError();
45
- return value;
46
- }
47
-
48
- function records(value) {
49
- if (!Array.isArray(value)) throw new CoachBookOutputError();
50
- return value.map(record);
51
- }
52
-
53
- function text(value) {
54
- if (typeof value !== 'string') throw new CoachBookOutputError();
55
- return value;
56
- }
57
-
58
- function integer(value) {
59
- if (!Number.isSafeInteger(value)) throw new CoachBookOutputError();
60
- return Number(value);
61
- }
62
-
63
- function number(value) {
64
- if (typeof value !== 'number' || !Number.isFinite(value)) throw new CoachBookOutputError();
65
- return value;
66
- }
67
-
68
- function boolean(value) {
69
- if (typeof value !== 'boolean') throw new CoachBookOutputError();
70
- return value;
71
- }
72
-
73
- function nullableText(value) {
74
- if (value === null || value === undefined || value === '') return null;
75
- return text(value);
76
- }
77
-
78
- function stringArray(value) {
79
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
80
- throw new CoachBookOutputError();
81
- }
82
- return [...value];
83
- }
84
-
85
- function optionalNumber(target, source, key) {
86
- if (source[key] !== undefined) target[key] = number(source[key]);
87
- }
88
-
89
- function optionalText(target, source, key) {
90
- if (source[key] !== undefined) target[key] = text(source[key]);
91
- }
92
-
93
- function projectRanges(value) {
94
- return records(value).map((range) => {
95
- const currentLocator = range.start !== undefined || range.end !== undefined;
96
- return currentLocator
97
- ? { start: integer(range.start), end: integer(range.end) }
98
- : { start: integer(range.pdfPageStart), end: integer(range.pdfPageEnd) };
99
- });
100
- }
101
-
102
- function projectCitations(value) {
103
- return records(value).map((citation) => ({
104
- label: text(citation.label),
105
- pdfPageStart: integer(citation.pdfPageStart),
106
- pdfPageEnd: integer(citation.pdfPageEnd),
107
- }));
108
- }
109
-
110
- function projectField(value) {
111
- const source = record(value);
112
- const projected = {
113
- id: text(source.id),
114
- type: text(source.type),
115
- label: text(source.label),
116
- required: boolean(source.required),
117
- };
118
- optionalText(projected, source, 'helpText');
119
- for (const key of ['minimum', 'maximum', 'decimalPlaces', 'minimumItems', 'maximumItems']) {
120
- optionalNumber(projected, source, key);
121
- }
122
- if (source.choices !== undefined) {
123
- projected.choices = records(source.choices).map((choice) => ({
124
- value: text(choice.value),
125
- label: text(choice.label),
126
- }));
127
- }
128
- return projected;
129
- }
130
-
131
- function projectPublicSpec(value) {
132
- const source = record(value);
133
- const projected = {
134
- schemaVersion: text(source.schemaVersion),
135
- title: text(source.title),
136
- summary: text(source.summary),
137
- category: text(source.category),
138
- estimatedMinutes: integer(source.estimatedMinutes),
139
- executorKind: text(source.executorKind),
140
- effectClass: text(source.effectClass),
141
- steps: records(source.steps).map((step) => ({
142
- id: text(step.id),
143
- title: text(step.title),
144
- description: text(step.description),
145
- fields: records(step.fields).map(projectField),
146
- })),
147
- artifactSections: records(source.artifactSections).map((section) => ({
148
- id: text(section.id),
149
- title: text(section.title),
150
- fieldIds: stringArray(section.fieldIds),
151
- calculationIds: stringArray(section.calculationIds),
152
- })),
153
- sourceCitations: projectCitations(source.sourceCitations),
154
- };
155
- optionalText(projected, source, 'availabilityClass');
156
- return projected;
157
- }
158
-
159
- function projectCatalogMethod(value) {
160
- const source = record(value);
161
- return {
162
- tool_spec_id: text(source.tool_spec_id),
163
- version: integer(source.version),
164
- title: text(source.title),
165
- sequence: integer(source.sequence),
166
- summary: text(source.summary),
167
- category: text(source.category),
168
- estimated_minutes: integer(source.estimated_minutes),
169
- implementation_status: text(source.implementation_status),
170
- availability_class: text(source.availability_class),
171
- executor_kind: text(source.executor_kind),
172
- effect_class: text(source.effect_class),
173
- runnable: boolean(source.runnable),
174
- specialized: boolean(source.specialized),
175
- source_page_ranges: projectRanges(source.source_page_ranges),
176
- };
177
- }
178
-
179
- function projectDescribedMethod(value) {
180
- const source = record(value);
181
- return { ...projectCatalogMethod(source), public_spec: projectPublicSpec(source.public_spec) };
182
- }
183
-
184
- function projectSummary(value) {
185
- const source = record(value);
186
- return {
187
- run_id: text(source.run_id),
188
- status: text(source.status),
189
- tool_spec_id: text(source.tool_spec_id),
190
- tool_spec_version: integer(source.tool_spec_version),
191
- artifact_count: integer(source.artifact_count),
192
- decision_count: integer(source.decision_count),
193
- reserved_charge_usd_micros: source.reserved_charge_usd_micros === undefined
194
- ? 0
195
- : integer(source.reserved_charge_usd_micros),
196
- retained_provider_cost_usd_micros: source.retained_provider_cost_usd_micros === undefined
197
- ? 0
198
- : integer(source.retained_provider_cost_usd_micros),
199
- charged_usd_micros: source.charged_usd_micros === undefined
200
- ? 0
201
- : integer(source.charged_usd_micros),
202
- released_usd_micros: source.released_usd_micros === undefined
203
- ? 0
204
- : integer(source.released_usd_micros),
205
- approved_at: nullableText(source.approved_at),
206
- started_at: nullableText(source.started_at),
207
- settled_at: nullableText(source.settled_at),
208
- created_at: nullableText(source.created_at),
209
- };
210
- }
211
-
212
- function projectArtifactValue(value) {
213
- if (typeof value === 'string' || typeof value === 'boolean') return value;
214
- if (typeof value === 'number' && Number.isFinite(value)) return value;
215
- return stringArray(value);
216
- }
217
-
218
- function projectArtifact(value) {
219
- if (value === null || value === undefined) return null;
220
- const source = record(value);
221
- return {
222
- title: text(source.title),
223
- sections: records(source.sections).map((section) => ({
224
- id: text(section.id),
225
- title: text(section.title),
226
- values: records(section.values).map((item) => ({
227
- id: text(item.id),
228
- label: text(item.label),
229
- value: projectArtifactValue(item.value),
230
- })),
231
- })),
232
- sourceCitations: projectCitations(source.sourceCitations),
233
- };
234
- }
235
-
236
- function projectBoundaries(value) {
237
- const source = record(value);
238
- return {
239
- providerUse: boolean(source.providerUse),
240
- spend: boolean(source.spend),
241
- contact: boolean(source.contact),
242
- publication: boolean(source.publication),
243
- durableCrmChange: boolean(source.durableCrmChange),
244
- connectorDispatch: boolean(source.connectorDispatch),
245
- outcomePromise: boolean(source.outcomePromise),
246
- separateApprovalRequired: boolean(source.separateApprovalRequired),
247
- };
248
- }
249
-
250
- function projectMethodResult(value) {
251
- if (value === null || value === undefined) return null;
252
- const source = record(value);
253
- return {
254
- schemaVersion: text(source.schemaVersion),
255
- toolSpecId: text(source.toolSpecId),
256
- toolSpecVersion: integer(source.toolSpecVersion),
257
- status: text(source.status),
258
- completedStepIds: stringArray(source.completedStepIds),
259
- nextStepId: nullableText(source.nextStepId),
260
- calculations: records(source.calculations).map((calculation) => ({
261
- id: text(calculation.id),
262
- label: text(calculation.label),
263
- rawValue: number(calculation.rawValue),
264
- formattedValue: text(calculation.formattedValue),
265
- })),
266
- decisions: records(source.decisions).map((decision) => ({
267
- id: text(decision.id),
268
- label: text(decision.label),
269
- outcome: text(decision.outcome),
270
- guidance: text(decision.guidance),
271
- })),
272
- artifact: projectArtifact(source.artifact),
273
- boundaries: projectBoundaries(source.boundaries),
274
- };
275
- }
276
-
277
- function projectReceipt(value) {
278
- if (value === null || value === undefined) return null;
279
- const source = record(value);
280
- const projected = {
281
- outcome: text(source.outcome),
282
- retained_provider_cost_usd_micros: integer(source.retained_provider_cost_usd_micros),
283
- charged_usd_micros: integer(source.charged_usd_micros),
284
- };
285
- for (const key of ['released_usd_micros', 'promo_released_usd_micros', 'paid_released_usd_micros', 'markup_multiplier']) {
286
- if (source[key] !== undefined) projected[key] = integer(source[key]);
287
- }
288
- for (const key of ['provider_use', 'external_effect_performed']) {
289
- if (source[key] !== undefined) projected[key] = boolean(source[key]);
290
- }
291
- if (source.created_at !== undefined) projected.created_at = nullableText(source.created_at);
292
- return projected;
293
- }
294
-
295
- function projectGenericRun(value) {
296
- const source = record(value);
297
- const method = record(source.method);
298
- const progress = record(source.progress);
299
- return {
300
- summary: projectSummary(source.summary),
301
- method: {
302
- tool_spec_id: text(method.tool_spec_id),
303
- method_id: text(method.method_id),
304
- sequence: integer(method.sequence),
305
- version: integer(method.version),
306
- title: text(method.title),
307
- executor_kind: text(method.executor_kind),
308
- effect_class: text(method.effect_class),
309
- public_spec: projectPublicSpec(method.public_spec),
310
- source_page_ranges: projectRanges(method.source_page_ranges),
311
- },
312
- progress: {
313
- completed_step_ids: stringArray(progress.completed_step_ids),
314
- next_step_id: nullableText(progress.next_step_id),
315
- result: projectMethodResult(progress.result),
316
- settlement_pending: boolean(progress.settlement_pending),
317
- },
318
- receipt: projectReceipt(source.receipt),
319
- };
320
- }
321
-
322
- function projectNicheDimension(value) {
323
- const source = record(value);
324
- const threshold = record(source.threshold);
325
- const evidenceGate = record(source.evidenceGate);
326
- return {
327
- dimension: text(source.dimension),
328
- score: number(source.score),
329
- threshold: {
330
- operator: text(threshold.operator),
331
- value: number(threshold.value),
332
- sourcePage: integer(threshold.sourcePage),
333
- },
334
- thresholdPassed: boolean(source.thresholdPassed),
335
- falsePositiveObserved: boolean(source.falsePositiveObserved),
336
- passed: boolean(source.passed),
337
- confidence: text(source.confidence),
338
- evidenceGate: {
339
- directSourcePresent: boolean(evidenceGate.directSourcePresent),
340
- counterevidenceCompleted: boolean(evidenceGate.counterevidenceCompleted),
341
- passed: boolean(evidenceGate.passed),
342
- failures: stringArray(evidenceGate.failures),
343
- },
344
- };
345
- }
346
-
347
- function projectNicheResult(value) {
348
- const source = record(value);
349
- const dimensions = record(source.dimensions);
350
- const plan = record(source.validationPlan);
351
- const pace = record(plan.paceObservationWeeks);
352
- const stopRules = record(plan.falsePositiveStopRules);
353
- const boundaries = record(source.boundaries);
354
- return {
355
- schemaVersion: text(source.schemaVersion),
356
- dimensions: {
357
- findability: projectNicheDimension(dimensions.findability),
358
- bragability: projectNicheDimension(dimensions.bragability),
359
- sellability: projectNicheDimension(dimensions.sellability),
360
- },
361
- overallStatus: text(source.overallStatus),
362
- confidence: text(source.confidence),
363
- recommendation: text(source.recommendation),
364
- validationPlan: {
365
- durationDays: integer(plan.durationDays),
366
- paceObservationWeeks: { minimum: integer(pace.minimum), maximum: integer(pace.maximum) },
367
- weeklyAcceptedCollaborationTarget: integer(plan.weeklyAcceptedCollaborationTarget),
368
- approximateAnnualCollaborationPace: integer(plan.approximateAnnualCollaborationPace),
369
- brandAssetsRequiredBeforeTest: boolean(plan.brandAssetsRequiredBeforeTest),
370
- trackedSignals: stringArray(plan.trackedSignals),
371
- falsePositiveStopRules: {
372
- findability: text(stopRules.findability),
373
- bragability: text(stopRules.bragability),
374
- sellability: text(stopRules.sellability),
375
- },
376
- humanDecisionRequired: boolean(plan.humanDecisionRequired),
377
- separateApprovalRequiredFor: stringArray(plan.separateApprovalRequiredFor),
378
- performsExternalAction: boolean(plan.performsExternalAction),
379
- promisesOutcome: boolean(plan.promisesOutcome),
380
- sourcePages: Array.isArray(plan.sourcePages) ? plan.sourcePages.map(integer) : (() => { throw new CoachBookOutputError(); })(),
381
- sourceNote: text(plan.sourceNote),
382
- },
383
- boundaries: {
384
- modelScoreAllowed: boolean(boundaries.modelScoreAllowed),
385
- providerUse: boolean(boundaries.providerUse),
386
- spend: boolean(boundaries.spend),
387
- contact: boolean(boundaries.contact),
388
- publication: boolean(boundaries.publication),
389
- outcomePromise: boolean(boundaries.outcomePromise),
390
- },
391
- };
392
- }
393
-
394
- function projectNicheRun(value) {
395
- const source = record(value);
396
- const artifact = source.artifact === null || source.artifact === undefined ? null : record(source.artifact);
397
- const decision = source.decision === null || source.decision === undefined ? null : record(source.decision);
398
- return {
399
- summary: projectSummary(source.summary),
400
- artifact: artifact ? {
401
- id: text(artifact.id),
402
- artifact_type: text(artifact.artifact_type),
403
- result: projectNicheResult(artifact.result),
404
- source_citations: records(artifact.source_citations).map((citation) => ({
405
- locator: text(citation.locator),
406
- page_start: integer(citation.page_start),
407
- page_end: integer(citation.page_end),
408
- supports: text(citation.supports),
409
- })),
410
- resume_supported_surfaces: stringArray(artifact.resume_supported_surfaces),
411
- created_at: nullableText(artifact.created_at),
412
- } : null,
413
- decision: decision ? {
414
- id: text(decision.id),
415
- decision_type: text(decision.decision_type),
416
- decision_status: text(decision.decision_status),
417
- decision_payload: {
418
- confirmed_by_user: boolean(record(decision.decision_payload).confirmed_by_user),
419
- tool_spec_id: text(record(decision.decision_payload).tool_spec_id),
420
- result_status: text(record(decision.decision_payload).result_status),
421
- performs_external_action: boolean(record(decision.decision_payload).performs_external_action),
422
- },
423
- created_at: nullableText(decision.created_at),
424
- } : null,
425
- receipt: projectReceipt(source.receipt),
426
- };
427
- }
428
-
429
- function hasForbiddenOutput(value, allowConfirmationToken = false) {
430
- if (typeof value === 'string') return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
431
- if (Array.isArray(value)) return value.some((entry) => hasForbiddenOutput(entry, false));
432
- if (!value || typeof value !== 'object') return false;
433
- return Object.entries(value).some(([key, child]) => {
434
- const normalized = normalizedKey(key);
435
- const allowedToken = allowConfirmationToken && normalized === 'confirmtoken';
436
- if (!allowedToken && (
437
- FORBIDDEN_OUTPUT_KEYS.has(normalized)
438
- || normalized.endsWith('token')
439
- || normalized.endsWith('secret')
440
- || normalized.endsWith('password')
441
- || normalized.endsWith('privatekey')
442
- || normalized.endsWith('digest')
443
- )) return true;
444
- return allowedToken ? false : hasForbiddenOutput(child, false);
445
- });
446
- }
447
-
448
- function assertPublicOutput(value) {
449
- if (hasForbiddenOutput(value)) throw new CoachBookOutputError();
450
- }
451
-
452
- const FBC_METHOD_ID_RE = /^fbc[.]method[.][0-9]{3}[.][a-z0-9]+(?:-[a-z0-9]+)*$/;
453
- export const FBC_METHOD_RANKER_VERSION = 'fbc.method-ranker.semantic-hybrid-v2';
454
- const FBC_RECOMMENDATION_OUTCOMES = new Set([
455
- 'recommended', 'clarification_required', 'no_runnable_match', 'no_match',
456
- ]);
457
- const FBC_RECOMMENDATION_CONFIDENCE = new Set(['high', 'medium', 'low']);
458
- const FBC_RECOMMENDATION_AVAILABILITY = new Set(['runnable_only', 'include_pending']);
459
- const FBC_RECOMMENDATION_EXECUTORS = new Set(['deterministic_single_step', 'guided_multi_step']);
460
- const FBC_RECOMMENDATION_EFFECTS = new Set(['none', 'action_preview']);
461
- const FBC_RECOMMENDATION_STAGES = new Set([
462
- 'purpose_and_capacity', 'oath', 'niche', 'collaboration', 'authority_content',
463
- 'audience_growth', 'strategy_session', 'offer_and_package', 'follow_up',
464
- 'client_map', 'super_coaching_delivery', 'results_and_proof', 'renewal_and_referral',
465
- ]);
466
- const FBC_RECOMMENDATION_REASONS = new Set(['goal_too_broad', 'close_scores', 'low_confidence']);
467
- const FBC_RECOMMENDATION_STATUSES = new Set(['implemented', 'method_owner_accepted', 'live_proven']);
468
- const SHA256_RE = /^[0-9a-f]{64}$/;
469
-
470
- function boundedText(value, minimum, maximum) {
471
- const result = text(value);
472
- if (result.length < minimum || result.length > maximum) throw new CoachBookOutputError();
473
- return result;
474
- }
475
-
476
- function enumText(value, allowed) {
477
- const result = text(value);
478
- if (!allowed.has(result)) throw new CoachBookOutputError();
479
- return result;
480
- }
481
-
482
- function nullableEnumText(value, allowed) {
483
- if (value === null || value === undefined) return null;
484
- return enumText(value, allowed);
485
- }
486
-
487
- function methodId(value) {
488
- const result = text(value);
489
- if (!FBC_METHOD_ID_RE.test(result)) throw new CoachBookOutputError();
490
- return result;
491
- }
492
-
493
- function projectRecommendationMatch(value, expectedRank, lane) {
494
- const source = record(value);
495
- const rank = integer(source.rank);
496
- const estimatedMinutes = integer(source.estimated_minutes);
497
- const score = number(source.score);
498
- if (
499
- rank !== expectedRank
500
- || estimatedMinutes < 1
501
- || estimatedMinutes > 240
502
- || score < 0
503
- || score > 1
504
- ) {
505
- throw new CoachBookOutputError();
506
- }
507
- const matchedIntents = stringArray(source.matched_intents);
508
- if (
509
- matchedIntents.length > 6
510
- || matchedIntents.some((intent) => intent.length < 1 || intent.length > 80)
511
- ) {
512
- throw new CoachBookOutputError();
513
- }
514
- const sourcePageRanges = projectRanges(source.source_page_ranges);
515
- if (sourcePageRanges.length > 20) throw new CoachBookOutputError();
516
- const implementationStatus = enumText(source.implementation_status, FBC_RECOMMENDATION_STATUSES);
517
- const runnable = boolean(source.runnable);
518
- const canStart = boolean(source.can_start);
519
- const laneIsValid = lane === 'runnable'
520
- ? ['method_owner_accepted', 'live_proven'].includes(implementationStatus)
521
- && runnable === true
522
- && canStart === true
523
- : implementationStatus === 'implemented'
524
- && runnable === false
525
- && canStart === false;
526
- if (!laneIsValid) throw new CoachBookOutputError();
527
- return {
528
- rank,
529
- tool_spec_id: methodId(source.tool_spec_id),
530
- title: boundedText(source.title, 3, 200),
531
- summary: boundedText(source.summary, 10, 1200),
532
- category: boundedText(source.category, 2, 160),
533
- estimated_minutes: estimatedMinutes,
534
- executor_kind: enumText(source.executor_kind, FBC_RECOMMENDATION_EXECUTORS),
535
- effect_class: enumText(source.effect_class, FBC_RECOMMENDATION_EFFECTS),
536
- implementation_status: implementationStatus,
537
- runnable,
538
- can_start: canStart,
539
- score,
540
- matched_intents: matchedIntents,
541
- use_when: boundedText(source.use_when, 4, 1000),
542
- source_page_ranges: sourcePageRanges,
543
- };
544
- }
545
-
546
- function projectRecommendationMatches(value, lane) {
547
- const source = records(value);
548
- if (source.length > 3) throw new CoachBookOutputError();
549
- const projected = source.map((match, index) => projectRecommendationMatch(match, index + 1, lane));
550
- const toolSpecIds = projected.map((match) => String(match.tool_spec_id));
551
- const ranks = projected.map((match) => Number(match.rank));
552
- if (new Set(toolSpecIds).size !== toolSpecIds.length || new Set(ranks).size !== ranks.length) {
553
- throw new CoachBookOutputError();
554
- }
555
- return projected;
556
- }
557
- function projectRecommendationClarification(value) {
558
- if (value === null || value === undefined) return null;
559
- const source = record(value);
560
- const choices = records(source.choices);
561
- if (choices.length < 1 || choices.length > 3) throw new CoachBookOutputError();
562
- const question = text(source.question);
563
- const instruction = text(source.instruction);
564
- if (
565
- question !== 'Which result do you want right now?'
566
- || instruction !== 'Ask this question exactly and do not choose for the user.'
567
- ) {
568
- throw new CoachBookOutputError();
569
- }
570
- return {
571
- reason: enumText(source.reason, FBC_RECOMMENDATION_REASONS),
572
- question,
573
- choices: choices.map((choice) => ({
574
- choice_id: methodId(choice.choice_id),
575
- label: boundedText(choice.label, 1, 200),
576
- description: boundedText(choice.description, 4, 1000),
577
- })),
578
- instruction,
579
- };
580
- }
581
-
582
- function projectRecommendationBoundaries(value) {
583
- const source = record(value);
584
- const projected = {
585
- public_metadata_only: boolean(source.public_metadata_only),
586
- source_retrieval: boolean(source.source_retrieval),
587
- protected_spec_access: boolean(source.protected_spec_access),
588
- provider_use: boolean(source.provider_use),
589
- writes: boolean(source.writes),
590
- ask_the_book: text(source.ask_the_book),
591
- };
592
- if (
593
- projected.public_metadata_only !== true
594
- || projected.source_retrieval !== false
595
- || projected.protected_spec_access !== false
596
- || projected.provider_use !== false
597
- || projected.writes !== false
598
- || projected.ask_the_book !== 'closed_by_product_decision'
599
- ) {
600
- throw new CoachBookOutputError();
601
- }
602
- return projected;
603
- }
604
-
605
- function assertRecommendationSourceSafe(source) {
606
- const {
607
- registry_digest: registryDigest,
608
- candidate_set_digest: candidateSetDigest,
609
- goal_digest: goalDigest,
610
- ...publicCandidate
611
- } = source;
612
- const digests = [registryDigest, candidateSetDigest, goalDigest];
613
- const presentDigests = digests.filter((digest) => digest !== undefined);
614
- if (presentDigests.length !== 0 && presentDigests.length !== digests.length) {
615
- throw new CoachBookOutputError();
616
- }
617
- for (const digest of presentDigests) {
618
- if (typeof digest !== 'string' || !SHA256_RE.test(digest)) throw new CoachBookOutputError();
619
- }
620
- assertPublicOutput(publicCandidate);
621
- }
622
-
623
- function projectRecommendation(value) {
624
- assertRecommendationSourceSafe(value);
625
- if (
626
- value.schema_version !== 'fbc.method-recommendations.v1'
627
- || value.ranker_version !== FBC_METHOD_RANKER_VERSION
628
- ) {
629
- throw new CoachBookOutputError();
630
- }
631
- const outcome = enumText(value.outcome, FBC_RECOMMENDATION_OUTCOMES);
632
- const confidence = nullableEnumText(value.confidence, FBC_RECOMMENDATION_CONFIDENCE);
633
- const context = record(value.context_used);
634
- const methodStage = nullableEnumText(context.method_stage, FBC_RECOMMENDATION_STAGES);
635
- const maxMinutes = context.max_minutes === null || context.max_minutes === undefined
636
- ? null
637
- : integer(context.max_minutes);
638
- if (maxMinutes !== null && (maxMinutes < 5 || maxMinutes > 240)) {
639
- throw new CoachBookOutputError();
640
- }
641
- const matches = projectRecommendationMatches(value.matches, 'runnable');
642
- const pendingMatches = projectRecommendationMatches(value.pending_matches, 'pending');
643
- const matchIds = matches.map((match) => String(match.tool_spec_id));
644
- const pendingIds = pendingMatches.map((match) => String(match.tool_spec_id));
645
- if (pendingIds.some((toolSpecId) => matchIds.includes(toolSpecId))) {
646
- throw new CoachBookOutputError();
647
- }
648
- const selectedToolSpecId = value.selected_tool_spec_id === null
649
- ? null
650
- : methodId(value.selected_tool_spec_id);
651
- const clarification = projectRecommendationClarification(value.clarification);
652
- const clarificationChoices = clarification === null ? [] : records(clarification.choices);
653
- const clarificationBindsMatches = clarification !== null
654
- && clarificationChoices.length === matches.length
655
- && clarificationChoices.every((choice, index) => (
656
- choice.choice_id === matches[index]?.tool_spec_id
657
- && choice.description === matches[index]?.use_when
658
- ));
659
- const firstMatchId = matches[0]?.tool_spec_id ?? null;
660
- const outcomeIsValid = outcome === 'recommended'
661
- ? matches.length > 0
662
- && selectedToolSpecId === firstMatchId
663
- && (confidence === 'high' || confidence === 'medium')
664
- && value.clarification === null
665
- : outcome === 'clarification_required'
666
- ? matches.length > 0
667
- && selectedToolSpecId === null
668
- && confidence === 'low'
669
- && clarificationBindsMatches
670
- : outcome === 'no_runnable_match'
671
- ? matches.length === 0
672
- && pendingMatches.length > 0
673
- && selectedToolSpecId === null
674
- && value.confidence === null
675
- && value.clarification === null
676
- : matches.length === 0
677
- && pendingMatches.length === 0
678
- && selectedToolSpecId === null
679
- && value.confidence === null
680
- && value.clarification === null;
681
- if (!outcomeIsValid || (outcome === 'clarification_required') !== (clarification !== null)) {
682
- throw new CoachBookOutputError();
683
- }
684
- return {
685
- ok: true,
686
- schema_version: 'fbc.method-recommendations.v1',
687
- ranker_version: FBC_METHOD_RANKER_VERSION,
688
- outcome,
689
- confidence,
690
- context_used: {
691
- method_stage: methodStage,
692
- availability_scope: enumText(context.availability_scope, FBC_RECOMMENDATION_AVAILABILITY),
693
- max_minutes: maxMinutes,
694
- executor_kind: nullableEnumText(context.executor_kind, FBC_RECOMMENDATION_EXECUTORS),
695
- effect_class: nullableEnumText(context.effect_class, FBC_RECOMMENDATION_EFFECTS),
696
- },
697
- matches,
698
- selected_tool_spec_id: selectedToolSpecId,
699
- clarification,
700
- pending_matches: pendingMatches,
701
- boundaries: projectRecommendationBoundaries(value.boundaries),
702
- };
703
- }
704
- function projectError(source) {
705
- const projected = { error: text(source.error) };
706
- if (source.message !== undefined) projected.message = text(source.message);
707
- if (source.billing_url !== undefined) {
708
- const billingUrl = text(source.billing_url);
709
- if (!billingUrl.startsWith('https://')) throw new CoachBookOutputError();
710
- projected.billing_url = billingUrl;
711
- }
712
- return projected;
713
- }
714
-
715
- function projectConfirmation(source) {
716
- if (source.error !== 'confirmation_required' || typeof source.confirm_token !== 'string') {
717
- throw new CoachBookOutputError();
718
- }
719
- if (source.confirm_token.length < 1 || source.confirm_token.length > 4096 || !/^[A-Za-z0-9._-]+$/.test(source.confirm_token)) {
720
- throw new CoachBookOutputError();
721
- }
722
- if (hasForbiddenOutput(source, true)) throw new CoachBookOutputError();
723
- const projected = {
724
- error: 'confirmation_required',
725
- confirm_token: source.confirm_token,
726
- };
727
- for (const key of ['what_would_happen', 'message', 'note', 'why']) optionalText(projected, source, key);
728
- if (source.expires_in_seconds !== undefined) projected.expires_in_seconds = integer(source.expires_in_seconds);
729
- return projected;
730
- }
731
-
732
- export function projectCoachBookConnectorResult(kind, value) {
733
- if (!['catalog', 'describe', 'recommendation', 'run'].includes(kind)) throw new CoachBookOutputError();
734
- const source = record(value);
735
- const data = record(source.data);
736
- const status = integer(source.status);
737
- const connectorOk = boolean(source.ok);
738
- let projectedData;
739
- if (status === 409 && data.error === 'confirmation_required') {
740
- projectedData = projectConfirmation(data);
741
- } else {
742
- if (!(connectorOk && kind === 'recommendation')) assertPublicOutput(data);
743
- if (!connectorOk) {
744
- projectedData = projectError(data);
745
- } else if (data.ok !== true) {
746
- throw new CoachBookOutputError();
747
- } else if (kind === 'catalog') {
748
- projectedData = { ok: true, methods: records(data.methods).map(projectCatalogMethod) };
749
- } else if (kind === 'describe') {
750
- projectedData = { ok: true, method: projectDescribedMethod(data.method) };
751
- } else if (kind === 'recommendation') {
752
- projectedData = projectRecommendation(data);
753
- } else {
754
- const run = record(data.run);
755
- projectedData = { ok: true, run: run.method ? projectGenericRun(run) : projectNicheRun(run) };
756
- }
757
- assertPublicOutput(projectedData);
758
- }
759
- return { ok: connectorOk, status, data: projectedData };
760
- }
1
+ const FORBIDDEN_OUTPUT_KEYS = new Set([
2
+ 'credential', 'credentials', 'accesstoken', 'refreshtoken', 'apikey',
3
+ 'password', 'secret', 'secretvalue', 'clientsecret', 'token',
4
+ 'authorization', 'bearer', 'privatekey', 'signature',
5
+ 'protectedspec', 'sourcetext', 'chunktext', 'excerpt', 'embedding',
6
+ 'vector', 'vectors', 'queryvector', 'clausevector', 'clausevectors',
7
+ 'anchor', 'anchors', 'anchorid', 'anchorids', 'semantictext', 'semantictexts',
8
+ 'semanticmodel', 'modelinternals',
9
+ 'systemprompt', 'providerconfig', 'connectorconfig', 'runtimeadapterid',
10
+ 'adaptersecret', 'sourcecode', 'skillfile', 'skillfiles', 'skillpath',
11
+ 'sqlquery', 'formula', 'expression', 'when', 'left', 'right',
12
+ 'forbiddeneffects', 'sourcedocumentid', 'sourcesha256',
13
+ ]);
14
+
15
+ const SECRET_VALUE_PATTERNS = [
16
+ /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/i,
17
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/i,
18
+ /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
19
+ /\bsk-(?:proj-|svcacct-|ant-)?[A-Za-z0-9_-]{20,}\b/i,
20
+ /\bgithub_pat_[A-Za-z0-9_]{40,}\b/,
21
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/,
22
+ /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/i,
23
+ /\b(?:sk|rk)_live_[A-Za-z0-9]{16,}\b/i,
24
+ /\bAIza[0-9A-Za-z_-]{30,}\b/,
25
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
26
+ /\bEA[A-Za-z0-9]{35,}\b/,
27
+ /\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|password)\s*[:=]\s*["']?[A-Za-z0-9_+/.=-]{16,}/i,
28
+ /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\/[^:\s/]+:[^@\s/]{6,}@/i,
29
+ /[?&](?:access_token|refresh_token|api_key|token|signature|sig)=[A-Za-z0-9_+/.=-]{16,}/i,
30
+ ];
31
+
32
+ export class CoachBookOutputError extends Error {
33
+ constructor() {
34
+ super('The Fully Booked Coach response failed public-output verification.');
35
+ this.name = 'CoachBookOutputError';
36
+ }
37
+ }
38
+
39
+ function normalizedKey(key) {
40
+ return key.toLowerCase().replace(/[^a-z0-9]/g, '');
41
+ }
42
+
43
+ function record(value) {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new CoachBookOutputError();
45
+ return value;
46
+ }
47
+
48
+ function records(value) {
49
+ if (!Array.isArray(value)) throw new CoachBookOutputError();
50
+ return value.map(record);
51
+ }
52
+
53
+ function text(value) {
54
+ if (typeof value !== 'string') throw new CoachBookOutputError();
55
+ return value;
56
+ }
57
+
58
+ function integer(value) {
59
+ if (!Number.isSafeInteger(value)) throw new CoachBookOutputError();
60
+ return Number(value);
61
+ }
62
+
63
+ function number(value) {
64
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new CoachBookOutputError();
65
+ return value;
66
+ }
67
+
68
+ function boolean(value) {
69
+ if (typeof value !== 'boolean') throw new CoachBookOutputError();
70
+ return value;
71
+ }
72
+
73
+ function nullableText(value) {
74
+ if (value === null || value === undefined || value === '') return null;
75
+ return text(value);
76
+ }
77
+
78
+ function stringArray(value) {
79
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
80
+ throw new CoachBookOutputError();
81
+ }
82
+ return [...value];
83
+ }
84
+
85
+ function optionalNumber(target, source, key) {
86
+ if (source[key] !== undefined) target[key] = number(source[key]);
87
+ }
88
+
89
+ function optionalText(target, source, key) {
90
+ if (source[key] !== undefined) target[key] = text(source[key]);
91
+ }
92
+
93
+ function projectRanges(value) {
94
+ return records(value).map((range) => {
95
+ const currentLocator = range.start !== undefined || range.end !== undefined;
96
+ return currentLocator
97
+ ? { start: integer(range.start), end: integer(range.end) }
98
+ : { start: integer(range.pdfPageStart), end: integer(range.pdfPageEnd) };
99
+ });
100
+ }
101
+
102
+ function projectCitations(value) {
103
+ return records(value).map((citation) => ({
104
+ label: text(citation.label),
105
+ pdfPageStart: integer(citation.pdfPageStart),
106
+ pdfPageEnd: integer(citation.pdfPageEnd),
107
+ }));
108
+ }
109
+
110
+ function projectField(value) {
111
+ const source = record(value);
112
+ const projected = {
113
+ id: text(source.id),
114
+ type: text(source.type),
115
+ label: text(source.label),
116
+ required: boolean(source.required),
117
+ };
118
+ optionalText(projected, source, 'helpText');
119
+ for (const key of ['minimum', 'maximum', 'decimalPlaces', 'minimumItems', 'maximumItems']) {
120
+ optionalNumber(projected, source, key);
121
+ }
122
+ if (source.choices !== undefined) {
123
+ projected.choices = records(source.choices).map((choice) => ({
124
+ value: text(choice.value),
125
+ label: text(choice.label),
126
+ }));
127
+ }
128
+ return projected;
129
+ }
130
+
131
+ function projectPublicSpec(value) {
132
+ const source = record(value);
133
+ const projected = {
134
+ schemaVersion: text(source.schemaVersion),
135
+ title: text(source.title),
136
+ summary: text(source.summary),
137
+ category: text(source.category),
138
+ estimatedMinutes: integer(source.estimatedMinutes),
139
+ executorKind: text(source.executorKind),
140
+ effectClass: text(source.effectClass),
141
+ steps: records(source.steps).map((step) => ({
142
+ id: text(step.id),
143
+ title: text(step.title),
144
+ description: text(step.description),
145
+ fields: records(step.fields).map(projectField),
146
+ })),
147
+ artifactSections: records(source.artifactSections).map((section) => ({
148
+ id: text(section.id),
149
+ title: text(section.title),
150
+ fieldIds: stringArray(section.fieldIds),
151
+ calculationIds: stringArray(section.calculationIds),
152
+ })),
153
+ sourceCitations: projectCitations(source.sourceCitations),
154
+ };
155
+ optionalText(projected, source, 'availabilityClass');
156
+ return projected;
157
+ }
158
+
159
+ function projectCatalogMethod(value) {
160
+ const source = record(value);
161
+ return {
162
+ tool_spec_id: text(source.tool_spec_id),
163
+ version: integer(source.version),
164
+ title: text(source.title),
165
+ sequence: integer(source.sequence),
166
+ summary: text(source.summary),
167
+ category: text(source.category),
168
+ estimated_minutes: integer(source.estimated_minutes),
169
+ implementation_status: text(source.implementation_status),
170
+ availability_class: text(source.availability_class),
171
+ executor_kind: text(source.executor_kind),
172
+ effect_class: text(source.effect_class),
173
+ runnable: boolean(source.runnable),
174
+ specialized: boolean(source.specialized),
175
+ source_page_ranges: projectRanges(source.source_page_ranges),
176
+ };
177
+ }
178
+
179
+ function projectDescribedMethod(value) {
180
+ const source = record(value);
181
+ return { ...projectCatalogMethod(source), public_spec: projectPublicSpec(source.public_spec) };
182
+ }
183
+
184
+ function projectSummary(value) {
185
+ const source = record(value);
186
+ return {
187
+ run_id: text(source.run_id),
188
+ status: text(source.status),
189
+ tool_spec_id: text(source.tool_spec_id),
190
+ tool_spec_version: integer(source.tool_spec_version),
191
+ artifact_count: integer(source.artifact_count),
192
+ decision_count: integer(source.decision_count),
193
+ reserved_charge_usd_micros: source.reserved_charge_usd_micros === undefined
194
+ ? 0
195
+ : integer(source.reserved_charge_usd_micros),
196
+ retained_provider_cost_usd_micros: source.retained_provider_cost_usd_micros === undefined
197
+ ? 0
198
+ : integer(source.retained_provider_cost_usd_micros),
199
+ charged_usd_micros: source.charged_usd_micros === undefined
200
+ ? 0
201
+ : integer(source.charged_usd_micros),
202
+ released_usd_micros: source.released_usd_micros === undefined
203
+ ? 0
204
+ : integer(source.released_usd_micros),
205
+ approved_at: nullableText(source.approved_at),
206
+ started_at: nullableText(source.started_at),
207
+ settled_at: nullableText(source.settled_at),
208
+ created_at: nullableText(source.created_at),
209
+ };
210
+ }
211
+
212
+ function projectArtifactValue(value) {
213
+ if (typeof value === 'string' || typeof value === 'boolean') return value;
214
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
215
+ return stringArray(value);
216
+ }
217
+
218
+ function projectArtifact(value) {
219
+ if (value === null || value === undefined) return null;
220
+ const source = record(value);
221
+ return {
222
+ title: text(source.title),
223
+ sections: records(source.sections).map((section) => ({
224
+ id: text(section.id),
225
+ title: text(section.title),
226
+ values: records(section.values).map((item) => ({
227
+ id: text(item.id),
228
+ label: text(item.label),
229
+ value: projectArtifactValue(item.value),
230
+ })),
231
+ })),
232
+ sourceCitations: projectCitations(source.sourceCitations),
233
+ };
234
+ }
235
+
236
+ function projectBoundaries(value) {
237
+ const source = record(value);
238
+ return {
239
+ providerUse: boolean(source.providerUse),
240
+ spend: boolean(source.spend),
241
+ contact: boolean(source.contact),
242
+ publication: boolean(source.publication),
243
+ durableCrmChange: boolean(source.durableCrmChange),
244
+ connectorDispatch: boolean(source.connectorDispatch),
245
+ outcomePromise: boolean(source.outcomePromise),
246
+ separateApprovalRequired: boolean(source.separateApprovalRequired),
247
+ };
248
+ }
249
+
250
+ function projectMethodResult(value) {
251
+ if (value === null || value === undefined) return null;
252
+ const source = record(value);
253
+ return {
254
+ schemaVersion: text(source.schemaVersion),
255
+ toolSpecId: text(source.toolSpecId),
256
+ toolSpecVersion: integer(source.toolSpecVersion),
257
+ status: text(source.status),
258
+ completedStepIds: stringArray(source.completedStepIds),
259
+ nextStepId: nullableText(source.nextStepId),
260
+ calculations: records(source.calculations).map((calculation) => ({
261
+ id: text(calculation.id),
262
+ label: text(calculation.label),
263
+ rawValue: number(calculation.rawValue),
264
+ formattedValue: text(calculation.formattedValue),
265
+ })),
266
+ decisions: records(source.decisions).map((decision) => ({
267
+ id: text(decision.id),
268
+ label: text(decision.label),
269
+ outcome: text(decision.outcome),
270
+ guidance: text(decision.guidance),
271
+ })),
272
+ artifact: projectArtifact(source.artifact),
273
+ boundaries: projectBoundaries(source.boundaries),
274
+ };
275
+ }
276
+
277
+ function projectReceipt(value) {
278
+ if (value === null || value === undefined) return null;
279
+ const source = record(value);
280
+ const projected = {
281
+ outcome: text(source.outcome),
282
+ retained_provider_cost_usd_micros: integer(source.retained_provider_cost_usd_micros),
283
+ charged_usd_micros: integer(source.charged_usd_micros),
284
+ };
285
+ for (const key of ['released_usd_micros', 'promo_released_usd_micros', 'paid_released_usd_micros', 'markup_multiplier']) {
286
+ if (source[key] !== undefined) projected[key] = integer(source[key]);
287
+ }
288
+ for (const key of ['provider_use', 'external_effect_performed']) {
289
+ if (source[key] !== undefined) projected[key] = boolean(source[key]);
290
+ }
291
+ if (source.created_at !== undefined) projected.created_at = nullableText(source.created_at);
292
+ return projected;
293
+ }
294
+
295
+ function projectGenericRun(value) {
296
+ const source = record(value);
297
+ const method = record(source.method);
298
+ const progress = record(source.progress);
299
+ return {
300
+ summary: projectSummary(source.summary),
301
+ method: {
302
+ tool_spec_id: text(method.tool_spec_id),
303
+ method_id: text(method.method_id),
304
+ sequence: integer(method.sequence),
305
+ version: integer(method.version),
306
+ title: text(method.title),
307
+ executor_kind: text(method.executor_kind),
308
+ effect_class: text(method.effect_class),
309
+ public_spec: projectPublicSpec(method.public_spec),
310
+ source_page_ranges: projectRanges(method.source_page_ranges),
311
+ },
312
+ progress: {
313
+ completed_step_ids: stringArray(progress.completed_step_ids),
314
+ next_step_id: nullableText(progress.next_step_id),
315
+ result: projectMethodResult(progress.result),
316
+ settlement_pending: boolean(progress.settlement_pending),
317
+ },
318
+ receipt: projectReceipt(source.receipt),
319
+ };
320
+ }
321
+
322
+ function projectNicheDimension(value) {
323
+ const source = record(value);
324
+ const threshold = record(source.threshold);
325
+ const evidenceGate = record(source.evidenceGate);
326
+ return {
327
+ dimension: text(source.dimension),
328
+ score: number(source.score),
329
+ threshold: {
330
+ operator: text(threshold.operator),
331
+ value: number(threshold.value),
332
+ sourcePage: integer(threshold.sourcePage),
333
+ },
334
+ thresholdPassed: boolean(source.thresholdPassed),
335
+ falsePositiveObserved: boolean(source.falsePositiveObserved),
336
+ passed: boolean(source.passed),
337
+ confidence: text(source.confidence),
338
+ evidenceGate: {
339
+ directSourcePresent: boolean(evidenceGate.directSourcePresent),
340
+ counterevidenceCompleted: boolean(evidenceGate.counterevidenceCompleted),
341
+ passed: boolean(evidenceGate.passed),
342
+ failures: stringArray(evidenceGate.failures),
343
+ },
344
+ };
345
+ }
346
+
347
+ function projectNicheResult(value) {
348
+ const source = record(value);
349
+ const dimensions = record(source.dimensions);
350
+ const plan = record(source.validationPlan);
351
+ const pace = record(plan.paceObservationWeeks);
352
+ const stopRules = record(plan.falsePositiveStopRules);
353
+ const boundaries = record(source.boundaries);
354
+ return {
355
+ schemaVersion: text(source.schemaVersion),
356
+ dimensions: {
357
+ findability: projectNicheDimension(dimensions.findability),
358
+ bragability: projectNicheDimension(dimensions.bragability),
359
+ sellability: projectNicheDimension(dimensions.sellability),
360
+ },
361
+ overallStatus: text(source.overallStatus),
362
+ confidence: text(source.confidence),
363
+ recommendation: text(source.recommendation),
364
+ validationPlan: {
365
+ durationDays: integer(plan.durationDays),
366
+ paceObservationWeeks: { minimum: integer(pace.minimum), maximum: integer(pace.maximum) },
367
+ weeklyAcceptedCollaborationTarget: integer(plan.weeklyAcceptedCollaborationTarget),
368
+ approximateAnnualCollaborationPace: integer(plan.approximateAnnualCollaborationPace),
369
+ brandAssetsRequiredBeforeTest: boolean(plan.brandAssetsRequiredBeforeTest),
370
+ trackedSignals: stringArray(plan.trackedSignals),
371
+ falsePositiveStopRules: {
372
+ findability: text(stopRules.findability),
373
+ bragability: text(stopRules.bragability),
374
+ sellability: text(stopRules.sellability),
375
+ },
376
+ humanDecisionRequired: boolean(plan.humanDecisionRequired),
377
+ separateApprovalRequiredFor: stringArray(plan.separateApprovalRequiredFor),
378
+ performsExternalAction: boolean(plan.performsExternalAction),
379
+ promisesOutcome: boolean(plan.promisesOutcome),
380
+ sourcePages: Array.isArray(plan.sourcePages) ? plan.sourcePages.map(integer) : (() => { throw new CoachBookOutputError(); })(),
381
+ sourceNote: text(plan.sourceNote),
382
+ },
383
+ boundaries: {
384
+ modelScoreAllowed: boolean(boundaries.modelScoreAllowed),
385
+ providerUse: boolean(boundaries.providerUse),
386
+ spend: boolean(boundaries.spend),
387
+ contact: boolean(boundaries.contact),
388
+ publication: boolean(boundaries.publication),
389
+ outcomePromise: boolean(boundaries.outcomePromise),
390
+ },
391
+ };
392
+ }
393
+
394
+ function projectNicheRun(value) {
395
+ const source = record(value);
396
+ const artifact = source.artifact === null || source.artifact === undefined ? null : record(source.artifact);
397
+ const decision = source.decision === null || source.decision === undefined ? null : record(source.decision);
398
+ return {
399
+ summary: projectSummary(source.summary),
400
+ artifact: artifact ? {
401
+ id: text(artifact.id),
402
+ artifact_type: text(artifact.artifact_type),
403
+ result: projectNicheResult(artifact.result),
404
+ source_citations: records(artifact.source_citations).map((citation) => ({
405
+ locator: text(citation.locator),
406
+ page_start: integer(citation.page_start),
407
+ page_end: integer(citation.page_end),
408
+ supports: text(citation.supports),
409
+ })),
410
+ resume_supported_surfaces: stringArray(artifact.resume_supported_surfaces),
411
+ created_at: nullableText(artifact.created_at),
412
+ } : null,
413
+ decision: decision ? {
414
+ id: text(decision.id),
415
+ decision_type: text(decision.decision_type),
416
+ decision_status: text(decision.decision_status),
417
+ decision_payload: {
418
+ confirmed_by_user: boolean(record(decision.decision_payload).confirmed_by_user),
419
+ tool_spec_id: text(record(decision.decision_payload).tool_spec_id),
420
+ result_status: text(record(decision.decision_payload).result_status),
421
+ performs_external_action: boolean(record(decision.decision_payload).performs_external_action),
422
+ },
423
+ created_at: nullableText(decision.created_at),
424
+ } : null,
425
+ receipt: projectReceipt(source.receipt),
426
+ };
427
+ }
428
+
429
+ function hasForbiddenOutput(value, allowConfirmationToken = false) {
430
+ if (typeof value === 'string') return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
431
+ if (Array.isArray(value)) return value.some((entry) => hasForbiddenOutput(entry, false));
432
+ if (!value || typeof value !== 'object') return false;
433
+ return Object.entries(value).some(([key, child]) => {
434
+ const normalized = normalizedKey(key);
435
+ const allowedToken = allowConfirmationToken && normalized === 'confirmtoken';
436
+ if (!allowedToken && (
437
+ FORBIDDEN_OUTPUT_KEYS.has(normalized)
438
+ || normalized.endsWith('token')
439
+ || normalized.endsWith('secret')
440
+ || normalized.endsWith('password')
441
+ || normalized.endsWith('privatekey')
442
+ || normalized.endsWith('digest')
443
+ )) return true;
444
+ return allowedToken ? false : hasForbiddenOutput(child, false);
445
+ });
446
+ }
447
+
448
+ function assertPublicOutput(value) {
449
+ if (hasForbiddenOutput(value)) throw new CoachBookOutputError();
450
+ }
451
+
452
+ const FBC_METHOD_ID_RE = /^fbc[.]method[.][0-9]{3}[.][a-z0-9]+(?:-[a-z0-9]+)*$/;
453
+ export const FBC_METHOD_RANKER_VERSION = 'fbc.method-ranker.semantic-hybrid-v2';
454
+ const FBC_RECOMMENDATION_OUTCOMES = new Set([
455
+ 'recommended', 'clarification_required', 'no_runnable_match', 'no_match',
456
+ ]);
457
+ const FBC_RECOMMENDATION_CONFIDENCE = new Set(['high', 'medium', 'low']);
458
+ const FBC_RECOMMENDATION_AVAILABILITY = new Set(['runnable_only', 'include_pending']);
459
+ const FBC_RECOMMENDATION_EXECUTORS = new Set(['deterministic_single_step', 'guided_multi_step']);
460
+ const FBC_RECOMMENDATION_EFFECTS = new Set(['none', 'action_preview']);
461
+ const FBC_RECOMMENDATION_STAGES = new Set([
462
+ 'purpose_and_capacity', 'oath', 'niche', 'collaboration', 'authority_content',
463
+ 'audience_growth', 'strategy_session', 'offer_and_package', 'follow_up',
464
+ 'client_map', 'super_coaching_delivery', 'results_and_proof', 'renewal_and_referral',
465
+ ]);
466
+ const FBC_RECOMMENDATION_REASONS = new Set(['goal_too_broad', 'close_scores', 'low_confidence']);
467
+ const FBC_RECOMMENDATION_STATUSES = new Set(['implemented', 'method_owner_accepted', 'live_proven']);
468
+ const SHA256_RE = /^[0-9a-f]{64}$/;
469
+
470
+ function boundedText(value, minimum, maximum) {
471
+ const result = text(value);
472
+ if (result.length < minimum || result.length > maximum) throw new CoachBookOutputError();
473
+ return result;
474
+ }
475
+
476
+ function enumText(value, allowed) {
477
+ const result = text(value);
478
+ if (!allowed.has(result)) throw new CoachBookOutputError();
479
+ return result;
480
+ }
481
+
482
+ function nullableEnumText(value, allowed) {
483
+ if (value === null || value === undefined) return null;
484
+ return enumText(value, allowed);
485
+ }
486
+
487
+ function methodId(value) {
488
+ const result = text(value);
489
+ if (!FBC_METHOD_ID_RE.test(result)) throw new CoachBookOutputError();
490
+ return result;
491
+ }
492
+
493
+ function projectRecommendationMatch(value, expectedRank, lane) {
494
+ const source = record(value);
495
+ const rank = integer(source.rank);
496
+ const estimatedMinutes = integer(source.estimated_minutes);
497
+ const score = number(source.score);
498
+ if (
499
+ rank !== expectedRank
500
+ || estimatedMinutes < 1
501
+ || estimatedMinutes > 240
502
+ || score < 0
503
+ || score > 1
504
+ ) {
505
+ throw new CoachBookOutputError();
506
+ }
507
+ const matchedIntents = stringArray(source.matched_intents);
508
+ if (
509
+ matchedIntents.length > 6
510
+ || matchedIntents.some((intent) => intent.length < 1 || intent.length > 80)
511
+ ) {
512
+ throw new CoachBookOutputError();
513
+ }
514
+ const sourcePageRanges = projectRanges(source.source_page_ranges);
515
+ if (sourcePageRanges.length > 20) throw new CoachBookOutputError();
516
+ const implementationStatus = enumText(source.implementation_status, FBC_RECOMMENDATION_STATUSES);
517
+ const runnable = boolean(source.runnable);
518
+ const canStart = boolean(source.can_start);
519
+ const laneIsValid = lane === 'runnable'
520
+ ? ['method_owner_accepted', 'live_proven'].includes(implementationStatus)
521
+ && runnable === true
522
+ && canStart === true
523
+ : implementationStatus === 'implemented'
524
+ && runnable === false
525
+ && canStart === false;
526
+ if (!laneIsValid) throw new CoachBookOutputError();
527
+ return {
528
+ rank,
529
+ tool_spec_id: methodId(source.tool_spec_id),
530
+ title: boundedText(source.title, 3, 200),
531
+ summary: boundedText(source.summary, 10, 1200),
532
+ category: boundedText(source.category, 2, 160),
533
+ estimated_minutes: estimatedMinutes,
534
+ executor_kind: enumText(source.executor_kind, FBC_RECOMMENDATION_EXECUTORS),
535
+ effect_class: enumText(source.effect_class, FBC_RECOMMENDATION_EFFECTS),
536
+ implementation_status: implementationStatus,
537
+ runnable,
538
+ can_start: canStart,
539
+ score,
540
+ matched_intents: matchedIntents,
541
+ use_when: boundedText(source.use_when, 4, 1000),
542
+ source_page_ranges: sourcePageRanges,
543
+ };
544
+ }
545
+
546
+ function projectRecommendationMatches(value, lane) {
547
+ const source = records(value);
548
+ if (source.length > 3) throw new CoachBookOutputError();
549
+ const projected = source.map((match, index) => projectRecommendationMatch(match, index + 1, lane));
550
+ const toolSpecIds = projected.map((match) => String(match.tool_spec_id));
551
+ const ranks = projected.map((match) => Number(match.rank));
552
+ if (new Set(toolSpecIds).size !== toolSpecIds.length || new Set(ranks).size !== ranks.length) {
553
+ throw new CoachBookOutputError();
554
+ }
555
+ return projected;
556
+ }
557
+ function projectRecommendationClarification(value) {
558
+ if (value === null || value === undefined) return null;
559
+ const source = record(value);
560
+ const choices = records(source.choices);
561
+ if (choices.length < 1 || choices.length > 3) throw new CoachBookOutputError();
562
+ const question = text(source.question);
563
+ const instruction = text(source.instruction);
564
+ if (
565
+ question !== 'Which result do you want right now?'
566
+ || instruction !== 'Ask this question exactly and do not choose for the user.'
567
+ ) {
568
+ throw new CoachBookOutputError();
569
+ }
570
+ return {
571
+ reason: enumText(source.reason, FBC_RECOMMENDATION_REASONS),
572
+ question,
573
+ choices: choices.map((choice) => ({
574
+ choice_id: methodId(choice.choice_id),
575
+ label: boundedText(choice.label, 1, 200),
576
+ description: boundedText(choice.description, 4, 1000),
577
+ })),
578
+ instruction,
579
+ };
580
+ }
581
+
582
+ function projectRecommendationBoundaries(value) {
583
+ const source = record(value);
584
+ const projected = {
585
+ public_metadata_only: boolean(source.public_metadata_only),
586
+ source_retrieval: boolean(source.source_retrieval),
587
+ protected_spec_access: boolean(source.protected_spec_access),
588
+ provider_use: boolean(source.provider_use),
589
+ writes: boolean(source.writes),
590
+ ask_the_book: text(source.ask_the_book),
591
+ };
592
+ if (
593
+ projected.public_metadata_only !== true
594
+ || projected.source_retrieval !== false
595
+ || projected.protected_spec_access !== false
596
+ || projected.provider_use !== false
597
+ || projected.writes !== false
598
+ || projected.ask_the_book !== 'closed_by_product_decision'
599
+ ) {
600
+ throw new CoachBookOutputError();
601
+ }
602
+ return projected;
603
+ }
604
+
605
+ function assertRecommendationSourceSafe(source) {
606
+ const {
607
+ registry_digest: registryDigest,
608
+ candidate_set_digest: candidateSetDigest,
609
+ goal_digest: goalDigest,
610
+ ...publicCandidate
611
+ } = source;
612
+ const digests = [registryDigest, candidateSetDigest, goalDigest];
613
+ const presentDigests = digests.filter((digest) => digest !== undefined);
614
+ if (presentDigests.length !== 0 && presentDigests.length !== digests.length) {
615
+ throw new CoachBookOutputError();
616
+ }
617
+ for (const digest of presentDigests) {
618
+ if (typeof digest !== 'string' || !SHA256_RE.test(digest)) throw new CoachBookOutputError();
619
+ }
620
+ assertPublicOutput(publicCandidate);
621
+ }
622
+
623
+ function projectRecommendation(value) {
624
+ assertRecommendationSourceSafe(value);
625
+ if (
626
+ value.schema_version !== 'fbc.method-recommendations.v1'
627
+ || value.ranker_version !== FBC_METHOD_RANKER_VERSION
628
+ ) {
629
+ throw new CoachBookOutputError();
630
+ }
631
+ const outcome = enumText(value.outcome, FBC_RECOMMENDATION_OUTCOMES);
632
+ const confidence = nullableEnumText(value.confidence, FBC_RECOMMENDATION_CONFIDENCE);
633
+ const context = record(value.context_used);
634
+ const methodStage = nullableEnumText(context.method_stage, FBC_RECOMMENDATION_STAGES);
635
+ const maxMinutes = context.max_minutes === null || context.max_minutes === undefined
636
+ ? null
637
+ : integer(context.max_minutes);
638
+ if (maxMinutes !== null && (maxMinutes < 5 || maxMinutes > 240)) {
639
+ throw new CoachBookOutputError();
640
+ }
641
+ const matches = projectRecommendationMatches(value.matches, 'runnable');
642
+ const pendingMatches = projectRecommendationMatches(value.pending_matches, 'pending');
643
+ const matchIds = matches.map((match) => String(match.tool_spec_id));
644
+ const pendingIds = pendingMatches.map((match) => String(match.tool_spec_id));
645
+ if (pendingIds.some((toolSpecId) => matchIds.includes(toolSpecId))) {
646
+ throw new CoachBookOutputError();
647
+ }
648
+ const selectedToolSpecId = value.selected_tool_spec_id === null
649
+ ? null
650
+ : methodId(value.selected_tool_spec_id);
651
+ const clarification = projectRecommendationClarification(value.clarification);
652
+ const clarificationChoices = clarification === null ? [] : records(clarification.choices);
653
+ const clarificationBindsMatches = clarification !== null
654
+ && clarificationChoices.length === matches.length
655
+ && clarificationChoices.every((choice, index) => (
656
+ choice.choice_id === matches[index]?.tool_spec_id
657
+ && choice.description === matches[index]?.use_when
658
+ ));
659
+ const firstMatchId = matches[0]?.tool_spec_id ?? null;
660
+ const outcomeIsValid = outcome === 'recommended'
661
+ ? matches.length > 0
662
+ && selectedToolSpecId === firstMatchId
663
+ && (confidence === 'high' || confidence === 'medium')
664
+ && value.clarification === null
665
+ : outcome === 'clarification_required'
666
+ ? matches.length > 0
667
+ && selectedToolSpecId === null
668
+ && confidence === 'low'
669
+ && clarificationBindsMatches
670
+ : outcome === 'no_runnable_match'
671
+ ? matches.length === 0
672
+ && pendingMatches.length > 0
673
+ && selectedToolSpecId === null
674
+ && value.confidence === null
675
+ && value.clarification === null
676
+ : matches.length === 0
677
+ && pendingMatches.length === 0
678
+ && selectedToolSpecId === null
679
+ && value.confidence === null
680
+ && value.clarification === null;
681
+ if (!outcomeIsValid || (outcome === 'clarification_required') !== (clarification !== null)) {
682
+ throw new CoachBookOutputError();
683
+ }
684
+ return {
685
+ ok: true,
686
+ schema_version: 'fbc.method-recommendations.v1',
687
+ ranker_version: FBC_METHOD_RANKER_VERSION,
688
+ outcome,
689
+ confidence,
690
+ context_used: {
691
+ method_stage: methodStage,
692
+ availability_scope: enumText(context.availability_scope, FBC_RECOMMENDATION_AVAILABILITY),
693
+ max_minutes: maxMinutes,
694
+ executor_kind: nullableEnumText(context.executor_kind, FBC_RECOMMENDATION_EXECUTORS),
695
+ effect_class: nullableEnumText(context.effect_class, FBC_RECOMMENDATION_EFFECTS),
696
+ },
697
+ matches,
698
+ selected_tool_spec_id: selectedToolSpecId,
699
+ clarification,
700
+ pending_matches: pendingMatches,
701
+ boundaries: projectRecommendationBoundaries(value.boundaries),
702
+ };
703
+ }
704
+ function projectError(source) {
705
+ const projected = { error: text(source.error) };
706
+ if (source.message !== undefined) projected.message = text(source.message);
707
+ if (source.billing_url !== undefined) {
708
+ const billingUrl = text(source.billing_url);
709
+ if (!billingUrl.startsWith('https://')) throw new CoachBookOutputError();
710
+ projected.billing_url = billingUrl;
711
+ }
712
+ return projected;
713
+ }
714
+
715
+ function projectConfirmation(source) {
716
+ if (source.error !== 'confirmation_required' || typeof source.confirm_token !== 'string') {
717
+ throw new CoachBookOutputError();
718
+ }
719
+ if (source.confirm_token.length < 1 || source.confirm_token.length > 4096 || !/^[A-Za-z0-9._-]+$/.test(source.confirm_token)) {
720
+ throw new CoachBookOutputError();
721
+ }
722
+ if (hasForbiddenOutput(source, true)) throw new CoachBookOutputError();
723
+ const projected = {
724
+ error: 'confirmation_required',
725
+ confirm_token: source.confirm_token,
726
+ };
727
+ for (const key of ['what_would_happen', 'message', 'note', 'why']) optionalText(projected, source, key);
728
+ if (source.expires_in_seconds !== undefined) projected.expires_in_seconds = integer(source.expires_in_seconds);
729
+ return projected;
730
+ }
731
+
732
+ export function projectCoachBookConnectorResult(kind, value) {
733
+ if (!['catalog', 'describe', 'recommendation', 'run'].includes(kind)) throw new CoachBookOutputError();
734
+ const source = record(value);
735
+ const data = record(source.data);
736
+ const status = integer(source.status);
737
+ const connectorOk = boolean(source.ok);
738
+ let projectedData;
739
+ if (status === 409 && data.error === 'confirmation_required') {
740
+ projectedData = projectConfirmation(data);
741
+ } else {
742
+ if (!(connectorOk && kind === 'recommendation')) assertPublicOutput(data);
743
+ if (!connectorOk) {
744
+ projectedData = projectError(data);
745
+ } else if (data.ok !== true) {
746
+ throw new CoachBookOutputError();
747
+ } else if (kind === 'catalog') {
748
+ projectedData = { ok: true, methods: records(data.methods).map(projectCatalogMethod) };
749
+ } else if (kind === 'describe') {
750
+ projectedData = { ok: true, method: projectDescribedMethod(data.method) };
751
+ } else if (kind === 'recommendation') {
752
+ projectedData = projectRecommendation(data);
753
+ } else {
754
+ const run = record(data.run);
755
+ projectedData = { ok: true, run: run.method ? projectGenericRun(run) : projectNicheRun(run) };
756
+ }
757
+ assertPublicOutput(projectedData);
758
+ }
759
+ return { ok: connectorOk, status, data: projectedData };
760
+ }