@bluestep-systems/bspecs 0.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 (83) hide show
  1. package/README.md +129 -0
  2. package/cli.js +74 -0
  3. package/package.json +30 -0
  4. package/src/prompts.js +74 -0
  5. package/src/scaffold.js +152 -0
  6. package/src/sync.js +123 -0
  7. package/src/utils.js +95 -0
  8. package/templates/claude/agents/b6p-code-review.md +81 -0
  9. package/templates/claude/agents/b6p-commenter.md +59 -0
  10. package/templates/claude/agents/b6p-task-implementer.md +77 -0
  11. package/templates/claude/hooks/block-generated-files.sh +16 -0
  12. package/templates/claude/hooks/block-tsc.sh +16 -0
  13. package/templates/claude/hooks/prettier-on-save.sh +21 -0
  14. package/templates/claude/instructions/b6p-platform.md.template +185 -0
  15. package/templates/claude/instructions/bsjs-development.md.template +430 -0
  16. package/templates/claude/instructions/conventions/always-snapshot.md.template +25 -0
  17. package/templates/claude/instructions/conventions/blueiq-no-ai-branding.md.template +11 -0
  18. package/templates/claude/instructions/conventions/date-format.md.template +27 -0
  19. package/templates/claude/instructions/conventions/endpoint-approach.md.template +9 -0
  20. package/templates/claude/instructions/conventions/formula-patterns.md.template +71 -0
  21. package/templates/claude/instructions/conventions/no-global-dollar.md.template +9 -0
  22. package/templates/claude/instructions/conventions/push-inner-draft.md.template +21 -0
  23. package/templates/claude/instructions/conventions/separate-files.md.template +17 -0
  24. package/templates/claude/instructions/conventions/single-script.md.template +28 -0
  25. package/templates/claude/instructions/conventions/snapshot-integrity.md.template +23 -0
  26. package/templates/claude/instructions/conventions/top-level-const-tdz.md.template +33 -0
  27. package/templates/claude/instructions/conventions/ts-in-template-literal.md.template +48 -0
  28. package/templates/claude/instructions/conventions/tsc-rootdir.md.template +17 -0
  29. package/templates/claude/instructions/gotchas/common-gotchas.md.template +91 -0
  30. package/templates/claude/instructions/gotchas/fetched-resource-code.md.template +9 -0
  31. package/templates/claude/instructions/index.md.template +82 -0
  32. package/templates/claude/instructions/reference/api-patterns.md.template +487 -0
  33. package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template +31 -0
  34. package/templates/claude/instructions/reference/chronounit-months.md.template +37 -0
  35. package/templates/claude/instructions/reference/code-patterns.md.template +265 -0
  36. package/templates/claude/instructions/reference/component-library.md.template +217 -0
  37. package/templates/claude/instructions/reference/crm-dashboard-inspo.md.template +17 -0
  38. package/templates/claude/instructions/reference/csv-parsing.md.template +18 -0
  39. package/templates/claude/instructions/reference/dashboard-design-system.md.template +38 -0
  40. package/templates/claude/instructions/reference/datetime-field-write.md.template +27 -0
  41. package/templates/claude/instructions/reference/design-system.md.template +150 -0
  42. package/templates/claude/instructions/reference/dpn-dashboard-framework.md.template +29 -0
  43. package/templates/claude/instructions/reference/endpoint-method-call.md.template +10 -0
  44. package/templates/claude/instructions/reference/endpoint-no-delete-method.md.template +9 -0
  45. package/templates/claude/instructions/reference/endpoint-output-channel.md.template +23 -0
  46. package/templates/claude/instructions/reference/endpoint-urls.md.template +15 -0
  47. package/templates/claude/instructions/reference/entry-delete.md.template +40 -0
  48. package/templates/claude/instructions/reference/file-execution.md.template +113 -0
  49. package/templates/claude/instructions/reference/http-requester.md.template +37 -0
  50. package/templates/claude/instructions/reference/id-full-vs-short.md.template +15 -0
  51. package/templates/claude/instructions/reference/internal-loopback-fetch.md.template +24 -0
  52. package/templates/claude/instructions/reference/localdate-parse.md.template +16 -0
  53. package/templates/claude/instructions/reference/merge-report-memo-json.md.template +25 -0
  54. package/templates/claude/instructions/reference/merge-report-static-index.md.template +29 -0
  55. package/templates/claude/instructions/reference/merge-report-urls.md.template +67 -0
  56. package/templates/claude/instructions/reference/multi-entry-in-multi-entry.md.template +21 -0
  57. package/templates/claude/instructions/reference/named-controls-submit.md.template +11 -0
  58. package/templates/claude/instructions/reference/new-entry-id.md.template +30 -0
  59. package/templates/claude/instructions/reference/relationship-field-set.md.template +37 -0
  60. package/templates/claude/instructions/reference/send-message-abort.md.template +37 -0
  61. package/templates/claude/instructions/reference/session-cookie-forwarding.md.template +31 -0
  62. package/templates/claude/instructions/reference/singleselect-null-copy.md.template +21 -0
  63. package/templates/claude/instructions/reference/staff-query-permission-gating.md.template +27 -0
  64. package/templates/claude/instructions/reference/timefield-vs-datetimefield.md.template +13 -0
  65. package/templates/claude/instructions/reference/user-zone-id.md.template +16 -0
  66. package/templates/claude/settings.json.template +46 -0
  67. package/templates/claude/skills/b6p-audit/SKILL.md +82 -0
  68. package/templates/claude/skills/b6p-pull/SKILL.md +123 -0
  69. package/templates/claude/skills/b6p-push/SKILL.md +70 -0
  70. package/templates/claude/skills/bug-fix/SKILL.md +28 -0
  71. package/templates/claude/skills/spec-create/SKILL.md +60 -0
  72. package/templates/claude/skills/spec-execute/SKILL.md +51 -0
  73. package/templates/claude/skills/spec-status/SKILL.md +20 -0
  74. package/templates/claude/skills/task-comment/SKILL.md +96 -0
  75. package/templates/claude/spec-templates/design.template.md +36 -0
  76. package/templates/claude/spec-templates/requirements.template.md +26 -0
  77. package/templates/claude/spec-templates/tasks.template.md +37 -0
  78. package/templates/module/README.md.template +46 -0
  79. package/templates/root/.gitignore.template +14 -0
  80. package/templates/root/.prettierrc.template +8 -0
  81. package/templates/root/CLAUDE.md.template +157 -0
  82. package/templates/root/README.md.template +58 -0
  83. package/templates/root/package.json.template +15 -0
