@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.
@@ -0,0 +1,98 @@
1
+ // Host-agnostic types for the DSL checker.
2
+ //
3
+ // The checker reports OFFSETS plus a 1-indexed line/column, and never an editor
4
+ // range: a language server turns offsets into `Position`s with its own document
5
+ // model, while a validator prints `logic/actions/x.dsl:12`. Emitting both keeps
6
+ // either host from having to re-derive the other.
7
+
8
+ import type { ActionExecutionMode } from "../actions";
9
+
10
+ export type DslSeverity = "error" | "warning" | "info";
11
+
12
+ /** One reported problem. `rule` is stable — hosts filter and suppress on it. */
13
+ export interface DslIssue {
14
+ /** Stable rule id, namespaced: `dsl/unknown-column`. */
15
+ rule: string;
16
+ severity: DslSeverity;
17
+ message: string;
18
+ /** Byte offsets into the source, for range-based hosts. */
19
+ start: number;
20
+ end: number;
21
+ /** 1-indexed position of `start`, for line-based reporters. */
22
+ line: number;
23
+ column: number;
24
+ }
25
+
26
+ /**
27
+ * Column membership test. Structurally satisfied by both `Set<string>` and
28
+ * `Map<string, unknown>`, so a host that already holds a column map passes it
29
+ * straight in rather than copying it into a set on every check.
30
+ */
31
+ export interface ColumnLookup {
32
+ has(name: string): boolean;
33
+ readonly size: number;
34
+ /**
35
+ * The column names, if the lookup can enumerate them — `Set` and `Map` both
36
+ * can. Matching is case-insensitive at the other end (`EntityColumnLookup`
37
+ * builds an OrdinalIgnoreCase set) and a mixed-case column name is legal, so
38
+ * the checker folds case itself when it can read the keys.
39
+ *
40
+ * Without it, a lookup that is not already case-insensitive will report a
41
+ * spelling the server accepts. A host that cannot expose keys should make
42
+ * `has` case-insensitive instead.
43
+ */
44
+ keys?(): Iterable<string>;
45
+ }
46
+
47
+ /** The resolved entity a script's record context refers to. */
48
+ export interface EntityShape {
49
+ /** Module-qualified, e.g. `fin.invoice` — used in messages. */
50
+ qualified: string;
51
+ /**
52
+ * Every declared name on the entity, **trait-expanded** — built-in traits
53
+ * and the module's own `traits.json` alike — and including the virtual
54
+ * columns (reference, set, formula), exactly as `EntityColumnLookup` builds
55
+ * the compiler's set.
56
+ *
57
+ * Handing over an unexpanded set is the one mistake that turns this rule
58
+ * into a wall of false errors: the identity PK and `[status]` live in
59
+ * traits, not in `fields`. When in doubt pass `currentEntity: null` — a
60
+ * skipped check costs an install round trip, a wrong one blocks a pack.
61
+ */
62
+ columns: ColumnLookup;
63
+ }
64
+
65
+ /**
66
+ * What the checker knows about the script's surroundings. Every field is
67
+ * optional and every rule that reads one FAILS OPEN: with an empty context the
68
+ * text-only rules still run and the rest stand down silently.
69
+ *
70
+ * That is deliberate. A false "unknown column" on a correct script is far more
71
+ * damaging than a missed one — an editor would underline working code, and a
72
+ * validator would block a pack — so a rule that cannot resolve what it needs
73
+ * reports nothing.
74
+ */
75
+ export interface DslContext {
76
+ /** Owning module code, for the qualify-your-entity-codes message. */
77
+ moduleCode?: string;
78
+ /** The action this script implements, from `ui/actions.json`. */
79
+ action?: {
80
+ code: string;
81
+ executionMode?: ActionExecutionMode;
82
+ /**
83
+ * True when a scheduled job invokes this action. A job runs as the
84
+ * system user with no current record, so record context is a hard
85
+ * error there even in a mode that would otherwise allow it. Only a
86
+ * reader that has the module's jobs can know this, so it is the host's
87
+ * to supply; absent, the rule stands down like every other.
88
+ */
89
+ viaJob?: boolean;
90
+ };
91
+ /**
92
+ * Entity behind the record context (`[field]`). `null` or absent when it
93
+ * can't be resolved — a bridge module acting on another module's entity —
94
+ * and then every column rule stands down. A partial set is worse than
95
+ * none: pass `null`, not the fragment. See `EntityShape.columns`.
96
+ */
97
+ currentEntity?: EntityShape | null;
98
+ }
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export type {
28
28
  FlagCd,
29
29
  NamedKind,
30
30
  TraitDef,
31
+ TraitsFile,
31
32
  } from "./types";
32
33
 
33
34
  export { AggType, AGG_TYPE_LIST } from "./aggregation";
