@chaprola/mcp-server 1.6.4 → 1.7.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/dist/index.js CHANGED
@@ -102,11 +102,11 @@ const server = new McpServer({
102
102
  - **Run programs:** chaprola_run (single execution), chaprola_run_each (per-record batch), chaprola_report (published reports)
103
103
  - **Email:** chaprola_email_send, chaprola_email_inbox, chaprola_email_read
104
104
  - **Web:** chaprola_search (Brave API), chaprola_fetch (URL → markdown)
105
- - **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields — NON-DESTRUCTIVE. Use this, NOT chaprola_import, to modify schemas on existing data.)
105
+ - **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields — NON-DESTRUCTIVE for in-place schema edits). Re-imports now preserve and widen existing schemas automatically when targeting an existing file.
106
106
  - **Export:** chaprola_export (JSON or FHIR — full round-trip: FHIR in, process, FHIR out)
107
107
  - **Schedule:** chaprola_schedule (cron jobs for any endpoint)
108
108
 
109
- **The programming language** is small and focused — about 15 commands. Read chaprola://cookbook before writing source code. Common patterns: aggregation, filtering, scoring, report formatting. Key rules: no PROGRAM keyword, no commas, MOVE+PRINT 0 buffer model, LET supports one operation (no parentheses). Named parameters: PARAM.name reads URL query params as strings; LET x = PARAM.name converts to numeric. Named output positions: U.name instead of U1-U20.
109
+ **The programming language** is small and focused — about 15 commands. Read chaprola://cookbook before writing source code. Common patterns: aggregation, filtering, scoring, report formatting. Key rules: no PROGRAM keyword, no commas, reports can use either the classic MOVE-to-U-buffer then PRINT 0 pattern or one-line PRINT concatenation, and LET supports one operation (no parentheses). Named parameters: PARAM.name reads URL query params as strings; LET x = PARAM.name converts to numeric. Named output positions: U.name instead of U1-U20.
110
110
 
111
111
  **Common misconceptions:**
112
112
  - "No JOINs" → Wrong. chaprola_query supports JOIN with hash and merge methods across files. Use chaprola_index to build indexes for fast lookups on join fields.