@@ -0,0 +1,487 @@
1
+ # API Patterns
2
+
3
+ > **Note**: for the complete API reference, see `declarations/B.d.ts`. This file contains usage patterns and examples, not API documentation.
4
+
5
+ ## Contents
6
+
7
+ - [Working with Java Optionals](#working-with-java-optionals)
8
+ - [Field Types and Access Patterns](#field-types-and-access-patterns)
9
+ - [mergeTag() Method](#mergetag-method)
10
+ - [Safe Field Value Extraction](#safe-field-value-extraction)
11
+ - [Record and Form Access](#record-and-form-access)
12
+ - [Multi-Entry Form (MEF) Entries](#multi-entry-form-mef-entries)
13
+ - [Query Access Patterns](#query-access-patterns)
14
+ - [Java Collections](#java-collections)
15
+ - [Writing Date and DateTime Fields](#writing-date-and-datetime-fields)
16
+ - [Committing Writes and Formula Triggers](#committing-writes-and-formula-triggers)
17
+ - [Endpoint (Maestro) Request/Response](#endpoint-maestro-requestresponse)
18
+ - [Merge Report Client-Side Integration](#merge-report-client-side-integration)
19
+ - [Base64 and Byte Arrays](#base64-and-byte-arrays)
20
+ - [User and Session Access](#user-and-session-access)
21
+ - [Type Safety](#type-safety)
22
+
23
+ ## Working with Java Optionals
24
+
25
+ ⚠️ BlueStep uses Java optionals, NOT standard JavaScript undefined/null checks.
26
+
27
+ ```typescript
28
+ // Get value with default
29
+ const value = field.opt().orElse('default value');
30
+
31
+ // Transform and unwrap
32
+ const transformed = field.opt().map(v => v.toUpperCase()).orElse('');
33
+
34
+ // Check if present
35
+ if (field.opt().isPresent()) {
36
+ const value = field.opt().get();
37
+ }
38
+
39
+ // Chain operations
40
+ const result = field.opt().map(v => v.trim()).filter(v => v.length > 0).orElse('empty');
41
+ ```
42
+
43
+ Common mistakes:
44
+
45
+ ```typescript
46
+ // ❌ Wrong - JavaScript optional chaining doesn't work with Java optionals
47
+ const value = record.forms.someForm?.fields.someField?.val();
48
+ // ✅ Correct - use Java optional methods
49
+ const value = record.forms.someForm.fields.someField.opt().orElse('');
50
+
51
+ // ❌ Wrong - direct value access may throw
52
+ if (field.val()) { ... }
53
+ // ✅ Correct - check with the optional first
54
+ if (field.opt().isPresent()) { const value = field.opt().get(); }
55
+ ```
56
+
57
+ ## Field Types and Access Patterns
58
+
59
+ ```typescript
60
+ // Text / Memo (MemoField behaves like a text field)
61
+ const text = textField.opt().orElse('');
62
+ const trimmed = textField.opt().map(v => v.trim()).orElse('');
63
+
64
+ // Number
65
+ const num = numberField.opt().orElse(0);
66
+
67
+ // Boolean
68
+ const bool = boolField.opt().orElse(false);
69
+ ```
70
+
71
+ ### Date/Time Fields (reading)
72
+
73
+ Date/Time fields return Java `LocalDateTime`, `LocalDate`, or `LocalTime` objects.
74
+
75
+ ```typescript
76
+ const dateTime = dateTimeField.opt().orElse(null);
77
+ if (dateTime) {
78
+ const year = dateTime.getYear();
79
+ const month = dateTime.getMonthValue();
80
+ const day = dateTime.getDayOfMonth();
81
+ const isoString = dateTime.toString(); // ISO string (common pattern)
82
+ }
83
+ ```
84
+
85
+ (For *writing* date/datetime fields, see [Writing Date and DateTime Fields](#writing-date-and-datetime-fields).)
86
+
87
+ ### Document Fields
88
+
89
+ ```typescript
90
+ const doc = docField.filename() ? { filename: docField.filename(), url: docField.permUrl() } : null;
91
+ ```
92
+
93
+ ### Signature Fields
94
+
95
+ ```typescript
96
+ const sig = sigField.optTimeStamp().map(ts => ({
97
+ user: sigField.optUser().map(u => u.fullName()).orElse(null),
98
+ dateTime: ts
99
+ })).orElse(null);
100
+ ```
101
+
102
+ ### Single-Select Fields
103
+
104
+ `.val()` returns the `OptionItem` object, not a string, and `.selectedName()` does NOT exist. To get the display label, use `optSelected().map(o => o.displayName())`.
105
+
106
+ ```typescript
107
+ // ✅ Correct
108
+ const qType: string = fields.questionType.optSelected().map((o: any) => o.displayName()).orElse('');
109
+
110
+ // ❌ Wrong - selectedName() does not exist on SingleSelectField
111
+ fields.questionType.selectedName();
112
+ // ❌ Wrong - String(o) returns the Java object toString, not the label
113
+ fields.questionType.opt().map((o: any) => String(o)).orElse('');
114
+ ```
115
+
116
+ ### Multi-Select Fields
117
+
118
+ ```typescript
119
+ // Array of selected option names — .selectedNames() returns EList<string>; use .toArray()
120
+ const selectedNames = multiSelectField.selectedNames().toArray();
121
+ const count = multiSelectField.selectedCount();
122
+
123
+ // Selected OptionItem objects (for display name + export value)
124
+ multiSelectField.selected().forEach((item: Bluestep.Relate.OptionItem) => {
125
+ const name = item.displayName();
126
+ const exportValue = item.exportValue();
127
+ });
128
+ ```
129
+
130
+ ⚠️ Use `.toArray()` to convert an `EList` to a real JavaScript array before JSON serialization or array operations.
131
+
132
+ ## mergeTag() Method
133
+
134
+ `mergeTag()` is available on all Field objects and generates the HTML for form-field components. It is used with the `fieldF` and `bsHorizFieldCol2F` components to produce properly formatted fields with labels, validation images, and inputs.
135
+
136
+ ```typescript
137
+ field.mergeTag(options?: string): string
138
+ ```
139
+
140
+ ### Option codes
141
+
142
+ | Option Code | Description |
143
+ |------------|-------------|
144
+ | `""` (empty) | View-only field (read-only display) |
145
+ | `"L"` | Label only |
146
+ | `"H"` | Hint/tooltip |
147
+ | `"F"` | Editable field (input element) |
148
+ | `"I"` | Validation image (must be used with `"F"`) |
149
+
150
+ ⚠️ The validation-image code `"I"` should only be used in combination with the editable-field code `"F"`. Codes can be combined in one string to generate multiple tags at once (e.g. `"FI"`, `"LF"`, `"LFI"`).
151
+
152
+ ```typescript
153
+ const input = field.mergeTag("F"); // editable field
154
+ const label = field.mergeTag("L"); // label
155
+ const hint = field.mergeTag("H"); // hint
156
+ const viewOnly = field.mergeTag(""); // view-only
157
+ const complete = field.mergeTag("LFI"); // label + field + validation image
158
+ ```
159
+
160
+ ### Using with `fieldF` / `bsHorizFieldCol2F`
161
+
162
+ ```typescript
163
+ import { fieldF, bsHorizFormCol2F, bsHorizFieldCol2F } from 'genericComponents';
164
+
165
+ const firstNameField = record.forms.userInfo.fields.firstName;
166
+
167
+ // Separate calls
168
+ const fieldHtml = fieldF({
169
+ label: firstNameField.mergeTag("L"),
170
+ validationImg: firstNameField.mergeTag("I"),
171
+ field: firstNameField.mergeTag("F")
172
+ });
173
+
174
+ // More efficient: generate field + validation together with "FI"
175
+ const fieldHtml2 = fieldF({
176
+ label: firstNameField.mergeTag("L"),
177
+ validationImg: '',
178
+ field: firstNameField.mergeTag("FI")
179
+ });
180
+ ```
181
+
182
+ Best practices: always use `mergeTag()` with `fieldF`/`bsHorizFieldCol2F` (they integrate with BlueStep's validation system); combine codes (`"FI"`/`"IF"`) rather than separate calls; never use `"I"` without `"F"`.
183
+
184
+ ## Safe Field Value Extraction
185
+
186
+ ```typescript
187
+ function getFieldValue(field: any, fieldType: string): any {
188
+ switch (fieldType) {
189
+ case 'Text Field':
190
+ case 'Text Area Field':
191
+ case 'Memo Field':
192
+ return field.opt().orElse(null);
193
+ case 'Number Field':
194
+ return field.opt().orElse(null);
195
+ case 'Boolean Field':
196
+ return field.opt().orElse(false);
197
+ case 'Date/Time Field':
198
+ return field.opt().orElse(null); // Java date object
199
+ case 'Document Field':
200
+ return field.filename() ? { filename: field.filename(), url: field.permUrl() } : null;
201
+ case 'Signature Field':
202
+ return field.optTimeStamp().map(ts => ({
203
+ user: field.optUser().map(u => u.fullName()).orElse(null),
204
+ dateTime: ts
205
+ })).orElse(null);
206
+ case 'Multiple Select List Field':
207
+ return (field as Bluestep.Relate.MultiSelectField).selectedNames().length > 0
208
+ ? (field as Bluestep.Relate.MultiSelectField).selectedNames().toArray()
209
+ : [];
210
+ default:
211
+ return field.opt().orElse(null);
212
+ }
213
+ }
214
+ ```
215
+
216
+ ## Record and Form Access
217
+
218
+ Access forms and fields by direct property (the FID string). `byFID['...']` is unnecessary for runtime field/form access and should not be used (it appears only inside `require()` in the legacy `imports.ts` registration — see [Query Access Patterns](#query-access-patterns)).
219
+
220
+ ```typescript
221
+ // ✅ Correct - direct property access
222
+ entry.fields.title.val()
223
+ client.forms.medicalTasks
224
+ const value = record.forms.formName.fields.fieldName.opt().orElse('default');
225
+
226
+ // ❌ Wrong - byFID is unnecessary at runtime
227
+ entry.fields.byFID['title'].val()
228
+ client.forms.byFID['medicalTasks']
229
+ ```
230
+
231
+ ### Linking to a record — `summaryUrl()`
232
+
233
+ There is no universal `/records/{id}` route. Use `record.summaryUrl()` for the relative URL to any record's page.
234
+
235
+ ```typescript
236
+ const url = client.summaryUrl(); // e.g. "/rop/connect/..."
237
+ // ❌ Wrong - no such universal route
238
+ const url = `/rop/records/${clientId}`;
239
+ ```
240
+
241
+ ### Record lookup by ID — use the `sysId` field, not `id().shortId()`
242
+
243
+ `client.id().shortId()` returns the internal relate record ID, which does NOT resolve with `optById`. Use the `sysId` field from the `name` form.
244
+
245
+ ```typescript
246
+ // ✅ Correct
247
+ const clientId = client.forms.name.fields.sysId.val();
248
+ // In a CURRENT_RECORD merge report:
249
+ const clientId = (name as any).fields.sysId.val();
250
+
251
+ // ❌ Wrong - shortId doesn't match what optById expects
252
+ const clientId = client.id().shortId();
253
+ ```
254
+
255
+ <!-- CONFLICT: client-ID field key — 01-Platform-Reference states the field key is `sysId` (not `systemID`/`systemId`) and that `id().shortId()` does NOT resolve with optById; Brandon's bluestep-knowledge merge-report examples read `name.fields.systemID.val()`. This may be form/org-specific (the field key depends on the form's schema). Needs human confirmation of the canonical key. -->
256
+
257
+ ## Multi-Entry Form (MEF) Entries
258
+
259
+ `mefReports` does NOT exist at runtime. Create entries on the form, and find them by iterating the form directly.
260
+
261
+ ```typescript
262
+ // Create a new entry — newEntry() is on the form
263
+ const entry = client.forms.medicalTasks.newEntry();
264
+ entry.fields.title.val('Task title');
265
+ entry.fields.status.val(false);
266
+ // ❌ Wrong - client.forms.medicalTasks.mefReports.newEntry();
267
+
268
+ // Find a specific entry by ID — iterate the pre-loaded entries
269
+ let foundEntry: any = null;
270
+ for (const e of client.forms.medicalTasks) {
271
+ if (e.id().shortId() === entryId) { foundEntry = e; break; }
272
+ }
273
+ // ❌ Wrong - mefReports is undefined at runtime
274
+ // client.forms.medicalTasks.mefReports.allEntries.query().optById(entryId);
275
+ ```
276
+
277
+ - **Entry IDs:** use `entry.id().shortId()` to get the string ID for serialization or comparison — it matches when iterating.
278
+ - **Entry creation date:** `entry.created()` returns a Java `Instant`. Convert to compare against a `LocalDate`:
279
+
280
+ ```typescript
281
+ const createdDate = B.time.LocalDate.ofInstant(entry.created(), B.time.ZoneId.systemDefault());
282
+ if (createdDate.isAfter(someLocalDate)) return; // e.g. exclude targets created after a note's service date
283
+ ```
284
+
285
+ ### Iterating single vs multi-entry forms
286
+
287
+ ```typescript
288
+ const isMultiEntry = formMetaData.isMultiEntry();
289
+ if (isMultiEntry) {
290
+ record.forms.multiEntryForm.entries().forEach((entry: any) => {
291
+ const value = entry.fields.fieldName.opt().orElse('');
292
+ });
293
+ } else {
294
+ const value = record.forms.singleEntryForm.fields.fieldName.opt().orElse('');
295
+ }
296
+ ```
297
+
298
+ ## Query Access Patterns
299
+
300
+ Which queries, forms, and fields a script can use are defined by the component's **form-import config on the platform**, regenerated into `declarations/index.d.ts` on `b6p pull`. A configured query is available in `app.ts` as a **bare top-level variable** named after the query FID — directly iterable, no `.query()` call:
301
+
302
+ ```typescript
303
+ // app.ts — the query is a top-level variable (configured on the platform, generated into declarations)
304
+ const unit = topLevelUnit[0];
305
+ const unitName = unit.forms.unitInformation.fields.unitName.opt().orElse('');
306
+ topLevelUnit.forEach((record: Record_topLevelUnit) => { /* ... */ });
307
+ ```
308
+
309
+ To add a query/field, or make a form writable, update the form-import config **on the platform** and `b6p pull` — do not hand-edit `declarations/index.d.ts`. A configured query's top-level variable carries the writable transaction context, so writes go through it directly (writable access is set per form in the import config, not chained in code).
310
+
311
+ For an ad-hoc query that is *not* in the import config, run it explicitly. This is a fresh **read-only** execution — use it for reads only; writes against it fail:
312
+
313
+ ```typescript
314
+ B.queries.byFID['staffQuery'].query().forEach((record: Bluestep.Relate.Record) => {
315
+ console.log(record.forms.nameForm.fields.firstName.opt().orElse(''));
316
+ });
317
+ ```
318
+
319
+ <details>
320
+ <summary>Legacy: <code>objects/imports.ts</code> registration (older modules only)</summary>
321
+
322
+ Older modules registered queries by hand in `objects/imports.ts` with `.require()`, which produced the same bare top-level variable and selected fields / writable context in code. **This file is not updated on pull and is not hand-written in current modules** — the platform form-import config replaces it. You may still encounter it:
323
+
324
+ ```typescript
325
+ // objects/imports.ts (legacy)
326
+ {
327
+ const forms = B.queries.byFID['topLevelUnit'].require().forms;
328
+ forms.byFID['unitInformation'].require({fields: ['unitName']});
329
+ // writable was an option INSIDE require(), never a chained .writable():
330
+ forms.byFID['medicalTasks'].mefReports.allEntries.require({fields: ['title','status'], writable: true});
331
+ }
332
+ ```
333
+
334
+ </details>
335
+
336
+ ## Java Collections
337
+
338
+ Query results and other Java collections do NOT have JavaScript array methods (`.filter()`, `.map()`). Use `.forEach()` and build an array.
339
+
340
+ ```typescript
341
+ // ❌ Wrong
342
+ const filtered = query.query().filter(r => condition);
343
+ // ✅ Correct
344
+ const results = [];
345
+ query.query().forEach((record: any) => { if (condition) results.push(record); });
346
+ ```
347
+
348
+ ## Writing Date and DateTime Fields
349
+
350
+ Use `B.time`, never `Java.Time` — `Java.Time` is a TypeScript type namespace only and does not exist at runtime.
351
+
352
+ ```typescript
353
+ // DateField — accepts B.time.LocalDate, B.time.Instant, or millis (number)
354
+ entry.fields.dueDate.dateVal(B.time.LocalDate.parse('2025-04-01'));
355
+ // ❌ Wrong - Java.Time is undefined at runtime
356
+ entry.fields.dueDate.dateVal(Java.Time.LocalDate.parse('2025-04-01'));
357
+
358
+ // DateTimeField — expects a ZonedDateTime, NOT epoch millis
359
+ entry.fields.completedAt.val(B.time.ZonedDateTime.now());
360
+ // ❌ Wrong - toEpochMilli() returns a Long → ClassCastException: Long cannot be cast to ZonedDateTime
361
+ entry.fields.completedAt.val(B.time.Instant.now().toEpochMilli());
362
+ ```
363
+
364
+ (For setting search values against date fields in MEF queries, that format differs — see [date-format](../conventions/date-format.md). For the `.val()` vs `.dateTimeVal()` overload trap, see [datetime-field-write](datetime-field-write.md).)
365
+
366
+ ## Committing Writes and Formula Triggers
367
+
368
+ - **Form-attached formulas have NO `B.commit()`.** There `B` is `Bluestep.Relate.CurrentRecordB`, which does not extend `IsCommitable`; field writes auto-flush when the form save completes. (Cross-record writes need the target form configured writable in the platform form-import config — legacy modules declared `writable: true` in `objects/imports.ts`.)
369
+ - **Endpoints, on-demand formulas, and scheduled formulas** — `B` IS `IsCommitable`; call `B.commit()` after writes.
370
+ - **Gate on create vs edit** in a form-attached formula (`cur` is a `FormEntry`):
371
+
372
+ ```typescript
373
+ if (cur.entry().justCreated()) { /* first-save path */ }
374
+ if (!cur.entry().justCreated() && cur.entry().triggerFormulas()) { /* edit-only path */ }
375
+ ```
376
+
377
+ ## Endpoint (Maestro) Request/Response
378
+
379
+ In endpoint (Maestro) scripts, the HTTP request/response are on `B.net`. `B.request`, `B.response`, and `B.out` do NOT exist in this context (`B.out` is a merge-report construct).
380
+
381
+ ```typescript
382
+ const { request, response } = B.net;
383
+ const action = request.parameter('action'); // string or null
384
+ const id = request.optParameter('id').orElse(''); // Java Optional
385
+ const body = JSON.parse(request.content() || '{}'); // request body as string
386
+ response.contentType('application/json; charset=UTF-8');
387
+ response.out(JSON.stringify({ success: true }));
388
+ ```
389
+
390
+ (For the full request/response method surface and an action-based router, see [code-patterns](code-patterns.md). For the no-`delete`-method endpoint rule and output channel, see [endpoint-method-call](endpoint-method-call.md) and [endpoint-output-channel](endpoint-output-channel.md).)
391
+
392
+ ## Merge Report Client-Side Integration
393
+
394
+ ### Intercepting the save — wrap `window.submitForm`
395
+
396
+ BlueStep's save button is `<a href="javascript:submitForm('Relate','commit')">`, which calls `submitForm()` → `form.submit()`. **`form.submit()` does NOT fire `addEventListener('submit', ...)`.** Wrap `window.submitForm` to intercept the save:
397
+
398
+ ```javascript
399
+ var _orig = window.submitForm;
400
+ window.submitForm = function() {
401
+ // your logic here
402
+ if (_orig) return _orig.apply(this, arguments);
403
+ };
404
+ ```
405
+
406
+ ### Disable widget inputs before the POST
407
+
408
+ If a merge report renders inputs with `name` attributes, BlueStep tries to save them as form fields and any unknown `name` causes "There was a problem storing the data." Disable widget inputs inside the override before calling through:
409
+
410
+ ```javascript
411
+ var _orig = window.submitForm;
412
+ window.submitForm = function() {
413
+ document.querySelectorAll('input[name^="tt-"], input[data-target-id], textarea[data-target-id]')
414
+ .forEach(function(el) { el.disabled = true; });
415
+ if (_orig) return _orig.apply(this, arguments);
416
+ };
417
+ ```
418
+
419
+ (See also [named-controls-submit](named-controls-submit.md).)
420
+
421
+ ### Script timing — native fields below the widget
422
+
423
+ A merge report's `<script>` is injected mid-page. Native BlueStep fields that render **after** the widget are not yet in the DOM when the script runs — defer those with `DOMContentLoaded`. Fields rendered **before** the script (inside the widget's own HTML) are accessible immediately.
424
+
425
+ ```javascript
426
+ document.addEventListener('DOMContentLoaded', function() {
427
+ var field = document.querySelector('[data-fid="treatmentTargetJSON"]');
428
+ if (field) field.closest('tr').style.display = 'none';
429
+ });
430
+ ```
431
+
432
+ (For passing per-row server data without a `DOMContentLoaded` race, see the queue/lazy-init pattern in [code-patterns](code-patterns.md).)
433
+
434
+ ## Base64 and Byte Arrays
435
+
436
+ The base64 helpers take `Java.ByteArray`, not `string`. Convert with the I/O round-trip:
437
+
438
+ ```typescript
439
+ const bytes = B.io.toByteArray(B.io.toInputStream(myString, "UTF-8"));
440
+
441
+ // Standard base64 — exposed on B
442
+ const standard = B.toBase64(bytes);
443
+ // URL-safe base64 — ONLY on B.text (RFC 4648 §5, e.g. Gmail messages/send raw field)
444
+ const encoded = B.text.toBaseUrl64(bytes);
445
+ // ❌ Wrong - B.toBaseUrl64 does not exist
446
+ const wrong = B.toBaseUrl64(bytes);
447
+ ```
448
+
449
+ `B.io.toInputStream(input, charset?)` and `B.io.toByteArray(inputStream)` are the canonical pair — don't reach for `Java.type('java.lang.String')...getBytes(...)`.
450
+
451
+ ## User and Session Access
452
+
453
+ ```typescript
454
+ // B.optUser — Java Optional (preferred, safe)
455
+ const fullName = B.optUser.map(u => u.fullName()).orElse('Anonymous');
456
+ const isSuper = B.optUser.map(u => u.isGlobalSuper()).orElse(false);
457
+ const email = B.optUser.map(u => u.email()).orElse('');
458
+ if (B.optUser.isPresent()) {
459
+ const user = B.optUser.get();
460
+ // user.firstName(), user.fullName(), user.email(), user.userName()
461
+ // user.unit(), user.record(), user.entry()
462
+ }
463
+
464
+ // B.user — direct access, returns User|null (equivalent to B.optUser.orElse(null)); prefer B.optUser
465
+ const user = B.user;
466
+ if (user) { const name = user.fullName(); }
467
+
468
+ // Layout detection
469
+ if (B.isLayout("MANAGE")) { /* Manage-specific behavior */ }
470
+ ```
471
+
472
+ ⚠️ `B.optUser` is a Java Optional — never serialize it directly into a template literal (it produces `"function () { [native code] }"`). Always resolve with `.map(...).orElse(...)` first.
473
+
474
+ ## Type Safety
475
+
476
+ BlueStep APIs often return `any`. Add safety with casts/annotations and type guards.
477
+
478
+ ```typescript
479
+ const value: string = field.opt().orElse('');
480
+
481
+ function isRecord(obj: any): obj is Bluestep.Relate.Record {
482
+ return obj && typeof obj.recordId === 'function';
483
+ }
484
+ if (isRecord(someObj)) { const id = someObj.recordId(); }
485
+ ```
486
+
487
+ (For broader TypeScript guidance, see the [BsJs development overview](../bsjs-development.md).)
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: Step-by-step playbook for wiring any new OpenAI/AI feature on summitridge into the BlueIQ credit gate + ledger — do this every time we build an AI feature
3
+ ---
4
+
5
+ **Whenever we build a new AI feature on summitridge, it MUST be wired into the BlueIQ credit system** (gate + ledger at `/b/aiCredits`, file 1471659 — see blueiq credit system for the endpoint spec). This is the repeatable recipe. Two integration patterns depending on context.
6
+
7
+ ## The contract (both patterns produce the same ledger row)
8
+ Per OpenAI "call" (define the granularity sensibly — e.g. one chat turn that fans out to many internal calls = ONE log with summed usage):
9
+ - **check** before spending → `{ok, allowed, used, limit, remaining, period}`. Proceed ONLY when `ok===true && allowed===true`. **Fail-closed.**
10
+ - **log** after the response → row fields: `function` (Text), `user` (Text), `model` (SingleSelect by export value), `promptTokens`, `cachedInputTokens`, `completionTokens`, `audioInputTokens`, `audioOutputTokens`, `audioSeconds`, `cost`, `credits` (all Number). Endpoint prices from its PRICING table; `credits = cost × 1000` (exact decimal, no rounding).
11
+ - **Token mapping:** `promptTokens` = TOTAL input (cached nested inside); pass `cachedInputTokens` too so cached is priced cheaply. Chat models: audio fields = 0. The model id passed MUST exist in `/b/aiCredits` PRICING or `log` hard-errors.
12
+
13
+ ## Pattern A — user-context consumer (endpoint or merge report with a session)
14
+ Used by `/b/aiAudio` (1471299), the Shift-Note Assistant (1471399), the Patient Record Chatbot (1470299/1470459). **Copy the helpers verbatim from `/b/aiAudio` (1471299) `draft/scripts/app.ts`:** `creditFetch`, `currentUserName`, `CreditCheck` interface, `checkCredits`, `logCredits`.
15
+ - Loopback is **internal `B.net.fetch("/b/aiCredits", {method:"POST", credentials:true, followRedirects:false, enableErrorStream:true, ...})`** — NOT external httpRequester (can't hairpin to the org's own host → `this.in is null`). `credentials:true` carries the caller's session so the login-required endpoint doesn't 403. See [internal loopback fetch](internal-loopback-fetch.md).
16
+ - Gate BEFORE the spend; on `!allowed` return distinct flags: `creditLimitReached:true` (out_of_credits) vs `creditCheckFailed:true` (gate_unreachable/gate_error). `checkCredits` returns a structured `reason` so the UI tells the true story, not a false "out of credits".
17
+ - Log AFTER (best-effort, never block the user's result; return a `creditLogWarning` on failure).
18
+ - Client UX: on `creditLimitReached` show a styled banner + disable input; on `creditCheckFailed` show a softer banner but allow retry.
19
+
20
+ ## Pattern B — scheduled / system-wide formula (NO session)
21
+ Used by the Daily Milieu Summary morning formula (1471599). A scheduled `CurrentRecordB` formula has **no user session and no inbound request**, so it CANNOT authenticate a loopback (would 403). Instead **write the `openAILogs` entry directly**:
22
+ - Prereq: add an `openAILogs` reference path to the formula in the BlueStep UI (multi-entry, writable) — it lives on the Facility/`community` record alongside `openAIIntegration`. Then `openAILogs.newEntry()` works.
23
+ - Price inline (mirror the relevant model's row from `/b/aiCredits` PRICING; comment it as a mirror). gpt-4o = in 2.50 / cached 1.25 / out 10.00 per 1M; `textIn = promptTokens − cached`; `credits = cost × 1000`.
24
+ - Set fields directly: `timestamp.val(B.time.ZonedDateTime.now())` (DateTimeField — use `.val()`, not `.dateTimeVal()`), `function`, `user:"System (scheduled)"`, `model` via `field.optionsByExport()[modelId]`, token fields, `cost`, `credits`, then `B.commit()`. Wrap best-effort (try/catch) AFTER the feature's own commit so logging can't lose the real output. No gate (background job — log only).
25
+
26
+ ## Always also
27
+ - **API key from the form**, never hardcoded: read `openAIIntegration.fields.apiKey` (SecretField) via a memoized `getOpenAiKey()` (endpoints use `community[0].forms.openAIIntegration...`; CurrentRecord formulas access `openAIIntegration` directly).
28
+ - **No "AI" in front-facing text** — brand is always "BlueIQ". The ledger `function` label is admin-facing and feature-named (e.g. "Shift Note Recorder", "Patient Record Chatbot", "Daily Milieu Summary", optionally "– {sub-context}"). See [blueiq no ai branding](../conventions/blueiq-no-ai-branding.md).
29
+ - Build (`tsc --rootDir . --skipLibCheck`) and snapshot every touched component.
30
+
31
+ Related: blueiq credit system, [internal loopback fetch](internal-loopback-fetch.md), [blueiq no ai branding](../conventions/blueiq-no-ai-branding.md), ai audio, [datetime field write](datetime-field-write.md), [singleselect null copy](singleselect-null-copy.md).
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: "ChronoUnit.MONTHS.between counts complete elapsed months (day-aware), NOT calendar-month boundaries — use (year-year)*12 + (month-month) when porting SQL DATEDIFF(MONTH) semantics"
3
+ ---
4
+
5
+ `B.time.ChronoUnit.MONTHS.between(a, b)` and SQL `DATEDIFF(MONTH, a, b)`
6
+ compute *different* numbers whenever `day(b) < day(a)`:
7
+
8
+ - **ChronoUnit.MONTHS.between** — counts *complete elapsed* months, day-aware.
9
+ `between(2024-06-07, 2026-05-05)` = 22 (because 23 complete months would
10
+ end on 2026-05-07).
11
+ - **SQL DATEDIFF(MONTH)** — counts calendar-month boundary crossings, day-blind.
12
+ Same input = 23 (`(2026-2024)*12 + (5-6)`).
13
+
14
+ For "treatment-month ordinal" / "Nth-month bucket" math (where month 1
15
+ starts on the admit date and month 2 begins on the next same-day-of-month),
16
+ you want the SQL semantic. Implement with a plain calendar diff:
17
+
18
+ ```ts
19
+ const calMonths = (Number(note.getYear()) - Number(admit.getYear())) * 12
20
+ + (Number(note.getMonthValue()) - Number(admit.getMonthValue()));
21
+ const adj = note.getDayOfMonth() >= admit.getDayOfMonth() ? calMonths : (calMonths - 1);
22
+ const treatmentMonth = adj + 1; // 1-indexed
23
+ ```
24
+
25
+ `ChronoUnit.WEEKS / DAYS / YEARS .between` have the same elapsed-count
26
+ semantics. DAYS happens to match SQL DATEDIFF(DAY) because both ignore
27
+ sub-day resolution, but WEEKS and YEARS will drift the same way MONTHS
28
+ does. When porting SQL date math, port the SQL formula faithfully — don't
29
+ substitute ChronoUnit conveniences.
30
+
31
+ **Discovered:** RISE Maestro v1 (alpineacademy/1515139) shipped with
32
+ `ChronoUnit.MONTHS.between` and was off-by-one on `monthsSinceAdmit` for
33
+ 12 of 53 overlapping rows in the verification diff. See
34
+ treatment targets sibling project — also does treatment-month
35
+ math.
36
+
37
+ Related: [localdate parse](localdate-parse.md) (LocalDate.parse — use B.time).