@coffer-org/plugin-documents 2.1.0 → 3.0.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
@@ -8,7 +8,12 @@ export default definePlugin({
8
8
  dependsOn: ['people'],
9
9
  libraries: [
10
10
  {
11
- meta: defineLibrary({ id: 'documents', label: 'documents.library.label', icon: 'lucide:id-card', agent: 'Personal documents — IDs, passports, licences (personal_document); tracks expiry.' }),
11
+ meta: defineLibrary({
12
+ id: 'documents',
13
+ label: 'documents.library.label',
14
+ icon: 'lucide:id-card',
15
+ agent: 'Personal documents — IDs, passports, licences (personal_document); tracks expiry.',
16
+ }),
12
17
  shelves: [personal_document],
13
18
  },
14
19
  ],
@@ -6,7 +6,7 @@ export default defineExtend({
6
6
  icon: 'lucide:globe',
7
7
  attachTo: [{ library: 'people', shelf: 'person' }],
8
8
  claude: `Canonical person identity (not tied to a physical document).
9
- nationality — ISO alpha-2. variants — given/family name per language (field.group multiple: lang+name+surname).`,
9
+ nationality — ISO alpha-2. variants — given/family name per language (field.keyed by lang: name+surname).`,
10
10
  fields: [
11
11
  field.select({ key: 'nationality', label: 'documents.person_identity.fields.nationality', options: 'countries' }),
12
12
  field.keyed({
@@ -6,7 +6,7 @@ export default defineShelf({
6
6
  label: 'documents.personal_document.label',
7
7
  icon: 'lucide:id-card',
8
8
  views: {
9
- list: { fields: ['owner', 'type', 'surname', 'expires'] },
9
+ list: ['owner', 'type', 'surname', 'expires'],
10
10
  },
11
11
  fields: [
12
12
  field.title({ key: 'name', label: 'core.fields.name' }),
@@ -29,7 +29,11 @@ export default defineShelf({
29
29
  field.date({ key: 'issued', label: 'documents.personal_document.fields.issued' }),
30
30
  field.reminder({ key: 'expires', label: 'documents.personal_document.fields.expires', rules: { lead: 60 } }),
31
31
  field.string({ key: 'authority', label: 'documents.personal_document.fields.authority', rules: { max: 200 } }),
32
- field.relation({ key: 'storage', label: 'documents.personal_document.fields.storage', options: { library: 'things', shelf: 'storage_location' } }),
32
+ field.relation({
33
+ key: 'storage',
34
+ label: 'documents.personal_document.fields.storage',
35
+ options: { library: 'things', shelf: 'storage_location' },
36
+ }),
33
37
  field.string({ key: 'mrz', label: 'documents.personal_document.fields.mrz', rules: { max: 200 } }),
34
38
  field.relation({
35
39
  key: 'owner',
@@ -37,7 +41,6 @@ export default defineShelf({
37
41
  options: { library: 'people', shelf: 'person' },
38
42
  }),
39
43
  field.image({ key: 'photos', label: 'documents.personal_document.fields.photos', multiple: true }),
40
- field.text({ key: 'notes', label: 'documents.personal_document.fields.notes', rules: { max: 1000 } }),
41
44
  field.select({
42
45
  key: 'type',
43
46
  label: 'core.fields.type',
@@ -53,6 +56,7 @@ export default defineShelf({
53
56
  { value: 'OT', title: 'documents.personal_document.options.type.OT' },
54
57
  ],
55
58
  }),
59
+ field.text({ key: 'notes', label: 'documents.personal_document.fields.notes', rules: { max: 1000 } }),
56
60
  field.tags({ key: 'tags', label: 'documents.personal_document.fields.tags' }),
57
61
  ],
58
62
  });
package/dist/schema.js CHANGED
@@ -4391,11 +4391,11 @@ function resolveUnits(u) {
4391
4391
  //#endregion
4392
4392
  //#region ../sdk/src/currencies.ts
4393
4393
  /**
4394
- * Валюти з вбудованого `Intl` (ISO 4217) БЕЗ hand-list і без npm-залежності.
4395
- * Коди `Intl.supportedValuesOf('currency')`; символи/назви локалізовані через
4394
+ * Currencies from built-in `Intl` (ISO 4217), with NO hand list or npm dependency.
4395
+ * Codes come from `Intl.supportedValuesOf('currency')`; symbols/names are localized through
4396
4396
  * `Intl.NumberFormat`/`Intl.DisplayNames`.
4397
4397
  */
4398
- /** Усі ISO 4217 коди. Fallback кілька основних (на випадок старого рантайму). */
4398
+ /** All ISO 4217 codes. Fallback: several major codes for older runtimes. */
4399
4399
  var CURRENCY_CODES = (() => {
4400
4400
  try {
4401
4401
  return Intl.supportedValuesOf("currency");
@@ -4410,26 +4410,26 @@ var CURRENCY_CODES = (() => {
4410
4410
  }
4411
4411
  })();
4412
4412
  var CODE_SET = new Set(CURRENCY_CODES);
4413
- /** Чи валідний ISO 4217 код. */
4413
+ /** Whether an ISO 4217 code is valid. */
4414
4414
  var isCurrencyCode = (c) => CODE_SET.has(c);
4415
4415
  //#endregion
4416
4416
  //#region ../sdk/src/fields/validation.ts
4417
- /** Структуроване повідомлення для zod: JSON {code, params}. Декодує mutate.ts. */
4417
+ /** Structured message for zod: JSON {code, params}. Decoded by mutate.ts. */
4418
4418
  function vmsg(code, params) {
4419
4419
  return JSON.stringify(params ? {
4420
4420
  code,
4421
4421
  params
4422
4422
  } : { code });
4423
4423
  }
4424
- /** v4 error-map: повідомлення для відсутнього значення (колишній required_error). */
4424
+ /** v4 error-map: message for a missing value (formerly required_error). */
4425
4425
  function reqErr(code = "required") {
4426
4426
  return { error: (iss) => iss.input === void 0 ? vmsg(code) : void 0 };
4427
4427
  }
4428
- /** v4 error-map: повідомлення для невалідного типу (колишній invalid_type_error). */
4428
+ /** v4 error-map: message for an invalid type (formerly invalid_type_error). */
4429
4429
  function typeErr(code = "invalid_type") {
4430
4430
  return { error: (iss) => iss.code === "invalid_type" ? vmsg(code) : void 0 };
4431
4431
  }
4432
- /** v4 error-map: required + invalid_type разом (колишні required_error + invalid_type_error). */
4432
+ /** v4 error-map: required + invalid_type together (formerly required_error + invalid_type_error). */
4433
4433
  function reqTypeErr() {
4434
4434
  return { error: (iss) => iss.code === "invalid_type" ? iss.input === void 0 ? vmsg("required") : vmsg("invalid_type") : void 0 };
4435
4435
  }
@@ -4447,10 +4447,10 @@ function jsonValue(raw) {
4447
4447
  return raw;
4448
4448
  }
4449
4449
  /**
4450
- * Фабрика для полів, що зберігають JSON і валідуються вкладеною zod-схемою.
4451
- * Приймає нативне object/array (native form-state) АБО JSON-рядок (legacy).
4452
- * Single-parse через jsonValue → inner.safeParse → issue `code`; рядок, що не
4453
- * розпарсився, лишається рядкомкод 'json'.
4450
+ * Factory for fields that store JSON and validate it with a nested zod schema.
4451
+ * Accepts a native object/array (native form state) OR a JSON string (legacy).
4452
+ * Single parse through jsonValue → inner.safeParse → issue `code`; an unparseable
4453
+ * string remains a string code 'json'.
4454
4454
  */
4455
4455
  function jsonRefined(inner, code) {
4456
4456
  return unknown().superRefine((raw, ctx) => {
@@ -4502,6 +4502,8 @@ function normalizeOpts(rawIn) {
4502
4502
  unique: r.unique,
4503
4503
  fixed: r.fixed,
4504
4504
  exts: r.exts,
4505
+ ext: r.ext,
4506
+ maxBytes: r.maxBytes,
4505
4507
  lead: r.lead,
4506
4508
  by: r.by,
4507
4509
  min: r.min,
@@ -4522,16 +4524,16 @@ function normalizeOpts(rawIn) {
4522
4524
  //#endregion
4523
4525
  //#region ../sdk/src/field-presets.ts
4524
4526
  /**
4525
- * Пресети полів тонкі обгортки над базовими примітивами з fields.ts.
4527
+ * Field presets are thin wrappers around the primitives in fields.ts.
4526
4528
  *
4527
- * Кожен пресет = один kind (один віджет). Пресети не приймають `format` —
4528
- * вони самі є семантичними типами. min/max/step через `config`.
4529
+ * Each preset = one kind (one widget). Presets do not accept `format`;
4530
+ * they are semantic types themselves. min/max/step go through `config`.
4529
4531
  *
4530
- * Пресети додаються до `f` через `composeF`, який гарантує, що жоден пресет
4531
- * не перебиває примітив.
4532
+ * Presets are added to `f` through `composeF`, which guarantees no preset
4533
+ * overrides a primitive.
4532
4534
  *
4533
- * Циклічний імпорт з fields.ts безпечний: фабрики/хелпери hoisted-декларації,
4534
- * а пресети викликають їх лише в тілі своїх функцій.
4535
+ * The cyclic import from fields.ts is safe: factories/helpers are hoisted declarations,
4536
+ * and presets call them only inside their function bodies.
4535
4537
  */
4536
4538
  function email(raw) {
4537
4539
  const o = normalizeOpts(raw);
@@ -4705,9 +4707,9 @@ function link(raw) {
4705
4707
  }, o.multiple ?? false));
4706
4708
  }
4707
4709
  var TEL_RE = /^\+?[\d\s()-]{4,}$/;
4708
- /** Loose URL: будь-який scheme:// АБО host-з-крапкою (+опц. порт/шлях). Без пробілів. */
4710
+ /** Loose URL: any scheme:// OR dotted host (optional port/path). No spaces. */
4709
4711
  var LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
4710
- /** CSS named colors (CSS Color Module L4) для f.colorname. */
4712
+ /** CSS named colors (CSS Color Module L4) for f.colorname. */
4711
4713
  var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
4712
4714
  "aliceblue",
4713
4715
  "antiquewhite",
@@ -4913,6 +4915,42 @@ function snippet(raw) {
4913
4915
  }
4914
4916
  });
4915
4917
  }
4918
+ /** A NUL byte, or U+FFFD left behind by decoding non-UTF-8 bytes as text. */
4919
+ function isBinaryText(v) {
4920
+ return v.includes("\0") || v.includes("�");
4921
+ }
4922
+ function source(raw) {
4923
+ const o = normalizeOpts(raw);
4924
+ const required = o.required ?? false;
4925
+ const ext = o.ext ?? "txt";
4926
+ const maxBytes = o.maxBytes ?? 262144;
4927
+ const s = string$1(reqTypeErr()).superRefine((v, ctx) => {
4928
+ if (isBinaryText(v)) {
4929
+ ctx.addIssue({
4930
+ code: ZodIssueCode.custom,
4931
+ message: vmsg("source_binary")
4932
+ });
4933
+ return;
4934
+ }
4935
+ if (new TextEncoder().encode(v).length > maxBytes) ctx.addIssue({
4936
+ code: ZodIssueCode.custom,
4937
+ message: vmsg("source_too_large", { maxBytes })
4938
+ });
4939
+ });
4940
+ return wrapKey(o, {
4941
+ kind: "source",
4942
+ label: o.label ?? "",
4943
+ required,
4944
+ prim: "text",
4945
+ column: "text",
4946
+ hints: {
4947
+ ext,
4948
+ maxBytes,
4949
+ noEditControl: true
4950
+ },
4951
+ zod: optionalize(s, required)
4952
+ });
4953
+ }
4916
4954
  function rating(raw) {
4917
4955
  const o = normalizeOpts(raw);
4918
4956
  const required = o.required ?? false;
@@ -4966,7 +5004,7 @@ function reminder(raw) {
4966
5004
  }
4967
5005
  var _real = real;
4968
5006
  var _int = int;
4969
- /** Відсоток 0..100 — real з rules:{min:0,max:100}. */
5007
+ /** Percentage 0..100 — real with rules:{min:0,max:100}. */
4970
5008
  function percent(o) {
4971
5009
  return _real({
4972
5010
  ...o,
@@ -4977,7 +5015,7 @@ function percent(o) {
4977
5015
  }
4978
5016
  });
4979
5017
  }
4980
- /** Рік — int з rules:{min:1900,max:2100}; межі можна перекрити через rules.min/max. */
5018
+ /** Year — int with rules:{min:1900,max:2100}; bounds can be overridden via rules.min/max. */
4981
5019
  function year(o) {
4982
5020
  return _int({
4983
5021
  ...o,
@@ -5049,6 +5087,7 @@ var presets = {
5049
5087
  tags,
5050
5088
  markdown,
5051
5089
  snippet,
5090
+ source,
5052
5091
  rating,
5053
5092
  duration,
5054
5093
  reminder,
@@ -5974,6 +6013,7 @@ function keyed(o) {
5974
6013
  };
5975
6014
  }
5976
6015
  var isCollectionGroup = (g) => g.key != null && g.multiple === true;
6016
+ var isEmbeddedGroup = (g) => g.key != null && g.multiple !== true;
5977
6017
  /** Horizontal divider. */
5978
6018
  function divider() {
5979
6019
  return { el: "divider" };
@@ -6030,13 +6070,6 @@ function wrapKey(opts, meta) {
6030
6070
  role: opts.role
6031
6071
  }
6032
6072
  };
6033
- if (opts.pinned) m = {
6034
- ...m,
6035
- hints: {
6036
- ...m.hints,
6037
- pinned: true
6038
- }
6039
- };
6040
6073
  if (opts.default !== void 0) m = {
6041
6074
  ...m,
6042
6075
  default: opts.default
@@ -6045,6 +6078,15 @@ function wrapKey(opts, meta) {
6045
6078
  ...m,
6046
6079
  hidden: opts.hidden
6047
6080
  };
6081
+ if (opts.derive) m = {
6082
+ ...m,
6083
+ derive: opts.derive,
6084
+ derived: true,
6085
+ hints: {
6086
+ ...m.hints,
6087
+ noEditControl: true
6088
+ }
6089
+ };
6048
6090
  if (opts.key) return {
6049
6091
  key: opts.key,
6050
6092
  type: m
@@ -6359,11 +6401,18 @@ function triState(raw) {
6359
6401
  zod: optionalize(s, required)
6360
6402
  });
6361
6403
  }
6404
+ /** Inline option entry → OptionItem (plain string = value and label at once). */
6405
+ function toOptionItem(o) {
6406
+ return typeof o === "string" ? {
6407
+ value: o,
6408
+ title: o
6409
+ } : o;
6410
+ }
6362
6411
  function select(raw) {
6363
6412
  const o = normalizeOpts(raw);
6364
6413
  const required = o.required ?? false;
6365
6414
  const source = typeof o.options === "string" ? o.options : null;
6366
- const inlineOpts = Array.isArray(o.options) ? o.options : [];
6415
+ const inlineOpts = Array.isArray(o.options) ? o.options.map(toOptionItem) : [];
6367
6416
  const s = inlineOpts.length > 0 ? _enum(inlineOpts.map((x) => x.value), { error: () => vmsg("enum") }) : string$1(reqErr());
6368
6417
  return wrapKey(o, applyMultiple({
6369
6418
  kind: "select",
@@ -6436,15 +6485,23 @@ function check(raw) {
6436
6485
  const required = o.required ?? false;
6437
6486
  const multiple = o.multiple ?? false;
6438
6487
  const slots = o.slots && o.slots.length > 0 ? o.slots : [""];
6488
+ const contentFields = o.fields?.length ? o.fields : [string({
6489
+ key: "text",
6490
+ label: "core.fields.text"
6491
+ })];
6492
+ const contentKeys = /* @__PURE__ */ new Set();
6493
+ for (const f of contentFields) {
6494
+ if (contentKeys.has(f.key)) throw new Error(`[field.check] duplicate content field '${f.key}'`);
6495
+ if (/^check\d+$/.test(f.key)) throw new Error(`[field.check] content field '${f.key}' is reserved for checkbox slots`);
6496
+ if (!multiple && (f.type.virtual || f.type.columns)) throw new Error(`[field.check] single content field '${f.key}' must be a scalar stored field`);
6497
+ contentKeys.add(f.key);
6498
+ }
6499
+ const checkFields = slots.map((label, i) => boolean({
6500
+ key: `check${i}`,
6501
+ label: label || "core.fields.check"
6502
+ }));
6439
6503
  if (multiple) {
6440
- const fields = slots.map((label, i) => boolean({
6441
- key: `check${i}`,
6442
- label: label || "core.fields.check"
6443
- }));
6444
- fields.push(string({
6445
- key: "text",
6446
- label: "core.fields.text"
6447
- }));
6504
+ const fields = [...checkFields, ...contentFields];
6448
6505
  return group({
6449
6506
  key: o.key,
6450
6507
  label: o.label ?? o.key,
@@ -6454,8 +6511,12 @@ function check(raw) {
6454
6511
  view: { kind: "checklist" }
6455
6512
  });
6456
6513
  }
6457
- const shape = { text: string$1() };
6458
- const columns = { text: "text" };
6514
+ const shape = {};
6515
+ const columns = {};
6516
+ for (const f of contentFields) {
6517
+ shape[f.key] = f.type.zod;
6518
+ columns[f.key] = f.type.column;
6519
+ }
6459
6520
  slots.forEach((_, i) => {
6460
6521
  shape[`check${i}`] = boolean$1();
6461
6522
  columns[`check${i}`] = "boolean";
@@ -6481,7 +6542,13 @@ function check(raw) {
6481
6542
  required,
6482
6543
  prim: "check",
6483
6544
  column: "text",
6484
- hints: { slots },
6545
+ hints: {
6546
+ slots,
6547
+ fields: contentFields.map((f) => ({
6548
+ key: f.key,
6549
+ ...toClient(f.type)
6550
+ }))
6551
+ },
6485
6552
  columns,
6486
6553
  zod: optionalize(s, required)
6487
6554
  });
@@ -6542,6 +6609,47 @@ function measured(raw) {
6542
6609
  zod: optionalize(s, required)
6543
6610
  });
6544
6611
  }
6612
+ function unit(raw) {
6613
+ const o = normalizeOpts(raw);
6614
+ const required = o.required ?? false;
6615
+ const units = resolveUnits(o.options);
6616
+ const s = string$1(reqErr()).refine((v) => units.some((u) => u.value === v), { message: vmsg("measured_unit") });
6617
+ return wrapKey(o, {
6618
+ kind: "unit",
6619
+ label: o.label ?? "",
6620
+ required,
6621
+ prim: "select",
6622
+ column: "text",
6623
+ hints: { source: typeof o.options === "string" ? o.options : null },
6624
+ options: units.map((u) => ({
6625
+ value: u.value,
6626
+ title: u.label
6627
+ })),
6628
+ zod: optionalize(s, required)
6629
+ });
6630
+ }
6631
+ function amount(raw) {
6632
+ const o = normalizeOpts(raw);
6633
+ const required = o.required ?? false;
6634
+ const cfg = o.config ?? {};
6635
+ let s = number(typeErr());
6636
+ if (cfg.min != null) s = s.min(cfg.min, { message: vmsg("min", { min: cfg.min }) });
6637
+ if (cfg.max != null) s = s.max(cfg.max, { message: vmsg("max", { max: cfg.max }) });
6638
+ return wrapKey(o, {
6639
+ kind: "amount",
6640
+ label: o.label ?? "",
6641
+ required,
6642
+ prim: "number",
6643
+ column: "real",
6644
+ hints: {
6645
+ unitFrom: o.unitFrom,
6646
+ min: cfg.min,
6647
+ max: cfg.max,
6648
+ step: cfg.step ?? "any"
6649
+ },
6650
+ zod: optionalize(s, required)
6651
+ });
6652
+ }
6545
6653
  function money(raw) {
6546
6654
  const o = normalizeOpts(raw);
6547
6655
  const required = o.required ?? false;
@@ -7019,6 +7127,8 @@ var PRIMITIVES = {
7019
7127
  json,
7020
7128
  check,
7021
7129
  measured,
7130
+ unit,
7131
+ amount,
7022
7132
  money,
7023
7133
  code,
7024
7134
  geo,
@@ -7064,7 +7174,7 @@ var field = new Proxy({}, {
7064
7174
  has: (_t, k) => k in composedField()
7065
7175
  });
7066
7176
  function toClient(field) {
7067
- const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
7177
+ const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived } = field;
7068
7178
  return {
7069
7179
  kind,
7070
7180
  label,
@@ -7075,7 +7185,8 @@ function toClient(field) {
7075
7185
  relation,
7076
7186
  virtual,
7077
7187
  json,
7078
- hidden
7188
+ hidden,
7189
+ derived
7079
7190
  };
7080
7191
  }
7081
7192
  //#endregion
@@ -7753,6 +7864,41 @@ var createDefaultQueryTester = function(query, options) {
7753
7864
  };
7754
7865
  createDefaultQueryTester.createEqualsOperation;
7755
7866
  //#endregion
7867
+ //#region ../sdk/src/derive.ts
7868
+ /**
7869
+ * Derived fields: a value the SERVER computes on every write and stores in a real
7870
+ * column. Unlike a computed StaticEl (`value: fn`), which the browser renders and
7871
+ * nothing persists, a derived field keeps its column — so list views, filters,
7872
+ * sorting, field.lookup and MCP consumers see it like any other field.
7873
+ *
7874
+ * The function is pure, synchronous and isomorphic: it receives the fully hydrated
7875
+ * record (base scalars, embedded groups, collections, extends) and no database
7876
+ * handle. That is what lets the same declaration run on the server at write time
7877
+ * and in the browser for live preview.
7878
+ *
7879
+ * Scope: top-level fields of the record only. A derived field inside a collection
7880
+ * row is not supported.
7881
+ */
7882
+ /** [key, spec] for every top-level derived field. Layout groups are flattened. */
7883
+ function derivedEntries(fields) {
7884
+ const out = [];
7885
+ for (const it of fields) if (isField(it)) {
7886
+ const spec = it.type.derive;
7887
+ if (spec) out.push([it.key, spec]);
7888
+ } else if (isGroup(it) && !it.key) out.push(...derivedEntries(it.fields));
7889
+ return out;
7890
+ }
7891
+ /** Keys a shelf's derive specs may reference: its own fields plus its collections. */
7892
+ function derivableKeys(fields) {
7893
+ const keys = /* @__PURE__ */ new Set();
7894
+ for (const it of fields) if (isField(it)) keys.add(it.key);
7895
+ else if (isGroup(it)) {
7896
+ if (it.key) keys.add(it.key);
7897
+ if (!it.key) for (const k of derivableKeys(it.fields)) keys.add(k);
7898
+ }
7899
+ return keys;
7900
+ }
7901
+ //#endregion
7756
7902
  //#region ../sdk/src/shelf.ts
7757
7903
  /** Flat [storageKey, FieldMeta] pairs of the parent table's columns.
7758
7904
  * layout group → flattened; embedded group → prefix `key__`; collection → skipped. */
@@ -7770,9 +7916,49 @@ function defineShelf(m) {
7770
7916
  const keys = fieldEntries(m.fields).map(([k]) => k);
7771
7917
  const dup = keys.find((k, i) => keys.indexOf(k) !== i);
7772
7918
  if (dup) throw new Error(`[shelf] ${m.library}/${m.shelf}: duplicate key '${dup}'`);
7773
- if (m.standalone !== false && !m.views?.list?.fields?.length) console.warn(`[shelf] ${m.library}/${m.shelf}: standalone shelf without an explicit views.list`);
7919
+ if (m.single && !m.claude) throw new Error(`[shelf] ${m.library}/${m.shelf}: single shelf requires \`claude\` the agent cannot find it otherwise`);
7920
+ if (m.views?.list !== void 0 && !Array.isArray(m.views.list)) throw new Error(`[shelf] ${m.library}/${m.shelf}: views.list is a flat array of field keys — the { kind, fields } form is gone; use views.list: [...] plus views.listKind`);
7921
+ if (m.standalone !== false && !m.single && !m.views?.list?.length) console.warn(`[shelf] ${m.library}/${m.shelf}: standalone shelf without an explicit views.list`);
7922
+ if (m.views?.title && !ownFieldEntries(m.fields).some(([k]) => k === m.views.title)) throw new Error(`[shelf] ${m.library}/${m.shelf}: views.title '${m.views.title}' is not a field`);
7923
+ if (m.views?.list?.includes(titleKey(m))) console.warn(`[shelf] ${m.library}/${m.shelf}: views.list repeats the title '${titleKey(m)}' — the title is declared separately`);
7924
+ const titles = ownFieldEntries(m.fields).filter(([, f]) => f.kind === "title");
7925
+ if (titles.length > 1) console.warn(`[shelf] ${m.library}/${m.shelf}: ${titles.length} title fields (${titles.map(([k]) => k).join(", ")}) — '${titles[0][0]}' wins`);
7926
+ if (m.standalone !== false && !m.single && titleKey(m) === "id") console.warn(`[shelf] ${m.library}/${m.shelf}: no title declared (field.title() or views.title) — the record renders no heading`);
7927
+ const known = derivableKeys(m.fields);
7928
+ for (const [key, spec] of derivedEntries(m.fields)) for (const dep of spec.deps) if (!known.has(dep)) throw new Error(`[defineShelf] ${m.library}/${m.shelf}: derive on '${key}' depends on unknown key '${dep}'`);
7774
7929
  return m;
7775
7930
  }
7931
+ /**
7932
+ * Contract of a real entity:
7933
+ * id — uuid (the ONLY required system field, auto).
7934
+ * The rest (including name) are regular fields in ShelfDef.fields.
7935
+ * created_at/updated_at — system infrastructure (outside fields, auto-managed).
7936
+ */
7937
+ /**
7938
+ * Keyed fields of the record itself, in declaration order: top level and plain
7939
+ * layout groups. An embedded group or a collection describes a sub-record, so its
7940
+ * fields are not candidates for the record's own heading.
7941
+ */
7942
+ function ownFieldEntries(items) {
7943
+ const out = [];
7944
+ for (const it of items) if (isField(it)) out.push([it.key, it.type]);
7945
+ else if (isGroup(it) && !isCollectionGroup(it) && !isEmbeddedGroup(it)) out.push(...ownFieldEntries(it.fields));
7946
+ return out;
7947
+ }
7948
+ /**
7949
+ * Key of the record's title field (lists, relation pickers, inlines, record heading).
7950
+ * Priority: views.title → the first keyed kind:'title' field → 'id'.
7951
+ *
7952
+ * The title is declared, never guessed: a shelf that wants a heading marks the field
7953
+ * with field.title(), or names another field through views.title when the heading is
7954
+ * not a text field (a date, a virtual value). A shelf that declares nothing resolves
7955
+ * to 'id' and renders no heading — every field stays in the body with its own label.
7956
+ */
7957
+ function titleKey(m) {
7958
+ if (m.views?.title) return m.views.title;
7959
+ const declared = ownFieldEntries(m.fields).find(([, f]) => f.kind === "title");
7960
+ return declared ? declared[0] : "id";
7961
+ }
7776
7962
  //#endregion
7777
7963
  //#region src/personal_document/index.ts
7778
7964
  var personal_document_default = defineShelf({
@@ -7780,12 +7966,12 @@ var personal_document_default = defineShelf({
7780
7966
  library: "documents",
7781
7967
  label: "documents.personal_document.label",
7782
7968
  icon: "lucide:id-card",
7783
- views: { list: { fields: [
7969
+ views: { list: [
7784
7970
  "owner",
7785
7971
  "type",
7786
7972
  "surname",
7787
7973
  "expires"
7788
- ] } },
7974
+ ] },
7789
7975
  fields: [
7790
7976
  field.title({
7791
7977
  key: "name",
@@ -7880,11 +8066,6 @@ var personal_document_default = defineShelf({
7880
8066
  label: "documents.personal_document.fields.photos",
7881
8067
  multiple: true
7882
8068
  }),
7883
- field.text({
7884
- key: "notes",
7885
- label: "documents.personal_document.fields.notes",
7886
- rules: { max: 1e3 }
7887
- }),
7888
8069
  field.select({
7889
8070
  key: "type",
7890
8071
  label: "core.fields.type",
@@ -7924,6 +8105,11 @@ var personal_document_default = defineShelf({
7924
8105
  }
7925
8106
  ]
7926
8107
  }),
8108
+ field.text({
8109
+ key: "notes",
8110
+ label: "documents.personal_document.fields.notes",
8111
+ rules: { max: 1e3 }
8112
+ }),
7927
8113
  field.tags({
7928
8114
  key: "tags",
7929
8115
  label: "documents.personal_document.fields.tags"
@@ -7954,7 +8140,7 @@ var person_identity_default = defineExtend({
7954
8140
  shelf: "person"
7955
8141
  }],
7956
8142
  claude: `Canonical person identity (not tied to a physical document).
7957
- nationality — ISO alpha-2. variants — given/family name per language (field.group multiple: lang+name+surname).`,
8143
+ nationality — ISO alpha-2. variants — given/family name per language (field.keyed by lang: name+surname).`,
7958
8144
  fields: [field.select({
7959
8145
  key: "nationality",
7960
8146
  label: "documents.person_identity.fields.nationality",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-documents",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -25,9 +25,9 @@
25
25
  "postpack": "node ../../scripts/swap-exports.mjs src"
26
26
  },
27
27
  "dependencies": {
28
- "@coffer-org/plugin-people": "^2.1.0",
29
- "@coffer-org/sdk": "^2.1.0",
30
- "@coffer-org/server": "^2.2.0"
28
+ "@coffer-org/plugin-people": "^3.0.0",
29
+ "@coffer-org/sdk": "^3.0.0",
30
+ "@coffer-org/server": "^3.0.0"
31
31
  },
32
32
  "coffer": {
33
33
  "schema": "dist/schema.js"