@@ -155,7 +155,7 @@ function readRef(filename) {
155
155
  server.resource("cookbook", "chaprola://cookbook", { description: "Chaprola language cookbook — syntax patterns, complete examples, and the import→compile→run workflow. READ THIS before writing any Chaprola source code.", mimeType: "text/markdown" }, async () => ({
156
156
  contents: [{ uri: "chaprola://cookbook", mimeType: "text/markdown", text: readRef("cookbook.md") }],
157
157
  }));
158
- server.resource("gotchas", "chaprola://gotchas", { description: "Common Chaprola mistakes — no parentheses in LET, no commas in PRINT, MOVE length must match field width, DEFINE names must not collide with fields. READ THIS before writing code.", mimeType: "text/markdown" }, async () => ({
158
+ server.resource("gotchas", "chaprola://gotchas", { description: "Common Chaprola mistakes — no parentheses in LET, no commas in PRINT, DEFINE names must not collide with fields, always pass primary_format to compile. READ THIS before writing code.", mimeType: "text/markdown" }, async () => ({
159
159
  contents: [{ uri: "chaprola://gotchas", mimeType: "text/markdown", text: readRef("gotchas.md") }],
160
160
  }));
161
161
  server.resource("endpoints", "chaprola://endpoints", { description: "Chaprola API endpoint reference — all 40 endpoints with request/response shapes", mimeType: "text/markdown" }, async () => ({
@@ -198,9 +198,12 @@ server.resource("ref-email", "chaprola://ref/email", { description: "Email syste
198
198
  server.resource("ref-gotchas", "chaprola://ref/gotchas", { description: "Common Chaprola mistakes — language, API, and secondary file pitfalls", mimeType: "text/markdown" }, async () => ({
199
199
  contents: [{ uri: "chaprola://ref/gotchas", mimeType: "text/markdown", text: readRef("ref-gotchas.md") }],
200
200
  }));
201
- server.resource("ref-auth", "chaprola://ref/auth", { description: "Authentication details — registration, login, BAA, MCP env vars", mimeType: "text/markdown" }, async () => ({
201
+ server.resource("ref-auth", "chaprola://ref/auth", { description: "Authentication details — registration, login, BAA, cross-user sharing, MCP env vars", mimeType: "text/markdown" }, async () => ({
202
202
  contents: [{ uri: "chaprola://ref/auth", mimeType: "text/markdown", text: readRef("ref-auth.md") }],
203
203
  }));
204
+ server.resource("ref-apps", "chaprola://ref/apps", { description: "Building apps on Chaprola — React/frontend architecture, site keys, single-owner vs multi-user, enterprise proxy pattern", mimeType: "text/markdown" }, async () => ({
205
+ contents: [{ uri: "chaprola://ref/apps", mimeType: "text/markdown", text: readRef("ref-apps.md") }],
206
+ }));
204
207
  // --- MCP Prompts ---
205
208
  server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read this before writing any Chaprola source code.", async () => ({
206
209
  messages: [{
@@ -213,7 +216,7 @@ server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read
213
216
  "- NO `PROGRAM` keyword — programs start directly with commands\n" +
214
217
  "- NO commas anywhere — all arguments are space-separated\n" +
215
218
  "- NO parentheses in LET — only `LET var = a OP b` (one operation)\n" +
216
- "- Output uses MOVE + PRINT 0 buffer model, NOT `PRINT field`\n" +
219
+ "- Output can use classic MOVE + PRINT 0 buffers or one-line PRINT concatenation (`PRINT \"label\" + P.field + rec`)\n" +
217
220
  "- Field addressing: P.fieldname (primary), S.fieldname (secondary)\n" +
218
221
  "- Loop pattern: `LET rec = 1` → `SEEK rec` → `IF EOF GOTO end` → process → `LET rec = rec + 1` → `GOTO loop`\n\n" +
219
222
  "## Minimal Example\n" +
@@ -222,10 +225,7 @@ server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read
222
225
  "LET rec = 1\n" +
223
226
  "100 SEEK rec\n" +
224
227
  " IF EOF GOTO 900\n" +
225
- " MOVE BLANKS U.1 40\n" +
226
- " MOVE P.name U.1 8\n" +
227
- " MOVE P.value U.12 6\n" +
228
- " PRINT 0\n" +
228
+ " PRINT P.name + \" — \" + P.value\n" +
229
229
  " LET rec = rec + 1\n" +
230
230
  " GOTO 100\n" +
231
231
  "900 END\n" +
@@ -246,6 +246,10 @@ server.tool("chaprola_hello", "Health check — verify the Chaprola API is runni
246
246
  const res = await fetch(url);
247
247
  return textResult(res);
248
248
  });
249
+ server.tool("chaprola_help", "Get the full Chaprola documentation bundle from POST /help. Call this before guessing when compile or run fails. No auth required.", {}, async () => {
250
+ const res = await publicFetch("POST", "/help", {});
251
+ return textResult(res);
252
+ });
249
253
  server.tool("chaprola_register", "Register a new Chaprola account. Returns an API key — save it immediately", {
250
254
  username: z.string().describe("3-40 chars, alphanumeric + hyphens/underscores, starts with letter"),
251
255
  passcode: z.string().describe("16-128 characters. Use a long, unique passcode"),
@@ -334,7 +338,7 @@ server.tool("chaprola_baa_status", "Check whether the authenticated user has sig
334
338
  return textResult(res);
335
339
  });
336
340
  // --- Import ---
337
- server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA). DESTRUCTIVE: This REPLACES both the format (.F) and data (.DA) files if they already exist. All existing data will be lost. Use chaprola_alter to modify field widths/schema on existing data. Use chaprola_import only for new data or when replacing entire datasets. Sign BAA first if handling PHI", {
341
+ server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA). If the target file already exists, Chaprola preserves the existing schema, widens matching fields as needed, keeps legacy fields as blanks, and appends new fields at the end. Use chaprola_alter for explicit in-place schema surgery. Sign BAA first if handling PHI", {
338
342
  project: z.string().describe("Project name"),
339
343
  name: z.string().describe("File name (without extension)"),
340
344
  data: z.string().describe("JSON array of record objects to import"),
@@ -351,7 +355,7 @@ server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F
351
355
  const res = await authedFetch("/import", body);
352
356
  return textResult(res);
353
357
  }));
354
- server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit). DESTRUCTIVE: The subsequent chaprola_import_process will replace existing data. Use chaprola_alter to modify schemas on existing data.", {
358
+ server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit). The subsequent chaprola_import_process preserves and widens an existing schema automatically when importing into an existing file.", {
355
359
  project: z.string().describe("Project name"),
356
360
  name: z.string().describe("File name (without extension)"),
357
361
  }, async ({ project, name }) => withBaaCheck(async () => {
@@ -359,7 +363,7 @@ server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large file
359
363
  const res = await authedFetch("/import-url", { userid: username, project, name });
360
364
  return textResult(res);
361
365
  }));
362
- server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files. DESTRUCTIVE: Replaces existing data if the file already exists. Use chaprola_alter to modify schemas on existing data.", {
366
+ server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files. If the target file already exists, the existing schema is preserved and widened automatically as needed.", {
363
367
  project: z.string().describe("Project name"),
364
368
  name: z.string().describe("File name (without extension)"),
365
369
  format: z.enum(["json", "fhir"]).optional().describe("Data format: json (default) or fhir"),
@@ -371,7 +375,7 @@ server.tool("chaprola_import_process", "Process a file previously uploaded to S3
371
375
  const res = await authedFetch("/import-process", body);
372
376
  return textResult(res);
373
377
  }));
374
- server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference. DESTRUCTIVE: Replaces existing data if the file already exists. Use chaprola_alter to modify schemas on existing data.", {
378
+ server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference.", {
375
379
  project: z.string().describe("Project name"),
376
380
  name: z.string().describe("Output file name (without extension)"),
377
381
  url: z.string().describe("Public URL to download (http/https only)"),
@@ -409,12 +413,12 @@ server.tool("chaprola_list", "List files in a project with optional wildcard pat
409
413
  return textResult(res);
410
414
  }));
411
415
  // --- Compile ---
412
- server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR). READ chaprola://cookbook BEFORE writing source. Key syntax: no PROGRAM keyword (start with commands), no commas, MOVE+PRINT 0 buffer model (not PRINT field), SEEK for primary records, OPEN/READ/WRITE/CLOSE for secondary files, LET supports one operation (no parentheses), field addressing via P.field/S.field requires primary_format/secondary_format params.", {
416
+ server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR). READ chaprola://cookbook BEFORE writing source. Key syntax: no PROGRAM keyword (start with commands), no commas, reports can use MOVE+PRINT 0 buffers or one-line PRINT concatenation, SEEK for primary records, OPEN/READ/WRITE/CLOSE for secondary files, LET supports one operation (no parentheses). Use primary_format to enable P.fieldname addressing (recommended) — the compiler resolves field names to positions and lengths from the format file. If compile fails, call chaprola_help before retrying.", {
413
417
  project: z.string().describe("Project name"),
414
418
  name: z.string().describe("Program name (without extension)"),
415
419
  source: z.string().describe("Chaprola source code"),
416
- primary_format: z.string().optional().describe("Primary data file name (enables P.fieldname addressing)"),
417
- secondary_format: z.string().optional().describe("Secondary format file name (enables S.fieldname addressing)"),
420
+ primary_format: z.string().optional().describe("Primary data file name enables P.fieldname addressing (recommended for all programs that reference data fields)"),
421
+ secondary_format: z.string().optional().describe("Secondary data file name enables S.fieldname addressing (required if using S.fieldname references)"),
418
422
  }, async ({ project, name, source, primary_format, secondary_format }) => withBaaCheck(async () => {
419
423
  const { username } = getCredentials();
420
424
  const body = { userid: username, project, name, source };
@@ -426,7 +430,7 @@ server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR)
426
430
  return textResult(res);
427
431
  }));
428
432
  // --- Run ---
429
- server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for large datasets (>100K records)", {
433
+ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for large datasets (>100K records). If runtime errors occur, call chaprola_help before retrying.", {
430
434
  project: z.string().describe("Project name"),
431
435
  name: z.string().describe("Program name (without extension)"),
432
436
  primary_file: z.string().optional().describe("Primary data file to load"),
