@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.
- package/README.md +129 -0
- package/cli.js +74 -0
- package/package.json +30 -0
- package/src/prompts.js +74 -0
- package/src/scaffold.js +152 -0
- package/src/sync.js +123 -0
- package/src/utils.js +95 -0
- package/templates/claude/agents/b6p-code-review.md +81 -0
- package/templates/claude/agents/b6p-commenter.md +59 -0
- package/templates/claude/agents/b6p-task-implementer.md +77 -0
- package/templates/claude/hooks/block-generated-files.sh +16 -0
- package/templates/claude/hooks/block-tsc.sh +16 -0
- package/templates/claude/hooks/prettier-on-save.sh +21 -0
- package/templates/claude/instructions/b6p-platform.md.template +185 -0
- package/templates/claude/instructions/bsjs-development.md.template +430 -0
- package/templates/claude/instructions/conventions/always-snapshot.md.template +25 -0
- package/templates/claude/instructions/conventions/blueiq-no-ai-branding.md.template +11 -0
- package/templates/claude/instructions/conventions/date-format.md.template +27 -0
- package/templates/claude/instructions/conventions/endpoint-approach.md.template +9 -0
- package/templates/claude/instructions/conventions/formula-patterns.md.template +71 -0
- package/templates/claude/instructions/conventions/no-global-dollar.md.template +9 -0
- package/templates/claude/instructions/conventions/push-inner-draft.md.template +21 -0
- package/templates/claude/instructions/conventions/separate-files.md.template +17 -0
- package/templates/claude/instructions/conventions/single-script.md.template +28 -0
- package/templates/claude/instructions/conventions/snapshot-integrity.md.template +23 -0
- package/templates/claude/instructions/conventions/top-level-const-tdz.md.template +33 -0
- package/templates/claude/instructions/conventions/ts-in-template-literal.md.template +48 -0
- package/templates/claude/instructions/conventions/tsc-rootdir.md.template +17 -0
- package/templates/claude/instructions/gotchas/common-gotchas.md.template +91 -0
- package/templates/claude/instructions/gotchas/fetched-resource-code.md.template +9 -0
- package/templates/claude/instructions/index.md.template +82 -0
- package/templates/claude/instructions/reference/api-patterns.md.template +487 -0
- package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template +31 -0
- package/templates/claude/instructions/reference/chronounit-months.md.template +37 -0
- package/templates/claude/instructions/reference/code-patterns.md.template +265 -0
- package/templates/claude/instructions/reference/component-library.md.template +217 -0
- package/templates/claude/instructions/reference/crm-dashboard-inspo.md.template +17 -0
- package/templates/claude/instructions/reference/csv-parsing.md.template +18 -0
- package/templates/claude/instructions/reference/dashboard-design-system.md.template +38 -0
- package/templates/claude/instructions/reference/datetime-field-write.md.template +27 -0
- package/templates/claude/instructions/reference/design-system.md.template +150 -0
- package/templates/claude/instructions/reference/dpn-dashboard-framework.md.template +29 -0
- package/templates/claude/instructions/reference/endpoint-method-call.md.template +10 -0
- package/templates/claude/instructions/reference/endpoint-no-delete-method.md.template +9 -0
- package/templates/claude/instructions/reference/endpoint-output-channel.md.template +23 -0
- package/templates/claude/instructions/reference/endpoint-urls.md.template +15 -0
- package/templates/claude/instructions/reference/entry-delete.md.template +40 -0
- package/templates/claude/instructions/reference/file-execution.md.template +113 -0
- package/templates/claude/instructions/reference/http-requester.md.template +37 -0
- package/templates/claude/instructions/reference/id-full-vs-short.md.template +15 -0
- package/templates/claude/instructions/reference/internal-loopback-fetch.md.template +24 -0
- package/templates/claude/instructions/reference/localdate-parse.md.template +16 -0
- package/templates/claude/instructions/reference/merge-report-memo-json.md.template +25 -0
- package/templates/claude/instructions/reference/merge-report-static-index.md.template +29 -0
- package/templates/claude/instructions/reference/merge-report-urls.md.template +67 -0
- package/templates/claude/instructions/reference/multi-entry-in-multi-entry.md.template +21 -0
- package/templates/claude/instructions/reference/named-controls-submit.md.template +11 -0
- package/templates/claude/instructions/reference/new-entry-id.md.template +30 -0
- package/templates/claude/instructions/reference/relationship-field-set.md.template +37 -0
- package/templates/claude/instructions/reference/send-message-abort.md.template +37 -0
- package/templates/claude/instructions/reference/session-cookie-forwarding.md.template +31 -0
- package/templates/claude/instructions/reference/singleselect-null-copy.md.template +21 -0
- package/templates/claude/instructions/reference/staff-query-permission-gating.md.template +27 -0
- package/templates/claude/instructions/reference/timefield-vs-datetimefield.md.template +13 -0
- package/templates/claude/instructions/reference/user-zone-id.md.template +16 -0
- package/templates/claude/settings.json.template +46 -0
- package/templates/claude/skills/b6p-audit/SKILL.md +82 -0
- package/templates/claude/skills/b6p-pull/SKILL.md +123 -0
- package/templates/claude/skills/b6p-push/SKILL.md +70 -0
- package/templates/claude/skills/bug-fix/SKILL.md +28 -0
- package/templates/claude/skills/spec-create/SKILL.md +60 -0
- package/templates/claude/skills/spec-execute/SKILL.md +51 -0
- package/templates/claude/skills/spec-status/SKILL.md +20 -0
- package/templates/claude/skills/task-comment/SKILL.md +96 -0
- package/templates/claude/spec-templates/design.template.md +36 -0
- package/templates/claude/spec-templates/requirements.template.md +26 -0
- package/templates/claude/spec-templates/tasks.template.md +37 -0
- package/templates/module/README.md.template +46 -0
- package/templates/root/.gitignore.template +14 -0
- package/templates/root/.prettierrc.template +8 -0
- package/templates/root/CLAUDE.md.template +157 -0
- package/templates/root/README.md.template +58 -0
- package/templates/root/package.json.template +15 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Deep technical reference for BsJs (BlueStep TypeScript). Read when writing or modifying component code.
|
|
3
|
+
applyTo: "**/draft/scripts/**/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# BsJs Development Reference
|
|
7
|
+
|
|
8
|
+
Deep reference for BlueStep TypeScript development. Critical rules live in `CLAUDE.md`; this file covers patterns, APIs, and conventions that apply when actually writing code.
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- [Module structure](#module-structure)
|
|
13
|
+
- [The `B` object — full API](#the-b-object--full-api)
|
|
14
|
+
- [Reading and writing fields](#reading-and-writing-fields)
|
|
15
|
+
- [Patterns by script type](#patterns-by-script-type)
|
|
16
|
+
- [`info/` configuration](#info-configuration)
|
|
17
|
+
- [TypeScript configuration & Graal compatibility](#typescript-configuration--graal-compatibility)
|
|
18
|
+
- [Imports — never fabricate](#imports--never-fabricate)
|
|
19
|
+
- [TS narrowing pitfalls (Graal/Java types)](#ts-narrowing-pitfalls-graaljava-types)
|
|
20
|
+
- [Error handling](#error-handling)
|
|
21
|
+
|
|
22
|
+
## Module structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
<project-root>/
|
|
26
|
+
└── U######/ ← Unit folder, created by `b6p pull`
|
|
27
|
+
└── ComponentName/
|
|
28
|
+
├── declarations/ ← platform-generated, DO NOT EDIT
|
|
29
|
+
│ ├── B.d.ts
|
|
30
|
+
│ ├── scriptlibrary.d.ts
|
|
31
|
+
│ ├── Globals.d.ts
|
|
32
|
+
│ └── index.d.ts ← platform-generated field/query/form declarations
|
|
33
|
+
└── draft/
|
|
34
|
+
├── scripts/ ← TypeScript source
|
|
35
|
+
│ ├── app.ts ← entry point
|
|
36
|
+
│ └── <feature>.ts ← split modules
|
|
37
|
+
├── objects/
|
|
38
|
+
│ └── imports.ts ← legacy artifact in older modules; not updated on pull
|
|
39
|
+
├── static/ ← MergeReport only: HTML, CSS, client JS
|
|
40
|
+
└── info/
|
|
41
|
+
├── config.json
|
|
42
|
+
├── metadata.json ← identifies component type (read to know what this is)
|
|
43
|
+
└── permissions.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A project may contain multiple Unit folders, each with multiple components of varying types. **Module split convention:** split complex logic into focused files under `scripts/`. `app.ts` is the entry point.
|
|
47
|
+
|
|
48
|
+
**Multi-file components with ES imports are supported** (verified on the platform — `SMS Data Diagnostics`, `app.ts` importing `cleanupDuplicates.ts`). A sibling file `export`s a symbol and `app.ts` pulls it in with a standard relative ES import (no file extension); it compiles and links correctly after push. Use this to split a large `app.ts` into focused modules:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// scripts/cleanupDuplicates.ts
|
|
52
|
+
export function cleanupDuplicates(): void { /* ... */ }
|
|
53
|
+
|
|
54
|
+
// scripts/app.ts
|
|
55
|
+
import { cleanupDuplicates } from "./cleanupDuplicates";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The `B` object — full API
|
|
59
|
+
|
|
60
|
+
### `B.net` — outbound HTTP
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const response = B.net.fetch("https://api.example.com/data", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ key: "value" }),
|
|
67
|
+
timeout: 30000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (response.ok) {
|
|
71
|
+
const data = JSON.parse(response.body);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `B.time` — date/time
|
|
76
|
+
|
|
77
|
+
Use `B.time` for any date/time work. Native `Date` is not supported.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const now = B.time.now();
|
|
81
|
+
const formatted = B.time.format(now, "yyyy-MM-dd HH:mm:ss");
|
|
82
|
+
const parsed = B.time.parse("2026-05-15", "yyyy-MM-dd");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `B.queries` — query objects
|
|
86
|
+
|
|
87
|
+
Queries are defined on the platform and exposed in `declarations/index.d.ts` (platform-generated). Reference them only after pulling.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const results = B.queries.activeClients.execute();
|
|
91
|
+
for (const record of results) {
|
|
92
|
+
// ...
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Unit scoping:** queries are unit-scoped by default. When re-using a query across units, call `clearSearchAndSort()` to reset filters:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const query = B.queries.allRecords;
|
|
100
|
+
query.execute(); // current unit
|
|
101
|
+
query.clearSearchAndSort();
|
|
102
|
+
query.unit = otherUnit;
|
|
103
|
+
query.execute(); // other unit
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `B.exports` — cross-formula data
|
|
107
|
+
|
|
108
|
+
Used to pass data between formulas in the same execution context.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
B.exports.totalScore = computeScore();
|
|
112
|
+
B.clearExports(); // when starting fresh
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `B.user` — current user
|
|
116
|
+
|
|
117
|
+
`B.user` is **null** in scheduled / cron scripts. Always guard:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
if (B.user) {
|
|
121
|
+
const userId = B.user.id;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `B.commit()` — manual transaction commit
|
|
126
|
+
|
|
127
|
+
The platform calls `B.commit()` automatically when the script finishes. Only call it manually when:
|
|
128
|
+
|
|
129
|
+
- You need a newly created entry's ID before the script ends.
|
|
130
|
+
- You need post-saves to fire before subsequent reads.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const newEntry = B.queries.someForm.getNewEntry();
|
|
134
|
+
newEntry.name.set("test");
|
|
135
|
+
B.commit();
|
|
136
|
+
const id = newEntry.id; // now available
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Reading and writing fields
|
|
140
|
+
|
|
141
|
+
### Read
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const value = entry.fieldName.val(); // current value
|
|
145
|
+
const exportValue = entry.statusField.selectedExportValue(); // dropdown export value
|
|
146
|
+
const all = entry.tagsField.allValues(); // multi-select
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`.val()` returns the field's value typed loosely (often `any`). When a field may be empty, prefer the Java-optional accessor with a default instead of null-checking the raw value:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const name = entry.nameField.opt().orElse(""); // string, never null
|
|
153
|
+
const score = entry.scoreField.opt().orElse(0);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Because projects compile with `strict: false` (see "TypeScript configuration & Graal compatibility"), `.opt().orElse()` plus explicit annotations are the main defense against silent `null`/`undefined` bugs.
|
|
157
|
+
|
|
158
|
+
### Write
|
|
159
|
+
|
|
160
|
+
Do not call `.writable()`. Field writability is configured on the platform — if the field is not configured as writable for this script type, the write will throw at runtime. Write directly:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
entry.nameField.set("new value");
|
|
164
|
+
entry.statusField.setByExportValue("active");
|
|
165
|
+
entry.tagsField.add("priority");
|
|
166
|
+
entry.tagsField.remove("legacy");
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Multi-entry forms
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
for (const entry of record.someForm.allEntries) {
|
|
173
|
+
entry.field.set("value");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const newEntry = record.someForm.getNewEntry();
|
|
177
|
+
newEntry.name.set("new");
|
|
178
|
+
|
|
179
|
+
oldEntry.delete();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Patterns by script type
|
|
183
|
+
|
|
184
|
+
### Post-Save
|
|
185
|
+
|
|
186
|
+
Runs after a record is saved. Use `justCreated()` to distinguish new vs. updated records. `curEntry` is the entry being saved.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
export function run(): void {
|
|
190
|
+
if (curEntry.justCreated()) {
|
|
191
|
+
sendWelcomeEmail();
|
|
192
|
+
} else {
|
|
193
|
+
syncToExternal();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Endpoint
|
|
199
|
+
|
|
200
|
+
Receives an HTTP request, returns a response. **Set `contentType` before writing the body. Use exactly one output method per request** (`out`, `stream`, or `redirect`).
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
export function run(): void {
|
|
204
|
+
const action = request.param("action");
|
|
205
|
+
switch (action) {
|
|
206
|
+
case "list": return listAll();
|
|
207
|
+
case "get": return getOne();
|
|
208
|
+
default: return badRequest();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function listAll(): void {
|
|
213
|
+
response.contentType = "application/json";
|
|
214
|
+
response.out(JSON.stringify({ items: getItems() }));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function badRequest(): void {
|
|
218
|
+
response.status = 400;
|
|
219
|
+
response.contentType = "text/plain";
|
|
220
|
+
response.out("Unknown action");
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Streaming (large responses)
|
|
225
|
+
|
|
226
|
+
NDJSON pattern for streaming large datasets:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
response.contentType = "application/x-ndjson";
|
|
230
|
+
const stream = response.stream();
|
|
231
|
+
for (const record of B.queries.largeQuery.execute()) {
|
|
232
|
+
stream.write(JSON.stringify({ id: record.id, name: record.name.val() }) + "\n");
|
|
233
|
+
}
|
|
234
|
+
stream.close();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Redirect
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
response.redirect("/some/path");
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### MergeReport
|
|
244
|
+
|
|
245
|
+
Backend logic in `scripts/`; **frontend in `static/`** (R3). Inject content into pages via `pageContent()`:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
export function run(): void {
|
|
249
|
+
pageContent("main").write(renderMain());
|
|
250
|
+
HEADER_SCRIPTS_BOTTOM().write(`<script src="${staticUrl("client.js")}"></script>`);
|
|
251
|
+
BODY().write(`<div class="footer">...</div>`);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### OnDemand / Field Formula
|
|
256
|
+
|
|
257
|
+
OnDemand formulas run on the **task pod** — well-suited for heavy or long-running work because they keep load off production pods. Trigger them asynchronously and let the result land in a record the caller can poll or react to.
|
|
258
|
+
|
|
259
|
+
**Critical latency caveat:** OnDemand has a ~5 second scheduler-queue delay before it starts, by design. This is fine for background / async work. It is the wrong tool for anything on a user's synchronous wait path — if a user is waiting for a response, an OnDemand hop adds ~5 s of irreducible latency before the work even begins. Prefer a synchronous task-pod **Endpoint** for user-facing create or update paths.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
export function run(): void {
|
|
263
|
+
const result = runFormula();
|
|
264
|
+
B.message.info(`Result: ${result}`);
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Scheduled / cron
|
|
269
|
+
|
|
270
|
+
`B.user` is null. Use stored credentials or hardcoded service identities, and guard accordingly.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
export function run(): void {
|
|
274
|
+
if (B.user) {
|
|
275
|
+
log.warn("Scheduled script should not have a user context");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
doScheduledWork();
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### WebSocket push
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
B.io.sendMessage(channelId, JSON.stringify({ type: "update", payload: data }));
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## `info/` configuration
|
|
289
|
+
|
|
290
|
+
### `config.json` (required)
|
|
291
|
+
|
|
292
|
+
```json
|
|
293
|
+
{
|
|
294
|
+
"language": "BsJs",
|
|
295
|
+
"entryPoint": "scripts/app.ts"
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### `metadata.json`
|
|
300
|
+
|
|
301
|
+
Identifies the component type. Examples:
|
|
302
|
+
|
|
303
|
+
- `"triggerType": "POST_SAVE"` — post-save
|
|
304
|
+
- `"triggerType": "ENDPOINT"` — endpoint
|
|
305
|
+
- `"triggerType": "ON_DEMAND"` — on-demand
|
|
306
|
+
- `"triggerType": "SCHEDULED"` — scheduled
|
|
307
|
+
|
|
308
|
+
### `permissions.json`
|
|
309
|
+
|
|
310
|
+
Defines who can execute / view this component. Managed on the platform; usually pulled, rarely hand-edited.
|
|
311
|
+
|
|
312
|
+
## TypeScript configuration & Graal compatibility
|
|
313
|
+
|
|
314
|
+
### tsconfig and strict mode
|
|
315
|
+
|
|
316
|
+
Each project root has a `tsconfig.json`. BlueStep projects run with **`strict: false`**, so the compiler will *not* catch every null/type error — compensate with `.opt().orElse()` and explicit type annotations. Typical settings:
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"compilerOptions": {
|
|
321
|
+
"target": "ES2020",
|
|
322
|
+
"module": "ESNext",
|
|
323
|
+
"strict": false,
|
|
324
|
+
"moduleResolution": "node",
|
|
325
|
+
"types": ["./declarations"]
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Do not run `tsc` locally — the platform compiles on push (a hook blocks local `tsc`). For why local builds / `rootDir` are avoided, see `conventions/tsc-rootdir.md`.
|
|
331
|
+
|
|
332
|
+
### Graal compatibility (server-side)
|
|
333
|
+
|
|
334
|
+
Server-side BsJs runs on **GraalVM**, not Node. Avoid relying on the newest language features there:
|
|
335
|
+
|
|
336
|
+
- Stage-3 / very new ECMAScript proposals
|
|
337
|
+
- Newer `Intl` APIs
|
|
338
|
+
- `WeakRef` and `FinalizationRegistry` (limited support)
|
|
339
|
+
|
|
340
|
+
Server-side `B.net.fetch` is **synchronous** — no `await`/Promise round-trip (see "`B.net` — outbound HTTP"). The `async`/`await` and browser `fetch().json()` patterns apply only to client JS shipped in a MergeReport's `static/`, which runs in the browser under normal compatibility rules.
|
|
341
|
+
|
|
342
|
+
## Imports — never fabricate
|
|
343
|
+
|
|
344
|
+
Query, form, and field references must exist in **the component you are editing**'s `declarations/index.d.ts` **before** you reference them in code. Form-field imports are per-component — another component's declarations file tells you nothing about this one. If a name is missing:
|
|
345
|
+
|
|
346
|
+
1. Add it to **this component's** form-import config on the platform.
|
|
347
|
+
2. Run `npx b6p pull "<DAV URL>"` to update this component's declarations.
|
|
348
|
+
3. Then reference it in TypeScript.
|
|
349
|
+
|
|
350
|
+
Hallucinating an import name silently passes type-check locally if the file is missing, but will fail at platform compile.
|
|
351
|
+
|
|
352
|
+
## TS narrowing pitfalls (Graal/Java types)
|
|
353
|
+
|
|
354
|
+
Some TypeScript patterns fail silently with the Java types exposed by `B` (especially `Java.Time.Instant`, `Java.Time.ZonedDateTime`, and any `Optional`-like interface). Avoiding them upfront saves edit → diagnostic → fix cycles.
|
|
355
|
+
|
|
356
|
+
### Narrowing broken in closures
|
|
357
|
+
|
|
358
|
+
**Anti-pattern:**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
let latest: Java.Time.Instant | null = null;
|
|
362
|
+
collection.forEach((item) => {
|
|
363
|
+
const inst = item.getInstant();
|
|
364
|
+
if (!latest || inst.isAfter(latest)) { // ❌ TS narrowing collapses to `never`
|
|
365
|
+
latest = inst;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
TypeScript cannot guarantee that the captured variable is still non-null when the next line of the callback executes, so it collapses the type to `never` and `.isAfter()` stops existing.
|
|
371
|
+
|
|
372
|
+
**Solutions, in order of preference:**
|
|
373
|
+
|
|
374
|
+
1. **Compare primitive values** (best): avoid the nullable union entirely by using epoch millis, ISO strings, or a sentinel value.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
let latestMs = -1;
|
|
378
|
+
collection.forEach((item) => {
|
|
379
|
+
const ms = item.getInstant().toEpochMilli();
|
|
380
|
+
if (ms > latestMs) latestMs = ms;
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
2. **Local alias with explicit type** (when the Java type must be kept): capture the variable in a `const` with the declared type before the comparison, forcing TS to re-narrow within the local scope.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
let latest: Java.Time.Instant | null = null;
|
|
388
|
+
collection.forEach((item) => {
|
|
389
|
+
const inst = item.getInstant();
|
|
390
|
+
const prev: Java.Time.Instant | null = latest; // explicit alias
|
|
391
|
+
if (prev === null || inst.isAfter(prev)) {
|
|
392
|
+
latest = inst;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
3. **Accumulate then reduce**: when the above feels forced, `forEach` into an array and sort/reduce outside the loop.
|
|
398
|
+
|
|
399
|
+
### Symptoms to recognize
|
|
400
|
+
|
|
401
|
+
- `Property '<x>' does not exist on type 'never'.` inside a callback that captures a nullable `let`: 99% of the time this is this pitfall.
|
|
402
|
+
- "Works the first time but breaks after refactoring to multi-step": suspect broken narrowing.
|
|
403
|
+
|
|
404
|
+
## Error handling
|
|
405
|
+
|
|
406
|
+
The platform surfaces uncaught exceptions in the script log. Patterns:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
try {
|
|
410
|
+
const response = B.net.fetch(url, { timeout: 5000 });
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
log.error(`Upstream returned ${response.status}`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
process(response.body);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
log.error(`Fetch failed: ${err.message}`);
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
For endpoints, prefer explicit status codes over throwing:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
if (!validInput) {
|
|
425
|
+
response.status = 400;
|
|
426
|
+
response.contentType = "application/json";
|
|
427
|
+
response.out(JSON.stringify({ error: "invalid input" }));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "BlueStep pull/push/snapshot workflow — always use the CLI scripts (never fetch manually) and always snapshot after every change via push → pull → snapshot"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Always use the BlueStep CLI scripts. Never use WebFetch, curl, or the Write tool to interact with BlueStep files manually.
|
|
6
|
+
|
|
7
|
+
- Pull: `node ~/.bluestep/pull.js <url>`
|
|
8
|
+
- Push: `node ~/.bluestep/push.js [folder]`
|
|
9
|
+
- Snapshot: `node ~/.bluestep/push.js --snapshot --comment "message" [folder]`
|
|
10
|
+
|
|
11
|
+
**Why:** A full system handles auth, WebDAV, folder naming by display name, `.b6p_url.json` tracking, and GraphQL snapshot-history recording. Bypassing it breaks the whole workflow.
|
|
12
|
+
|
|
13
|
+
**How to apply:** Any time the user says "pull", "push", or "snapshot" in a BlueStep context, use the CLI scripts. Credentials are in `~/.bluestep/config.json` — never prompt for them. Always snapshot after every code change — do not wait to be asked. Always include a `--comment` with a 1–3 sentence summary of what changed and why.
|
|
14
|
+
|
|
15
|
+
## Snapshot workflow — always push → pull → snapshot
|
|
16
|
+
|
|
17
|
+
Never run `--snapshot` directly after editing. The correct sequence is:
|
|
18
|
+
|
|
19
|
+
1. `node ~/.bluestep/push.js [folder]` — push source files to draft (BlueStep compiles TypeScript server-side).
|
|
20
|
+
2. `node ~/.bluestep/pull.js <url>` — pull to get the freshly compiled `.build/` files from the server.
|
|
21
|
+
3. `node ~/.bluestep/push.js --snapshot --comment "summary of changes" [folder]` — now the snapshot has the correct compiled JS and records history.
|
|
22
|
+
|
|
23
|
+
**Why:** `push.js` skips `.build/` directories (compiled artifacts), and BlueStep compiles TypeScript on the server. Snapshotting without pulling first captures stale local `.build/` files and reverts the compiled output to an older version. The pull step fetches the server-compiled JS before snapshotting. The `--comment` flag records a GraphQL history entry (author, timestamp, message) matching what the VS Code extension does.
|
|
24
|
+
|
|
25
|
+
Related: [snapshot integrity](snapshot-integrity.md) (including locally-compiled `.build/*` in the snapshot `saveState`), [push inner draft](push-inner-draft.md), [tsc rootdir](tsc-rootdir.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "On BlueStep BlueIQ work, never use \"AI\" in front-facing/user-visible text — the brand is always \"BlueIQ\""
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
On the BlueStep BlueIQ stack (summitridge: `/b/aiAudio` 1471299, `/b/aiCredits` 1471659, Shift Note Assistant 1471399, AI Audio Tester 1471304), never use the word "AI" in any front-facing / user-visible text. The user-facing brand is always **BlueIQ**. E.g. "out of monthly credits" (not "AI credits"), "BlueIQ Audio endpoint alive" (not "AI Audio"), "BlueIQ Assistant" (not "AI Audio Tester").
|
|
6
|
+
|
|
7
|
+
**Why:** BlueIQ is the brand name customers see; AI is not the front-facing branding for this product line.
|
|
8
|
+
|
|
9
|
+
**How to apply:** Scrub "AI" only from user-visible strings: HTML/UI text, button/panel labels, status messages, and JSON `error`/`status`/`message` fields that surface to a user. Leave as-is (exempt): the provider's real name "OpenAI"; internal identifiers and URL paths (`/b/aiCredits`, `/b/aiAudio`, `AI_CREDITS_PATH`, `openAIIntegration`, `openAILogs`, `aiaud-*` CSS classes); and code comments. Admin-only ledger "function" labels were also rebranded to "BlueIQ …" for consistency, but they are lower priority.
|
|
10
|
+
|
|
11
|
+
Related: [blueiq credit integration playbook](../reference/blueiq-credit-integration-playbook.md).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "BlueStep addSearch needs MM/DD/YYYY for date fields — YYYY/MM/DD passes validation but silently fails to filter; raw Java LocalDate objects also fail"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
When building MEF queries with `addSearch(field, op, value)` against a date or date/time field, pass the value as a string in `MM/DD/YYYY` format (US-order, slash-separated) — what BlueStep's `parseDate` documents as its default at `B.d.ts:2660`.
|
|
6
|
+
|
|
7
|
+
**Why:** Two BlueStep date formats coexist and they are NOT interchangeable for search input:
|
|
8
|
+
|
|
9
|
+
- `B.time.B6P_LOCAL_DATE = ofPattern("uuuu/MM/dd")` — the serialization/display format. `YYYY/MM/DD` strings ARE accepted by `addSearch` validation as "well-formatted" (no error thrown), but the comparison silently fails to apply — the predicate is dropped and all rows pass through unfiltered. Learned the hard way on the summitridge Report System (twice).
|
|
10
|
+
- `B.time.parseDate` default = `MM/dd/yyyy` — the actual parser BlueStep uses for date search values. This is the format `addSearch` wants.
|
|
11
|
+
|
|
12
|
+
ISO-8601 `YYYY-MM-DD` (with dashes, what HTML `<input type="date">` emits) throws the explicit error: *"Searching a date/time field requires a null, a date/time value or a String containing a well formatted date/time value."*
|
|
13
|
+
|
|
14
|
+
Passing a raw Java `LocalDate` object from `B.time.LocalDate` also fails — Graal.js's JS↔Java marshalling does not produce a usable string representation when the value flows into `addSearch`. (However, `B.time.ZonedDateTime` instances appear to work — see `gardenplazaofvalleyview\(DOR) Daily Occupancy Report` for a working example.)
|
|
15
|
+
|
|
16
|
+
**How to apply:**
|
|
17
|
+
|
|
18
|
+
- Format a JS `Date` to BlueStep search format as `MM/DD/YYYY` with slashes: `${m}/${day}/${y}`, zero-padded.
|
|
19
|
+
- Convert ISO `YYYY-MM-DD` from HTML date inputs to `MM/DD/YYYY` before calling `addSearch`.
|
|
20
|
+
- Working precedent: `rop\DPN Scoring\draft\scripts\app.ts:261` — `toMDY = (iso) => "${m}/${d}/${y}"`.
|
|
21
|
+
- Do NOT pass raw `LocalDate` / `LocalDateTime` Java objects to `addSearch` — always convert to BlueStep-format strings first.
|
|
22
|
+
- For datetime fields specifically, the format is unverified but likely `MM/DD/YYYY hh:mm:ss a` (the input-side analog of `B6P_LOCAL_DATETIME`'s output format). The summitridge Report System catalog has no datetime fields yet, so this is theoretical.
|
|
23
|
+
|
|
24
|
+
**The trap that bit twice on the summitridge Report System:**
|
|
25
|
+
|
|
26
|
+
1. Initial bug 2026-04-24: passed ISO `YYYY-MM-DD` → got the well-formatted-date error → fixed by switching to `YYYY/MM/DD`.
|
|
27
|
+
2. Regression 2026-04-26: `YYYY/MM/DD` made the error stop, but filters silently returned all rows. Real fix: switch to `MM/DD/YYYY`. Don't be fooled by the absence of an error — a date filter that's actually applied produces a *narrower* row count.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Research-first approach for BlueStep endpoints — gather API docs, read type declarations, study existing examples before writing"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
For any new BlueStep endpoint or integration, front-load research before writing code: read the external API docs, study the `B.d.ts` declarations for HTTP/request/response patterns, and look at existing endpoint examples in the org. Write with full context.
|
|
6
|
+
|
|
7
|
+
**Why:** This avoids trial-and-error iteration on BlueStep's GraalJS environment, where testing requires pushing to the server. The research-first pass has produced correct code on the first try.
|
|
8
|
+
|
|
9
|
+
**How to apply:** Gather API docs + type declarations + existing examples up front. When delegating to a `bluestep-dev` agent, pass comprehensive context including exact method signatures and patterns.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Correct BSJS formula patterns — form access, field reads/writes, HTTP, error handling, B.* utilities, DocumentLinkField"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Use these patterns as the baseline for every BlueStep post-save (or any) formula. Do not deviate without first checking a working formula — endpoints and merge reports behave differently, and formulas need special care.
|
|
6
|
+
|
|
7
|
+
**Why:** Hard-won from multiple failed iterations on a document-summarizer formula. The recurring failures were wrong form access and wrong HTTP response reading.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
- Line 1 is ALWAYS: `/// <reference path='../../../scriptlibrary' />`
|
|
12
|
+
- Formulas run as bare top-level statements — NO IIFE, no `main()`, no `export default`.
|
|
13
|
+
- No bare `return` statements — use a guard `if` block, or `throw` inside try/catch, for early exits.
|
|
14
|
+
|
|
15
|
+
## Form / field access
|
|
16
|
+
|
|
17
|
+
- A form/query configured in the component's platform form-import config (regenerated into `declarations/index.d.ts` on pull) is available as a **top-level variable directly in `app.ts`**, named after its FID — do NOT use `B.currentRecord()`. (Older modules registered this by hand in a now-legacy `objects/imports.ts`.)
|
|
18
|
+
- Fields: `myForm.fields.fieldName`.
|
|
19
|
+
- Field read: `.val()` (direct) or `.opt().orElse(default)` (safe).
|
|
20
|
+
- Field write: `.val(newValue)` for scalars (string, bool, date, number).
|
|
21
|
+
- Clear a field: `.clear()`.
|
|
22
|
+
- Single-select dropdown (200): `.val(exportValueString)` — `.options()` does NOT exist on these, it throws "Unknown identifier: options".
|
|
23
|
+
- Multi-select checkbox (201): `.options().filter(op => ...).forEach(op => op.selected(true))`.
|
|
24
|
+
- Set by ID: `.set(B.util.toId(idString))`.
|
|
25
|
+
|
|
26
|
+
## HTTP + response reading
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const stream = B.net.fetch(url, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ...' },
|
|
32
|
+
body: JSON.stringify(payload),
|
|
33
|
+
connectionTimeout: 60_000,
|
|
34
|
+
readTimeout: 60_000
|
|
35
|
+
});
|
|
36
|
+
const responseText = B.io.fromInputStream(stream); // ← always use this, not BufferedReader
|
|
37
|
+
const data = JSON.parse(responseText);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Error handling
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
try {
|
|
44
|
+
// logic — use throw "message" for controlled early exits
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const sw = B.io.stringWriter();
|
|
47
|
+
if (e.printStackTrace) e.printStackTrace(B.io.toPrintWriter(sw));
|
|
48
|
+
else sw.write(e.toString());
|
|
49
|
+
myForm.fields.resultField.val('[Error: ' + sw.toString() + ']');
|
|
50
|
+
B.net.sendMessage('Formula failed: ' + sw.toString(), true); // modal alert to user
|
|
51
|
+
B.io.printStackTrace(e); // server log
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## B.* utilities
|
|
56
|
+
|
|
57
|
+
- `B.io.fromInputStream(stream)` — read HTTP response to string.
|
|
58
|
+
- `B.io.stringWriter()` + `B.io.toPrintWriter(sw)` — capture Java stack traces.
|
|
59
|
+
- `B.io.printStackTrace(e)` — write exception to server log.
|
|
60
|
+
- `B.toBase64(javaByteArray)` — base64-encode a `Java.ByteArray`.
|
|
61
|
+
- `B.net.fetch(url, params)` — outbound HTTP.
|
|
62
|
+
- `B.net.sendMessage(html, true)` — modal alert to current user.
|
|
63
|
+
- `B.util.toId(string)` — convert a string to a `Bluestep.Id`.
|
|
64
|
+
- `B.time.ZonedDateTime.now()` — current timestamp.
|
|
65
|
+
|
|
66
|
+
## DocumentLinkField
|
|
67
|
+
|
|
68
|
+
- Cast: `formEntry.fields.document as unknown as Bluestep.Relate.DocumentLinkField`.
|
|
69
|
+
- `docField.toBytes({})` — get the file as a `Java.ByteArray`.
|
|
70
|
+
- `docField.filename()` — get the filename string.
|
|
71
|
+
- `docField.contentSize()` — size in bytes (0 = no file attached).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "BlueStep pages use jQuery — never define a global $ function or variable in merge-report scripts or you silently break Save and all page interactivity"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Never define a global function or variable named `$` in BlueStep merge-report scripts. BlueStep loads jQuery, and the entire page (form submission, change management, modals, navigation) depends on `$` being jQuery. Overwriting it with a `querySelector` wrapper silently breaks the Save button and all page interactivity.
|
|
6
|
+
|
|
7
|
+
**Why:** This caused a hard-to-diagnose bug where Save did nothing — jQuery's `$` was replaced by a plain `querySelector` helper, breaking `submitForm` and all BlueStep internals.
|
|
8
|
+
|
|
9
|
+
**How to apply:** Use `qs()` (or another non-colliding name) for `querySelector` helpers. Also avoid overwriting other common globals like `el` if they might collide with BlueStep or library internals.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Always pass the inner draft/ folder (not the component root) to push.js, otherwise files land in a draft/draft/* dead zone on BlueStep"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
When pushing a BlueStep component, always pass the **inner `draft/` folder** as the local-folder argument to `push.js`, never the component root.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# CORRECT — relative paths come out as "scripts/app.ts", "static/script.ts"
|
|
9
|
+
node ~/.bluestep/push.js [--snapshot --comment "..."] \
|
|
10
|
+
"C:\...\Bluestep Pull Requests\<org>\<...path>\<Component Name>\draft" \
|
|
11
|
+
"https://<org>.bluestep.net/files/<id>/draft/"
|
|
12
|
+
|
|
13
|
+
# WRONG — relative paths come out as "draft/scripts/app.ts" and land in draft/draft/*
|
|
14
|
+
node ~/.bluestep/push.js "...\<Component Name>" "..."
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Why:** `push.js` (`~/.bluestep/push.js`, lines 388–396) walks the supplied local folder and computes each file's path relative to it, then appends that relative path to the target URL. If the local folder is the component root (which contains `draft/` as a subfolder), every file's relative path starts with `draft/`, and concatenated with a target URL that already ends in `/draft/` you get `.../draft/draft/scripts/app.ts` — a parallel ghost tree BlueStep does not load from. The live files BlueStep serves live at the URL root (`.../draft/scripts/...`), not under a nested `draft/`.
|
|
18
|
+
|
|
19
|
+
**How to apply:** Every time you invoke `push.js` for a BlueStep component, the `localFolder` argument's last path segment must be `draft` (or `snapshot`). If you accidentally push at the wrong level, the symptom is silent staleness — pushes "succeed" but the live JS never updates, and browser caches and source maps mislead you into thinking the bug is in code that is actually fine. Diagnose by inspecting the BlueStep file tree for a duplicate `draft/` subfolder or unexpected files at the root.
|
|
20
|
+
|
|
21
|
+
**Related — the deriver fails when components are nested deeper than `<org>/<scriptName>`.** `push.js`'s URL deriver expects exactly three path segments before `draft/` (org / scriptName / type). The Report System uses `summitridge/Report System/<Component>/draft` — four segments — so auto-derive fails. Fix: read each component's `.b6p_metadata.json` and pass the `url` explicitly as the second argument. All three Report System components (Maestro, Viewer, Builder) need this on every push.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Don't pile CSS/HTML/JS into app.ts — use the dedicated files (styles.css, index.html, script.ts) that the pulled folder already has"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
When a BlueStep pull contains dedicated files (`styles.css`, `index.html`, `script.ts`, etc.) alongside `app.ts`, put each kind of content in the file that is named for it. Do not dump all CSS and HTML into a single giant `B.out = ` template literal in `app.ts`.
|
|
6
|
+
|
|
7
|
+
**Why:** The folder layout exists precisely so concerns are separated — CSS in `styles.css`, markup in `index.html`, client interactivity in `script.ts`. Stuffing everything into `app.ts` defeats the structure and makes the code unmaintainable. The user has called this out multiple times.
|
|
8
|
+
|
|
9
|
+
**How to apply:**
|
|
10
|
+
|
|
11
|
+
- Before writing or modifying a BlueStep file, list the folder contents and identify which dedicated files exist.
|
|
12
|
+
- Route content to its natural file: CSS → `styles.css`, markup template → `index.html` (or a separate template file), client-side JS → `script.ts`, server-side logic → `app.ts`.
|
|
13
|
+
- For merge reports specifically: the server-side TS can read sibling files via `B.io.fromInputStream(...)` (or the appropriate API) and inject them into `B.out` — investigate the existing API rather than inlining.
|
|
14
|
+
- For endpoints with `static/` folders: HTML/CSS/JS load from their own files; `app.ts` is the request handler only.
|
|
15
|
+
- If unsure how multiple files relate in a given BlueStep component type, check [api patterns](../reference/api-patterns.md) and look at how other working components are organized.
|
|
16
|
+
|
|
17
|
+
**Repeated offense — do not do this again.**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "BlueStep's server-side build compiles only root static/script.ts to .build/script.js — subdirectory .ts files are NOT compiled"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Keep all BlueStep merge-report client code in ONE file: `static/script.ts`. BlueStep compiles only the root `static/script.ts` → `.build/script.js`. It does NOT recursively compile `static/util/*.ts`, `static/pages/*.ts`, etc. — even though a `tsconfig.json` with `"include": ["**/*.ts"]` suggests it should.
|
|
6
|
+
|
|
7
|
+
Symptom when you get this wrong: silent 404s on every subdirectory `.build/*.js`, producing a completely blank page (no errors — the scripts just don't load).
|
|
8
|
+
|
|
9
|
+
**Why:** Discovered 2026-04-15 while building the CRM Intelligence Dashboard (beh/1433876). A modular architecture with `util/escape.ts`, `util/dates.ts`, `pages/overview.ts`, etc. produced an empty `.build/` and a blank page. Consolidating everything into a single `static/script.ts` (matching the Havenwood Census Dashboard pattern) fixed it immediately.
|
|
10
|
+
|
|
11
|
+
**How to apply:**
|
|
12
|
+
|
|
13
|
+
- Put all merge-report client code in one file: `static/script.ts`.
|
|
14
|
+
- Use banner comments to organize logical sections (TYPES, UTILITIES, COMPONENTS, PAGES, ENTRY POINT).
|
|
15
|
+
- `static/index.html` should load only `styles.css`, any CDN scripts (e.g. Chart.js), and `.build/script.js`.
|
|
16
|
+
- Server endpoint code follows the same rule: `scripts/app.ts` is the only runtime-loaded entry. Confirmed on server-side endpoints 2026-04-24 — a sibling `scripts/runAction.ts` containing runtime functions compiled to its own `.build/scripts/runAction.js`, but BlueStep never loaded it, so `handleRunAction is not defined` at runtime.
|
|
17
|
+
|
|
18
|
+
**Exception — pure type files are safe to keep separate:**
|
|
19
|
+
|
|
20
|
+
- A `scripts/types.ts` that contains ONLY `interface`/`type`/type-alias declarations (zero runtime emit) can live alongside `app.ts`. TypeScript sees it at compile time; no JS is emitted for it; BlueStep has nothing to load or miss. Verified on the summitridge Report System Maestro.
|
|
21
|
+
- The moment you add a runtime value (a `const`, `function`, or `class` that emits), it becomes invisible unless merged into `app.ts`.
|
|
22
|
+
|
|
23
|
+
**Gotcha — module-level `const` + top-level dispatcher = temporal dead zone (TDZ) errors:**
|
|
24
|
+
|
|
25
|
+
- If `scripts/app.ts` starts with a top-level `try { ... switch(action) { case "x": handleX(); } }` and `handleX` references a `const` declared *later* in the same file, Graal.js throws `ReferenceError: X is not defined` at runtime. Function declarations hoist; `const` bindings do NOT. See [top level const tdz](top-level-const-tdz.md).
|
|
26
|
+
- Fix: put all module-level constants ABOVE the top-level dispatcher block. Seen on the Report Data Maestro 2026-04-24 (`MAX_PAGE_SIZE`, `OPS_BY_TYPE`, etc.).
|
|
27
|
+
|
|
28
|
+
If the architecture is genuinely too big for one file, concatenate at author time (source section comments, clear banners) rather than relying on module resolution — BlueStep's compiler does not do it for you.
|