@dinoxx/dinox-cli 2.1.1 → 2.3.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.
Files changed (68) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/commands/domain/options.d.ts +10 -0
  3. package/dist/commands/domain/options.js +75 -0
  4. package/dist/commands/domain/run.d.ts +24 -0
  5. package/dist/commands/domain/run.js +137 -0
  6. package/dist/commands/notes/actions.js +72 -20
  7. package/dist/commands/notes/createFromTemplate.d.ts +35 -0
  8. package/dist/commands/notes/createFromTemplate.js +58 -0
  9. package/dist/commands/notes/descriptors.js +12 -5
  10. package/dist/commands/notes/dryRun.d.ts +1 -0
  11. package/dist/commands/notes/dryRun.js +8 -2
  12. package/dist/commands/notes/repo/read.js +4 -23
  13. package/dist/commands/notes/repo/shared.d.ts +2 -0
  14. package/dist/commands/notes/repo/write.d.ts +3 -0
  15. package/dist/commands/notes/repo/write.js +16 -5
  16. package/dist/commands/properties/index.d.ts +16 -0
  17. package/dist/commands/properties/index.js +640 -0
  18. package/dist/commands/properties/repo.d.ts +150 -0
  19. package/dist/commands/properties/repo.js +272 -0
  20. package/dist/commands/savedViews/fields.d.ts +28 -0
  21. package/dist/commands/savedViews/fields.js +87 -0
  22. package/dist/commands/savedViews/index.js +203 -164
  23. package/dist/commands/savedViews/model.d.ts +27 -69
  24. package/dist/commands/savedViews/model.js +63 -279
  25. package/dist/commands/savedViews/repo.d.ts +64 -62
  26. package/dist/commands/savedViews/repo.js +309 -428
  27. package/dist/commands/schema/renderSkills.js +29 -5
  28. package/dist/commands/schema/spec.js +4 -0
  29. package/dist/commands/templates/index.d.ts +4 -0
  30. package/dist/commands/templates/index.js +290 -0
  31. package/dist/commands/templates/model.d.ts +12 -34
  32. package/dist/commands/templates/model.js +39 -83
  33. package/dist/commands/templates/repo.d.ts +53 -0
  34. package/dist/commands/templates/repo.js +78 -0
  35. package/dist/skills/registry.js +7 -0
  36. package/dist/utils/errorCodes.d.ts +212 -0
  37. package/dist/utils/errorCodes.js +53 -0
  38. package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
  39. package/dist/vendor/electron/domain/note-templates.js +232 -0
  40. package/dist/vendor/electron/domain/properties.d.ts +10 -0
  41. package/dist/vendor/electron/domain/properties.js +1538 -0
  42. package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
  43. package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
  44. package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
  45. package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
  46. package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
  47. package/dist/vendor/electron/domain/saved-views.js +1117 -0
  48. package/dist/vendor/electron/rpc.d.ts +22 -0
  49. package/dist/vendor/electron/rpc.js +27 -0
  50. package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
  51. package/dist/vendor/electron/shared/ai-errors.js +107 -0
  52. package/dist/vendor/electron/shared/external-types.d.ts +2 -0
  53. package/dist/vendor/electron/shared/external-types.js +1 -0
  54. package/dist/vendor/electron/shared/index.d.ts +9 -0
  55. package/dist/vendor/electron/shared/index.js +7 -0
  56. package/dist/vendor/electron/shared/note-properties.d.ts +312 -0
  57. package/dist/vendor/electron/shared/note-properties.js +982 -0
  58. package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
  59. package/dist/vendor/electron/shared/note-templates.js +84 -0
  60. package/dist/vendor/electron/shared/powersync-read-models.d.ts +1710 -0
  61. package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
  62. package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
  63. package/dist/vendor/electron/shared/saved-views.js +981 -0
  64. package/dist/vendor/electron/sql-utils.d.ts +5 -0
  65. package/dist/vendor/electron/sql-utils.js +32 -0
  66. package/package.json +6 -3
  67. package/dist/commands/savedViews/semantics.d.ts +0 -46
  68. package/dist/commands/savedViews/semantics.js +0 -738