@@ -478,6 +482,20 @@ server.tool("chaprola_run_each", "Run a compiled .PR program against every recor
478
482
  const res = await authedFetch("/run-each", body);
479
483
  return textResult(res);
480
484
  }));
485
+ // --- Systemhelp ---
486
+ server.tool("chaprola_systemhelp", "Send your program name and error message. Chaprola will read your source, intent file, and data schema to diagnose and fix the problem. See POST /help for examples.", {
487
+ project: z.string().describe("Project name"),
488
+ name: z.string().describe("Program name (without extension)"),
489
+ error: z.string().optional().describe("Error message from compile or runtime (copy verbatim if available)"),
490
+ request: z.string().describe("Plain-language description of the problem. Include context: what changed, what you expected, what happened instead."),
491
+ }, async ({ project, name, error, request }) => withBaaCheck(async () => {
492
+ const { username } = getCredentials();
493
+ const body = { userid: username, project, name, request };
494
+ if (error)
495
+ body.error = error;
496
+ const res = await authedFetch("/systemhelp", body);
497
+ return textResult(res);
498
+ }));
481
499
  // --- Publish ---
482
500
  server.tool("chaprola_publish", "Publish a compiled program for public access via /report", {
483
501
  project: z.string().describe("Project name"),
@@ -620,7 +638,7 @@ server.tool("chaprola_merge", "Merge two sorted data files into one. Both must s
620
638
  return textResult(res);
621
639
  }));
622
640
  // --- Schema: Format + Alter ---
