@dforge-core/metadata 0.0.45 → 0.0.46

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,24 @@ 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.46] — 2026-10-25
9
+
10
+ ### Added
11
+
12
+ - **Register column slots:** `REGISTER_COLUMN_SLOTS` lists every place a register declaration names a
13
+ stored column — the entity it lives on, what to call it, whether it is summed. `registerColumnSlot(path,
14
+ kind?)` resolves a path to its slot and `registerColumnHost(def, host)` names the entity a slot is on, so an
15
+ editor can offer the right entity's columns at a path. `checkRegister` reads columns through the same table.
16
+ - **`column-not-numeric` warning:** a column that a balance, turnover or accounting register adds up (its
17
+ resources, and its totals cache's) must store a scalar number. It runs when the `RegisterEntityLookup` returns
18
+ `datatypes` (a new optional `Map` of storage type per column) and skips a column whose type is unknown.
19
+ An information register's resources are returned, not summed, so they may be any type.
20
+ - `isNumericDatatype()` and the `RegisterHosts` type.
21
+
22
+ ### Changed
23
+
24
+ - **`unknown-column` message** for a register column quotes the entity: `... isn't a stored column of 'stock'.`
25
+
8
26
  ## [0.0.45] — 2026-10-24
9
27
 
10
28
  ### Added
package/dist/index.d.ts CHANGED
@@ -1702,6 +1702,8 @@ interface RegisterDef {
1702
1702
  debitField: string;
1703
1703
  creditField: string;
1704
1704
  }>;
1705
+ /** Accounting only: the posting column a loaded posting keeps its document's key in, so `exclude` can leave it out. */
1706
+ importedKeyField?: string;
1705
1707
  resources: Record<string, RegisterResourceDef>;
1706
1708
  totals?: RegisterTotalsDef;
1707
1709
  /** `balance`: may the state go below zero (default false). */
@@ -1785,6 +1787,35 @@ interface RegisterIssue {
1785
1787
  code: string;
1786
1788
  }
1787
1789
 
1790
+ /** The entity a column slot is stored on. `carrier` is `entity` on a balance / information register, `journal` otherwise. */
1791
+ type RegisterColumnHost = "entity" | "journal" | "carrier" | "totals" | "composition";
1792
+ interface RegisterColumnSlot {
1793
+ /** Where in a declaration; `*` matches any key or index at that depth. */
1794
+ path: readonly string[];
1795
+ on: RegisterColumnHost;
1796
+ /** What the column is called in a message; `{0}`, `{1}`… stand for the `*` matches in order. */
1797
+ what: string;
1798
+ /**
1799
+ * The kinds of register for which the column is added up and so must store a
1800
+ * number. An information register's resources are what a slice returns — a
1801
+ * currency code as readily as a price — so it is not among them.
1802
+ */
1803
+ numericFor?: readonly RegisterKind[];
1804
+ }
1805
+ declare const REGISTER_COLUMN_SLOTS: readonly RegisterColumnSlot[];
1806
+ interface ResolvedColumnSlot {
1807
+ on: RegisterColumnHost;
1808
+ /** The column's name for a message, with the path's keys filled in. */
1809
+ what: string;
1810
+ numeric: boolean;
1811
+ }
1812
+ /** The slot a path inside one declaration names a column at; nothing for a path that isn't a column. `kind` is the register's, which decides whether the column must be numeric. */
1813
+ declare function registerColumnSlot(path: IssuePath, kind?: RegisterKind): ResolvedColumnSlot | undefined;
1814
+ /** The code of the entity a slot's host names in this declaration. */
1815
+ declare function registerColumnHost(def: Raw, on: RegisterColumnHost): string | undefined;
1816
+ /** Whether a storage type (`numeric(18,2)`, `int8`) is a scalar number; the whole type must match, so `integer[]` is not. */
1817
+ declare const isNumericDatatype: (datatype: string) => boolean;
1818
+
1788
1819
  /**
1789
1820
  * What an entity a declaration names looks like, as far as the caller knows.
1790
1821
  * `columns` is left undefined when they can't be listed (a dependency known
@@ -1792,6 +1823,8 @@ interface RegisterIssue {
1792
1823
  */
1793
1824
  interface RegisterEntityInfo {
1794
1825
  columns?: Iterable<string>;
1826
+ /** Storage type per column (`numeric(18,2)`); a column missing here has no numeric rule run on it. */
1827
+ datatypes?: ReadonlyMap<string, string>;
1795
1828
  }
1796
1829
  /**
1797
1830
  * Resolves an entity code as `registers.json` writes it — bare for the module's
@@ -1800,6 +1833,12 @@ interface RegisterEntityInfo {
1800
1833
  * the module is known and has no such entity.
1801
1834
  */
1802
1835
  type RegisterEntityLookup = (code: string) => RegisterEntityInfo | null | undefined;
1836
+ type ResolvedRegisterEntity = RegisterEntityInfo | null | undefined;
1837
+ /** The entities of a declaration resolved so far, by the slot host that names them. */
1838
+ type RegisterHosts = Partial<Record<RegisterColumnHost, {
1839
+ entity: ResolvedRegisterEntity;
1840
+ code: string | undefined;
1841
+ }>>;
1803
1842
 
1804
1843
  /**
1805
1844
  * Check one declaration against the schema's per-kind rules and the installer's
@@ -2062,4 +2101,4 @@ declare const flagDefs: readonly {
2062
2101
  label: string;
2063
2102
  }[];
2064
2103
 
2065
- export { AGG_TYPE_LIST, type AccumulationConfig, AggType, type AlignType, type AuditHistoryLevel, type BaseDatatypeCd, type CalendarViewConfig, type CardFieldDef, type CardFieldEntry, type CardLayout, type CardLayoutDef, type CardLayoutsFile, type CardSection, type CardSetConfig, type CardTabRef, type ColumnDef, type ColumnGroupSection, type ColumnTypeCd, type ColumnTypeDef, DEP_COLUMN_TYPES, type DataSource, type DataViewDef, type DataViewsFile, type Dataset, type DepColumn, type DepColumnMode, type DepColumnType, type DepEntity, type DepProvenance, type DepProvenanceKind, type DepsFile, type DeriveOptions, type DiagramDef, type DiagramDetail, type DiagramEntity, type DiagramFile, type DiagramRelation, type DiagramViewConfig, type DomainDef, type DomainsFile, type EntityDef, type EntityEvent, type EntityReaction, type EntityReference, type EntityViewColumnDef, type EntityViewDef, FILTER_GROUP_OPERATORS, FILTER_OPERATORS, type FieldDef, type FieldLink, type FieldOption, type FieldTypeCd, type FieldTypeDef, type Filter, type FilterCondition, type FilterGroup, type FilterGroupOperator, type FilterOperator, type FlagCd, type FolderDef, type FolderEntityBinding, type FoldersFile, type JobDef, type JobsFile, type KanbanViewConfig, type LedgerRegistryEntry, type ListLevelConfig, type ListViewConfig, type ManifestAuthor, type ManifestDef, type MenuDef, type MenuItemDef, type MenuItemType, type MenusFile, type ModuleDependency, type ModuleFeature, NUMERIC_TOTAL, type NamedKind, type NumberSequenceDef, type ParamDef, type ParamDefBase, type PeriodConfig, type PrintMargins, type PrintPageSettings, type PrintTemplateDef, type PrintTemplatesFile, REGISTER_CODE, REGISTER_COLUMN_NAME, REGISTER_KINDS, REGISTER_TOTALS_PERIODS, type RegisterBindingParams, type RegisterBindingTarget, type RegisterCompositionDef, type RegisterDef, type RegisterEntityInfo, type RegisterEntityLookup, type RegisterIssue, type RegisterKind, type RegisterMapValue, type RegisterResourceDef, type RegisterSource, type RegisterTotalsDef, type RegisterTotalsPeriod, type RegistersFile, type ReportDef, type ReportEntityAttachment, type ReportLayout, type ReportPanel, type ReportQuery, type ReportsFile, type RoleDef, type RolesFile, type SeedDataFile, type SeedDataRepeat, type SetSection, type SettingBaseDatatype, type SettingConstraintDef, type SettingDef, type SettingsFile, type SignConfig, type SortClause, type SpColumnDef, type SpParamDef, type StoredProcedureDef, type StoredProceduresFile, type TabGroupSection, type TraitCd, type TraitDef, type TraitFieldConflict, type TraitsFile, type TranslatedAction, type TranslatedEntity, type TranslatedMenu, type TranslatedOption, type TranslatedReport, type TranslatedWithOptions, type TranslationsFile, type TreeGridViewConfig, type TriggerDef, type TriggersFile, type ViewColumn, type ViewType, type VizType, type WebhookPayload, type WebhookSubscription, type WebhooksFile, baseDatatypes, baseToDbDatatype, chartTypes, checkRegister, checkRegisterBinding, checkRegisters, columnTypes, dataViewKinds, defaultParams, deriveBaseDatatype, deriveDbDatatype, expandTrait, expandTraits, fieldTypeCds, fieldTypes, fieldTypesByColumnType, flagDefs, getColumnType, getFieldType, getIdentityKeys, getLinks, getPrimaryKeys, getTrait, isFieldTypeCd, isPlainColumnRef, isRegisterKind, registerCarrier, registerDimensions, registerLineMode, registerRefusedProps, registerRequiredProps, registerResourceShape, registerResources, settingDefsOf, traitFieldConflicts, traits, vizTypes };
2104
+ export { AGG_TYPE_LIST, type AccumulationConfig, AggType, type AlignType, type AuditHistoryLevel, type BaseDatatypeCd, type CalendarViewConfig, type CardFieldDef, type CardFieldEntry, type CardLayout, type CardLayoutDef, type CardLayoutsFile, type CardSection, type CardSetConfig, type CardTabRef, type ColumnDef, type ColumnGroupSection, type ColumnTypeCd, type ColumnTypeDef, DEP_COLUMN_TYPES, type DataSource, type DataViewDef, type DataViewsFile, type Dataset, type DepColumn, type DepColumnMode, type DepColumnType, type DepEntity, type DepProvenance, type DepProvenanceKind, type DepsFile, type DeriveOptions, type DiagramDef, type DiagramDetail, type DiagramEntity, type DiagramFile, type DiagramRelation, type DiagramViewConfig, type DomainDef, type DomainsFile, type EntityDef, type EntityEvent, type EntityReaction, type EntityReference, type EntityViewColumnDef, type EntityViewDef, FILTER_GROUP_OPERATORS, FILTER_OPERATORS, type FieldDef, type FieldLink, type FieldOption, type FieldTypeCd, type FieldTypeDef, type Filter, type FilterCondition, type FilterGroup, type FilterGroupOperator, type FilterOperator, type FlagCd, type FolderDef, type FolderEntityBinding, type FoldersFile, type JobDef, type JobsFile, type KanbanViewConfig, type LedgerRegistryEntry, type ListLevelConfig, type ListViewConfig, type ManifestAuthor, type ManifestDef, type MenuDef, type MenuItemDef, type MenuItemType, type MenusFile, type ModuleDependency, type ModuleFeature, NUMERIC_TOTAL, type NamedKind, type NumberSequenceDef, type ParamDef, type ParamDefBase, type PeriodConfig, type PrintMargins, type PrintPageSettings, type PrintTemplateDef, type PrintTemplatesFile, REGISTER_CODE, REGISTER_COLUMN_NAME, REGISTER_COLUMN_SLOTS, REGISTER_KINDS, REGISTER_TOTALS_PERIODS, type RegisterBindingParams, type RegisterBindingTarget, type RegisterColumnHost, type RegisterColumnSlot, type RegisterCompositionDef, type RegisterDef, type RegisterEntityInfo, type RegisterEntityLookup, type RegisterHosts, type RegisterIssue, type RegisterKind, type RegisterMapValue, type RegisterResourceDef, type RegisterSource, type RegisterTotalsDef, type RegisterTotalsPeriod, type RegistersFile, type ReportDef, type ReportEntityAttachment, type ReportLayout, type ReportPanel, type ReportQuery, type ReportsFile, type ResolvedColumnSlot, type RoleDef, type RolesFile, type SeedDataFile, type SeedDataRepeat, type SetSection, type SettingBaseDatatype, type SettingConstraintDef, type SettingDef, type SettingsFile, type SignConfig, type SortClause, type SpColumnDef, type SpParamDef, type StoredProcedureDef, type StoredProceduresFile, type TabGroupSection, type TraitCd, type TraitDef, type TraitFieldConflict, type TraitsFile, type TranslatedAction, type TranslatedEntity, type TranslatedMenu, type TranslatedOption, type TranslatedReport, type TranslatedWithOptions, type TranslationsFile, type TreeGridViewConfig, type TriggerDef, type TriggersFile, type ViewColumn, type ViewType, type VizType, type WebhookPayload, type WebhookSubscription, type WebhooksFile, baseDatatypes, baseToDbDatatype, chartTypes, checkRegister, checkRegisterBinding, checkRegisters, columnTypes, dataViewKinds, defaultParams, deriveBaseDatatype, deriveDbDatatype, expandTrait, expandTraits, fieldTypeCds, fieldTypes, fieldTypesByColumnType, flagDefs, getColumnType, getFieldType, getIdentityKeys, getLinks, getPrimaryKeys, getTrait, isFieldTypeCd, isNumericDatatype, isPlainColumnRef, isRegisterKind, registerCarrier, registerColumnHost, registerColumnSlot, registerDimensions, registerLineMode, registerRefusedProps, registerRequiredProps, registerResourceShape, registerResources, settingDefsOf, traitFieldConflicts, traits, vizTypes };
package/dist/index.js CHANGED
@@ -469,7 +469,8 @@ var ACCOUNTING_PROPERTIES = [
469
469
  "creditAccount",
470
470
  "debitAnalyticFields",
471
471
  "creditAnalyticFields",
472
- "sideDimensions"
472
+ "sideDimensions",
473
+ "importedKeyField"
473
474
  ];
474
475
  var BALANCE_PROPERTIES = ["allowNegative", "autoCreate"];
475
476
  var REFUSED = {
@@ -493,8 +494,79 @@ var KIND_LABEL = {
493
494
  accounting: { sentence: "An accounting register", inline: "an accounting register" }
494
495
  };
495
496
 
497
+ // src/registers/columns.ts
498
+ var SUMMED = ["balance", "turnover", "accounting"];
499
+ var slot = (path, on, what, summed = false) => ({
500
+ path: path.split("."),
501
+ on,
502
+ what,
503
+ ...summed ? { numericFor: SUMMED } : {}
504
+ });
505
+ var REGISTER_COLUMN_SLOTS = [
506
+ slot("journalDirection", "journal", "Journal direction column"),
507
+ slot("journalDate", "journal", "Journal date column"),
508
+ slot("period", "entity", "Period column"),
509
+ slot("recorder", "entity", "Recorder column"),
510
+ slot("dimensions.*", "carrier", "Dimension"),
511
+ slot("resources.*.field", "carrier", "Resource '{0}' column", true),
512
+ slot("resources.*.debitField", "carrier", "Resource '{0}' column", true),
513
+ slot("resources.*.creditField", "carrier", "Resource '{0}' column", true),
514
+ slot("account", "journal", "Account column"),
515
+ slot("debitAccount", "journal", "Debit account column"),
516
+ slot("creditAccount", "journal", "Credit account column"),
517
+ slot("analyticFields.*", "journal", "Analytic slot column"),
518
+ slot("debitAnalyticFields.*", "journal", "Analytic slot column"),
519
+ slot("creditAnalyticFields.*", "journal", "Analytic slot column"),
520
+ slot("sideDimensions.*.debitField", "journal", "Dimension '{0}' debit column"),
521
+ slot("sideDimensions.*.creditField", "journal", "Dimension '{0}' credit column"),
522
+ slot("composition.account", "composition", "Composition 'account' column"),
523
+ slot("composition.slot", "composition", "Composition 'slot' column"),
524
+ slot("composition.analytic", "composition", "Composition 'analytic' column"),
525
+ slot("totals.periodField", "totals", "Totals period column"),
526
+ slot("totals.accountField", "totals", "Totals account column"),
527
+ slot("totals.resources.*.inField", "totals", "Totals '{0}' inField", true),
528
+ slot("totals.resources.*.outField", "totals", "Totals '{0}' outField", true),
529
+ slot("totals.resources.*.debitField", "totals", "Totals '{0}' debitField", true),
530
+ slot("totals.resources.*.creditField", "totals", "Totals '{0}' creditField", true)
531
+ ];
532
+ function registerColumnSlot(path, kind) {
533
+ for (const candidate of REGISTER_COLUMN_SLOTS) {
534
+ if (candidate.path.length !== path.length) {
535
+ continue;
536
+ }
537
+ const captured = [];
538
+ const matches = candidate.path.every((segment, i) => {
539
+ if (segment !== "*") {
540
+ return segment === path[i];
541
+ }
542
+ captured.push(String(path[i]));
543
+ return true;
544
+ });
545
+ if (matches) {
546
+ return { on: candidate.on, what: candidate.what.replace(/\{(\d+)\}/g, (_, n) => captured[Number(n)] ?? ""), numeric: !!kind && !!candidate.numericFor?.includes(kind) };
547
+ }
548
+ }
549
+ return void 0;
550
+ }
551
+ function registerColumnHost(def, on) {
552
+ switch (on) {
553
+ case "entity":
554
+ return safeTrim(def.entity);
555
+ case "journal":
556
+ return safeTrim(def.journal);
557
+ case "carrier":
558
+ return registerCarrier(def);
559
+ case "totals":
560
+ return isRecord(def.totals) ? safeTrim(def.totals.entity) : void 0;
561
+ case "composition":
562
+ return isRecord(def.composition) ? safeTrim(def.composition.entity) : void 0;
563
+ }
564
+ }
565
+ var NUMERIC_DATATYPE = /^(?:(?:numeric|decimal)(?:\s*\(\s*\d+(?:\s*,\s*\d+)?\s*\))?|int[248]?|integer|bigint|smallint|(?:big|small)?serial[248]?|float[48]?|double precision|real|money)$/i;
566
+ var isNumericDatatype = (datatype) => NUMERIC_DATATYPE.test(datatype.trim());
567
+
496
568
  // src/registers/entities.ts
497
- function createEntityChecks(lookup, error, warning) {
569
+ function createEntityChecks(lookup, error, warning, hosts, kind) {
498
570
  const resolved = /* @__PURE__ */ new Map();
499
571
  const columnSets = /* @__PURE__ */ new WeakMap();
500
572
  const entityAt = (path, code, what) => {
@@ -519,17 +591,27 @@ function createEntityChecks(lookup, error, warning) {
519
591
  }
520
592
  return columnSets.get(entity) ?? null;
521
593
  };
522
- const columnAt = (path, entity, entityCode, column, what) => {
594
+ const columnAt = (path, column) => {
523
595
  if (typeof column !== "string" || !column.trim()) {
524
596
  return;
525
597
  }
598
+ const slot2 = registerColumnSlot(path, kind);
599
+ if (!slot2) {
600
+ return;
601
+ }
526
602
  if (!REGISTER_COLUMN_NAME.test(column)) {
527
- error(path, "column-name", `${what} '${column}' isn't a column name (lowercase letters, digits and '_').`);
603
+ error(path, "column-name", `${slot2.what} '${column}' isn't a column name (lowercase letters, digits and '_').`);
528
604
  return;
529
605
  }
530
- const columns = columnsOf(entity);
606
+ const host = hosts[slot2.on];
607
+ const columns = columnsOf(host?.entity);
531
608
  if (columns && !columns.has(column)) {
532
- warning(path, "unknown-column", `${what} '${column}' isn't a stored column of ${entityCode}.`);
609
+ warning(path, "unknown-column", `${slot2.what} '${column}' isn't a stored column of ${host?.code ? `'${host.code}'` : `the ${slot2.on} entity`}.`);
610
+ return;
611
+ }
612
+ const datatype = host?.entity?.datatypes?.get(column);
613
+ if (slot2.numeric && datatype && !isNumericDatatype(datatype)) {
614
+ warning(path, "column-not-numeric", `${slot2.what} '${column}' is ${datatype}, not a number.`);
533
615
  }
534
616
  };
535
617
  return { entityAt, columnAt };
@@ -576,7 +658,7 @@ function checkProperties({ def, kind, error }) {
576
658
  error(["recordChangesOnly"], "record-changes-only", "'recordChangesOnly' needs a 'recorder' \u2014 only a document's record can be compared with the slice in effect.");
577
659
  }
578
660
  }
579
- function resolveEntities(def, kind, checks) {
661
+ function resolveEntities(def, kind, checks, hosts) {
580
662
  const { entityAt, columnAt } = checks;
581
663
  const entityCode = safeTrim(def.entity);
582
664
  const journalCode = safeTrim(def.journal);
@@ -586,13 +668,17 @@ function resolveEntities(def, kind, checks) {
586
668
  if (kind === "accounting") {
587
669
  entityAt(["chart"], safeTrim(def.chart), "Chart");
588
670
  }
589
- columnAt(["journalDirection"], journal, journalCode, def.journalDirection, "Journal direction column");
590
- columnAt(["journalDate"], journal, journalCode, def.journalDate, "Journal date column");
591
- columnAt(["period"], entity, entityCode, def.period, "Period column");
592
- columnAt(["recorder"], entity, entityCode, def.recorder, "Recorder column");
593
- return { journalCode, journal, carrierCode: registerCarrier(def), carrier: carriedByEntity ? entity : journal };
671
+ const carrier = carriedByEntity ? entity : journal;
672
+ hosts.entity = { entity, code: entityCode };
673
+ hosts.journal = { entity: journal, code: journalCode };
674
+ hosts.carrier = { entity: carrier, code: registerCarrier(def) };
675
+ columnAt(["journalDirection"], def.journalDirection);
676
+ columnAt(["journalDate"], def.journalDate);
677
+ columnAt(["period"], def.period);
678
+ columnAt(["recorder"], def.recorder);
679
+ return { journalCode };
594
680
  }
595
- function checkDimensions({ def, dimensions, sides, carrier, carrierCode, columnAt, error }) {
681
+ function checkDimensions({ def, dimensions, sides, columnAt, error }) {
596
682
  if (!Array.isArray(def.dimensions) || dimensions.length === 0) {
597
683
  error(["dimensions"], "dimensions", "A register needs at least one dimension.");
598
684
  }
@@ -605,7 +691,7 @@ function checkDimensions({ def, dimensions, sides, carrier, carrierCode, columnA
605
691
  }
606
692
  dimensions.forEach((name, i) => {
607
693
  if (!(name in sides)) {
608
- columnAt(["dimensions", i], carrier, carrierCode, name, "Dimension");
694
+ columnAt(["dimensions", i], name);
609
695
  }
610
696
  });
611
697
  }
@@ -635,7 +721,7 @@ function checkResourceShape(ctx, name, resource, path) {
635
721
  }
636
722
  }
637
723
  function checkResources(ctx) {
638
- const { def, kind, resources, dimensions, carrier, carrierCode, columnAt, error } = ctx;
724
+ const { def, kind, resources, dimensions, columnAt, error } = ctx;
639
725
  if (!isRecord(def.resources) || resources.length === 0) {
640
726
  error(["resources"], "resources", "A register needs at least one resource.");
641
727
  }
@@ -655,7 +741,7 @@ function checkResources(ctx) {
655
741
  if (!column) {
656
742
  continue;
657
743
  }
658
- columnAt(path(key), carrier, carrierCode, column, `Resource '${name}' column`);
744
+ columnAt(path(key), column);
659
745
  const other = storage.get(column);
660
746
  if (other && other !== name) {
661
747
  error(path(key), "resource-column-shared", `Resources '${other}' and '${name}' both store into '${column}' \u2014 posting would add both into one column.`);
@@ -672,10 +758,10 @@ function checkResources(ctx) {
672
758
  }
673
759
  }
674
760
  function checkAnalyticSlotColumns(ctx, key, columns) {
675
- columns.forEach((column, i) => ctx.columnAt([key, i], ctx.journal, ctx.journalCode, column, "Analytic slot column"));
761
+ columns.forEach((column, i) => ctx.columnAt([key, i], column));
676
762
  }
677
763
  function checkSideDimensions(ctx) {
678
- const { dimensions, sides, journal, journalCode, columnAt, error } = ctx;
764
+ const { dimensions, sides, columnAt, error } = ctx;
679
765
  for (const [name, side] of Object.entries(sides)) {
680
766
  const base = ["sideDimensions", name];
681
767
  if (!dimensions.includes(name)) {
@@ -690,12 +776,12 @@ function checkSideDimensions(ctx) {
690
776
  same: [...base, "creditField"],
691
777
  sameMessage: `Two-sided dimension '${name}': the two columns must differ.`
692
778
  });
693
- columnAt([...base, "debitField"], journal, journalCode, debitField, `Dimension '${name}' debit column`);
694
- columnAt([...base, "creditField"], journal, journalCode, creditField, `Dimension '${name}' credit column`);
779
+ columnAt([...base, "debitField"], debitField);
780
+ columnAt([...base, "creditField"], creditField);
695
781
  }
696
782
  }
697
783
  function checkCompoundAccounts(ctx) {
698
- const { def, journal, journalCode, columnAt, error } = ctx;
784
+ const { def, columnAt, error } = ctx;
699
785
  refuseKeys(def, ["account", "analyticFields"], error, "line-mode", (key) => `'${key}' belongs to a classic register; a compound one names 'debitAccount' and 'creditAccount'.`);
700
786
  const debit = safeTrim(def.debitAccount);
701
787
  const credit = safeTrim(def.creditAccount);
@@ -705,8 +791,8 @@ function checkCompoundAccounts(ctx) {
705
791
  same: ["creditAccount"],
706
792
  sameMessage: "'debitAccount' and 'creditAccount' must differ \u2014 one column would record no correspondence."
707
793
  });
708
- columnAt(["debitAccount"], journal, journalCode, debit, "Debit account column");
709
- columnAt(["creditAccount"], journal, journalCode, credit, "Credit account column");
794
+ columnAt(["debitAccount"], debit);
795
+ columnAt(["creditAccount"], credit);
710
796
  const debitSlots = stringList(def.debitAnalyticFields);
711
797
  const creditSlots = stringList(def.creditAnalyticFields);
712
798
  if (debitSlots.length > MAX_ANALYTIC_SLOTS || creditSlots.length > MAX_ANALYTIC_SLOTS) {
@@ -723,12 +809,12 @@ function checkCompoundAccounts(ctx) {
723
809
  return needsComposition;
724
810
  }
725
811
  function checkClassicAccounts(ctx) {
726
- const { def, journal, journalCode, columnAt, error } = ctx;
812
+ const { def, columnAt, error } = ctx;
727
813
  refuseKeys(def, ["debitAccount", "creditAccount", "debitAnalyticFields", "creditAnalyticFields", "sideDimensions"], error, "line-mode", (key) => `'${key}' belongs to a compound register.`);
728
814
  if (!safeTrim(def.account)) {
729
815
  error(["account"], "accounts", "A classic register names the posting's account column ('account').");
730
816
  }
731
- columnAt(["account"], journal, journalCode, safeTrim(def.account), "Account column");
817
+ columnAt(["account"], safeTrim(def.account));
732
818
  const slots = stringList(def.analyticFields);
733
819
  if (slots.length > MAX_ANALYTIC_SLOTS) {
734
820
  error(["analyticFields"], "analytic-slots", `At most ${MAX_ANALYTIC_SLOTS} analytic slots.`);
@@ -737,7 +823,7 @@ function checkClassicAccounts(ctx) {
737
823
  return slots.length > 0;
738
824
  }
739
825
  function checkComposition(ctx, needsComposition) {
740
- const { def, entityAt, columnAt, error } = ctx;
826
+ const { def, entityAt, columnAt, hosts, error } = ctx;
741
827
  const composition = isRecord(def.composition) ? def.composition : void 0;
742
828
  if (needsComposition && !composition) {
743
829
  error(["composition"], "composition", "Analytic slots need a 'composition' \u2014 where the chart says which analytic each account takes in which slot.");
@@ -754,9 +840,9 @@ function checkComposition(ctx, needsComposition) {
754
840
  }
755
841
  }
756
842
  const code = safeTrim(composition.entity);
757
- const entity = entityAt(["composition", "entity"], code, "Composition entity");
843
+ hosts.composition = { entity: entityAt(["composition", "entity"], code, "Composition entity"), code };
758
844
  for (const key of ["account", "slot", "analytic"]) {
759
- columnAt(["composition", key], entity, code, composition[key], `Composition '${key}' column`);
845
+ columnAt(["composition", key], composition[key]);
760
846
  }
761
847
  }
762
848
  function checkAccounting(ctx) {
@@ -771,7 +857,7 @@ function checkAccounting(ctx) {
771
857
  checkComposition(ctx, mode === "compound" ? checkCompoundAccounts(ctx) : checkClassicAccounts(ctx));
772
858
  }
773
859
  function checkTotals(ctx) {
774
- const { def, kind, dimensions, resources, entityAt, columnAt, error } = ctx;
860
+ const { def, kind, dimensions, resources, entityAt, columnAt, hosts, error } = ctx;
775
861
  const totals = isRecord(def.totals) ? def.totals : void 0;
776
862
  if (!totals || kind === "info") {
777
863
  return;
@@ -780,7 +866,7 @@ function checkTotals(ctx) {
780
866
  if (!totalsCode) {
781
867
  error(["totals", "entity"], "totals", "The totals cache needs its entity.");
782
868
  }
783
- const totalsEntity = entityAt(["totals", "entity"], totalsCode, "Totals entity");
869
+ hosts.totals = { entity: entityAt(["totals", "entity"], totalsCode, "Totals entity"), code: totalsCode };
784
870
  const period = safeTrim(totals.period);
785
871
  if (!period || !REGISTER_TOTALS_PERIODS.includes(period)) {
786
872
  error(["totals", "period"], "totals", "The totals cache needs a period: day, month, quarter or year.");
@@ -788,12 +874,12 @@ function checkTotals(ctx) {
788
874
  if (!safeTrim(totals.periodField)) {
789
875
  error(["totals", "periodField"], "totals", "The totals cache needs 'periodField' \u2014 the column holding each period's first date.");
790
876
  }
791
- columnAt(["totals", "periodField"], totalsEntity, totalsCode, safeTrim(totals.periodField), "Totals period column");
877
+ columnAt(["totals", "periodField"], safeTrim(totals.periodField));
792
878
  if (kind === "accounting") {
793
879
  if (!safeTrim(totals.accountField)) {
794
880
  error(["totals", "accountField"], "totals", "An accounting register's cache needs 'accountField'.");
795
881
  }
796
- columnAt(["totals", "accountField"], totalsEntity, totalsCode, safeTrim(totals.accountField), "Totals account column");
882
+ columnAt(["totals", "accountField"], safeTrim(totals.accountField));
797
883
  stringList(totals.dimensions).forEach((name, i) => {
798
884
  if (!dimensions.includes(name)) {
799
885
  error(["totals", "dimensions", i], "totals", `Totals dimension '${name}' isn't a dimension of the register.`);
@@ -819,7 +905,7 @@ function checkTotals(ctx) {
819
905
  if (!safeTrim(entry[key])) {
820
906
  error(["totals", "resources", name], "totals", `Totals resource '${name}' needs '${key}'.`);
821
907
  }
822
- columnAt(["totals", "resources", name, key], totalsEntity, totalsCode, safeTrim(entry[key]), `Totals '${name}' ${key}`);
908
+ columnAt(["totals", "resources", name, key], safeTrim(entry[key]));
823
909
  }
824
910
  }
825
911
  for (const name of Object.keys(cached)) {
@@ -855,14 +941,16 @@ function checkRegister(raw, lookup) {
855
941
  }
856
942
  const kind = def.kind;
857
943
  checkProperties({ def, kind, error });
858
- const checks = createEntityChecks(lookup, error, warning);
944
+ const hosts = {};
945
+ const checks = createEntityChecks(lookup, error, warning, hosts, kind);
859
946
  const ctx = {
860
947
  def,
861
948
  kind,
862
949
  error,
863
950
  warning,
951
+ hosts,
864
952
  ...checks,
865
- ...resolveEntities(def, kind, checks),
953
+ ...resolveEntities(def, kind, checks, hosts),
866
954
  dimensions: registerDimensions(def),
867
955
  sides: isRecord(def.sideDimensions) ? def.sideDimensions : {},
868
956
  resources: registerResources(def)
@@ -1116,6 +1204,7 @@ export {
1116
1204
  NUMERIC_TOTAL,
1117
1205
  REGISTER_CODE,
1118
1206
  REGISTER_COLUMN_NAME,
1207
+ REGISTER_COLUMN_SLOTS,
1119
1208
  REGISTER_KINDS,
1120
1209
  REGISTER_TOTALS_PERIODS,
1121
1210
  baseDatatypes,
@@ -1142,9 +1231,12 @@ export {
1142
1231
  getPrimaryKeys,
1143
1232
  getTrait,
1144
1233
  isFieldTypeCd,
1234
+ isNumericDatatype,
1145
1235
  isPlainColumnRef,
1146
1236
  isRegisterKind,
1147
1237
  registerCarrier,
1238
+ registerColumnHost,
1239
+ registerColumnSlot,
1148
1240
  registerDimensions,
1149
1241
  registerLineMode,
1150
1242
  registerRefusedProps,