@dforge-core/metadata 0.0.24 → 0.0.26

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,61 @@ 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.26] — 2026-09-15
9
+
10
+ ### Added
11
+
12
+ - **`traitFieldConflicts()` and the `TraitFieldConflict` type** report the collisions
13
+ that make the installer refuse an entity: a trait contributing a field the entity
14
+ already has under a different type identity (`columnType ?? dbDatatype ??
15
+ fieldTypeCd`, the identity `TraitExpander.TryAddField` compares). Install throws on
16
+ one, so a module that hits it cannot install — offline readers now name the field,
17
+ both types, and whether the definition being kept is an authored field or another
18
+ trait's. Expansion itself stays total, so an editor can still draw a field list.
19
+
20
+ ### Fixed
21
+
22
+ - **A field code that names an inherited property is a field like any other.** The
23
+ merge read `sink.fields[key]` against a plain object, so a trait defining
24
+ `constructor` found `Object.prototype`'s, contributed no column, and reported a
25
+ conflict against a type identity read off a function. Own-property lookups
26
+ throughout, on a prototype-less merge map — which also fixes the write side, where a
27
+ field named `__proto__` set the prototype instead of a key — and on the
28
+ `localTraits` lookup, which is parsed JSON with the same hazard.
29
+
30
+ - **Two templated keys that collide on one entity are a conflict, not a silent
31
+ overwrite.** A trait defining both `{entity}_id` and `x_id` collapsed them while
32
+ templating, for entity `x`, before the merge ever saw them: the later definition won
33
+ and nothing was reported, while install rejected the entity. Each field is now
34
+ resolved and merged on its own, the way the installer resolves and adds one key at a
35
+ time — the first definition wins, the second is reported.
36
+
37
+ ## [0.0.25] — 2026-09-15
38
+
39
+ ### Added
40
+
41
+ - **`templateChunkValue()` and the `StringArg` type** are documented as exports of
42
+ `@dforge-core/metadata/dsl`. Both landed in 0.0.24 without an entry:
43
+ `CallRef.firstStringArg` became a `StringArg`, which reads a template literal as
44
+ well as a quoted string — reassembled from the chunks the lexer splits one into,
45
+ with `interpolated` marking the `${…}` holes and a space standing in for each —
46
+ so a rule that reads a call's first argument sees the multi-line form too.
47
+
48
+ ### Fixed
49
+
50
+ - **`dsl/unknown-builtin` scopes a declared helper to its block, because the
51
+ compiler does.** `execute:` and `onBeforeStart:` are compiled to separate
52
+ scripts and each gets its own `CheckUndefinedReferences` pass, so a `function`
53
+ declared in one is undefined in the other — and the rule, which read
54
+ declarations across the whole file, accepted the call that install rejects. A
55
+ declaration above the first header belongs to no block and stays visible to
56
+ both; nothing legal can sit there.
57
+
58
+ - **`dsl/job-record-context` stands down on a `records` the script binds
59
+ itself.** `var records = select(…)` is the very refactor the rule asks for, so
60
+ reporting it left the author with a diagnostic that its own fix could not
61
+ clear. Shadowing is read exactly as the execution-mode rule already reads it.
62
+
8
63
  ## [0.0.24] — 2026-09-15
9
64
 
10
65
  ### Added
package/dist/dsl/index.js CHANGED
@@ -1163,12 +1163,20 @@ function checkDsl(text, ctx = {}) {
1163
1163
  );
1164
1164
  }
1165
1165
  }
1166
- const declaredFunctions = /* @__PURE__ */ new Set();
1166
+ const declaredFunctions = /* @__PURE__ */ new Map();
1167
1167
  for (let i = 0; i < parsed.tokens.length; i++) {
1168
- if (parsed.tokens[i].text !== "function") continue;
1168
+ const token = parsed.tokens[i];
1169
+ if (token.text !== "function") continue;
1169
1170
  const name = parsed.tokens[i + 1];
1170
- if (name?.kind === "ident") declaredFunctions.add(name.text);
1171
+ if (name?.kind !== "ident") continue;
1172
+ const blocks = declaredFunctions.get(name.text) ?? /* @__PURE__ */ new Set();
1173
+ blocks.add(blockOf(parsed, token.start));
1174
+ declaredFunctions.set(name.text, blocks);
1171
1175
  }
1176
+ const isDeclaredIn = (name, block) => {
1177
+ const blocks = declaredFunctions.get(name);
1178
+ return blocks !== void 0 && (blocks.has(null) || blocks.has(block));
1179
+ };
1172
1180
  const reportedUnknown = /* @__PURE__ */ new Set();
1173
1181
  for (const call of parsed.calls) {
1174
1182
  if (call.block !== "execute" && call.block !== "onBeforeStart") continue;
@@ -1180,7 +1188,7 @@ function checkDsl(text, ctx = {}) {
1180
1188
  if (isMethodName(parsed.tokens, idx)) continue;
1181
1189
  }
1182
1190
  if (BUILTIN_BY_NAME.has(name) || CONTROL_KEYWORDS.has(name) || KEYWORDS_BEFORE_PAREN.has(name) || JS_GLOBALS.has(name) || FORMULA_ONLY.has(name) || // Its own rule reports this one, with better wording.
1183
- name === "userId" || declaredFunctions.has(name) || parsed.locals.has(name) || reportedUnknown.has(name)) {
1191
+ name === "userId" || isDeclaredIn(name, call.block) || parsed.locals.has(name) || reportedUnknown.has(name)) {
1184
1192
  continue;
1185
1193
  }
1186
1194
  reportedUnknown.add(name);
@@ -1205,9 +1213,11 @@ function checkDsl(text, ctx = {}) {
1205
1213
  if (ref.global !== "old" || !inScript(ref.block)) continue;
1206
1214
  bound.push({ span: ref.span, read: `old[${ref.property}]` });
1207
1215
  }
1208
- for (const span of parsed.recordsRefs) {
1209
- if (!inScript(blockOf(parsed, span.start))) continue;
1210
- bound.push({ span, read: "records" });
1216
+ if (!parsed.locals.has("records")) {
1217
+ for (const span of parsed.recordsRefs) {
1218
+ if (!inScript(blockOf(parsed, span.start))) continue;
1219
+ bound.push({ span, read: "records" });
1220
+ }
1211
1221
  }
1212
1222
  const first = bound.sort((a, b) => a.span.start - b.span.start)[0];
1213
1223
  if (first) {