623
- server.tool("chaprola_format", "Inspect a data file's schema — returns field names, positions, lengths, types, and PHI flags", {
641
+ server.tool("chaprola_format", "Inspect a data file's schema — returns field names, types, and PHI flags. Use this to understand the data structure before writing programs.", {
624
642
  project: z.string().describe("Project name"),
625
643
  name: z.string().describe("Data file name (without .F extension)"),
626
644
  }, async ({ project, name }) => withBaaCheck(async () => {
@@ -628,7 +646,7 @@ server.tool("chaprola_format", "Inspect a data file's schema — returns field n
628
646
  const res = await authedFetch("/format", { userid: username, project, name });
629
647
  return textResult(res);
630
648
  }));
631
- server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename fields, add new fields, drop fields. NON-DESTRUCTIVE: Transforms existing data to match the new schema. This is the ONLY safe way to change field widths or schema on existing data files. Unlike chaprola_import which replaces all data, chaprola_alter preserves and reformats existing records.", {
649
+ server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename fields, add new fields, drop fields. NON-DESTRUCTIVE: Transforms existing data to match the new schema. Use this for explicit schema surgery on existing data files; re-imports now preserve and widen existing schemas automatically, but chaprola_alter still handles rename/drop/narrow operations.", {
632
650
  project: z.string().describe("Project name"),
633
651
  name: z.string().describe("Data file name (without extension)"),
634
652
  alter: z.array(z.object({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chaprola/mcp-server",
3
- "version": "1.6.4",
4
- "description": "MCP server for Chaprola — agent-first data platform. Gives AI agents 47 tools for structured data storage, record CRUD, querying, schema inspection, web search, URL fetching, scheduled jobs, and execution via plain HTTP.",
3
+ "version": "1.7.0",
4
+ "description": "MCP server for Chaprola — agent-first data platform. Gives AI agents tools for structured data storage, record CRUD, querying, schema inspection, documentation lookup, web search, URL fetching, scheduled jobs, scoped site keys, and execution via plain HTTP.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -51,6 +51,39 @@ If you do handle PHI, sign the BAA once per account:
51
51
 
52
52
  These are read by the MCP server and injected into every authenticated request automatically.
53
53
 
54
+ ## Cross-User Project Sharing
55
+
56
+ By default, only the project owner can read and write their data. To grant another user access, create an `access.json` file in the project root:
57
+
58
+ ```
59
+ s3://chaprola-2026/{owner}/{project}/access.json
60
+ ```
61
+
62
+ **Format:**
63
+ ```json
64
+ {
65
+ "owner": "tawni",
66
+ "project": "social",
67
+ "discoverable": false,
68
+ "description": "Content calendar and social media",
69
+ "writers": [
70
+ {"username": "cal", "granted_at": "2026-04-06T17:25:16Z"},
71
+ {"username": "nora", "granted_at": "2026-04-06T17:25:16Z"}
72
+ ]
73
+ }
74
+ ```
75
+
76
+ **How it works:**
77
+ - Any user in the `writers` list gets full read+write access to that project through all API endpoints (query, update-record, insert-record, delete-record, export, compile, run, run-each, etc.)
78
+ - The shared user passes the **owner's** userid in requests: `{"userid": "tawni", "project": "social", ...}` — authenticated with their own API key
79
+ - Every protected endpoint checks: (1) does the API key's username match the `userid`? If not, (2) is the user listed as a writer in `access.json`?
80
+ - No `access.json` = no shared access (owner-only)
81
+ - `discoverable` is reserved for future use (project discovery/listing)
82
+
83
+ **To set up sharing:** Create or update the `access.json` file via S3 directly or through the `/sv` admin interface.
84
+
85
+ **To revoke access:** Remove the user from the `writers` array, or delete `access.json` entirely.
86
+
54
87
  ## Credential Recovery
55
88
 
56
89
  If your API key stops working (403):
@@ -19,43 +19,48 @@ POST /run {userid, project, name: "REPORT", primary_file: "STAFF", record: 1}
19
19
  |-------|---------|--------------------------|
20
20
  | R1–R20 | HULDRA elements (parameters) | No — HULDRA overwrites these |
21
21
  | R21–R40 | HULDRA objectives (error metrics) | No — HULDRA reads these |
22
- | R41–R50 | Scratch space | **Yes — always use R41–R50 for DEFINE VARIABLE** |
22
+ | R41–R99 | Scratch space | **Yes — always use R41–R99 for DEFINE VARIABLE** |
23
23
 
24
- For non-HULDRA programs, R1–R40 are technically available but using R41–R50 is a good habit.
24
+ For non-HULDRA programs, R1–R40 are technically available but using R41–R99 is a good habit.
25
25
 
26
- ## PRINT: Output from U Buffer
26
+ ## PRINT: Preferred Output Methods
27
27
 
28
- ```
29
- PRINT 0 — output the ENTIRE U buffer contents, then clear it
30
- PRINT N output exactly N characters from U buffer (no clear)
28
+ **Concatenation (preferred):**
29
+ ```chaprola
30
+ PRINT P.name + " " + P.department + " $" + R41
31
+ PRINT "Total: " + R42
32
+ PRINT P.last_name // single field, auto-trimmed
33
+ PRINT "Hello from Chaprola!" // literal string
31
34
  ```
32
35
 
33
- Use `PRINT N` when you've placed data at specific positions and want clean output without trailing garbage. Use `PRINT 0` for quick output of everything.
36
+ - String literals are copied as-is.
37
+ - P./S./U./X. fields are auto-trimmed (trailing spaces removed).
38
+ - R-variables print as integers when no fractional part, otherwise as floats.
39
+ - Concatenation auto-flushes the line.
34
40
 
41
+ **U buffer output (for fixed-width columnar reports only):**
35
42
  ```chaprola
36
- MOVE "Hello" U.1 5
37
- PRINT 5 // Outputs "Hello" — exactly 5 chars, no trailing spaces
43
+ CLEAR U
44
+ MOVE P.name U.1 20
45
+ PUT sal INTO U.22 10 D 2
46
+ PRINT 0 // output entire U buffer, then clear
38
47
  ```
39
48
 
40
49
  ## Hello World (no data file)
41
50
 
42
51
  ```chaprola
43
- MOVE "Hello from Chaprola!" U.1 20
44
- PRINT 0
52
+ PRINT "Hello from Chaprola!"
45
53
  END
46
54
  ```
47
55
 
48
56
  ## Loop Through All Records
49
57
 
50
58
  ```chaprola
51
- DEFINE VARIABLE rec R1
59
+ DEFINE VARIABLE rec R41
52
60
  LET rec = 1
53
61
  100 SEEK rec
54
62
  IF EOF GOTO 900
55
- MOVE BLANKS U.1 40
56
- MOVE P.name U.1 8
57
- MOVE P.salary U.12 6
58
- PRINT 0
63
+ PRINT P.name + " — " + P.salary
59
64
  LET rec = rec + 1
60
65
  GOTO 100
61
66
  900 END
@@ -65,10 +70,8 @@ LET rec = 1
65
70
 
66
71
  ```chaprola
67
72
  GET sal FROM P.salary
68
- IF sal LT 80000 GOTO 200 // skip low earners
69
- MOVE P.name U.1 8
70
- PUT sal INTO U.12 10 D 0 // D=dollar format
71
- PRINT 0
73
+ IF sal LT 80000 GOTO 200 // skip low earners
74
+ PRINT P.name + " — " + R41
72
75
  200 LET rec = rec + 1
73
76
  ```
74
77
 
@@ -76,10 +79,10 @@ PRINT 0
76
79
 
77
80
  ```chaprola
78
81
  OPEN "DEPARTMENTS" 0
79
- FIND match FROM S.dept_code 3 USING P.dept_code
80
- IF match EQ 0 GOTO 200 // no match
81
- READ match // load matched secondary record
82
- MOVE S.dept_name U.12 15 // now accessible
82
+ FIND match FROM S.dept_code USING P.dept_code
83
+ IF match EQ 0 GOTO 200 // no match
84
+ READ match // load matched secondary record
85
+ PRINT P.name + " " + S.dept_name
83
86
  ```
84
87
 
85
88
  Compile with both formats so the compiler resolves fields from both files:
@@ -105,14 +108,47 @@ IF EQUAL U.200 U.180 12 GOTO 200 // match — jump to handler
105
108
  ## Read-Modify-Write (UPDATE)
106
109
 
107
110
  ```chaprola
108
- READ match // load record
109
- GET bal FROM S.balance // read current value
110
- LET bal = bal + amt // modify
111
- PUT bal INTO S.balance 8 F 0 // write back to S memory
112
- WRITE match // flush to disk
113
- CLOSE // flush all at end
111
+ READ match // load record
112
+ GET bal FROM S.balance // read current value
113
+ LET bal = bal + amt // modify
114
+ PUT bal INTO S.balance F 0 // write back to S memory (length auto-filled)
115
+ WRITE match // flush to disk
116
+ CLOSE // flush all at end
117
+ ```
118
+
119
+ ## Date Arithmetic
120
+
121
+ ```chaprola
122
+ GET DATE R41 FROM X.primary_modified // when was file last changed?
123
+ GET DATE R42 FROM X.utc_time // what time is it now?
124
+ LET R43 = R42 - R41 // difference in seconds
125
+ LET R43 = R43 / 86400 // convert to days
126
+ IF R43 GT 30 PRINT "WARNING: file is over 30 days old" ;
114
127
  ```
115
128
 
129
+ ## Get Current User
130
+
131
+ ```chaprola
132
+ PRINT "Logged in as: " + X.username
133
+ ```
134
+
135
+ ## System Text Properties (X.)
136
+
137
+ Access system metadata by property name — no numeric positions needed:
138
+
139
+ | Property | Description |
140
+ |----------|-------------|
141
+ | `X.year` | Year (four digits) |
142
+ | `X.julian` | Julian date (1–366) |
143
+ | `X.hour` | Hour (military time) |
144
+ | `X.minute` | Minute (0–59) |
145
+ | `X.username` | Authenticated user |
146
+ | `X.record_num` | Primary file record number |
147
+ | `X.utc_time` | UTC datetime (ISO 8601) |
148
+ | `X.elapsed` | Elapsed execution time |
149
+ | `X.primary_modified` | Primary file Last-Modified |
150
+ | `X.secondary_modified` | Secondary file Last-Modified |
151
+
116
152
  ## Async for Large Datasets
117
153
 
118
154
  ```bash
@@ -140,9 +176,7 @@ SEEK 1
140
176
  GOTO 300
141
177
  200 GET cardlvl FROM P.level
142
178
  IF cardlvl NE lvl GOTO 300 // filter by level param
143
- MOVE P.kanji U.1 4
144
- MOVE P.reading U.6 10
145
- PRINT 0
179
+ PRINT P.kanji + " — " + P.reading
146
180
  300 LET rec = rec + 1
147
181
  SEEK rec
148
182
  GOTO 100
@@ -208,7 +242,7 @@ POST /query {
208
242
  | `I` | Integer (right-justified) | ` 1234` |
209
243
  | `E` | Scientific notation | `1.23E+03` |
210
244
 
211
- Syntax: `PUT R1 INTO U.30 10 D 2` (R-var, location, width, format, decimals)
245
+ Syntax: `PUT R41 INTO P.salary D 2` (R-var, field name, format, decimals — length auto-filled)
212
246
 
213
247
  ## Common Field Widths
214
248
 
@@ -227,19 +261,19 @@ Use these when sizing MOVE lengths and U buffer positions.
227
261
 
228
262
  | Prefix | Description |
229
263
  |--------|-------------|
230
- | `P` | Primary data file (current record) |
231
- | `S` | Secondary data file (current record) |
264
+ | `P` | Primary data file use field names: `P.salary`, `P.name` |
265
+ | `S` | Secondary data file use field names: `S.dept`, `S.emp_id` |
232
266
  | `U` | User buffer (scratch for output) |
233
- | `X` | System text (date, time, filenames) |
267
+ | `X` | System text use property names: `X.username`, `X.utc_time` |
234
268
 
235
269
  ## Math Intrinsics
236
270
 
237
271
  ```chaprola
238
- LET R2 = EXP R1 // e^R1
239
- LET R2 = LOG R1 // ln(R1)
240
- LET R2 = SQRT R1 // √R1
241
- LET R2 = ABS R1 // |R1|
242
- LET R3 = POW R1 R2 // R1^R2
272
+ LET R42 = EXP R41 // e^R41
273
+ LET R42 = LOG R41 // ln(R41)
274
+ LET R42 = SQRT R41 // √R41
275
+ LET R42 = ABS R41 // |R41|
276
+ LET R43 = POW R41 R42 // R41^R42
243
277
  ```
244
278
 
245
279
  ## Import-Download: URL → Dataset (Parquet, Excel, CSV, JSON)
@@ -281,7 +315,7 @@ HULDRA finds the best parameter values for a mathematical model by minimizing th
281
315
  |-------|---------|-------------|
282
316
  | R1–R20 | **Elements** (parameters to optimize) | HULDRA sets these before each VM run |
283
317
  | R21–R40 | **Objectives** (error metrics) | Your program computes and stores these |
284
- | R41–R50 | **Scratch space** | Your program uses these for temp variables |
318
+ | R41–R99 | **Scratch space** | Your program uses these for temp variables |
285
319
 
286
320
  ### Complete Example: Fit a Linear Model
287
321
 
@@ -11,21 +11,60 @@ LET temp = qty + bonus
11
11
  LET result = price * temp
12
12
  ```
13
13
 
14
+ ### No built-in functions
15
+ There are NO functions with parentheses. No STR(), INT(), ABS(), LEN(), TRIM(), SUBSTR(), CONCAT(), FORMAT(), TOSTRING(), etc.
16
+ - To convert number to text for output: `PRINT "Total: " + R41`
17
+ - To write number to a field: `PUT R41 INTO P.salary D 2`
18
+ - To convert text to number: `GET R41 FROM P.salary`
19
+ - To clear a field: `MOVE BLANKS P.notes` or `CLEAR U`
20
+
21
+ ### Use field names, not numeric positions
22
+ Always use `P.salary`, `S.dept`, `X.username` — not `P.63 10`, `S.30 4`, `X.28 8`. The compiler auto-fills positions and lengths from the format file.
23
+ ```chaprola
24
+ // PREFERRED: field names — readable, resilient to format changes
25
+ GET R41 FROM P.salary
26
+ PRINT P.name + " — " + P.department
27
+ MOVE X.username U.1
28
+
29
+ // AVOID: numeric positions — fragile, unreadable
30
+ GET R41 FROM P.63 10
31
+ ```
32
+
33
+ ### Use PRINT concatenation, not MOVE + PRINT 0
34
+ ```chaprola
35
+ // PREFERRED: direct concatenation
36
+ PRINT P.name + " earns $" + R41
37
+
38
+ // AVOID: old MOVE-to-buffer pattern
39
+ MOVE BLANKS U.1 80
40
+ MOVE P.name U.1 20
41
+ PUT R41 INTO U.22 10 D 2
42
+ PRINT 0
43
+ ```
44
+
45
+ ### Use CLEAR, not MOVE BLANKS for full regions
46
+ ```chaprola
47
+ CLEAR U // clear entire user buffer
48
+ CLEAR P // clear entire primary region
49
+ CLEAR S // clear entire secondary region
50
+ MOVE BLANKS P.notes // clear a single field (length auto-filled)
51
+ ```
52
+
14
53
  ### IF EQUAL compares a literal to a location
15
- Cannot compare two memory locations. Copy to U buffer first.
54
+ Cannot compare two memory locations directly. Copy to U buffer first.
16
55
  ```chaprola
17
56
  MOVE P.txn_type U.76 6
18
57
  IF EQUAL "CREDIT" U.76 GOTO 200
19
58
  ```
20
59
 
21
- ### MOVE length must match field width
22
- `MOVE P.name U.1 20` copies 20 chars starting at the field if `name` is 8 chars wide, the extra 12 bleed into adjacent fields. Always match the format file width.
60
+ ### MOVE literal auto-pads to field width
61
+ `MOVE "Jones" P.name` auto-fills the rest of the field with blanks. No need to clear first.
23
62
 
24
63
  ### DEFINE VARIABLE names must not collide with field names
25
- If the format has a `balance` field, don't `DEFINE VARIABLE balance R3`. Use `bal` instead. The compiler confuses the alias with the field name.
64
+ If the format has a `balance` field, don't `DEFINE VARIABLE balance R41`. Use `bal` instead. The compiler confuses the alias with the field name.
26
65
 
27
66
  ### R-variables are floating point
28
- All R1–R50 are 64-bit floats. `7 / 2 = 3.5`. Use PUT with `I` format to display as integer.
67
+ All R1–R99 are 64-bit floats. `7 / 2 = 3.5`. Use PUT with `I` format to display as integer.
29
68
 
30
69
  ### Statement numbers are labels, not line numbers
31
70
  Only number lines that are GOTO/CALL targets. Don't number every line.
@@ -36,6 +75,13 @@ Always check `IF match EQ 0` after FIND before calling READ.
36
75
  ### PRINT 0 clears the U buffer
37
76
  After PRINT 0, the buffer is empty. No need to manually clear between prints unless reusing specific positions.
38
77
 
78
+ ### GET DATE / PUT DATE — no FOR keyword needed with property names
79
+ ```chaprola
80
+ GET DATE R41 FROM X.utc_time // correct — length auto-filled
81
+ GET DATE R42 FROM X.primary_modified // correct — length auto-filled
82
+ PUT DATE R41 INTO U.1 20 // U buffer needs explicit length
83
+ ```
84
+
39
85
  ## Import
40
86
 
41
87
  ### Field widths come from the longest value
@@ -74,8 +120,8 @@ Always CLOSE before END if you wrote to the secondary file. Unflushed writes are
74
120
 
75
121
  ## HULDRA Optimization
76
122
 
77
- ### Use R41–R50 for scratch variables, not R1–R20
78
- R1–R20 are reserved for HULDRA elements. R21–R40 are reserved for objectives. Your VALUE program's DEFINE VARIABLE declarations must use R41–R50 only.
123
+ ### Use R41–R99 for scratch variables, not R1–R20
124
+ R1–R20 are reserved for HULDRA elements. R21–R40 are reserved for objectives. Your VALUE program's DEFINE VARIABLE declarations must use R41–R99 only.
79
125
  ```chaprola
80
126
  // WRONG: DEFINE VARIABLE counter R1 (HULDRA will overwrite this)
81
127
  // RIGHT: DEFINE VARIABLE counter R41
@@ -0,0 +1,193 @@
1
+ # Building Apps on Chaprola — Architecture Reference
2
+
3
+ ## Overview
4
+
5
+ Chaprola is a backend for frontend apps. Your React, Vue, Svelte, or Laravel app calls `api.chaprola.org` directly from the browser. No proxy server, no middleware, no infrastructure to manage.
6
+
7
+ ## Two Architectures
8
+
9
+ ### 1. Single-Owner App (simplest)
10
+
11
+ One Chaprola account owns all the data. The app uses a **site key** locked to its domain. All users see the same data.
12
+
13
+ **Use cases:** dashboards, public reports, internal tools, data viewers, portfolio sites.
14
+
15
+ ```
16
+ Browser → React App → api.chaprola.org (site key in Authorization header)
17
+ ```
18
+
19
+ **Setup:**
20
+ 1. Register a Chaprola account: `POST /register`
21
+ 2. Sign the BAA if handling health data: `POST /sign-baa`
22
+ 3. Create a site key locked to your domain:
23
+ ```json
24
+ POST /create-site-key
25
+ {"userid": "myapp", "origin": "https://myapp.example.com", "label": "production"}
26
+ ```
27
+ Response: `{"site_key": "site_a1b2c3..."}`
28
+ 4. Use the site key in your frontend:
29
+ ```javascript
30
+ const resp = await fetch('https://api.chaprola.org/query', {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Authorization': 'Bearer site_a1b2c3...',
34
+ 'Content-Type': 'application/json'
35
+ },
36
+ body: JSON.stringify({ userid: 'myapp', project: 'main', file: 'products', where: [] })
37
+ })
38
+ ```
39
+
40
+ **Security model:** The site key is checked against the `Origin` HTTP header, which browsers set automatically. This prevents other websites from using your key (CORS-level protection). However, Origin headers are trivially spoofable from non-browser clients (curl, Postman, scripts). Anyone who extracts the site key from your JavaScript has full access to the account's data. **Use this pattern only for public or semi-public data** — dashboards, product catalogs, published reports. For private data, use the multi-user pattern (each user authenticates individually) or the enterprise proxy pattern.
41
+
42
+ ### 2. Multi-User App (each user has their own account)
43
+
44
+ Each app user registers their own Chaprola account. The app stores their API key in the browser session. Each user has their own data silo.
45
+
46
+ **Use cases:** SaaS apps, multi-tenant platforms, apps where users own their data.
47
+
48
+ ```
49
+ Browser → React App → api.chaprola.org (user's own API key)
50
+ ```
51
+
52
+ **Setup:**
53
+ 1. Your app's registration form calls Chaprola directly:
54
+ ```javascript
55
+ // User signs up
56
+ const resp = await fetch('https://api.chaprola.org/register', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({ username: 'alice', passcode: 'their-secure-passcode' })
60
+ })
61
+ const { api_key } = await resp.json()
62
+ sessionStorage.setItem('chaprola_key', api_key)
63
+ ```
64
+
65
+ 2. Your app's login form:
66
+ ```javascript
67
+ const resp = await fetch('https://api.chaprola.org/login', {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ username: 'alice', passcode: 'their-secure-passcode' })
71
+ })
72
+ const { api_key } = await resp.json()
73
+ sessionStorage.setItem('chaprola_key', api_key)
74
+ ```
75
+
76
+ 3. All subsequent API calls use the user's key:
77
+ ```javascript
78
+ const key = sessionStorage.getItem('chaprola_key')
79
+ const resp = await fetch('https://api.chaprola.org/query', {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Authorization': `Bearer ${key}`,
83
+ 'Content-Type': 'application/json'
84
+ },
85
+ body: JSON.stringify({ userid: 'alice', project: 'mydata', file: 'tasks', where: [] })
86
+ })
87
+ ```
88
+
89
+ **Security model:** Each user authenticates individually. User A cannot access User B's data (userid enforcement). No shared secrets in the frontend. API keys are per-user, stored in the browser session.
90
+
91
+ **Team data sharing:** If users need to share a project, the project owner creates an `access.json` file listing writers. See the auth reference for details.
92
+
93
+ ## Which Architecture Should I Use?
94
+
95
+ | Question | Single-Owner | Multi-User |
96
+ |----------|:---:|:---:|
97
+ | One person/org owns all the data? | Yes | |
98
+ | Users create accounts and own their data? | | Yes |
99
+ | Public dashboard or report viewer? | Yes | |
100
+ | SaaS with multiple tenants? | | Yes |
101
+ | Internal tool for a small team? | Yes | |
102
+ | App where privacy between users matters? | | Yes |
103
+ | Data is sensitive or private? | No — use Multi-User or Enterprise | Yes |
104
+
105
+ **Rule of thumb:** If you'd be uncomfortable with someone viewing all the data in that Chaprola account, don't use the single-owner pattern. The site key will be in your JavaScript source — assume it's public.
106
+
107
+ ## Enterprise Customers
108
+
109
+ If your enterprise requires that API keys never touch the browser:
110
+
111
+ Run your own backend proxy. Your React/Laravel frontend authenticates users through your own auth system (OAuth, SSO, SAML). Your backend holds the Chaprola API key server-side and proxies requests.
112
+
113
+ ```
114
+ User → Enterprise App → Enterprise Backend (holds API key) → api.chaprola.org
115
+ ```
116
+
117
+ This is the same pattern used with Stripe, Twilio, and other APIs. Chaprola does not provide a managed proxy — your infrastructure team owns this layer.
118
+
119
+ **Most apps do not need this.** The site key + per-user auth model handles the vast majority of use cases without any backend infrastructure.
120
+
121
+ ## Site Keys Reference
122
+
123
+ Site keys are API keys locked to a specific browser origin. They prevent your key from being used on other websites.
124
+
125
+ ```json
126
+ // Create
127
+ POST /create-site-key
128
+ {"userid": "myapp", "origin": "https://myapp.example.com", "label": "production"}
129
+ // Response: {"site_key": "site_...", "origin": "https://myapp.example.com"}
130
+
131
+ // List
132
+ POST /list-site-keys
133
+ {"userid": "myapp"}
134
+
135
+ // Delete
136
+ POST /delete-site-key
137
+ {"userid": "myapp", "site_key": "site_..."}
138
+ ```
139
+
140
+ **Key facts:**
141
+ - Format: `site_` prefix + 64 hex chars
142
+ - Locked to one origin (exact match on the `Origin` HTTP header)
143
+ - Never expire — persist until explicitly deleted
144
+ - Not affected by `/login` (login rotates the main API key, not site keys)
145
+ - Same permissions as the main API key for that account
146
+ - Create multiple site keys for different environments (dev, staging, production)
147
+
148
+ ## Deploying Your App
149
+
150
+ For static React/Vue/Svelte apps, Chaprola can host them:
151
+
152
+ ```json
153
+ POST /app/deploy
154
+ {"userid": "myapp", "project": "main", "app_name": "myapp"}
155
+ ```
156
+
157
+ Your app is served at `https://chaprola.org/apps/{userid}/{app_name}/`. Or deploy anywhere (Vercel, Netlify, S3) and use a site key locked to that domain.
158
+
159
+ ## Common Patterns
160
+
161
+ ### Import data, then query it
162
+ ```javascript
163
+ // Import
164
+ await fetch('https://api.chaprola.org/import', {
165
+ method: 'POST',
166
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
167
+ body: JSON.stringify({ userid: 'myapp', project: 'main', name: 'products', data: [...] })
168
+ })
169
+
170
+ // Query
171
+ const resp = await fetch('https://api.chaprola.org/query', {
172
+ method: 'POST',
173
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
174
+ body: JSON.stringify({ userid: 'myapp', project: 'main', file: 'products', where: [{ field: 'category', op: 'eq', value: 'electronics' }] })
175
+ })
176
+ const { records } = await resp.json()
177
+ ```
178
+
179
+ ### Insert a record
180
+ ```javascript
181
+ await fetch('https://api.chaprola.org/insert-record', {
182
+ method: 'POST',
183
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ userid: 'myapp', project: 'main', file: 'tasks', record: { title: 'New task', status: 'open', due: '2026-04-15' } })
185
+ })
186
+ ```
187
+
188
+ ### Run a published report
189
+ ```javascript
190
+ // No auth needed for public reports
191
+ const resp = await fetch('https://api.chaprola.org/report?userid=myapp&project=main&name=DASHBOARD&format=html')
192
+ const html = await resp.text()
193
+ ```
@@ -2,13 +2,18 @@
2
2
 
3
3
  ## Language
4
4
  - **No parentheses in LET.** `LET result = price * qty` only. For `price * (qty + bonus)`: use `LET temp = qty + bonus` then `LET result = price * temp`.
5
+ - **No built-in functions.** No STR(), INT(), ABS(), LEN(), TRIM(), SUBSTR(), etc. Use PUT/GET/MOVE/PRINT concatenation instead.
6
+ - **Use field names, not numeric positions.** `P.salary` not `P.63 10`. `X.username` not `X.15 10`. The compiler auto-fills positions and lengths.
7
+ - **Use PRINT concatenation for output.** `PRINT P.name + " — " + R41` instead of MOVE-to-U-buffer + PRINT 0.
8
+ - **Use CLEAR for full regions.** `CLEAR U` instead of `MOVE BLANKS U.1 65536`. Also `CLEAR P`, `CLEAR S`.
9
+ - **MOVE literal auto-pads.** `MOVE "Jones" P.name` fills the rest of the field with blanks. No need to clear first.
5
10
  - **IF EQUAL compares literal to location.** To compare two locations, copy both to U buffer first.
6
- - **MOVE length must match field width.** If `name` is 8 chars wide, `MOVE P.name U.1 20` bleeds into adjacent fields.
7
11
  - **DEFINE VARIABLE names must not collide with field names.** If format has `balance`, don't `DEFINE VARIABLE balance R41`.
8
12
  - **R-variables are 64-bit floats.** `7 / 2 = 3.5`. Use PUT with `I` format for integer display.
9
13
  - **FIND returns 0 on no match.** Always check `IF match EQ 0` before READ.
10
14
  - **PRINT 0 clears the U buffer.** No need to manually clear between prints.
11
15
  - **Statement numbers are labels, not line numbers.** Only number GOTO/CALL targets.
16
+ - **GET DATE / PUT DATE need no FOR keyword** with property names: `GET DATE R41 FROM X.utc_time`
12
17
 
13
18
  ## API
14
19
  - **NEVER use `/import` to change field widths.** `/import` REPLACES existing data. Use `/alter` to widen/narrow fields while preserving data. See `chaprola://ref/schema`.
@@ -8,7 +8,7 @@ HULDRA finds optimal parameter values for a mathematical model by minimizing the
8
8
  |-------|---------|-------------|
9
9
  | R1–R20 | Elements (parameters to optimize) | HULDRA sets before each run |
10
10
  | R21–R40 | Objectives (error metrics) | Your program computes these |
11
- | R41–R50 | Scratch space | Your program's temp variables |
11
+ | R41–R99 | Scratch space | Your program's temp variables |
12
12
 
13
13
  ## POST /optimize
14
14
  ```json
@@ -12,33 +12,53 @@ POST /publish {userid, project, name, primary_file, acl?: "public|authenticated|
12
12
 
13
13
  | Prefix | Description |
14
14
  |--------|-------------|
15
- | `P` | Primary data file (current record) |
16
- | `S` | Secondary data file (current record) |
17
- | `U` | User buffer (output scratch) |
18
- | `X` | System text (date, time, filenames) |
15
+ | `P` | Primary data file access fields by name: `P.salary`, `P.last_name` |
16
+ | `S` | Secondary data file access fields by name: `S.dept`, `S.emp_id` |
17
+ | `U` | User buffer (scratch for intermediate data) |
18
+ | `X` | System text access by property name: `X.username`, `X.utc_time`, `X.record_num` |
19
+
20
+ ### System Text Properties (X.)
21
+
22
+ | Property | Description |
23
+ |----------|-------------|
24
+ | `X.year` | Year (four digits) |
25
+ | `X.julian` | Julian date (1–366) |
26
+ | `X.hour` | Hour (military time, 0–23) |
27
+ | `X.minute` | Minute (0–59) |
28
+ | `X.username` | Authenticated user (first 10 chars) |
29
+ | `X.record_num` | Record number of primary file |
30
+ | `X.display_file` | Display filename |
31
+ | `X.data_file` | Data filename |
32
+ | `X.proc_file` | Procedure filename |
33
+ | `X.utc_time` | UTC datetime (ISO 8601, 20 chars) |
34
+ | `X.elapsed` | Elapsed execution time (SSSSS.CC, 9 chars) |
35
+ | `X.primary_modified` | Primary file Last-Modified (ISO 8601, 20 chars) |
36
+ | `X.secondary_modified` | Secondary file Last-Modified (ISO 8601, 20 chars) |
19
37
 
20
38
  ## Language Essentials
21
39
 
22
40
  ```chaprola
23
- // Loop through records
41
+ // Loop through records and print with concatenation
24
42
  DEFINE VARIABLE rec R41
25
43
  LET rec = 1
26
44
  100 SEEK rec
27
45
  IF EOF GOTO 900
28
- MOVE P.name U.1 20 // copy field to output buffer
29
- GET sal FROM P.salary // numeric field R variable
30
- PUT sal INTO U.22 10 D 2 // R variable → formatted output
31
- PRINT 0 // output full U buffer, clear it
46
+ GET sal FROM P.salary
47
+ PRINT P.name + " — " + P.department + " $" + sal
32
48
  LET rec = rec + 1
33
49
  GOTO 100
34
50
  900 END
35
51
  ```
36
52
 
37
- - `PRINT 0` output entire U buffer and clear. `PRINT N` — output exactly N chars.
38
- - `MOVE BLANKS U.1 80` — clear a region. `MOVE "literal" U.1 7` — move literal.
39
- - `IF EQUAL "text" U.50 4 GOTO 200` — compare literal to memory location.
40
- - `U.name` — named positions (auto-allocated by compiler): `MOVE P.name U.name 20`
41
- - `DEFINE VARIABLE counter R41` — alias R-variable. **Use R41-R50** (R1-R40 reserved for HULDRA).
53
+ - **PRINT concatenation (preferred):** `PRINT P.name + " earns " + R41` — auto-trims fields, auto-formats numbers, auto-flushes.
54
+ - `PRINT P.fieldname` — output a single field (auto-flush, auto-trim).
55
+ - `PRINT "literal"` — output a literal string (auto-flush).
56
+ - `PRINT 0` — output entire U buffer (less common, for columnar reports).
57
+ - `CLEAR U` — clear entire user buffer. `CLEAR P` / `CLEAR S` — clear primary/secondary region.
58
+ - `MOVE BLANKS P.notes` — clear a single field (length auto-filled).
59
+ - `MOVE "Active" P.status` — write literal to field (auto-padded to field width).
60
+ - `IF EQUAL "text" P.status GOTO 200` — compare literal to field.
61
+ - `DEFINE VARIABLE counter R41` — alias R-variable. **Use R41-R99** (R1-R40 reserved for HULDRA).
42
62
 
43
63
  ## PUT Format Codes
44
64
 
@@ -49,7 +69,7 @@ LET rec = 1
49
69
  | `I` | Integer (right-justified) | ` 1234` |
50
70
  | `E` | Scientific notation | `1.23E+03` |
51
71
 
52
- Syntax: `PUT R41 INTO U.30 10 D 2` — (R-var, location, width, format, decimals)
72
+ Syntax: `PUT R41 INTO P.salary D 2` — (R-var, location, width auto-filled from field name, format, decimals)
53
73
 
54
74
  ## Math
55
75
 
@@ -59,14 +79,24 @@ LET R43 = EXP R41 // also: LOG, SQRT, ABS
59
79
  LET R44 = POW R41 R42 // R41^R42
60
80
  ```
61
81
 
82
+ ## Date Arithmetic
83
+
84
+ ```chaprola
85
+ GET DATE R41 FROM X.primary_modified // parse timestamp → epoch seconds
86
+ GET DATE R42 FROM X.utc_time // current UTC time
87
+ LET R43 = R42 - R41 // difference in seconds
88
+ LET R43 = R43 / 86400 // convert to days
89
+ PUT DATE R42 INTO U.1 20 // write epoch as ISO 8601 string
90
+ ```
91
+
62
92
  ## Secondary Files (FIND/JOIN)
63
93
 
64
94
  ```chaprola
65
95
  OPEN "DEPARTMENTS" 0 // open secondary file
66
- FIND match FROM S.dept_code 3 USING P.dept_code
96
+ FIND match FROM S.dept_code USING P.dept_code
67
97
  IF match EQ 0 GOTO 200 // 0 = no match
68
98
  READ match // load matched record
69
- MOVE S.dept_name U.30 15
99
+ PRINT P.name + " — " + S.dept_name
70
100
  WRITE match // write back if modified
71
101
  CLOSE // flush + close
72
102
  ```