@@ -0,0 +1,982 @@
1
+ // Generated by scripts/sync-electron-domain.ts from dinox-electron/packages/shared/src/note-properties.ts. Do not edit manually.
2
+ /**
3
+ * Note Properties domain types + pure helpers.
4
+ *
5
+ * Property values live on `c_note.properties` as a JSON map (key -> value), while
6
+ * `c_property_def` provides the account-owned semantic catalog (key/name/type/
7
+ * options). Unknown keys remain readable and degrade to untyped text.
8
+ *
9
+ * This module is pure (no DB, no React) so it can be shared by the main-process
10
+ * command handlers and the renderer UI.
11
+ */
12
+ export const NOTE_PROPERTY_LIMITS = {
13
+ keyLength: 64,
14
+ textLength: 2000,
15
+ arrayLength: 50,
16
+ mutationKeys: 100,
17
+ };
18
+ // ── Property types ──────────────────────────────────────────
19
+ /** Property types with a stable value contract and public editing support. */
20
+ export const ACTIVE_PROPERTY_TYPES = [
21
+ 'text',
22
+ 'number',
23
+ 'date',
24
+ 'select',
25
+ 'multi_select',
26
+ 'status',
27
+ 'url',
28
+ 'email',
29
+ 'phone',
30
+ 'checkbox',
31
+ 'files',
32
+ 'relation',
33
+ 'unique_id',
34
+ 'place',
35
+ ];
36
+ /** Types whose storage format is locked but not yet editable. None remain. */
37
+ export const RESERVED_PROPERTY_TYPES = [];
38
+ export const PROPERTY_OPTION_COLORS = [
39
+ 'gray',
40
+ 'red',
41
+ 'orange',
42
+ 'yellow',
43
+ 'green',
44
+ 'blue',
45
+ 'purple',
46
+ 'pink',
47
+ ];
48
+ export const STATUS_PROPERTY_GROUPS = ['todo', 'in_progress', 'complete'];
49
+ export const DEFAULT_STATUS_OPTION_INPUTS = [
50
+ { label: '未开始', color: 'gray', group: 'todo' },
51
+ { label: '进行中', color: 'blue', group: 'in_progress' },
52
+ { label: '已完成', color: 'green', group: 'complete' },
53
+ ];
54
+ export const PROPERTY_OPTION_LIMITS = {
55
+ items: 200,
56
+ labelLength: 120,
57
+ };
58
+ export const PROPERTY_OPTION_LABEL_MAX_LENGTH = PROPERTY_OPTION_LIMITS.labelLength;
59
+ export const PROPERTY_NUMBER_PRECISION_MAX = 12;
60
+ export const PROPERTY_DEF_CONFIG_KEY_PREFIX = 'property_def.config.v1.';
61
+ export function buildPropertyDefConfigKey(definitionId) {
62
+ const id = definitionId.trim();
63
+ if (!id)
64
+ throw new Error('INVALID_PROPERTY_DEF_ID');
65
+ return `${PROPERTY_DEF_CONFIG_KEY_PREFIX}${id}`;
66
+ }
67
+ export function isPropertyDefSource(value) {
68
+ return value === 'manual' || value === 'ai';
69
+ }
70
+ // ── Validation / sanitization ───────────────────────────────
71
+ const MAX_PROPERTY_KEY_LENGTH = NOTE_PROPERTY_LIMITS.keyLength;
72
+ const MAX_PROPERTY_TEXT_LENGTH = NOTE_PROPERTY_LIMITS.textLength;
73
+ const MAX_PROPERTY_ARRAY_LENGTH = NOTE_PROPERTY_LIMITS.arrayLength;
74
+ /**
75
+ * Property key validity: non-empty, <=64 chars, no newlines/tabs. A key is one
76
+ * flat name. Field refs keep a single `prop.` prefix and take the remainder as
77
+ * that name, so dots inside the name stay part of the name. `__proto__` is
78
+ * reserved because common object validators cannot round-trip it safely.
79
+ */
80
+ export function isPropertyKeyValid(key) {
81
+ if (typeof key !== 'string')
82
+ return false;
83
+ const trimmed = key.trim();
84
+ if (!trimmed || trimmed !== key)
85
+ return false;
86
+ if (trimmed.length > MAX_PROPERTY_KEY_LENGTH)
87
+ return false;
88
+ if (trimmed === '__proto__')
89
+ return false;
90
+ if (/[\n\r\t]/.test(trimmed))
91
+ return false;
92
+ return true;
93
+ }
94
+ const FILE_PROPERTY_RESOURCE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
95
+ const UNIQUE_ID_PREFIX_MAX_LENGTH = 12;
96
+ export function isFilePropertyPathValid(path) {
97
+ return (typeof path === 'string' &&
98
+ path.length > 0 &&
99
+ path.length <= 1024 &&
100
+ !/[\\\x00-\x1f:]/.test(path) &&
101
+ path.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..'));
102
+ }
103
+ export function isFilePropertyRef(value) {
104
+ if (!isRecord(value))
105
+ return false;
106
+ return (typeof value.resourceId === 'string' &&
107
+ FILE_PROPERTY_RESOURCE_ID.test(value.resourceId) &&
108
+ (value.path === undefined || isFilePropertyPathValid(value.path)));
109
+ }
110
+ /** Reject a malformed file list instead of dropping attachments. An empty list is `[]`. */
111
+ export function normalizeFilePropertyValue(value) {
112
+ if (!Array.isArray(value) ||
113
+ value.length > MAX_PROPERTY_ARRAY_LENGTH ||
114
+ !value.every(isFilePropertyRef)) {
115
+ return null;
116
+ }
117
+ const refs = [];
118
+ const identities = new Set();
119
+ const paths = new Set();
120
+ for (const item of value) {
121
+ const resourceId = item.resourceId.toLowerCase();
122
+ const identity = JSON.stringify([resourceId, item.path ?? null]);
123
+ if (identities.has(identity))
124
+ continue;
125
+ if (item.path && paths.has(item.path))
126
+ return null;
127
+ identities.add(identity);
128
+ if (item.path)
129
+ paths.add(item.path);
130
+ refs.push({ resourceId, ...(item.path ? { path: item.path } : {}) });
131
+ }
132
+ return refs;
133
+ }
134
+ export function normalizePlacePropertyValue(value) {
135
+ if (!isRecord(value) || typeof value.resourceId === 'string')
136
+ return null;
137
+ const lat = typeof value.lat === 'number' ? value.lat : Number.NaN;
138
+ const lon = typeof value.lon === 'number' ? value.lon : Number.NaN;
139
+ if (!Number.isFinite(lat) || !Number.isFinite(lon))
140
+ return null;
141
+ if (lat < -90 || lat > 90 || lon < -180 || lon > 180)
142
+ return null;
143
+ const name = typeof value.name === 'string' ? value.name.trim().slice(0, 200) : '';
144
+ return name ? { lat, lon, name } : { lat, lon };
145
+ }
146
+ export function isPlacePropertyValue(value) {
147
+ return normalizePlacePropertyValue(value) !== null;
148
+ }
149
+ export function formatPlaceProperty(value) {
150
+ const name = value.name?.trim();
151
+ if (name)
152
+ return name;
153
+ return `${value.lat.toFixed(5)}, ${value.lon.toFixed(5)}`;
154
+ }
155
+ export function normalizeUniqueIdPrefix(raw, fallbackName) {
156
+ const cleaned = (raw ?? '')
157
+ .toUpperCase()
158
+ .replace(/[^A-Z0-9]/g, '')
159
+ .slice(0, UNIQUE_ID_PREFIX_MAX_LENGTH);
160
+ if (cleaned)
161
+ return cleaned;
162
+ const fromName = (fallbackName ?? '')
163
+ .toUpperCase()
164
+ .replace(/[^A-Z0-9]/g, '')
165
+ .slice(0, 8);
166
+ return fromName || 'ID';
167
+ }
168
+ export function isIssuedUniqueId(value) {
169
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && Number.isSafeInteger(value);
170
+ }
171
+ export function parseUniqueIdPropertyConfig(raw) {
172
+ if (!raw?.trim())
173
+ return undefined;
174
+ try {
175
+ const config = JSON.parse(raw);
176
+ if (!isRecord(config) || config.version !== 1 || !isRecord(config.uniqueId))
177
+ return undefined;
178
+ const prefix = normalizeUniqueIdPrefix(typeof config.uniqueId.prefix === 'string' ? config.uniqueId.prefix : undefined);
179
+ const nextRaw = Number(config.uniqueId.next);
180
+ const next = Number.isInteger(nextRaw) && nextRaw >= 1 ? nextRaw : 1;
181
+ return { prefix, next };
182
+ }
183
+ catch {
184
+ return undefined;
185
+ }
186
+ }
187
+ export function serializeUniqueIdPropertyConfig(config) {
188
+ return JSON.stringify({
189
+ version: 1,
190
+ uniqueId: {
191
+ prefix: normalizeUniqueIdPrefix(config.prefix),
192
+ next: Number.isInteger(config.next) && config.next >= 1 ? config.next : 1,
193
+ },
194
+ });
195
+ }
196
+ export function formatUniqueIdProperty(value, config) {
197
+ if (!isIssuedUniqueId(value))
198
+ return '';
199
+ const prefix = config?.prefix?.trim();
200
+ return prefix ? `${prefix}-${value}` : String(value);
201
+ }
202
+ function isStatusPropertyGroup(value) {
203
+ return (typeof value === 'string' &&
204
+ STATUS_PROPERTY_GROUPS.includes(value));
205
+ }
206
+ function sameFilePropertyRefs(left, right) {
207
+ return (left.length === right.length &&
208
+ left.every((item, index) => item.resourceId === right[index]?.resourceId && item.path === right[index]?.path));
209
+ }
210
+ function samePlaceProperty(left, right) {
211
+ return left.lat === right.lat && left.lon === right.lon && left.name === right.name;
212
+ }
213
+ function sanitizePropertyValue(value) {
214
+ if (typeof value === 'string') {
215
+ return value.length > MAX_PROPERTY_TEXT_LENGTH
216
+ ? value.slice(0, MAX_PROPERTY_TEXT_LENGTH)
217
+ : value;
218
+ }
219
+ if (typeof value === 'number')
220
+ return Number.isFinite(value) ? value : null;
221
+ if (typeof value === 'boolean')
222
+ return value;
223
+ const place = normalizePlacePropertyValue(value);
224
+ if (place)
225
+ return place;
226
+ if (Array.isArray(value)) {
227
+ if (value.length === 0)
228
+ return null;
229
+ if (value.some((item) => item !== null && typeof item === 'object')) {
230
+ const files = normalizeFilePropertyValue(value);
231
+ return files && files.length > 0 ? files : null;
232
+ }
233
+ const items = value
234
+ .filter((item) => typeof item === 'string')
235
+ .map((item) => item.length > MAX_PROPERTY_TEXT_LENGTH ? item.slice(0, MAX_PROPERTY_TEXT_LENGTH) : item)
236
+ .filter((item) => item.trim().length > 0)
237
+ .slice(0, MAX_PROPERTY_ARRAY_LENGTH);
238
+ return items.length > 0 ? items : null;
239
+ }
240
+ return null;
241
+ }
242
+ // ── Parse / serialize the note properties JSON ──────────────
243
+ function copyOwnProperties(source) {
244
+ return Object.fromEntries(Object.entries(source));
245
+ }
246
+ function setOwnProperty(target, key, value) {
247
+ Object.defineProperty(target, key, {
248
+ configurable: true,
249
+ enumerable: true,
250
+ value,
251
+ writable: true,
252
+ });
253
+ }
254
+ /**
255
+ * Strictly parse the raw JSON object. `null` means the stored value is present
256
+ * but malformed or uses an unsupported top-level shape.
257
+ */
258
+ export function tryParseRawNoteProperties(raw) {
259
+ if (!raw)
260
+ return {};
261
+ let parsed;
262
+ try {
263
+ parsed = JSON.parse(raw);
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
269
+ return null;
270
+ return copyOwnProperties(parsed);
271
+ }
272
+ /** Forgiving read projection used by UI/read paths; malformed storage appears empty. */
273
+ export function parseRawNoteProperties(raw) {
274
+ return tryParseRawNoteProperties(raw) ?? {};
275
+ }
276
+ /** Parse the supported three-state view; unknown values are omitted from the UI. */
277
+ export function parseStoredNoteProperties(raw) {
278
+ const result = {};
279
+ for (const [key, value] of Object.entries(parseRawNoteProperties(raw))) {
280
+ if (!isPropertyKeyValid(key))
281
+ continue;
282
+ if (value === null) {
283
+ setOwnProperty(result, key, null);
284
+ continue;
285
+ }
286
+ const sanitized = sanitizePropertyValue(value);
287
+ if (sanitized !== null) {
288
+ setOwnProperty(result, key, typeof sanitized === 'string' && !sanitized.trim() ? null : sanitized);
289
+ continue;
290
+ }
291
+ if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
292
+ setOwnProperty(result, key, null);
293
+ }
294
+ }
295
+ return result;
296
+ }
297
+ /** Read only committed values; declared-but-empty slots are intentionally omitted. */
298
+ export function parseNoteProperties(raw) {
299
+ const stored = parseStoredNoteProperties(raw);
300
+ return Object.fromEntries(Object.entries(stored).filter((entry) => entry[1] !== null));
301
+ }
302
+ /** Serialize persisted values + empty slots; an empty object normalizes to NULL. */
303
+ export function serializeStoredNoteProperties(properties) {
304
+ return serializeRawNoteProperties(properties);
305
+ }
306
+ /** Serialize a lossless raw property object; an empty object normalizes to NULL. */
307
+ export function serializeRawNoteProperties(properties) {
308
+ const keys = Object.keys(properties);
309
+ if (keys.length === 0)
310
+ return null;
311
+ return JSON.stringify(properties);
312
+ }
313
+ /** Serialize the non-empty value view. */
314
+ export function serializeNoteProperties(properties) {
315
+ return serializeStoredNoteProperties(properties);
316
+ }
317
+ function propertyValueEquals(a, b) {
318
+ if (isPlacePropertyValue(b)) {
319
+ const place = normalizePlacePropertyValue(a);
320
+ return place !== null && samePlaceProperty(place, b);
321
+ }
322
+ if (Array.isArray(b) && b.length > 0 && b.every(isFilePropertyRef)) {
323
+ const files = normalizeFilePropertyValue(a);
324
+ return files !== null && sameFilePropertyRefs(files, b);
325
+ }
326
+ if (Array.isArray(a) && Array.isArray(b)) {
327
+ return a.length === b.length && a.every((item, i) => item === b[i]);
328
+ }
329
+ return a === b;
330
+ }
331
+ /**
332
+ * Legacy value merge: null removes a key, non-null values overwrite, and all
333
+ * unrelated JSON entries are preserved. Returns the list of changed keys.
334
+ */
335
+ export function mergeNotePropertiesPatch(current, patch) {
336
+ const next = copyOwnProperties(current);
337
+ const changedKeys = [];
338
+ for (const [key, value] of Object.entries(patch)) {
339
+ if (!isPropertyKeyValid(key))
340
+ continue;
341
+ if (value === null) {
342
+ if (Object.hasOwn(next, key)) {
343
+ delete next[key];
344
+ changedKeys.push(key);
345
+ }
346
+ continue;
347
+ }
348
+ const sanitized = sanitizePropertyValue(value);
349
+ if (sanitized === null)
350
+ continue;
351
+ const existing = next[key];
352
+ if (Object.hasOwn(next, key) && propertyValueEquals(existing, sanitized)) {
353
+ continue;
354
+ }
355
+ setOwnProperty(next, key, sanitized);
356
+ changedKeys.push(key);
357
+ }
358
+ return { next, changedKeys };
359
+ }
360
+ /**
361
+ * Fill only fields that are still explicitly declared as empty. Missing fields
362
+ * and fields with any current value are skipped, allowing AI/review flows to
363
+ * avoid overwriting a concurrent user edit in the same transaction.
364
+ */
365
+ export function fillEmptyNoteProperties(current, values) {
366
+ const next = copyOwnProperties(current);
367
+ const changedKeys = [];
368
+ const skippedKeys = [];
369
+ for (const [key, value] of Object.entries(values)) {
370
+ if (!isPropertyKeyValid(key))
371
+ continue;
372
+ const sanitized = sanitizePropertyValue(value);
373
+ if (sanitized === null)
374
+ continue;
375
+ if (!Object.hasOwn(next, key) || next[key] !== null) {
376
+ skippedKeys.push(key);
377
+ continue;
378
+ }
379
+ setOwnProperty(next, key, sanitized);
380
+ changedKeys.push(key);
381
+ }
382
+ return { next, changedKeys, skippedKeys };
383
+ }
384
+ /**
385
+ * Apply an explicit field-slot operation: declare adds only missing slots, clear
386
+ * makes every target explicitly empty, and remove deletes the slot entirely.
387
+ */
388
+ export function applyNotePropertySlotOperation(current, operation, keys) {
389
+ const next = copyOwnProperties(current);
390
+ const changedKeys = [];
391
+ const seen = new Set();
392
+ for (const key of keys) {
393
+ if (!isPropertyKeyValid(key) || seen.has(key))
394
+ continue;
395
+ seen.add(key);
396
+ if (operation === 'remove') {
397
+ if (Object.hasOwn(next, key)) {
398
+ delete next[key];
399
+ changedKeys.push(key);
400
+ }
401
+ continue;
402
+ }
403
+ if (operation === 'declare') {
404
+ if (!Object.hasOwn(next, key)) {
405
+ setOwnProperty(next, key, null);
406
+ changedKeys.push(key);
407
+ }
408
+ continue;
409
+ }
410
+ if (!Object.hasOwn(next, key) || next[key] !== null) {
411
+ setOwnProperty(next, key, null);
412
+ changedKeys.push(key);
413
+ }
414
+ }
415
+ return { next, changedKeys };
416
+ }
417
+ const TRUE_STRINGS = new Set(['true', '1', 'yes', '是']);
418
+ const FALSE_STRINGS = new Set(['false', '0', 'no', '否']);
419
+ const ISO_DATE_PREFIX_PATTERN = /^([+-]?\d{4,6})-(\d{2})-(\d{2})(?:$|[Tt\s])/;
420
+ function hasValidCalendarDatePrefix(value) {
421
+ const match = ISO_DATE_PREFIX_PATTERN.exec(value);
422
+ if (!match)
423
+ return true;
424
+ const year = Number(match[1]);
425
+ const month = Number(match[2]);
426
+ const day = Number(match[3]);
427
+ if (!Number.isInteger(year) || month < 1 || month > 12 || day < 1)
428
+ return false;
429
+ const daysInMonth = [
430
+ 31,
431
+ year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
432
+ 31,
433
+ 30,
434
+ 31,
435
+ 30,
436
+ 31,
437
+ 31,
438
+ 30,
439
+ 31,
440
+ 30,
441
+ 31,
442
+ ];
443
+ return day <= (daysInMonth[month - 1] ?? 0);
444
+ }
445
+ /** Normalize accepted date input and reject ISO-like strings with impossible calendar dates. */
446
+ export function normalizeDateValueToIso(value) {
447
+ if (typeof value === 'string') {
448
+ const trimmed = value.trim();
449
+ if (!trimmed || !hasValidCalendarDatePrefix(trimmed))
450
+ return null;
451
+ const date = new Date(trimmed);
452
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
453
+ }
454
+ if (typeof value === 'number' && Number.isFinite(value)) {
455
+ const date = new Date(value);
456
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
457
+ }
458
+ return null;
459
+ }
460
+ /** Normalize a configured date value without conflating a calendar date with an instant. */
461
+ export function normalizeDatePropertyValue(value, mode) {
462
+ if (mode === 'datetime')
463
+ return normalizeDateValueToIso(value);
464
+ if (typeof value === 'string') {
465
+ const trimmed = value.trim();
466
+ const plainDateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
467
+ if (plainDateMatch) {
468
+ return hasValidCalendarDatePrefix(trimmed) ? trimmed : null;
469
+ }
470
+ }
471
+ return normalizeDateValueToIso(value)?.slice(0, 10) ?? null;
472
+ }
473
+ function decimalPlaces(value) {
474
+ const [coefficient = '', exponentText = '0'] = Math.abs(value).toString().toLowerCase().split('e');
475
+ const fractionLength = coefficient.includes('.') ? coefficient.length - coefficient.indexOf('.') - 1 : 0;
476
+ const exponent = Number(exponentText);
477
+ return Math.max(0, fractionLength - exponent);
478
+ }
479
+ /** Validate a number against its semantic constraints without rounding the stored value. */
480
+ export function normalizeNumberPropertyValue(value, config) {
481
+ const normalized = typeof value === 'number'
482
+ ? value
483
+ : typeof value === 'string' && value.trim()
484
+ ? Number(value.trim())
485
+ : Number.NaN;
486
+ if (!Number.isFinite(normalized))
487
+ return null;
488
+ if (config.min !== undefined && normalized < config.min)
489
+ return null;
490
+ if (config.max !== undefined && normalized > config.max)
491
+ return null;
492
+ if (config.precision !== undefined && decimalPlaces(normalized) > config.precision)
493
+ return null;
494
+ if (config.step !== undefined) {
495
+ const base = config.min ?? 0;
496
+ const quotient = (normalized - base) / config.step;
497
+ const nearest = Math.round(quotient);
498
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(quotient)) * 8;
499
+ if (Math.abs(quotient - nearest) > tolerance)
500
+ return null;
501
+ }
502
+ return normalized;
503
+ }
504
+ /** Validate and canonicalize an absolute URL under the definition's allowed schemes. */
505
+ export function normalizeUrlPropertyValue(value, config) {
506
+ if (typeof value !== 'string')
507
+ return null;
508
+ const trimmed = value.trim();
509
+ if (!trimmed)
510
+ return null;
511
+ let parsed;
512
+ try {
513
+ parsed = new URL(trimmed);
514
+ }
515
+ catch {
516
+ return null;
517
+ }
518
+ if (parsed.username || parsed.password)
519
+ return null;
520
+ const scheme = parsed.protocol.slice(0, -1);
521
+ const allowedSchemes = config?.schemes ?? ['http', 'https'];
522
+ if ((scheme !== 'http' && scheme !== 'https') || !allowedSchemes.includes(scheme)) {
523
+ return null;
524
+ }
525
+ return parsed.href;
526
+ }
527
+ /**
528
+ * Coerce a candidate value into the legal shape for a target type; returns null
529
+ * when it cannot be coerced. Only does shape coercion — the select-option
530
+ * whitelist belongs to the validation/policy layer.
531
+ */
532
+ export function coercePropertyValue(type, value) {
533
+ switch (type) {
534
+ case 'text':
535
+ case 'url':
536
+ case 'email':
537
+ case 'phone': {
538
+ if (typeof value === 'string') {
539
+ const trimmed = value.trim();
540
+ return trimmed ? sanitizePropertyValue(trimmed) : null;
541
+ }
542
+ if (typeof value === 'number' && Number.isFinite(value))
543
+ return String(value);
544
+ if (typeof value === 'boolean')
545
+ return String(value);
546
+ return null;
547
+ }
548
+ case 'number': {
549
+ if (typeof value === 'number' && Number.isFinite(value))
550
+ return value;
551
+ if (typeof value === 'string') {
552
+ const parsed = Number(value.trim());
553
+ return Number.isFinite(parsed) && value.trim() !== '' ? parsed : null;
554
+ }
555
+ return null;
556
+ }
557
+ case 'date': {
558
+ return normalizeDateValueToIso(value);
559
+ }
560
+ case 'select':
561
+ case 'status': {
562
+ if (typeof value === 'string') {
563
+ const trimmed = value.trim();
564
+ return trimmed ? sanitizePropertyValue(trimmed) : null;
565
+ }
566
+ return null;
567
+ }
568
+ case 'checkbox': {
569
+ if (typeof value === 'boolean')
570
+ return value;
571
+ if (typeof value === 'number')
572
+ return value !== 0;
573
+ if (typeof value === 'string') {
574
+ const lowered = value.trim().toLowerCase();
575
+ if (TRUE_STRINGS.has(lowered))
576
+ return true;
577
+ if (FALSE_STRINGS.has(lowered))
578
+ return false;
579
+ }
580
+ return null;
581
+ }
582
+ case 'multi_select':
583
+ case 'relation': {
584
+ const array = Array.isArray(value)
585
+ ? value
586
+ : typeof value === 'string' && value.trim()
587
+ ? [value]
588
+ : null;
589
+ if (!array || array.some((item) => item !== null && typeof item === 'object'))
590
+ return null;
591
+ return sanitizePropertyValue(array);
592
+ }
593
+ case 'files': {
594
+ const files = normalizeFilePropertyValue(value);
595
+ return files && files.length > 0 ? files : null;
596
+ }
597
+ case 'place':
598
+ return normalizePlacePropertyValue(value);
599
+ case 'unique_id': {
600
+ if (isIssuedUniqueId(value))
601
+ return value;
602
+ if (typeof value === 'string') {
603
+ const match = /^([A-Z0-9]{1,12}-)?(\d+)$/i.exec(value.trim());
604
+ if (!match)
605
+ return null;
606
+ const parsed = Number(match[2]);
607
+ return isIssuedUniqueId(parsed) ? parsed : null;
608
+ }
609
+ return null;
610
+ }
611
+ default:
612
+ return null;
613
+ }
614
+ }
615
+ /** UI display value -> chip text (arrays joined with "、"). */
616
+ export function formatPropertyValue(value) {
617
+ if (isPlacePropertyValue(value))
618
+ return formatPlaceProperty(value);
619
+ if (Array.isArray(value)) {
620
+ if (value.length > 0 && value.every(isFilePropertyRef)) {
621
+ return value.map((item) => item.path || item.resourceId).join('、');
622
+ }
623
+ return value.join('、');
624
+ }
625
+ if (typeof value === 'boolean')
626
+ return value ? '✓' : '✗';
627
+ return String(value);
628
+ }
629
+ /** Infer a type from the value shape when the catalog is missing (orphan keys). */
630
+ export function inferPropertyTypeFromValue(value) {
631
+ if (isPlacePropertyValue(value))
632
+ return 'place';
633
+ if (Array.isArray(value)) {
634
+ return value.length > 0 && value.every(isFilePropertyRef) ? 'files' : 'multi_select';
635
+ }
636
+ if (typeof value === 'number')
637
+ return 'number';
638
+ if (typeof value === 'boolean')
639
+ return 'checkbox';
640
+ return 'text';
641
+ }
642
+ const KNOWN_PROPERTY_TYPES = new Set([
643
+ ...ACTIVE_PROPERTY_TYPES,
644
+ ...RESERVED_PROPERTY_TYPES,
645
+ ]);
646
+ export function isPropertyType(value) {
647
+ return typeof value === 'string' && KNOWN_PROPERTY_TYPES.has(value);
648
+ }
649
+ function isRecord(value) {
650
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
651
+ }
652
+ export function isPropertyOptionColor(value) {
653
+ return (typeof value === 'string' &&
654
+ PROPERTY_OPTION_COLORS.includes(value));
655
+ }
656
+ /** Normalize the user-visible option identity used for display and uniqueness. */
657
+ export function normalizePropertyOptionLabel(value) {
658
+ if (typeof value !== 'string')
659
+ return null;
660
+ const normalized = value.normalize('NFKC').trim();
661
+ if (!normalized || normalized.length > PROPERTY_OPTION_LABEL_MAX_LENGTH)
662
+ return null;
663
+ return normalized;
664
+ }
665
+ /** Case-insensitive uniqueness key. Callers should reject an empty result. */
666
+ export function propertyOptionLabelKey(value) {
667
+ return normalizePropertyOptionLabel(value)?.toLowerCase() ?? '';
668
+ }
669
+ export function findPropertyOptionById(options, id) {
670
+ return options.find((option) => option.id === id);
671
+ }
672
+ export function findPropertyOptionByLabel(options, label) {
673
+ const key = propertyOptionLabelKey(label);
674
+ return key ? options.find((option) => propertyOptionLabelKey(option.label) === key) : undefined;
675
+ }
676
+ function normalizePropertyOption(value) {
677
+ if (!isRecord(value))
678
+ return null;
679
+ if (typeof value.id !== 'string' || !value.id.trim() || value.id.trim() !== value.id)
680
+ return null;
681
+ const label = normalizePropertyOptionLabel(value.label);
682
+ if (!label || !isPropertyOptionColor(value.color))
683
+ return null;
684
+ if (!Number.isSafeInteger(value.sortRank) || typeof value.archived !== 'boolean')
685
+ return null;
686
+ if (value.group !== undefined && !isStatusPropertyGroup(value.group))
687
+ return null;
688
+ return {
689
+ id: value.id,
690
+ label,
691
+ color: value.color,
692
+ sortRank: value.sortRank,
693
+ archived: value.archived,
694
+ ...(isStatusPropertyGroup(value.group) ? { group: value.group } : {}),
695
+ };
696
+ }
697
+ /** Validate, deduplicate, and put options in their canonical `(sortRank,id)` order. */
698
+ export function normalizePropertyOptions(values) {
699
+ if (values.length > PROPERTY_OPTION_LIMITS.items)
700
+ return undefined;
701
+ const options = [];
702
+ const ids = new Set();
703
+ const labelKeys = new Set();
704
+ for (const value of values) {
705
+ const option = normalizePropertyOption(value);
706
+ if (!option)
707
+ return undefined;
708
+ const labelKey = propertyOptionLabelKey(option.label);
709
+ if (ids.has(option.id) || labelKeys.has(labelKey))
710
+ return undefined;
711
+ ids.add(option.id);
712
+ labelKeys.add(labelKey);
713
+ options.push(option);
714
+ }
715
+ return options.sort((left, right) => left.sortRank - right.sortRank || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
716
+ }
717
+ /** Parse both the stable v1 envelope and legacy label arrays without conflating them. */
718
+ export function parsePropertyDefOptionsEnvelope(raw) {
719
+ if (!raw || !raw.trim())
720
+ return { status: 'empty' };
721
+ let parsed;
722
+ try {
723
+ parsed = JSON.parse(raw);
724
+ }
725
+ catch {
726
+ return { status: 'invalid' };
727
+ }
728
+ if (Array.isArray(parsed)) {
729
+ const labels = [];
730
+ const seen = new Set();
731
+ for (const item of parsed) {
732
+ const label = normalizePropertyOptionLabel(item);
733
+ if (!label)
734
+ return { status: 'invalid' };
735
+ const labelKey = propertyOptionLabelKey(label);
736
+ if (seen.has(labelKey))
737
+ continue;
738
+ if (labels.length >= PROPERTY_OPTION_LIMITS.items)
739
+ return { status: 'invalid' };
740
+ seen.add(labelKey);
741
+ labels.push(label);
742
+ }
743
+ return { status: 'legacy', labels };
744
+ }
745
+ if (!isRecord(parsed))
746
+ return { status: 'invalid' };
747
+ if (parsed.version !== 1) {
748
+ return Object.hasOwn(parsed, 'version')
749
+ ? { status: 'unsupported', version: parsed.version }
750
+ : { status: 'invalid' };
751
+ }
752
+ if (!Array.isArray(parsed.items))
753
+ return { status: 'invalid' };
754
+ const options = normalizePropertyOptions(parsed.items);
755
+ return options ? { status: 'valid', options } : { status: 'invalid' };
756
+ }
757
+ /** Read only migrated stable options. Legacy labels remain migration-only data. */
758
+ export function parsePropertyDefOptions(raw) {
759
+ const parsed = parsePropertyDefOptionsEnvelope(raw);
760
+ return parsed.status === 'valid' ? parsed.options : undefined;
761
+ }
762
+ /** Read legacy label options for the explicit migration command only. */
763
+ export function parseLegacyPropertyDefOptions(raw) {
764
+ const parsed = parsePropertyDefOptionsEnvelope(raw);
765
+ return parsed.status === 'legacy' ? parsed.labels : undefined;
766
+ }
767
+ export function serializePropertyDefOptions(options) {
768
+ const normalized = normalizePropertyOptions(options);
769
+ if (!normalized)
770
+ throw new Error('INVALID_PROPERTY_OPTIONS');
771
+ const envelope = { version: 1, items: normalized };
772
+ return JSON.stringify(envelope);
773
+ }
774
+ function optionalFiniteNumber(value, key) {
775
+ if (!Object.hasOwn(value, key))
776
+ return undefined;
777
+ const candidate = value[key];
778
+ return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : null;
779
+ }
780
+ function optionalConfigText(value, key) {
781
+ if (!Object.hasOwn(value, key))
782
+ return undefined;
783
+ const candidate = value[key];
784
+ if (typeof candidate !== 'string')
785
+ return null;
786
+ const normalized = candidate.normalize('NFKC').trim();
787
+ return normalized && normalized.length <= 120 ? normalized : null;
788
+ }
789
+ const FALLBACK_ISO_4217_CODES = new Set('AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BRL BSD BTN BWP BYN BZD CAD CDF CHF CLP CNY COP CRC CUC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HRK HTG HUF IDR ILS INR IQD IRR ISK JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA MKD MMK MNT MOP MRU MUR MVR MWK MXN MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SLL SOS SRD SSP STN SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VES VND VUV WST XAF XCD XCG XDR XOF XPF XSU YER ZAR ZMW ZWG ZWL'.split(' '));
790
+ const ISO_4217_CODES = (() => {
791
+ const supportedValuesOf = Intl.supportedValuesOf;
792
+ if (typeof supportedValuesOf === 'function') {
793
+ try {
794
+ return new Set([...FALLBACK_ISO_4217_CODES, ...supportedValuesOf.call(Intl, 'currency')]);
795
+ }
796
+ catch {
797
+ // Older runtimes fall back to the embedded ISO list.
798
+ }
799
+ }
800
+ return FALLBACK_ISO_4217_CODES;
801
+ })();
802
+ /** Validate and canonicalize a structured property-definition config object. */
803
+ export function normalizePropertyDefConfig(value) {
804
+ if (!isRecord(value) || value.version !== 1)
805
+ return undefined;
806
+ switch (value.type) {
807
+ case 'date':
808
+ return value.mode === 'date' || value.mode === 'datetime'
809
+ ? { version: 1, type: 'date', mode: value.mode }
810
+ : undefined;
811
+ case 'number': {
812
+ if (value.format !== 'number' && value.format !== 'percent' && value.format !== 'currency') {
813
+ return undefined;
814
+ }
815
+ const precision = optionalFiniteNumber(value, 'precision');
816
+ const min = optionalFiniteNumber(value, 'min');
817
+ const max = optionalFiniteNumber(value, 'max');
818
+ const step = optionalFiniteNumber(value, 'step');
819
+ const currency = optionalConfigText(value, 'currency');
820
+ const unit = optionalConfigText(value, 'unit');
821
+ const normalizedCurrency = currency?.toUpperCase();
822
+ if (precision === null ||
823
+ min === null ||
824
+ max === null ||
825
+ step === null ||
826
+ currency === null ||
827
+ unit === null ||
828
+ (precision !== undefined &&
829
+ (!Number.isSafeInteger(precision) ||
830
+ precision < 0 ||
831
+ precision > PROPERTY_NUMBER_PRECISION_MAX)) ||
832
+ (step !== undefined && step <= 0) ||
833
+ (min !== undefined && max !== undefined && min > max) ||
834
+ (normalizedCurrency !== undefined && !ISO_4217_CODES.has(normalizedCurrency)) ||
835
+ (value.format === 'currency' && currency === undefined)) {
836
+ return undefined;
837
+ }
838
+ return {
839
+ version: 1,
840
+ type: 'number',
841
+ format: value.format,
842
+ ...(precision === undefined ? {} : { precision }),
843
+ ...(normalizedCurrency === undefined ? {} : { currency: normalizedCurrency }),
844
+ ...(unit === undefined ? {} : { unit }),
845
+ ...(min === undefined ? {} : { min }),
846
+ ...(max === undefined ? {} : { max }),
847
+ ...(step === undefined ? {} : { step }),
848
+ };
849
+ }
850
+ case 'select':
851
+ return value.mode === 'open' || value.mode === 'closed'
852
+ ? { version: 1, type: 'select', mode: value.mode }
853
+ : undefined;
854
+ case 'multi_select': {
855
+ if (value.mode !== 'open' && value.mode !== 'closed')
856
+ return undefined;
857
+ const maxSelections = optionalFiniteNumber(value, 'maxSelections');
858
+ if (maxSelections === null ||
859
+ (maxSelections !== undefined &&
860
+ (!Number.isSafeInteger(maxSelections) ||
861
+ maxSelections < 1 ||
862
+ maxSelections > NOTE_PROPERTY_LIMITS.arrayLength))) {
863
+ return undefined;
864
+ }
865
+ return {
866
+ version: 1,
867
+ type: 'multi_select',
868
+ mode: value.mode,
869
+ ...(maxSelections === undefined ? {} : { maxSelections }),
870
+ };
871
+ }
872
+ case 'url': {
873
+ if (!Object.hasOwn(value, 'schemes'))
874
+ return { version: 1, type: 'url' };
875
+ if (!Array.isArray(value.schemes) || value.schemes.length === 0)
876
+ return undefined;
877
+ if (value.schemes.some((scheme) => scheme !== 'http' && scheme !== 'https')) {
878
+ return undefined;
879
+ }
880
+ const selected = new Set(value.schemes);
881
+ const schemes = ['http', 'https'].filter((scheme) => selected.has(scheme));
882
+ return { version: 1, type: 'url', schemes };
883
+ }
884
+ default:
885
+ return undefined;
886
+ }
887
+ }
888
+ /** Parse a companion config without hiding malformed or future-version rows. */
889
+ export function parsePropertyDefConfigEnvelope(raw) {
890
+ if (!raw || !raw.trim())
891
+ return { status: 'empty' };
892
+ let parsed;
893
+ try {
894
+ parsed = JSON.parse(raw);
895
+ }
896
+ catch {
897
+ return { status: 'invalid' };
898
+ }
899
+ if (isRecord(parsed) && parsed.version !== 1 && Object.hasOwn(parsed, 'version')) {
900
+ return { status: 'unsupported', version: parsed.version };
901
+ }
902
+ const config = normalizePropertyDefConfig(parsed);
903
+ return config ? { status: 'valid', config } : { status: 'invalid' };
904
+ }
905
+ /** Parse only an explicit valid companion config; other states return undefined. */
906
+ export function parsePropertyDefConfig(raw) {
907
+ const parsed = parsePropertyDefConfigEnvelope(raw);
908
+ return parsed.status === 'valid' ? parsed.config : undefined;
909
+ }
910
+ /** Option definitions stay open so unmatched input can create options inline. */
911
+ export function getDefaultPropertyDefConfig(type, _defaults = 'legacy') {
912
+ switch (type) {
913
+ case 'date':
914
+ return { version: 1, type: 'date', mode: 'date' };
915
+ case 'number':
916
+ return { version: 1, type: 'number', format: 'number' };
917
+ case 'select':
918
+ return { version: 1, type: 'select', mode: 'open' };
919
+ case 'multi_select':
920
+ return { version: 1, type: 'multi_select', mode: 'open' };
921
+ case 'url':
922
+ return { version: 1, type: 'url', schemes: ['http', 'https'] };
923
+ default:
924
+ return undefined;
925
+ }
926
+ }
927
+ export function resolvePropertyDefConfig(type, raw) {
928
+ const parsed = parsePropertyDefConfigEnvelope(raw);
929
+ if (parsed.status === 'empty')
930
+ return getDefaultPropertyDefConfig(type, 'legacy');
931
+ return parsed.status === 'valid' && parsed.config.type === type ? parsed.config : undefined;
932
+ }
933
+ export function serializePropertyDefConfig(config) {
934
+ const normalized = normalizePropertyDefConfig(config);
935
+ if (!normalized)
936
+ throw new Error('INVALID_PROPERTY_DEF_CONFIG');
937
+ return JSON.stringify(normalized);
938
+ }
939
+ /** Map a raw c_property_def row into the domain object (unknown type -> text). */
940
+ export function mapPropertyDef(record) {
941
+ const rawType = record.type ?? 'text';
942
+ const type = (KNOWN_PROPERTY_TYPES.has(rawType) ? rawType : 'text');
943
+ const parsedOptions = parsePropertyDefOptionsEnvelope(record.options);
944
+ const parsedConfig = parsePropertyDefConfigEnvelope(record.config_json);
945
+ const hasOptionSemantics = type === 'select' || type === 'multi_select' || type === 'status';
946
+ const parsedUniqueId = type === 'unique_id' ? parseUniqueIdPropertyConfig(record.options) : undefined;
947
+ const uniqueIdInvalid = type === 'unique_id' && Boolean(record.options?.trim()) && parsedUniqueId === undefined;
948
+ const explicitConfigMatches = parsedConfig.status === 'valid' && parsedConfig.config.type === type;
949
+ return {
950
+ id: record.id ?? '',
951
+ key: record.key ?? '',
952
+ name: record.name || record.key || '',
953
+ type,
954
+ options: hasOptionSemantics && parsedOptions.status === 'valid' ? parsedOptions.options : undefined,
955
+ ...(type === 'unique_id'
956
+ ? {
957
+ uniqueId: parsedUniqueId ?? {
958
+ prefix: normalizeUniqueIdPrefix(undefined, record.name || record.key),
959
+ next: 1,
960
+ },
961
+ }
962
+ : {}),
963
+ config: parsedConfig.status === 'empty'
964
+ ? getDefaultPropertyDefConfig(type, 'legacy')
965
+ : explicitConfigMatches
966
+ ? parsedConfig.config
967
+ : undefined,
968
+ configInvalid: parsedConfig.status === 'invalid' ||
969
+ parsedConfig.status === 'unsupported' ||
970
+ (parsedConfig.status === 'valid' && !explicitConfigMatches),
971
+ optionsMigrationPending: hasOptionSemantics &&
972
+ (parsedOptions.status === 'empty' || parsedOptions.status === 'legacy'),
973
+ optionsInvalid: uniqueIdInvalid ||
974
+ (hasOptionSemantics &&
975
+ (parsedOptions.status === 'invalid' || parsedOptions.status === 'unsupported')),
976
+ description: record.description || undefined,
977
+ icon: record.icon || undefined,
978
+ showOnCard: Number(record.show_on_card ?? 0) === 1,
979
+ source: isPropertyDefSource(record.source) ? record.source : 'manual',
980
+ sortRank: Number(record.sort_rank ?? 0),
981
+ };
982
+ }