@dforge-core/metadata 0.0.22 → 0.0.24

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/CHANGELOG.md CHANGED
@@ -5,6 +5,207 @@ All notable changes to `@dforge-core/metadata` are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.24] — 2026-09-15
9
+
10
+ ### Added
11
+
12
+ - **Module-local traits now expand.** `expandTrait` / `expandTraits` take an optional
13
+ third argument: a module's own root `traits.json` (new `TraitsFile` type). A module
14
+ may declare traits of its own, and an entity names them exactly like a platform trait
15
+ — so a reader that knows only this registry sees an entity missing columns that
16
+ install will give it. In the editor that surfaced as eight false "unknown column"
17
+ errors on `file-library/summarize.dsl`, whose `file_item` entity takes
18
+ `processing_status` / `processing_result` / `processing_error` from the module's own
19
+ `file-library` trait.
20
+
21
+ A code declared in both wins locally, mirroring the installer
22
+ (`TraitExpanderFactory.ForPackage` overlays the module's traits on the platform ones).
23
+ Expansion is now cycle-guarded: `includes` is author-supplied once local traits are in
24
+ play, and an unguarded walk is an editor hang rather than a bad expansion. The guard is
25
+ the current path rather than every trait already seen, so a trait two includes both
26
+ reach still expands under each — otherwise which definition won a key collision would
27
+ depend on which branch got there first.
28
+
29
+ - **The DSL checker gained the rules that lived only in the module validator**
30
+ (`dforge-mcp`'s regex `dsl-check.ts`), ported onto the token stream:
31
+ `dsl/empty-script`, `dsl/missing-execute`, `dsl/duplicate-block`, `dsl/block-order`,
32
+ `dsl/formula-only-function` (`TODAY()` / `NOW()` / `CURRENT_USER_ID()` in a JavaScript
33
+ block), `dsl/sql-placeholder` (`:name` where the platform binds `@name`),
34
+ `dsl/sql-concat`, `dsl/unknown-builtin` and `dsl/job-record-context`. `DslContext.action`
35
+ gained `viaJob` for the last of these — a scheduled job runs as the system user with no
36
+ current record, which only a reader holding the module's jobs can know.
37
+
38
+ Being on the token stream rather than on regexes over blanked source, several are
39
+ sharper than the originals: `::text` is no longer read as a `:name` placeholder, a
40
+ statement built from a variable is skipped rather than guessed at, and a declared
41
+ helper — including the method shorthand `f(x) { … }` — is not an unknown built-in.
42
+
43
+ Each honours the disambiguation the older rules already carry. `dsl/sql-placeholder`
44
+ reads the statement with its string literals and comments blanked, so `':draft'` is
45
+ quoted data rather than a binding. `dsl/job-record-context` skips a local's array
46
+ literal and, in batch `execute:`, a keyword one — the same terms the column rule
47
+ uses. And the formula-only table is a `Map`, since its key is a name out of the
48
+ script and an object would answer `toString` from the prototype, reporting a
49
+ declared helper as formula-only and offering native source as the fix.
50
+ `stringValue()` now decodes `\n`, `\t` and `\r` rather than dropping the
51
+ backslash, so a multi-line SQL literal reads as the statement it is.
52
+
53
+ `dsl/block-order` follows `ParseBlocks` per block rather than as one sequence.
54
+ Each header regex has its own lookahead set, and only a block the one above it
55
+ omits is swallowed — `schema:` is listed by every other set, so it may sit
56
+ anywhere before `execute:`, which the earlier rank comparison rejected.
57
+ `dsl/job-record-context` covers all three forms `JobRegistrar.RequiresRecordContext`
58
+ rejects, not `[field]` alone: `old[field]` and `records` are record context too.
59
+ Both SQL rules read a template literal, which is how multi-line SQL is written
60
+ — a `${…}` hole counts as concatenation, and the chunks either side of it are
61
+ still scanned for `:name`. And `Set`, `Map`, `Promise`, `Symbol`, `BigInt` and
62
+ `encodeURI` / `decodeURI` joined the globals `dsl/unknown-builtin` stands down
63
+ on; Jint exposes them and `new Set()` was drawing a warning. A constructor is
64
+ skipped outright: `new Foo()` names no host function for the catalog to hold.
65
+
66
+ ### Changed
67
+
68
+ - **`BLOCK_KINDS` is a `readonly` tuple**, not `BlockKind[]`. `BlockKind` now
69
+ derives from it, so the order check and the type it ranks cannot drift. The
70
+ narrowing is visible to callers: `BLOCK_KINDS.includes(s)` no longer accepts a
71
+ plain `string`, and the array can no longer be assigned to a mutable
72
+ `BlockKind[]`. Test the value first (`BLOCK_KINDS.some((k) => k === s)`) where
73
+ that bites.
74
+
75
+ ## [0.0.23] — 2026-09-14
76
+
77
+ ### Added
78
+
79
+ - **`@dforge-core/metadata/dsl` — the action-DSL analyzer**, as a second entry point:
80
+ a lexer, a parser and the static rules for `logic/actions/*.dsl`. It was implemented
81
+ twice — once in the language server (token-based, module-aware, drawing squiggles) and
82
+ once in the module validator (regex-based, module-blind, gating a pack) — with only
83
+ three rules in common, so an editor hint and a pre-pack failure disagreed about the
84
+ same script.
85
+
86
+ The rules take what they know about the module through a small injected `DslContext`
87
+ (module code, the action, and the columns of the entity behind the record context)
88
+ rather than a loader, so the same rule serves a language server and a file-walking
89
+ validator. Every context field is optional and every rule that reads one fails open:
90
+ with an empty context the text-only rules still run and the rest stand down silently.
91
+ A false "unknown column" underlines working code and blocks a pack; a missed one costs
92
+ an install round trip.
93
+
94
+ `checkDsl(text, ctx)` returns `DslIssue[]` carrying both byte offsets and a 1-indexed
95
+ line/column, so a range-based host and a `file.dsl:12` reporter each get what they
96
+ need without re-deriving it. `parseDsl`, `tokenize` and the built-in catalog are
97
+ exported too, for completion, hover and go-to-definition. `sendEmail` in that
98
+ catalog is `sendEmail(to, subjectOrTemplate, dataOrBody?)` — three arguments,
99
+ the second choosing template or raw mode, as `ScriptContext.sendEmail` does;
100
+ the fourth argument it used to advertise never existed. `applyProfile` is
101
+ `applyProfile(json, profile)`, taking the inline `{ map, lines? }` object the
102
+ runtime demands — the profile code it used to advertise throws, profile-by-code
103
+ being a later slice. `ocrExtract` states the return type its mode decides: a
104
+ JSON string to `JSON.parse()` without `opts.mode`, a parsed object with it.
105
+ And `download` sets a URL on the action result rather than fetching anything
106
+ into storage, which is what it used to claim.
107
+
108
+ Deliberately **not** in the root barrel: the root export is registries and types that
109
+ the web app and the VS Code webview pull into a browser bundle, and importing it must
110
+ not drag a tokenizer in behind it. The package also declares `sideEffects: false` now.
111
+
112
+ The two existing implementations live in other repos; the duplication ends when
113
+ the language server and the module validator import this entry point, not when
114
+ it ships.
115
+
116
+ Recognising a block the way `ActionDslCompiler.ParseBlocks` recognises one:
117
+ five kinds including `schema:`, anchored at column 0, matched case-insensitively,
118
+ with a leading BOM stripped and `execute:` running to end-of-file. The anchor is
119
+ the load-bearing part — an indented `params:` is an object key inside a block
120
+ body, and treating it as a header fragmented the document and fabricated a
121
+ param list. A header that *is* indented is now reported (`dsl/indented-block-header`),
122
+ since the compiler reads it as body text and runs the block it meant to open
123
+ as empty.
124
+
125
+ `[field]` detection is the compiler's `(?<!\w)\[(\w+)\]`, character for
126
+ character rather than by previous token: the lookbehind does not skip
127
+ whitespace, so a read that opens a line is a read however the line above
128
+ ended. Column and param names match case-insensitively, as `EntityColumnLookup`
129
+ and `ValidateParamUsages` do.
130
+
131
+ A rule is scoped to the block whose compiler actually imposes it. Single-hop
132
+ ref navigation is the DSL's `RxRefNav`, so `canExecute:` — handed to the
133
+ formula engine verbatim, where a chain has no depth limit — is exempt. The
134
+ batch-mode "no current record" rule is `execute:` alone, since
135
+ `onBeforeStart:` is compiled per-record in every mode. And `records[0][field]`
136
+ is one indexed read, matching `RxRecordsFieldRead`, which runs ahead of the
137
+ generic `[field]` rewrite: its column is still checked, but it is not a
138
+ current-record read.
139
+
140
+ What the single-hop rule refuses is a second **bracketed** hop, not a second
141
+ hop: the compiler rewrites the first one and then throws on a surviving `.[`,
142
+ so `[vehicle].plate.length` is one hop followed by ordinary JavaScript on the
143
+ value it returned. Batch mode applies no `[field]` rewrite at all, so
144
+ `var ids = [id]` is the array literal it looks like — the same local-name
145
+ disambiguation the column rule uses. And a param declared inline with its
146
+ header (`params: qty: number required`) is declared: `^params:\s*` swallows
147
+ the newline, making the header's own line the block's first.
148
+
149
+ The column check covers every way the compiler reaches a column, not only the
150
+ bare `[field]`: `old[field]` and a batch loop variable's `x[field]` are fed
151
+ into the same `fieldRefs` set server-side, so a typo in either is the same
152
+ compile error. A template literal is tokenized as prose and holes rather than
153
+ as one opaque token, so `${…}` is analyzed and the text around it is not.
154
+ A param may be named after a block (`execute: number` is a legal `RxParamDecl`
155
+ line) without being read as a header — though its type has to share its line,
156
+ as that single-line regex requires. Nothing inside `params:` is a field read
157
+ at all: `RxKeyValue` takes a bracketed option list, so `options=[cash]` is an
158
+ option list.
159
+
160
+ Case is folded on both sides of the column lookup — the names once per check
161
+ when the lookup can enumerate them, which `Set` and `Map` both can, and the
162
+ reference on the way in — so a mixed-case column name matches whichever case
163
+ the script spells it in. `ColumnLookup` gained an optional `keys()` for that.
164
+
165
+ `dsl/top-level-return` reads function scope rather than brace depth, and is
166
+ an error rather than a warning: both blocks compile to a bare script, and
167
+ Esprima's `ParseScript` refuses a return outside a function wherever it sits,
168
+ so `if (x) { return }` and `if (x) return` fail exactly as a return on its own
169
+ line does. `return` is reserved, so it is a statement unless it is being used
170
+ as a name — a property, an object key, or a method shorthand, told from
171
+ `return (expr)` by the body brace after the parameter list. `function`, function expressions, arrows and method shorthand all
172
+ open a scope where it is legal; an `if` or `for` body does not.
173
+
174
+ A regex literal is one token, so `/return/` and `/[^0-9]/` carry no statement
175
+ and no field read. The `/` is read as division after a value and as a regex
176
+ after an operator or a keyword. `)` and `}` each depend on what they closed —
177
+ a control condition and a statement block leave a statement position, so
178
+ `if (x) /re/.test(s)` and `if (x) {}` then `/re/` are regexes, while
179
+ `(a + b) / 2` and `{ a: 1 }` then `/` divide — and a template chunk on whether
180
+ it stopped at `${` or ran to the backtick.
181
+
182
+ A keyword literal is never a column: in batch `execute:`, which applies no
183
+ `[field]` rewrite at all, `[true]`, `[false]` and `[NULL]` are the array
184
+ elements they look like. In every other block the compiler does rewrite them,
185
+ so the check stands there.
186
+
187
+ `locals` covers function parameters as well as `var`/`let`/`const` and `for`
188
+ heads — declarations, expressions, arrows and method shorthand alike — since a
189
+ parameter binds a name the same way, and `function wrap(value) { return [value] }`
190
+ holds an array literal rather than a field read.
191
+
192
+ Local-name disambiguation is confined to the bare bracket that needs it.
193
+ `var ids = [orderId]` is ambiguous with a field read and only the name tells
194
+ them apart, but `r[stauts]` and `records[0][stauts]` name a column outright —
195
+ the compiler rewrites each to `.get('stauts')` with the literal name — so a
196
+ same-named local no longer hides the typo.
197
+
198
+ Checked against every action in the module tree — 70 actions, each against its
199
+ own trait-expanded entity: no false positives.
200
+
201
+ ## [0.0.22] — 2026-08-28
202
+
203
+ ### Changed
204
+
205
+ - `entity.schema.json` — per-constraint validation: `expression` is check-only,
206
+ and a `unique` constraint needs its key. Shipped with the DB-enforced stock
207
+ availability work (`54622c5c`).
208
+
8
209
  ## [0.0.21] — 2026-08-24
9
210
 
10
211
  ### Changed
package/README.md CHANGED
@@ -32,6 +32,7 @@ pnpm add @dforge-core/metadata
32
32
  | **Scalar enums** | `FieldTypeCd`, `BaseDatatypeCd`, `ColumnTypeCd`, `FlagCd`, `AlignType` |
33
33
  | **Module-file types** | `ManifestDef`, `EntityDef`, `DataViewsFile`, `ReportsFile`, `MenusFile`, `FoldersFile`, `RolesFile`, `SettingsFile`, `JobsFile`, `TriggersFile`, `WebhooksFile`, `PrintTemplatesFile`, `SeedDataFile`, `ActionsFile`, … |
34
34
  | **Filter expression** | `Filter`, `FilterCondition`, `FilterGroup`, `SortClause` |
35
+ | **DSL analyzer** | `./dsl` — `checkDsl`, `parseDsl`, `tokenize`, `BUILTINS` (see [Action DSL](#action-dsl)) |
35
36
  | **JSON schemas** | `./schemas/<name>.schema.json` (see [Schemas](#json-schemas)) |
36
37
 
37
38
  ## Registries & derivation
@@ -94,6 +95,50 @@ const entity: EntityDef = {
94
95
  > returned by the API (`metadata.getModel`) is a different contract — see
95
96
  > `@dforge/sdk`'s `Model*` types, which reuse the scalar enums from this package.
96
97
 
98
+ ## Action DSL
99
+
100
+ `@dforge-core/metadata/dsl` is a separate entry point holding the lexer, parser
101
+ and static rules for an action body (`logic/actions/*.dsl`). One implementation
102
+ serves both hosts that check these files: the language server, which turns
103
+ issues into squiggles, and the module validator, which gates a pack.
104
+
105
+ ```ts
106
+ import { checkDsl } from "@dforge-core/metadata/dsl";
107
+
108
+ const issues = checkDsl(source, {
109
+ moduleCode: "wms",
110
+ action: { code: "transfer_stock", executionMode: "single" },
111
+ currentEntity: { qualified: "wms.stock_movement", columns: entityColumns },
112
+ });
113
+ // → [{ rule: "dsl/unknown-column", severity: "error", message, start, end, line, column }]
114
+ ```
115
+
116
+ What a rule knows about the module arrives through that context — a module code,
117
+ the action, and a column lookup (any `Set` or `Map` will do) — rather than
118
+ through a module loader, which is what lets the same rule run in an editor and
119
+ in a file walker. **Every field is optional and every rule that reads one fails
120
+ open**: with an empty context the text-only rules still run and the rest stand
121
+ down silently. A false "unknown column" underlines working code and blocks a
122
+ pack; a missed one costs an install round trip.
123
+
124
+ One obligation comes with `currentEntity`: `columns` must be **trait-expanded**,
125
+ built-in traits and the module's own `traits.json` alike, because the identity
126
+ PK and `[status]` live in traits rather than in `fields`. An unexpanded set
127
+ turns the rule into a wall of false errors. When the entity can't be resolved —
128
+ a bridge module acting on another module's entity — pass `null` rather than the
129
+ fragment you do have.
130
+
131
+ Issues carry both byte offsets and a 1-indexed `line`/`column`, so a
132
+ range-based host and a `file.dsl:12` reporter each get what they need.
133
+
134
+ The blocks, the `[field]` lookbehind and the case rules follow
135
+ `ActionDslCompiler` rather than the prose docs: five block kinds including
136
+ `schema:`, headers anchored at column 0 (an indented one is body text, and is
137
+ reported), and `execute:` running to end-of-file.
138
+
139
+ It is not re-exported from the package root: importing `@dforge-core/metadata`
140
+ in a browser bundle must not pull a tokenizer in behind it.
141
+
97
142
  ## JSON schemas
98
143
 
99
144
  The 14 canonical schemas are shipped alongside the types and stay byte-identical
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Execution mode. 'single' (default) and 'each' run the DSL per selected record;
3
+ * 'batch' runs once with all records exposed as `__records`.
4
+ */
5
+ type ActionExecutionMode = "single" | "each" | "batch";
6
+ /** A single action declaration (value in the `ui/actions.json` map). */
7
+ interface ActionDef {
8
+ /** Human-readable description. */
9
+ description: string;
10
+ /** Button label. */
11
+ label: string;
12
+ /** Bootstrap icon class (e.g. 'bi-telephone-check'). */
13
+ icon: string;
14
+ /** Entity code this action targets. */
15
+ entityCode: string;
16
+ /** Per-record vs. batch execution. */
17
+ executionMode: ActionExecutionMode;
18
+ /** DSL script name (file under `logic/actions/<script>.dsl`). */
19
+ script: string;
20
+ /**
21
+ * Run the whole selection in one transaction, so a failure on any record
22
+ * rolls back the records already processed. **Defaults to `true`** when
23
+ * omitted (`ActionDef.IsTransacted` in the installer is a non-nullable
24
+ * `bool` initialised to `true`), so omit it only when you want atomicity.
25
+ *
26
+ * `false` opens no transaction, and what follows a failure depends on
27
+ * {@link ActionDef.executionMode}: `single` / `each` report it and continue
28
+ * with the next record, so use that for independent per-record work that
29
+ * should process as many records as pass; `batch` is a single script
30
+ * invocation with no loop to continue, so the run simply ends with its
31
+ * earlier writes committed.
32
+ *
33
+ * On a run queued to the background the **rollback** guarantee is lost
34
+ * outright — the worker opens no transaction, so nothing is ever undone. What
35
+ * remains of the flag there depends on the mode: the worker's `single`/`each`
36
+ * loop still reads it for control flow (stop after the first failed record vs.
37
+ * continue), while its `batch` path never reads it at all — one invocation,
38
+ * which ends on failure under either value.
39
+ */
40
+ isTransacted?: boolean;
41
+ /** Display order. */
42
+ orderNum?: number;
43
+ /**
44
+ * @deprecated Not read by the installer — use {@link ActionDef.isAsync}.
45
+ * The module manifest model binds `isAsync` only, so this key is silently
46
+ * ignored and the action installs as synchronous.
47
+ */
48
+ async?: boolean;
49
+ /**
50
+ * Permit background execution. It does not force it: `action.execute`
51
+ * carries its own `async` argument and the server branches on that, so an
52
+ * action with parameters is offered to the user as both "Run" (inline) and
53
+ * "Run in Background". A parameterless `isAsync` action is queued directly.
54
+ */
55
+ isAsync?: boolean;
56
+ }
57
+ /** A `ui/actions.json` file: action code → definition. */
58
+ type ActionsFile = Record<string, ActionDef>;
59
+
60
+ export type { ActionExecutionMode as A, ActionDef as a, ActionsFile as b };
@@ -0,0 +1,251 @@
1
+ import { A as ActionExecutionMode } from '../actions-DvYTrCbP.js';
2
+
3
+ type TokenKind = "ident" | "number" | "string" | "template" | "regex" | "punct" | "comment";
4
+ interface Token {
5
+ kind: TokenKind;
6
+ /** Source text, verbatim — quotes included for strings. */
7
+ text: string;
8
+ start: number;
9
+ end: number;
10
+ line: number;
11
+ character: number;
12
+ /** True when a line break sits between this token and the previous one. */
13
+ startsLine: boolean;
14
+ }
15
+ declare function tokenize(text: string): Token[];
16
+ /** Strip the surrounding quotes from a string token's text. */
17
+ declare function stringValue(token: Token): string;
18
+ /**
19
+ * The literal text of one template chunk, delimiters removed. A chunk opens on
20
+ * a backtick or on the `}` that closed the hole before it, and closes on a
21
+ * backtick, on the `${` of the next hole, or on the end of the file.
22
+ */
23
+ declare function templateChunkValue(token: Token): string;
24
+
25
+ /**
26
+ * In the order `ActionDslCompiler.ParseBlocks` expects them in a file.
27
+ *
28
+ * `BlockKind` derives from this array rather than standing beside it, so the
29
+ * two cannot drift: the order check ranks a kind by its index here, and a kind
30
+ * the array does not list could not have been produced in the first place.
31
+ */
32
+ declare const BLOCK_KINDS: readonly ["params", "canExecute", "schema", "onBeforeStart", "execute"];
33
+ type BlockKind = (typeof BLOCK_KINDS)[number];
34
+ interface Span {
35
+ start: number;
36
+ end: number;
37
+ }
38
+ interface DslBlock {
39
+ kind: BlockKind;
40
+ label: Span;
41
+ /** Span of the block body — label end to the next label (or EOF). */
42
+ body: Span;
43
+ }
44
+ interface DslParam {
45
+ name: string;
46
+ nameSpan: Span;
47
+ /** fieldTypeCd, or the referenced entity code when `isRef`. */
48
+ type: string;
49
+ typeSpan: Span;
50
+ isRef: boolean;
51
+ required: boolean;
52
+ label?: string;
53
+ }
54
+ /**
55
+ * A `[field]` read. `navigation` holds the property hops that follow it, each
56
+ * remembering whether it was written `.[target]` or `.target`: the compiler
57
+ * rewrites the FIRST hop either way, and then only a surviving `.[` is the
58
+ * multi-hop compile error. A dotted tail is plain JavaScript on the value the
59
+ * hop returned, so `[customer].[code]`, `[a].b.c` and `[plate].length` all
60
+ * compile.
61
+ */
62
+ interface FieldRef {
63
+ name: string;
64
+ span: Span;
65
+ /** Span including the brackets. */
66
+ outerSpan: Span;
67
+ navigation: Array<{
68
+ name: string;
69
+ span: Span;
70
+ bracketed: boolean;
71
+ }>;
72
+ block: BlockKind | null;
73
+ /**
74
+ * False when the read is bound to a numbered record rather than the current
75
+ * one — `records[0][status]`, which `RxRecordsFieldRead` rewrites ahead of
76
+ * the generic `[field]` pass. The name is still a column of the entity, so
77
+ * the column rules apply; the batch-mode rule does not.
78
+ */
79
+ isCurrentRecord: boolean;
80
+ /** Index of the token that opens this read — the `[`. */
81
+ startIndex: number;
82
+ /**
83
+ * Index of the last token this read consumes, navigation included. A hop is
84
+ * 2 tokens as `.prop` but 4 as `.[prop]`, so callers that need the token
85
+ * after the read cannot compute it from `navigation.length`.
86
+ */
87
+ endIndex: number;
88
+ }
89
+ /** `params[x]` / `old[x]` — subscript reads off a DSL global. */
90
+ interface GlobalRef {
91
+ global: "params" | "old" | "records";
92
+ property: string;
93
+ span: Span;
94
+ block: BlockKind | null;
95
+ }
96
+ interface CallRef {
97
+ name: string;
98
+ nameSpan: Span;
99
+ /** First argument when it is a string or template literal — the entity code, usually. */
100
+ firstStringArg?: StringArg;
101
+ block: BlockKind | null;
102
+ }
103
+ interface StringArg {
104
+ value: string;
105
+ span: Span;
106
+ /**
107
+ * A template literal with `${…}` holes, which `value` holds a space in
108
+ * place of: literal text to read, but not a constant.
109
+ */
110
+ interpolated: boolean;
111
+ /** Index of the last token the literal consumes — its final chunk. */
112
+ endIndex: number;
113
+ }
114
+ interface DslDocument {
115
+ blocks: DslBlock[];
116
+ params: DslParam[];
117
+ fieldRefs: FieldRef[];
118
+ globalRefs: GlobalRef[];
119
+ calls: CallRef[];
120
+ tokens: Token[];
121
+ /**
122
+ * Every mention of `records`, subscript (`records[x]`) or bare
123
+ * (`for x in records`). `globalRefs` only holds the subscript form, so a
124
+ * rule that needs to point at the batch loop has nothing to anchor to.
125
+ */
126
+ recordsRefs: Span[];
127
+ /** Names bound by `var`/`let`/`const` or a `for` head, plus declared params. */
128
+ locals: Set<string>;
129
+ /**
130
+ * Loop variables bound by `for x in records {` — `RxForLoop` and nothing
131
+ * else, so an ordinary `for (var x of xs)` is not one. Their `x[field]`
132
+ * subscripts read the entity's columns, which is why they come back as
133
+ * field reads.
134
+ */
135
+ recordsLoopVars: Set<string>;
136
+ }
137
+ declare function parseDsl(text: string): DslDocument;
138
+
139
+ interface Builtin {
140
+ name: string;
141
+ signature: string;
142
+ returns: string;
143
+ doc: string;
144
+ /**
145
+ * Index of the argument that names an entity, when the built-in takes one.
146
+ * Drives entity-code completion and the qualify-your-codes diagnostic.
147
+ */
148
+ entityArg?: number;
149
+ }
150
+ declare const BUILTINS: Builtin[];
151
+ declare const BUILTIN_BY_NAME: Map<string, Builtin>;
152
+ /** Bare identifiers the runtime injects — no parentheses. */
153
+ declare const BUILTIN_VALUES: Record<string, string>;
154
+
155
+ type DslSeverity = "error" | "warning" | "info";
156
+ /** One reported problem. `rule` is stable — hosts filter and suppress on it. */
157
+ interface DslIssue {
158
+ /** Stable rule id, namespaced: `dsl/unknown-column`. */
159
+ rule: string;
160
+ severity: DslSeverity;
161
+ message: string;
162
+ /** Byte offsets into the source, for range-based hosts. */
163
+ start: number;
164
+ end: number;
165
+ /** 1-indexed position of `start`, for line-based reporters. */
166
+ line: number;
167
+ column: number;
168
+ }
169
+ /**
170
+ * Column membership test. Structurally satisfied by both `Set<string>` and
171
+ * `Map<string, unknown>`, so a host that already holds a column map passes it
172
+ * straight in rather than copying it into a set on every check.
173
+ */
174
+ interface ColumnLookup {
175
+ has(name: string): boolean;
176
+ readonly size: number;
177
+ /**
178
+ * The column names, if the lookup can enumerate them — `Set` and `Map` both
179
+ * can. Matching is case-insensitive at the other end (`EntityColumnLookup`
180
+ * builds an OrdinalIgnoreCase set) and a mixed-case column name is legal, so
181
+ * the checker folds case itself when it can read the keys.
182
+ *
183
+ * Without it, a lookup that is not already case-insensitive will report a
184
+ * spelling the server accepts. A host that cannot expose keys should make
185
+ * `has` case-insensitive instead.
186
+ */
187
+ keys?(): Iterable<string>;
188
+ }
189
+ /** The resolved entity a script's record context refers to. */
190
+ interface EntityShape {
191
+ /** Module-qualified, e.g. `fin.invoice` — used in messages. */
192
+ qualified: string;
193
+ /**
194
+ * Every declared name on the entity, **trait-expanded** — built-in traits
195
+ * and the module's own `traits.json` alike — and including the virtual
196
+ * columns (reference, set, formula), exactly as `EntityColumnLookup` builds
197
+ * the compiler's set.
198
+ *
199
+ * Handing over an unexpanded set is the one mistake that turns this rule
200
+ * into a wall of false errors: the identity PK and `[status]` live in
201
+ * traits, not in `fields`. When in doubt pass `currentEntity: null` — a
202
+ * skipped check costs an install round trip, a wrong one blocks a pack.
203
+ */
204
+ columns: ColumnLookup;
205
+ }
206
+ /**
207
+ * What the checker knows about the script's surroundings. Every field is
208
+ * optional and every rule that reads one FAILS OPEN: with an empty context the
209
+ * text-only rules still run and the rest stand down silently.
210
+ *
211
+ * That is deliberate. A false "unknown column" on a correct script is far more
212
+ * damaging than a missed one — an editor would underline working code, and a
213
+ * validator would block a pack — so a rule that cannot resolve what it needs
214
+ * reports nothing.
215
+ */
216
+ interface DslContext {
217
+ /** Owning module code, for the qualify-your-entity-codes message. */
218
+ moduleCode?: string;
219
+ /** The action this script implements, from `ui/actions.json`. */
220
+ action?: {
221
+ code: string;
222
+ executionMode?: ActionExecutionMode;
223
+ /**
224
+ * True when a scheduled job invokes this action. A job runs as the
225
+ * system user with no current record, so record context is a hard
226
+ * error there even in a mode that would otherwise allow it. Only a
227
+ * reader that has the module's jobs can know this, so it is the host's
228
+ * to supply; absent, the rule stands down like every other.
229
+ */
230
+ viaJob?: boolean;
231
+ };
232
+ /**
233
+ * Entity behind the record context (`[field]`). `null` or absent when it
234
+ * can't be resolved — a bridge module acting on another module's entity —
235
+ * and then every column rule stands down. A partial set is worse than
236
+ * none: pass `null`, not the fragment. See `EntityShape.columns`.
237
+ */
238
+ currentEntity?: EntityShape | null;
239
+ }
240
+
241
+ /**
242
+ * Check a DSL body. Returns [] for a clean script.
243
+ *
244
+ * Issues come back in source-feature order (field reads, params, execution
245
+ * mode, block spelling, built-ins, top-level returns, inline assignments)
246
+ * rather than sorted by position — a host that wants them in file order can
247
+ * sort on `start`.
248
+ */
249
+ declare function checkDsl(text: string, ctx?: DslContext): DslIssue[];
250
+
251
+ export { BLOCK_KINDS, BUILTINS, BUILTIN_BY_NAME, BUILTIN_VALUES, type BlockKind, type Builtin, type CallRef, type ColumnLookup, type DslBlock, type DslContext, type DslDocument, type DslIssue, type DslParam, type DslSeverity, type EntityShape, type FieldRef, type GlobalRef, type Span, type StringArg, type Token, type TokenKind, checkDsl, parseDsl, stringValue, templateChunkValue, tokenize };