package/src/traits.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // these as checkboxes and previews the expanded fields, so authors never
7
7
  // hand-write `created_date` / `{entity}_id` etc.
8
8
 
9
- import type { FieldDef, TraitDef } from "./types";
9
+ import type { FieldDef, TraitDef, TraitsFile } from "./types";
10
10
 
11
11
  export const traits: readonly TraitDef[] = [
12
12
  {
@@ -107,21 +107,61 @@ function templateFields(fields: Record<string, FieldDef>, entity: string): Recor
107
107
  * for `entityName`, with `{entity}` / `{Entity}` tokens resolved. Returns an
108
108
  * empty object for unknown or marker traits. Field order follows include-depth
109
109
  * first, then declaration order; later definitions win on key collisions.
110
+ *
111
+ * `localTraits` is a module's own `traits.json`. A module may declare traits of
112
+ * its own, and an entity in it names them exactly like a platform trait — so a
113
+ * reader that knows only this registry sees an entity missing columns that
114
+ * install will give it. Pass the file and those traits resolve; a code declared
115
+ * in both wins locally, mirroring the installer, which overlays the module's
116
+ * traits on the platform ones (`TraitExpanderFactory.ForPackage`).
110
117
  */
111
- export function expandTrait(traitCd: string, entityName: string): Record<string, FieldDef> {
112
- const trait = byCd.get(traitCd);
113
- if (!trait) return {};
118
+ export function expandTrait(
119
+ traitCd: string,
120
+ entityName: string,
121
+ localTraits?: TraitsFile,
122
+ ): Record<string, FieldDef> {
123
+ return expandTraitInner(traitCd, entityName, localTraits, new Set());
124
+ }
125
+
126
+ function expandTraitInner(
127
+ traitCd: string,
128
+ entityName: string,
129
+ localTraits: TraitsFile | undefined,
130
+ path: Set<string>,
131
+ ): Record<string, FieldDef> {
132
+ // `includes` is author-supplied once local traits are in play, so a cycle is
133
+ // reachable from a module file. Unguarded that is an editor hang, not a bad
134
+ // expansion.
135
+ //
136
+ // The guard is the current path, not every trait already visited: in a
137
+ // diamond — two includes that both reach a third — the third has to expand
138
+ // under each of them, or which definition wins a key collision would depend
139
+ // on which branch got there first.
140
+ if (path.has(traitCd)) return {};
141
+ path.add(traitCd);
142
+
143
+ const trait = localTraits?.[traitCd] ?? byCd.get(traitCd);
114
144
  const result: Record<string, FieldDef> = {};
115
- for (const included of trait.includes ?? []) {
116
- Object.assign(result, expandTrait(included, entityName));
145
+ if (trait) {
146
+ for (const included of trait.includes ?? []) {
147
+ Object.assign(result, expandTraitInner(included, entityName, localTraits, path));
148
+ }
149
+ Object.assign(result, templateFields(trait.fields ?? {}, entityName));
117
150
  }
118
- Object.assign(result, templateFields(trait.fields, entityName));
151
+
152
+ path.delete(traitCd);
119
153
  return result;
120
154
  }
121
155
 
122
156
  /** Expand several traits in order into one merged field map. */
123
- export function expandTraits(traitCds: readonly string[], entityName: string): Record<string, FieldDef> {
157
+ export function expandTraits(
158
+ traitCds: readonly string[],
159
+ entityName: string,
160
+ localTraits?: TraitsFile,
161
+ ): Record<string, FieldDef> {
124
162
  const result: Record<string, FieldDef> = {};
125
- for (const cd of traitCds) Object.assign(result, expandTrait(cd, entityName));
163
+ for (const cd of traitCds) {
164
+ Object.assign(result, expandTraitInner(cd, entityName, localTraits, new Set()));
165
+ }
126
166
  return result;
127
167
  }
package/src/types.ts CHANGED
@@ -179,6 +179,21 @@ export interface TraitDef {
179
179
  references?: Record<string, unknown>;
180
180
  }
181
181
 
182
+ /**
183
+ * A module's own `traits.json`, keyed by trait code — the key is the `cd`, so a
184
+ * definition here is otherwise a {@link TraitDef} with everything optional
185
+ * (a marker trait declares no fields at all).
186
+ */
187
+ export interface TraitsFile {
188
+ [traitCd: string]: {
189
+ description?: string;
190
+ includes?: string[];
191
+ fields?: Record<string, FieldDef>;
192
+ references?: Record<string, unknown>;
193
+ constraints?: Record<string, unknown>;
194
+ };
195
+ }
196
+
182
197
  /** Description of a column kind for friendly pickers. */
183
198
  export interface ColumnTypeDef {
184
199
  cd: ColumnTypeCd;