@mulmoclaude/core 0.13.1 → 0.15.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 (59) hide show
  1. package/assets/helps/billing-clients-worklog.md +15 -12
  2. package/assets/helps/collection-skills.md +67 -22
  3. package/assets/helps/todo-collection.md +13 -7
  4. package/dist/{deriveAll-BQ_p5HSB.cjs → calendarGrid-CaR-q36Y.js} +352 -74
  5. package/dist/calendarGrid-CaR-q36Y.js.map +1 -0
  6. package/dist/calendarGrid-gN4sei7s.cjs +1070 -0
  7. package/dist/calendarGrid-gN4sei7s.cjs.map +1 -0
  8. package/dist/collection/core/backlinks.d.ts +20 -0
  9. package/dist/collection/core/ids.d.ts +12 -0
  10. package/dist/collection/core/recordZ.d.ts +23 -0
  11. package/dist/collection/core/schema.d.ts +72 -418
  12. package/dist/collection/core/schemaZ.d.ts +878 -0
  13. package/dist/collection/core/templatePath.d.ts +34 -0
  14. package/dist/collection/core/uiTypes.d.ts +22 -0
  15. package/dist/collection/core/where.d.ts +9 -15
  16. package/dist/collection/index.cjs +50 -330
  17. package/dist/collection/index.cjs.map +1 -1
  18. package/dist/collection/index.d.ts +2 -0
  19. package/dist/collection/index.js +14 -301
  20. package/dist/collection/index.js.map +1 -1
  21. package/dist/collection/paths.cjs +1 -1
  22. package/dist/collection/paths.cjs.map +1 -1
  23. package/dist/collection/paths.js +1 -1
  24. package/dist/collection/paths.js.map +1 -1
  25. package/dist/collection/registry/server/index.cjs +1 -1
  26. package/dist/collection/registry/server/index.js +1 -1
  27. package/dist/collection/server/discovery.d.ts +2 -244
  28. package/dist/collection/server/index.cjs +8 -2
  29. package/dist/collection/server/index.d.ts +1 -0
  30. package/dist/collection/server/index.js +3 -2
  31. package/dist/collection/server/io.d.ts +9 -0
  32. package/dist/collection/server/ontology.d.ts +35 -0
  33. package/dist/collection/server/templatePath.d.ts +1 -34
  34. package/dist/collection/server/validate.d.ts +11 -7
  35. package/dist/collection-watchers/index.cjs +5 -4
  36. package/dist/collection-watchers/index.cjs.map +1 -1
  37. package/dist/collection-watchers/index.js +3 -2
  38. package/dist/collection-watchers/index.js.map +1 -1
  39. package/dist/feeds/index.cjs +4 -4
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/ingestTypes.d.ts +15 -40
  42. package/dist/feeds/server/index.cjs +5 -5
  43. package/dist/feeds/server/index.js +3 -3
  44. package/dist/{ingestTypes-C7EheYZX.cjs → ingestTypes-DG8RExWt.cjs} +3 -3
  45. package/dist/ingestTypes-DG8RExWt.cjs.map +1 -0
  46. package/dist/{ingestTypes-CmJeOUJc.js → ingestTypes-V4upKeBx.js} +2 -2
  47. package/dist/ingestTypes-V4upKeBx.js.map +1 -0
  48. package/dist/{server-DKXXFTbw.js → server-BNTR7ySK.js} +531 -196
  49. package/dist/server-BNTR7ySK.js.map +1 -0
  50. package/dist/{server-G6GtOdaW.cjs → server-bsNrf2yW.cjs} +572 -213
  51. package/dist/server-bsNrf2yW.cjs.map +1 -0
  52. package/package.json +1 -1
  53. package/dist/deriveAll-BQ_p5HSB.cjs.map +0 -1
  54. package/dist/deriveAll-CMFXDQ_G.js +0 -506
  55. package/dist/deriveAll-CMFXDQ_G.js.map +0 -1
  56. package/dist/ingestTypes-C7EheYZX.cjs.map +0 -1
  57. package/dist/ingestTypes-CmJeOUJc.js.map +0 -1
  58. package/dist/server-DKXXFTbw.js.map +0 -1
  59. package/dist/server-G6GtOdaW.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
2
- const require_deriveAll = require("./deriveAll-BQ_p5HSB.cjs");
2
+ const require_calendarGrid = require("./calendarGrid-gN4sei7s.cjs");
3
3
  const require_collection_paths = require("./collection/paths.cjs");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
@@ -80,7 +80,6 @@ var log = {
80
80
  //#endregion
81
81
  //#region src/collection/server/paths.ts
82
82
  var SCHEMA_FILE = "schema.json";
83
- var SAFE_SLUG_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
84
83
  /** Sanitise a user-supplied slug into a safe directory-name leaf.
85
84
  * Returns null for anything that fails the slug whitelist OR isn't a
86
85
  * basename (i.e. survives `path.basename` round-trip unchanged).
@@ -88,12 +87,11 @@ var SAFE_SLUG_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
88
87
  * `js/path-injection` sanitiser. */
89
88
  function safeSlugName(slug) {
90
89
  if (typeof slug !== "string") return null;
91
- if (!SAFE_SLUG_PATTERN.test(slug)) return null;
90
+ if (!require_calendarGrid.SAFE_SLUG_PATTERN.test(slug)) return null;
92
91
  const basename = node_path.default.basename(slug);
93
92
  if (basename !== slug) return null;
94
93
  return basename;
95
94
  }
96
- var SAFE_RECORD_ID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
97
95
  /** Sanitise a user-supplied record id into a safe filename stem. Like
98
96
  * `safeSlugName` but tolerates interior dots (so natural keys work),
99
97
  * while still rejecting any `..` substring, path separators, and
@@ -101,7 +99,7 @@ var SAFE_RECORD_ID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
101
99
  * `js/path-injection` sanitiser CodeQL recognises on `safeSlugName`. */
102
100
  function safeRecordId(recordId) {
103
101
  if (typeof recordId !== "string") return null;
104
- if (!SAFE_RECORD_ID_PATTERN.test(recordId)) return null;
102
+ if (!require_calendarGrid.SAFE_RECORD_ID_PATTERN.test(recordId)) return null;
105
103
  if (recordId.includes("..")) return null;
106
104
  const basename = node_path.default.basename(recordId);
107
105
  if (basename !== recordId) return null;
@@ -241,7 +239,9 @@ async function writeFileAtomic(filePath, content) {
241
239
  * inside an otherwise-contained data dir — without this, a record
242
240
  * file could symlink to /etc/passwd and the detail endpoint would
243
241
  * happily serve it. Returns false on ENOENT and on any other lstat
244
- * failure so the caller's "missing" branch covers those cases too. */
242
+ * failure so the caller's "missing" branch covers those cases too.
243
+ * Exported so `ontology.ts`'s record COUNT classifies entries with the
244
+ * SAME lstat logic — the two must agree on what a record file is. */
245
245
  async function isRegularFile(filePath) {
246
246
  try {
247
247
  return (await (0, node_fs_promises.lstat)(filePath)).isFile();
@@ -636,13 +636,112 @@ ${dataJson}
636
636
  ${templateText}`;
637
637
  }
638
638
  //#endregion
639
+ //#region src/collection/core/recordZ.ts
640
+ /** The emptiness rule shared by `required` and the "only check present
641
+ * values" gate. NOT a truthiness check — `0` and `false` are filled. */
642
+ var isEmptyValue = (value) => value === void 0 || value === null || value === "";
643
+ /** The historical write-gate checks, verbatim: required non-empty, enum
644
+ * membership (compared as strings, so a numeric `5` satisfies `"5"`). */
645
+ function enforcedProblem(key, spec, value) {
646
+ const empty = isEmptyValue(value);
647
+ if (spec.required && empty) return `missing required field '${key}'`;
648
+ if (!empty && spec.type === "enum" && !spec.values.includes(String(value))) return `'${key}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
649
+ return null;
650
+ }
651
+ /** Numeric coercion for the strict `number`/`money` check: a plain number,
652
+ * or a non-blank numeric string (renderers coerce those via `Number(...)`,
653
+ * so they display fine). Anything else — arrays (`[]` stringifies to `""`
654
+ * = 0, `[42]` to `"42"`), booleans, objects — is NaN and gets flagged. */
655
+ function coerceNumeric(value) {
656
+ if (typeof value === "number") return value;
657
+ if (typeof value === "string" && value.trim() !== "") return Number(value);
658
+ return NaN;
659
+ }
660
+ /** Report-only per-type checks on a PRESENT value. Date / datetime reuse the
661
+ * calendar's STRICT civil parsers (`parseIsoDate` / `parseIsoDateTime`), so
662
+ * the lint flags exactly the values the calendar / trigger / spawn code
663
+ * would silently drop — impossible days like `2026-02-30`, and datetimes
664
+ * outside the canonical `YYYY-MM-DDTHH:MM[:SS]` shape (e.g. a `Z` suffix,
665
+ * which the day view can't place). `string`-backed types accept anything
666
+ * stringifiable; `ref` existence is out of scope. */
667
+ function strictTypeProblem(key, spec, value) {
668
+ switch (spec.type) {
669
+ case "number":
670
+ case "money": return Number.isFinite(coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
671
+ case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
672
+ case "date": return require_calendarGrid.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
673
+ case "datetime": return require_calendarGrid.parseIsoDateTime(value) !== null ? null : `'${key}' = '${String(value)}' is not a YYYY-MM-DDTHH:MM datetime (seconds optional, no timezone suffix — the shape the calendar parses)`;
674
+ default: return null;
675
+ }
676
+ }
677
+ /** Strict check for a PRESENT `table` value: an array of row objects, each
678
+ * row conforming to the sub-schema (required / enum / typed sub-values).
679
+ * First row problem wins, prefixed with the row number so the fix is
680
+ * locatable. */
681
+ function strictTableProblem(key, spec, value) {
682
+ if (!Array.isArray(value)) return `'${key}' = '${String(value)}' is not an array of rows (a 'table' field stores an array of row objects)`;
683
+ for (let index = 0; index < value.length; index++) {
684
+ const row = value[index];
685
+ if (!row || typeof row !== "object" || Array.isArray(row)) return `'${key}' row ${index + 1} is not an object`;
686
+ for (const [subKey, subSpec] of Object.entries(spec.of)) {
687
+ const subValue = row[subKey];
688
+ const problem = enforcedProblem(subKey, subSpec, subValue) ?? (isEmptyValue(subValue) ? null : strictTypeProblem(subKey, subSpec, subValue));
689
+ if (problem) return `'${key}' row ${index + 1}: ${problem}`;
690
+ }
691
+ }
692
+ return null;
693
+ }
694
+ /** First problem for one field's stored value under `tier`, or null.
695
+ * Enforced checks always run (and their messages never vary by tier — the
696
+ * scan and the write gate must agree on them); strict adds the per-type
697
+ * layer on present values only. */
698
+ function recordFieldProblem(key, spec, value, tier) {
699
+ const enforced = enforcedProblem(key, spec, value);
700
+ if (enforced || tier === "enforced") return enforced;
701
+ if (isEmptyValue(value)) return null;
702
+ if (spec.type === "table") return strictTableProblem(key, spec, value);
703
+ return strictTypeProblem(key, spec, value);
704
+ }
705
+ var compiled = /* @__PURE__ */ new WeakMap();
706
+ /** Compile `schema.fields` into a zod validator for a stored record.
707
+ * Loose object: unknown keys are allowed and any declared key may be
708
+ * absent (records are user files, not parse-and-rewrite targets —
709
+ * callers validate, they never persist the parse output). The checks run
710
+ * as ONE object-level refine iterating fields in declaration order —
711
+ * per-key shape schemas can't express "key may be absent BUT its absence
712
+ * must still reach the required check", and the single loop keeps the
713
+ * first reported issue identical to the historical first-problem-wins
714
+ * contract. */
715
+ function compileRecordZ(schema, tier) {
716
+ const cached = compiled.get(schema)?.[tier];
717
+ if (cached) return cached;
718
+ const stored = Object.entries(schema.fields).filter(([, spec]) => !require_calendarGrid.COMPUTED_TYPES.has(spec.type));
719
+ const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
720
+ for (const [key, spec] of stored) {
721
+ const problem = recordFieldProblem(key, spec, record[key], tier);
722
+ if (problem) ctx.addIssue({
723
+ code: "custom",
724
+ message: problem,
725
+ path: [key]
726
+ });
727
+ }
728
+ });
729
+ const entry = compiled.get(schema) ?? {};
730
+ entry[tier] = validator;
731
+ compiled.set(schema, entry);
732
+ return validator;
733
+ }
734
+ /** First schema problem on an in-memory record under `tier`, or null. One
735
+ * issue per record keeps the report short and the fix obvious (the
736
+ * historical contract of `validateRecordObject`). */
737
+ function firstRecordProblem(record, schema, tier) {
738
+ const result = compileRecordZ(schema, tier).safeParse(record);
739
+ if (result.success) return null;
740
+ return result.error.issues[0]?.message ?? "record failed schema validation";
741
+ }
742
+ //#endregion
639
743
  //#region src/collection/server/validate.ts
640
744
  var MAX_ISSUES = 25;
641
- var COMPUTED_TYPES = /* @__PURE__ */ new Set([
642
- "derived",
643
- "embed",
644
- "toggle"
645
- ]);
646
745
  /** Read every `<id>.json` under the collection's dataDir and report the
647
746
  * ones that won't load or violate the schema. An empty list means every
648
747
  * record is fine. */
@@ -705,72 +804,70 @@ async function inspectRecord(fullPath, name, schema) {
705
804
  file: name,
706
805
  problem: "not a JSON object — skipped, won't appear"
707
806
  };
708
- const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema);
807
+ const problem = validateRecordObject(parsed, name.replace(/\.json$/, ""), schema, "strict");
709
808
  return problem ? {
710
809
  file: name,
711
810
  problem
712
811
  } : null;
713
812
  }
714
813
  /** First schema problem on an in-memory record (primaryKey↔id mismatch,
715
- * missing required, bad enum value), or null when it's fine. One issue
716
- * per record keeps the report short and the fix obvious. Pure +
717
- * exported so write paths (manageCollection putItems) can gate on the
718
- * SAME rules the post-hoc file scan reports `itemId` is the id the
719
- * record is (or would be) stored under. */
720
- function validateRecordObject(record, itemId, schema) {
814
+ * then the compiled per-field checks see `../core/recordZ` for the two
815
+ * tiers), or null when it's fine. One issue per record keeps the report
816
+ * short and the fix obvious. Pure + exported so write paths
817
+ * (manageCollection putItems) can gate on the SAME enforced rules the
818
+ * post-hoc file scan reports — `itemId` is the id the record is (or
819
+ * would be) stored under. The default `"enforced"` tier keeps every
820
+ * write gate on the historical three checks; only pass `"strict"` from
821
+ * report-only surfaces. */
822
+ function validateRecordObject(record, itemId, schema, tier = "enforced") {
721
823
  const idValue = record[schema.primaryKey];
722
824
  if (typeof idValue !== "string" || idValue !== itemId) return `'${schema.primaryKey}' is '${String(idValue ?? "")}' but must equal the filename ('${itemId}'), or the record can't be opened`;
723
- for (const [field, spec] of Object.entries(schema.fields)) {
724
- if (COMPUTED_TYPES.has(spec.type)) continue;
725
- const value = record[field];
726
- const empty = value === void 0 || value === null || value === "";
727
- if (spec.required && empty) return `missing required field '${field}'`;
728
- if (!empty && spec.type === "enum" && spec.values && !spec.values.includes(String(value))) return `'${field}' = '${String(value)}' is not one of [${spec.values.join(", ")}]`;
729
- }
730
- return null;
825
+ return firstRecordProblem(record, schema, tier);
731
826
  }
732
827
  //#endregion
733
- //#region src/collection/server/discovery.ts
734
- var refRefine = (spec) => {
735
- if (spec.type !== "ref") return true;
736
- if (typeof spec.to !== "string") return false;
737
- return safeSlugName(spec.to) !== null;
738
- };
739
- var refMessage = {
740
- message: "fields with type 'ref' must declare a `to` that is a valid collection slug (alphanumeric / hyphen / underscore, no path separators)",
741
- path: ["to"]
742
- };
743
- var isDateLike = (type) => type === "date" || type === "datetime";
744
- var isTimeStringField = (type) => type === "string" || type === "text";
745
- var embedRefine = (spec) => {
746
- if (spec.type !== "embed") return true;
747
- if (typeof spec.to !== "string" || safeSlugName(spec.to) === null) return false;
748
- return (typeof spec.id === "string" && spec.id.trim().length > 0) !== (typeof spec.idField === "string" && spec.idField.trim().length > 0);
749
- };
750
- var embedMessage = {
751
- message: "fields with type 'embed' must declare a `to` (valid collection slug) and exactly one of `id` (a fixed record's primary key) or `idField` (a sibling field naming the per-record target)",
752
- path: ["id"]
753
- };
754
- var enumRefine = (spec) => spec.type !== "enum" || Array.isArray(spec.values) && spec.values.length > 0 && spec.values.every((value) => typeof value === "string" && value.length > 0);
755
- var enumMessage = {
756
- message: "fields with type 'enum' must declare a non-empty `values` array of non-empty strings",
757
- path: ["values"]
828
+ //#region src/collection/core/schemaZ.ts
829
+ /** Optional visibility predicate shared by actions and fields: the target
830
+ * shows only when the open record's `field` (stringified) is one of `in`.
831
+ * Domain-free `field` is any non-empty key, `in` a non-empty array of
832
+ * non-empty values; the host never interprets the meaning.
833
+ *
834
+ * `trim().min(1)` rather than bare `min(1)` so a whitespace-only string
835
+ * (" ") fails validation otherwise the cell formatter / dropdown would
836
+ * render visual blanks that look like missing data. Applied consistently to
837
+ * every "non-empty string" slot in this file (CodeRabbit PR #1497). */
838
+ var WhenZ = zod.z.object({
839
+ field: zod.z.string().trim().min(1),
840
+ in: zod.z.array(zod.z.string().trim().min(1)).min(1)
841
+ });
842
+ var fieldBase = {
843
+ label: zod.z.string().min(1),
844
+ primary: zod.z.boolean().optional(),
845
+ required: zod.z.boolean().optional(),
846
+ when: WhenZ.optional()
758
847
  };
759
- var currencyRefine = (spec) => {
760
- if (!(spec.type === "money" || spec.type === "derived" && spec.display === "money")) return true;
761
- const hasLiteral = typeof spec.currency === "string" && spec.currency.trim().length > 0;
762
- const hasPointer = typeof spec.currencyField === "string" && spec.currencyField.trim().length > 0;
763
- return hasLiteral || hasPointer;
848
+ var currencyKeys = {
849
+ currency: zod.z.string().trim().min(1).optional(),
850
+ currencyField: zod.z.string().trim().min(1).optional()
764
851
  };
852
+ var hasCurrencySource = (spec) => spec.currency !== void 0 || spec.currencyField !== void 0;
765
853
  var currencyMessage = {
766
854
  message: "fields that render as money (type 'money', or 'derived' with display 'money') must declare either a literal `currency` (ISO 4217 code, e.g. 'USD', 'JPY') or a `currencyField` naming the record field that holds the code",
767
855
  path: ["currency"]
768
856
  };
769
- var WhenSchema = zod.z.object({
770
- field: zod.z.string().trim().min(1),
771
- in: zod.z.array(zod.z.string().trim().min(1)).min(1)
772
- });
773
- var SubFieldSpecSchema = zod.z.object({
857
+ var slugMessage = (key) => ({
858
+ message: `\`${key}\` must be a valid collection slug (alphanumeric / hyphen / underscore, no path separators)`,
859
+ path: [key]
860
+ });
861
+ /** The plain scalar field types. Stored and edited as primitive values; no
862
+ * variant-specific keys.
863
+ * - `image`: a workspace-relative image path (e.g. a `data/attachments/...`
864
+ * upload); rendered as an <img> in the detail view (not the list table —
865
+ * a per-row fetch is too expensive at scale). Stored as a plain string.
866
+ * - `file`: a workspace-relative file path as a plain string (e.g. an
867
+ * `artifacts/html/<name>.html` app). Rendered as a clickable link in both
868
+ * the list table and the detail view: HTML / SVG artifacts open their
869
+ * rendered form in a new tab; any other path opens in the File Explorer. */
870
+ var ScalarFieldZ = zod.z.object({
774
871
  type: zod.z.enum([
775
872
  "string",
776
873
  "text",
@@ -780,18 +877,39 @@ var SubFieldSpecSchema = zod.z.object({
780
877
  "datetime",
781
878
  "boolean",
782
879
  "markdown",
783
- "ref",
784
- "money",
785
- "enum"
880
+ "image",
881
+ "file"
786
882
  ]),
883
+ ...fieldBase
884
+ });
885
+ /** A link to another collection: the record stores the target item's
886
+ * primary-key slug and the host renders a clickable link + dropdown picker.
887
+ * `to` must be a real slug (not `../foo`, not `mc-clients/extra` — see
888
+ * Codex P2 on PR #1495); whether the target collection exists resolves
889
+ * fail-soft at render time, never here. */
890
+ var RefFieldZ = zod.z.object({
891
+ type: zod.z.literal("ref"),
892
+ ...fieldBase,
893
+ to: zod.z.string().min(1)
894
+ }).refine((spec) => require_calendarGrid.isSafeSlug(spec.to), slugMessage("to"));
895
+ /** A money amount. See `currencyKeys` for the currency-source contract. */
896
+ var MoneyFieldZ = zod.z.object({
897
+ type: zod.z.literal("money"),
898
+ ...fieldBase,
899
+ ...currencyKeys
900
+ }).refine(hasCurrencySource, currencyMessage);
901
+ /** A closed set of allowed string values. The form renders a `<select>`
902
+ * populated from `values`; storage is a plain string. */
903
+ var EnumFieldZ = zod.z.object({
904
+ type: zod.z.literal("enum"),
905
+ ...fieldBase,
906
+ values: zod.z.array(zod.z.string().trim().min(1)).min(1)
907
+ });
908
+ var subFieldBase = {
787
909
  label: zod.z.string().min(1),
788
- required: zod.z.boolean().optional(),
789
- to: zod.z.string().min(1).optional(),
790
- currency: zod.z.string().trim().min(1).optional(),
791
- currencyField: zod.z.string().trim().min(1).optional(),
792
- values: zod.z.array(zod.z.string().trim().min(1)).min(1).optional()
793
- }).refine(refRefine, refMessage).refine(enumRefine, enumMessage).refine(currencyRefine, currencyMessage);
794
- var FieldSpecSchema = zod.z.object({
910
+ required: zod.z.boolean().optional()
911
+ };
912
+ var SubScalarFieldZ = zod.z.object({
795
913
  type: zod.z.enum([
796
914
  "string",
797
915
  "text",
@@ -800,62 +918,145 @@ var FieldSpecSchema = zod.z.object({
800
918
  "date",
801
919
  "datetime",
802
920
  "boolean",
803
- "markdown",
804
- "ref",
805
- "money",
806
- "enum",
807
- "table",
808
- "derived",
809
- "embed",
810
- "image",
811
- "file",
812
- "toggle"
921
+ "markdown"
813
922
  ]),
814
- label: zod.z.string().min(1),
815
- primary: zod.z.boolean().optional(),
816
- required: zod.z.boolean().optional(),
817
- to: zod.z.string().min(1).optional(),
818
- id: zod.z.string().trim().min(1).optional(),
819
- idField: zod.z.string().trim().min(1).optional(),
820
- currency: zod.z.string().trim().min(1).optional(),
821
- currencyField: zod.z.string().trim().min(1).optional(),
822
- values: zod.z.array(zod.z.string().trim().min(1)).min(1).optional(),
823
- field: zod.z.string().trim().min(1).optional(),
824
- onValue: zod.z.string().trim().min(1).optional(),
825
- offValue: zod.z.string().trim().min(1).optional(),
826
- of: zod.z.record(zod.z.string(), SubFieldSpecSchema).optional(),
827
- formula: zod.z.string().trim().min(1).optional(),
828
- /** Inner type to render a derived value as (e.g. `"money"`).
829
- * Restricted to the non-composite display targets — derived
830
- * values are scalars, so rendering them via `table` or another
831
- * `derived` would be meaningless. */
923
+ ...subFieldBase
924
+ });
925
+ var SubRefFieldZ = zod.z.object({
926
+ type: zod.z.literal("ref"),
927
+ ...subFieldBase,
928
+ to: zod.z.string().min(1)
929
+ }).refine((spec) => require_calendarGrid.isSafeSlug(spec.to), slugMessage("to"));
930
+ var SubMoneyFieldZ = zod.z.object({
931
+ type: zod.z.literal("money"),
932
+ ...subFieldBase,
933
+ ...currencyKeys
934
+ }).refine(hasCurrencySource, currencyMessage);
935
+ var SubEnumFieldZ = zod.z.object({
936
+ type: zod.z.literal("enum"),
937
+ ...subFieldBase,
938
+ values: zod.z.array(zod.z.string().trim().min(1)).min(1)
939
+ });
940
+ var SubFieldSpecZ = zod.z.discriminatedUnion("type", [
941
+ SubScalarFieldZ,
942
+ SubRefFieldZ,
943
+ SubMoneyFieldZ,
944
+ SubEnumFieldZ
945
+ ]);
946
+ /** A flat sub-table: each row is a record of `of`'s sub-schema (insertion
947
+ * order = column order). v0 disallows nested tables and derived columns to
948
+ * keep the editor + evaluator simple. */
949
+ var TableFieldZ = zod.z.object({
950
+ type: zod.z.literal("table"),
951
+ ...fieldBase,
952
+ of: zod.z.record(zod.z.string(), SubFieldSpecZ)
953
+ }).refine((spec) => Object.keys(spec.of).length > 0, {
954
+ message: "fields with type 'table' must declare a non-empty `of` (sub-schema for each row)",
955
+ path: ["of"]
956
+ });
957
+ /** A computed scalar: `formula` is a tiny expression evaluated against the
958
+ * record — `+ - * /`, parens, identifier refs to top-level fields,
959
+ * `sum(tableField[].col)`, and `sum(tableField[].col * tableField[].col)`
960
+ * (see `./derivedFormula`). `display` picks the inner type the value renders
961
+ * as (default `"number"`) — restricted to the non-composite display targets,
962
+ * since a derived value is a scalar. Never stored; computed by `deriveAll`
963
+ * on both server and client. */
964
+ var DerivedFieldZ = zod.z.object({
965
+ type: zod.z.literal("derived"),
966
+ ...fieldBase,
967
+ formula: zod.z.string().trim().min(1),
832
968
  display: zod.z.enum([
833
969
  "string",
834
970
  "number",
835
971
  "money",
836
972
  "date"
837
973
  ]).optional(),
838
- when: WhenSchema.optional()
839
- }).refine(refRefine, refMessage).refine(enumRefine, enumMessage).refine(embedRefine, embedMessage).refine(currencyRefine, currencyMessage).refine((spec) => spec.type !== "table" || spec.of !== void 0 && Object.keys(spec.of).length > 0, {
840
- message: "fields with type 'table' must declare a non-empty `of` (sub-schema for each row)",
841
- path: ["of"]
842
- }).refine((spec) => spec.type !== "derived" || typeof spec.formula === "string" && spec.formula.length > 0, {
843
- message: "fields with type 'derived' must declare a non-empty `formula` (see src/utils/collections/derivedFormula.ts)",
844
- path: ["formula"]
845
- }).refine((spec) => spec.type !== "toggle" || typeof spec.field === "string" && spec.field.length > 0 && typeof spec.onValue === "string" && typeof spec.offValue === "string", {
846
- message: "fields with type 'toggle' must declare `field` (the enum field to project), `onValue`, and `offValue`",
847
- path: ["field"]
848
- });
849
- var ActionSpecSchema = zod.z.object({
974
+ ...currencyKeys
975
+ }).refine((spec) => spec.display !== "money" || hasCurrencySource(spec), currencyMessage);
976
+ /** Pulls a record from another collection into the read-only detail view.
977
+ * Display-only — nothing is stored on this record, so it never appears in
978
+ * the list table or the edit form. Must declare a valid `to` slug (same
979
+ * path-traversal guard as `ref`) AND exactly one of `id` (a fixed target
980
+ * record, e.g. `me` for the singleton profile — same for every record) or
981
+ * `idField` (a sibling top-level field naming the per-record target, e.g.
982
+ * an invoice's `issuerId` selecting which profile to embed as the bill-from
983
+ * block; an absent/empty value resolves fail-soft to "no record"). The
984
+ * `idField` target is validated to be a real `ref`/`string` field by a
985
+ * schema-level refine below. */
986
+ var EmbedFieldZ = zod.z.object({
987
+ type: zod.z.literal("embed"),
988
+ ...fieldBase,
989
+ to: zod.z.string().min(1),
990
+ id: zod.z.string().trim().min(1).optional(),
991
+ idField: zod.z.string().trim().min(1).optional()
992
+ }).refine((spec) => require_calendarGrid.isSafeSlug(spec.to) && spec.id !== void 0 !== (spec.idField !== void 0), {
993
+ message: "fields with type 'embed' must declare a `to` (valid collection slug) and exactly one of `id` (a fixed record's primary key) or `idField` (a sibling field naming the per-record target)",
994
+ path: ["id"]
995
+ });
996
+ /** Display-only REVERSE refs (plan step ② of plans/collection-ontology.md):
997
+ * a read-only sub-table of the records in collection `from` whose `via`
998
+ * ref field stores THIS record's primary key. Stores nothing (joins
999
+ * `COMPUTED_TYPES`); resolution is shared server/client via
1000
+ * `core/backlinks.ts`. `display` names the `from` columns to show;
1001
+ * `filter` (the standard `when` shape, matched against each SOURCE
1002
+ * record) narrows the rows. Validation is shape-only, like `embed`:
1003
+ * `from` must be a safe slug, but whether it exists — and whether `via` /
1004
+ * `display` name real fields there — resolves fail-soft at render
1005
+ * (empty sub-table). Do NOT add cross-schema existence checks here. */
1006
+ var BacklinksFieldZ = zod.z.object({
1007
+ type: zod.z.literal("backlinks"),
1008
+ ...fieldBase,
1009
+ from: zod.z.string().min(1),
1010
+ via: zod.z.string().trim().min(1),
1011
+ display: zod.z.array(zod.z.string().trim().min(1)).min(1),
1012
+ filter: WhenZ.optional()
1013
+ }).refine((spec) => require_calendarGrid.isSafeSlug(spec.from), slugMessage("from"));
1014
+ /** A checkbox that is a pure PROJECTION of an `enum` field — it stores
1015
+ * nothing of its own. Checked when the enum named by `field` equals
1016
+ * `onValue`; toggling writes `onValue` / `offValue` back to that enum
1017
+ * field. Lets a "done" checkbox front a kanban `status` field with the enum
1018
+ * as the single source of truth (no separate stored boolean to keep in
1019
+ * sync). `field` / `onValue` / `offValue` are validated against the target
1020
+ * enum's `values` by a schema-level refine below. */
1021
+ var ToggleFieldZ = zod.z.object({
1022
+ type: zod.z.literal("toggle"),
1023
+ ...fieldBase,
1024
+ field: zod.z.string().trim().min(1),
1025
+ onValue: zod.z.string().trim().min(1),
1026
+ offValue: zod.z.string().trim().min(1)
1027
+ });
1028
+ var FieldSpecZ = zod.z.discriminatedUnion("type", [
1029
+ ScalarFieldZ,
1030
+ RefFieldZ,
1031
+ MoneyFieldZ,
1032
+ EnumFieldZ,
1033
+ TableFieldZ,
1034
+ DerivedFieldZ,
1035
+ EmbedFieldZ,
1036
+ BacklinksFieldZ,
1037
+ ToggleFieldZ
1038
+ ]);
1039
+ /** A schema-declared record action, rendered as a button in the read-only
1040
+ * detail view. Domain-free: the host validates the shape; the meaning
1041
+ * (which role, which template) is data. v1 ships only `kind: "chat"` —
1042
+ * start a new chat in `role` with a templated seed prompt; the enum
1043
+ * reserves room for a future `"mutate"` (status transitions) without
1044
+ * another schema-shape change. */
1045
+ var ActionSpecZ = zod.z.object({
850
1046
  id: zod.z.string().trim().min(1),
851
1047
  label: zod.z.string().trim().min(1),
852
1048
  icon: zod.z.string().trim().min(1).optional(),
853
1049
  kind: zod.z.enum(["chat"]),
854
1050
  role: zod.z.string().trim().min(1),
855
1051
  template: zod.z.string().trim().min(1).refine(require_collection_paths.isSafeActionTemplatePath, "must be a safe path under `templates/` (e.g. `templates/invoice.md`; no `..`, no leading `/`, no backslash)"),
856
- when: WhenSchema.optional()
857
- });
858
- var CustomViewSchema = zod.z.object({
1052
+ when: WhenZ.optional()
1053
+ });
1054
+ /** A custom (LLM-authored) HTML view registration. Domain-free: the host
1055
+ * validates the shape; the view's behaviour lives in the HTML file. `file`
1056
+ * is constrained to `views/*.html` (path-safe) so the view-file reader can
1057
+ * never reach the data folder or the schema/template files. `id` is
1058
+ * validated to be a real slug + unique by schema-level refines below. */
1059
+ var CustomViewZ = zod.z.object({
859
1060
  id: zod.z.string().trim().min(1),
860
1061
  label: zod.z.string().trim().min(1),
861
1062
  icon: zod.z.string().trim().min(1).optional(),
@@ -868,7 +1069,14 @@ var CustomViewSchema = zod.z.object({
868
1069
  imageFields: zod.z.array(zod.z.string().trim().min(1)).optional(),
869
1070
  imageMaxEdge: zod.z.number().int().min(1).optional()
870
1071
  });
871
- var EveryLiteralSchema = zod.z.object({
1072
+ /** Recurrence advance for `spawn.every`. `interval` is a positive integer
1073
+ * count of `unit`s (`interval: 3` + `unit: "month"` = quarterly);
1074
+ * `dayOfMonth` (month/year only) is the CANONICAL day-of-month anchor
1075
+ * (1-31, read from the rule and clamped per-month at compute time so "31st
1076
+ * of every month" never drifts) or the `"last"` sentinel for end-of-month.
1077
+ * `.strict()` so the union below cleanly rejects an object carrying BOTH
1078
+ * `unit` and `fromField` (it fails this arm on the unknown `fromField`). */
1079
+ var EveryLiteralZ = zod.z.object({
872
1080
  unit: zod.z.enum([
873
1081
  "day",
874
1082
  "week",
@@ -878,17 +1086,114 @@ var EveryLiteralSchema = zod.z.object({
878
1086
  interval: zod.z.number().int().min(1),
879
1087
  dayOfMonth: zod.z.union([zod.z.number().int().min(1).max(31), zod.z.literal("last")]).optional()
880
1088
  }).strict();
881
- var EveryFieldDrivenSchema = zod.z.object({
1089
+ /** Field-driven recurrence: pick the interval per-record by an `enum`
1090
+ * field's value — one collection can mix daily / weekly / monthly
1091
+ * obligations in a single list. `map` keys are validated to exactly cover
1092
+ * that field's `values` by a `CollectionSchemaZ` refine (which can see the
1093
+ * sibling `fields`); here each map value just has to be a well-formed
1094
+ * literal `every`. `.strict()` mirrors the literal arm so a both-keys
1095
+ * object fails this arm too. */
1096
+ var EveryFieldDrivenZ = zod.z.object({
882
1097
  fromField: zod.z.string().trim().min(1),
883
- map: zod.z.record(zod.z.string(), EveryLiteralSchema)
1098
+ map: zod.z.record(zod.z.string(), EveryLiteralZ)
884
1099
  }).strict();
885
- var EverySchema = zod.z.union([EveryLiteralSchema, EveryFieldDrivenSchema]);
886
- var SpawnSchema = zod.z.object({
887
- when: WhenSchema.optional(),
888
- every: EverySchema,
1100
+ /** Either a single literal interval (applied to every record) or the
1101
+ * field-driven map. Two `.strict()` arms mean "both keys" and "neither
1102
+ * key" both fail validation, with no extra refine. */
1103
+ var EveryZ = zod.z.union([EveryLiteralZ, EveryFieldDrivenZ]);
1104
+ /** Host-driven recurrence: when a record satisfies `when` (default:
1105
+ * "`completionField` value ∈ `completionDoneValues`"), the host creates the
1106
+ * next record with a forward-advanced `triggerField` date. `carry` copies
1107
+ * record fields verbatim onto the successor; `set` forces fixed values
1108
+ * (typically resetting the status field to its pending value). The
1109
+ * successor's id and contents are a pure function of (source record, this
1110
+ * rule); creation is create-if-absent, so the mechanism stays convergent. */
1111
+ var SpawnZ = zod.z.object({
1112
+ when: WhenZ.optional(),
1113
+ every: EveryZ,
889
1114
  carry: zod.z.array(zod.z.string().trim().min(1)).optional(),
890
1115
  set: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
891
1116
  });
1117
+ /** Declarative retrieval config for a Feed (a collection that refills itself
1118
+ * from the internet). `http-json` needs `itemsAt` (a path to the items
1119
+ * array) only when the response body isn't itself the array; rss/atom yield
1120
+ * items natively and ignore it — so no kind-specific requirement here. */
1121
+ var DeclarativeIngestZ = zod.z.object({
1122
+ kind: zod.z.enum(require_calendarGrid.INGEST_KINDS),
1123
+ url: zod.z.string().url(),
1124
+ schedule: zod.z.enum(require_calendarGrid.FEED_SCHEDULES),
1125
+ atHour: zod.z.number().int().min(0).max(23).optional(),
1126
+ itemsAt: zod.z.string().trim().min(1).optional(),
1127
+ map: zod.z.record(zod.z.string().trim().min(1), zod.z.string().trim().min(1)),
1128
+ idFrom: zod.z.string().trim().min(1).optional(),
1129
+ maxItems: zod.z.number().int().min(0).optional()
1130
+ });
1131
+ /** Agent-performed retrieval. Valid on any collection (the primary consumer
1132
+ * is skill-backed collections — feeds keep their declarative kinds). No
1133
+ * `url`/`map`: the worker owns retrieval and record shape, seeded by
1134
+ * `template` + a summary of every record, run in `role`. `template` is
1135
+ * validated the SAME way an action's template is (safe path under
1136
+ * `templates/`), so the skill-bridge mirrors it identically. */
1137
+ var AgentIngestZ = zod.z.object({
1138
+ kind: zod.z.literal(require_calendarGrid.AGENT_INGEST_KIND),
1139
+ schedule: zod.z.enum(require_calendarGrid.FEED_SCHEDULES),
1140
+ atHour: zod.z.number().int().min(0).max(23).optional(),
1141
+ role: zod.z.string().trim().min(1),
1142
+ template: zod.z.string().trim().min(1).refine(require_collection_paths.isSafeActionTemplatePath, "must be a safe path under `templates/` (e.g. `templates/refresh.md`; no `..`, no leading `/`, no backslash)")
1143
+ });
1144
+ /** `ingest` is a discriminated union on `kind`: the three declarative
1145
+ * retrievers fetch-and-map; `agent` dispatches a hidden worker. Optional on
1146
+ * every schema — skill-backed collections usually omit it; only feeds
1147
+ * discovered from `<workspace>/feeds/` are REQUIRED to carry it (gated by
1148
+ * `acceptParsedSchema`). */
1149
+ var IngestZ = zod.z.discriminatedUnion("kind", [DeclarativeIngestZ, AgentIngestZ]);
1150
+ var ValueRefZ = zod.z.object({
1151
+ record: zod.z.string().trim().min(1).optional(),
1152
+ field: zod.z.string().trim().min(1)
1153
+ });
1154
+ var WhereCondZ = zod.z.object({
1155
+ field: zod.z.string().trim().min(1),
1156
+ op: zod.z.enum([
1157
+ "eq",
1158
+ "ne",
1159
+ "in",
1160
+ "gt",
1161
+ "gte",
1162
+ "lt",
1163
+ "lte",
1164
+ "contains"
1165
+ ]),
1166
+ value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
1167
+ valueFrom: ValueRefZ.optional()
1168
+ }).refine((cond) => cond.value !== void 0 !== (cond.valueFrom !== void 0), {
1169
+ message: "a where condition must declare exactly one of `value` (a literal) or `valueFrom` (a reference to another record's field), never both or neither",
1170
+ path: ["value"]
1171
+ }).refine((cond) => cond.value === void 0 || cond.op === "in" === Array.isArray(cond.value), {
1172
+ message: "`in` requires an array `value` (the allowed set); every other op requires a single string `value`",
1173
+ path: ["value"]
1174
+ });
1175
+ var WhereZ = zod.z.array(WhereCondZ);
1176
+ var DynamicIconSourceZ = zod.z.object({
1177
+ collection: zod.z.string().trim().min(1),
1178
+ from: zod.z.enum([
1179
+ "latest",
1180
+ "first",
1181
+ "when"
1182
+ ]).optional(),
1183
+ orderBy: zod.z.string().trim().min(1).optional(),
1184
+ where: WhereZ.optional()
1185
+ });
1186
+ var DynamicIconRuleZ = zod.z.object({
1187
+ where: WhereZ,
1188
+ icon: zod.z.string().trim().min(1)
1189
+ });
1190
+ var DynamicIconSpecZ = zod.z.object({
1191
+ source: DynamicIconSourceZ,
1192
+ rules: zod.z.array(DynamicIconRuleZ),
1193
+ fallback: zod.z.string().trim().min(1).optional()
1194
+ });
1195
+ var isDateLike = (type) => type === "date" || type === "datetime";
1196
+ var isTimeStringField = (type) => type === "string" || type === "text";
892
1197
  var CODE_FIELD_TYPES = /* @__PURE__ */ new Set([
893
1198
  "string",
894
1199
  "text",
@@ -926,7 +1231,7 @@ function spawnSuccessorStartsInert(schema) {
926
1231
  }
927
1232
  function fieldDrivenSpawnEvery(schema) {
928
1233
  const every = schema.spawn?.every;
929
- if (!every || !require_deriveAll.isFieldDrivenEvery(every)) return null;
1234
+ if (!every || !("fromField" in every)) return null;
930
1235
  return every;
931
1236
  }
932
1237
  function fieldDrivenFromFieldIsEnum(schema) {
@@ -954,93 +1259,30 @@ function fieldDrivenFromFieldCarried(schema) {
954
1259
  }
955
1260
  return (carry ?? []).includes(driven.fromField);
956
1261
  }
957
- var DeclarativeIngestZ = zod.z.object({
958
- kind: zod.z.enum(require_deriveAll.INGEST_KINDS),
959
- url: zod.z.string().url(),
960
- schedule: zod.z.enum(require_deriveAll.FEED_SCHEDULES),
961
- atHour: zod.z.number().int().min(0).max(23).optional(),
962
- itemsAt: zod.z.string().trim().min(1).optional(),
963
- map: zod.z.record(zod.z.string().trim().min(1), zod.z.string().trim().min(1)),
964
- idFrom: zod.z.string().trim().min(1).optional(),
965
- maxItems: zod.z.number().int().min(0).optional()
966
- });
967
- var AgentIngestZ = zod.z.object({
968
- kind: zod.z.literal(require_deriveAll.AGENT_INGEST_KIND),
969
- schedule: zod.z.enum(require_deriveAll.FEED_SCHEDULES),
970
- atHour: zod.z.number().int().min(0).max(23).optional(),
971
- role: zod.z.string().trim().min(1),
972
- template: zod.z.string().trim().min(1).refine(require_collection_paths.isSafeActionTemplatePath, "must be a safe path under `templates/` (e.g. `templates/refresh.md`; no `..`, no leading `/`, no backslash)")
973
- });
974
- var IngestSchemaZ = zod.z.discriminatedUnion("kind", [DeclarativeIngestZ, AgentIngestZ]);
975
- var ValueRefZ = zod.z.object({
976
- record: zod.z.string().trim().min(1).optional(),
977
- field: zod.z.string().trim().min(1)
978
- });
979
- var WhereCondZ = zod.z.object({
980
- field: zod.z.string().trim().min(1),
981
- op: zod.z.enum([
982
- "eq",
983
- "ne",
984
- "in",
985
- "gt",
986
- "gte",
987
- "lt",
988
- "lte",
989
- "contains"
990
- ]),
991
- value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
992
- valueFrom: ValueRefZ.optional()
993
- }).refine((cond) => cond.value !== void 0 !== (cond.valueFrom !== void 0), {
994
- message: "a where condition must declare exactly one of `value` (a literal) or `valueFrom` (a reference to another record's field), never both or neither",
995
- path: ["value"]
996
- }).refine((cond) => cond.value === void 0 || cond.op === "in" === Array.isArray(cond.value), {
997
- message: "`in` requires an array `value` (the allowed set); every other op requires a single string `value`",
998
- path: ["value"]
999
- });
1000
- var WhereZ = zod.z.array(WhereCondZ);
1001
- var DynamicIconSourceZ = zod.z.object({
1002
- collection: zod.z.string().trim().min(1),
1003
- from: zod.z.enum([
1004
- "latest",
1005
- "first",
1006
- "when"
1007
- ]).optional(),
1008
- orderBy: zod.z.string().trim().min(1).optional(),
1009
- where: WhereZ.optional()
1010
- });
1011
- var DynamicIconRuleZ = zod.z.object({
1012
- where: WhereZ,
1013
- icon: zod.z.string().trim().min(1)
1014
- });
1015
- var DynamicIconSpecZ = zod.z.object({
1016
- source: DynamicIconSourceZ,
1017
- rules: zod.z.array(DynamicIconRuleZ),
1018
- fallback: zod.z.string().trim().min(1).optional()
1019
- });
1020
1262
  var CollectionSchemaZ = zod.z.object({
1021
1263
  title: zod.z.string().min(1),
1022
1264
  icon: zod.z.string().min(1),
1023
1265
  dataPath: zod.z.string().min(1),
1024
1266
  primaryKey: zod.z.string().min(1),
1025
1267
  singleton: zod.z.string().trim().min(1).optional(),
1026
- fields: zod.z.record(zod.z.string(), FieldSpecSchema),
1027
- actions: zod.z.array(ActionSpecSchema).optional(),
1028
- collectionActions: zod.z.array(ActionSpecSchema).optional(),
1268
+ fields: zod.z.record(zod.z.string(), FieldSpecZ),
1269
+ actions: zod.z.array(ActionSpecZ).optional(),
1270
+ collectionActions: zod.z.array(ActionSpecZ).optional(),
1029
1271
  completionField: zod.z.string().trim().min(1).optional(),
1030
1272
  completionDoneValues: zod.z.array(zod.z.string().trim().min(1)).min(1).optional(),
1031
1273
  displayField: zod.z.string().trim().min(1).optional(),
1032
1274
  triggerField: zod.z.string().trim().min(1).optional(),
1033
1275
  triggerLeadDays: zod.z.number().int().min(0).optional(),
1034
- spawn: SpawnSchema.optional(),
1276
+ spawn: SpawnZ.optional(),
1035
1277
  calendarField: zod.z.string().trim().min(1).optional(),
1036
1278
  calendarEndField: zod.z.string().trim().min(1).optional(),
1037
1279
  calendarTimeField: zod.z.string().trim().min(1).optional(),
1038
1280
  kanbanField: zod.z.string().trim().min(1).optional(),
1039
- views: zod.z.array(CustomViewSchema).optional(),
1040
- notifyWhen: WhenSchema.optional(),
1041
- ingest: IngestSchemaZ.optional(),
1281
+ views: zod.z.array(CustomViewZ).optional(),
1282
+ notifyWhen: WhenZ.optional(),
1283
+ ingest: IngestZ.optional(),
1042
1284
  dynamicIcon: DynamicIconSpecZ.optional()
1043
- }).refine((schema) => schema.singleton === void 0 || safeRecordId(schema.singleton) !== null, {
1285
+ }).refine((schema) => schema.singleton === void 0 || require_calendarGrid.isSafeRecordId(schema.singleton), {
1044
1286
  message: "schema `singleton` must be a valid item id (alphanumeric / hyphen / underscore / interior dot, no `..` or path separators)",
1045
1287
  path: ["singleton"]
1046
1288
  }).refine((schema) => schema.actions === void 0 || new Set(schema.actions.map((action) => action.id)).size === schema.actions.length, {
@@ -1131,13 +1373,15 @@ var CollectionSchemaZ = zod.z.object({
1131
1373
  }).refine((schema) => schema.notifyWhen === void 0 || schema.fields[schema.notifyWhen.field] !== void 0, {
1132
1374
  message: "schema `notifyWhen.field` must name a top-level field declared in `fields`",
1133
1375
  path: ["notifyWhen"]
1134
- }).refine((schema) => schema.views === void 0 || schema.views.every((view) => safeSlugName(view.id) !== null), {
1376
+ }).refine((schema) => schema.views === void 0 || schema.views.every((view) => require_calendarGrid.isSafeSlug(view.id)), {
1135
1377
  message: "every `views[].id` must be a valid slug (alphanumeric / hyphen / underscore, no path separators)",
1136
1378
  path: ["views"]
1137
1379
  }).refine((schema) => schema.views === void 0 || new Set(schema.views.map((view) => view.id)).size === schema.views.length, {
1138
1380
  message: "schema `views` must have unique `id`s",
1139
1381
  path: ["views"]
1140
1382
  });
1383
+ //#endregion
1384
+ //#region src/collection/server/discovery.ts
1141
1385
  function applyFeedSchemaDefaults(parsed, slug) {
1142
1386
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1143
1387
  const obj = parsed;
@@ -1320,6 +1564,72 @@ function toDetail(collection) {
1320
1564
  };
1321
1565
  }
1322
1566
  //#endregion
1567
+ //#region src/collection/server/ontology.ts
1568
+ /** Extract the relations a schema declares, in field declaration order:
1569
+ * top-level `ref` / `embed` / `backlinks` fields plus `ref` sub-fields
1570
+ * inside `table` columns. Pure — exported so the phase-2 graph panel
1571
+ * can reuse it on already-loaded schemas. */
1572
+ function schemaRelations(schema) {
1573
+ const relations = [];
1574
+ for (const [key, spec] of Object.entries(schema.fields)) {
1575
+ if (spec.type === "ref" || spec.type === "embed") relations.push({
1576
+ field: key,
1577
+ kind: spec.type,
1578
+ to: spec.to
1579
+ });
1580
+ if (spec.type === "backlinks") relations.push({
1581
+ field: key,
1582
+ kind: "backlinks",
1583
+ to: spec.from
1584
+ });
1585
+ if (spec.type !== "table") continue;
1586
+ for (const [subKey, subSpec] of Object.entries(spec.of)) if (subSpec.type === "ref") relations.push({
1587
+ field: `${key}.${subKey}`,
1588
+ kind: "ref",
1589
+ to: subSpec.to
1590
+ });
1591
+ }
1592
+ return relations;
1593
+ }
1594
+ /** Count the record files in a collection's data dir — the same
1595
+ * `<id>.json` entries `listItems` considers, WITHOUT parsing them (the
1596
+ * ontology is a summary; a malformed record is still a record). Each
1597
+ * candidate is classified by the SHARED `isRegularFile` lstat helper —
1598
+ * the exact file-disclosure defense `listItems` applies — so a
1599
+ * symlinked record never counts and the count can't diverge from the
1600
+ * readable set on any filesystem (Codex review on PR #2099, twice).
1601
+ * Fail-soft: a missing dir or a dataDir escaping the workspace via
1602
+ * symlink counts 0. */
1603
+ async function countRecordFiles(dataDir, workspaceRoot) {
1604
+ if (!isContainedInRoot(dataDir, workspaceRoot)) return 0;
1605
+ try {
1606
+ const names = (await (0, node_fs_promises.readdir)(dataDir)).filter((name) => name.endsWith(".json") && !name.startsWith("."));
1607
+ return (await Promise.all(names.map((name) => isRegularFile(node_path.default.join(dataDir, name))))).filter(Boolean).length;
1608
+ } catch {
1609
+ return 0;
1610
+ }
1611
+ }
1612
+ async function toOntologyEntry(collection, workspaceRoot) {
1613
+ const { schema } = collection;
1614
+ return {
1615
+ slug: collection.slug,
1616
+ title: schema.title,
1617
+ icon: schema.icon,
1618
+ primaryKey: schema.primaryKey,
1619
+ displayField: schema.displayField ?? schema.primaryKey,
1620
+ recordCount: await countRecordFiles(collection.dataDir, workspaceRoot),
1621
+ relations: schemaRelations(schema)
1622
+ };
1623
+ }
1624
+ /** Build the workspace ontology: every discovered collection (slug-sorted,
1625
+ * discovery's order), each with its outbound relations and a
1626
+ * readdir-cheap record count — fine to call on demand. */
1627
+ async function buildWorkspaceOntology(opts = {}) {
1628
+ const workspaceRoot = opts.workspaceRoot ?? getWorkspaceRoot();
1629
+ const collections = await discoverCollections(opts);
1630
+ return Promise.all(collections.map((collection) => toOntologyEntry(collection, workspaceRoot)));
1631
+ }
1632
+ //#endregion
1323
1633
  //#region src/collection/server/derive.ts
1324
1634
  /** Slugs of every collection referenced by a `ref` field — top-level
1325
1635
  * and one level into `table` sub-fields (nested tables are
@@ -1342,6 +1652,13 @@ function uniqueEmbedTargets(schema) {
1342
1652
  for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
1343
1653
  return [...targets];
1344
1654
  }
1655
+ /** Slugs of every SOURCE collection a `backlinks` field reverses over —
1656
+ * loaded exactly like ref/embed targets (whole collection, once). */
1657
+ function uniqueBacklinkSources(schema) {
1658
+ const sources = /* @__PURE__ */ new Set();
1659
+ for (const field of Object.values(schema.fields)) if (field.type === "backlinks" && field.from.length > 0) sources.add(field.from);
1660
+ return [...sources];
1661
+ }
1345
1662
  async function loadTarget(slug, opts) {
1346
1663
  const target = await loadCollection(slug, opts);
1347
1664
  if (!target) return null;
@@ -1349,18 +1666,23 @@ async function loadTarget(slug, opts) {
1349
1666
  const byId = {};
1350
1667
  for (const item of items) {
1351
1668
  const itemId = item[target.schema.primaryKey];
1352
- if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_deriveAll.deriveAll(target.schema, item, {});
1669
+ if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_calendarGrid.deriveAll(target.schema, item, {});
1353
1670
  }
1354
1671
  return {
1355
1672
  schema: target.schema,
1356
1673
  byId
1357
1674
  };
1358
1675
  }
1359
- /** Load every ref/embed target collection once. Unknown / unloadable
1360
- * targets are simply absent — downstream derefs resolve to null, the
1361
- * same fail-soft the UI renders as an em-dash. */
1676
+ /** Load every ref/embed target and backlink source collection once.
1677
+ * Unknown / unloadable targets are simply absent — downstream derefs
1678
+ * resolve to null (em-dash) and backlinks to an empty row set, the
1679
+ * same fail-soft the UI renders. */
1362
1680
  async function loadLinkedTargets(schema, opts) {
1363
- const slugs = [.../* @__PURE__ */ new Set([...uniqueRefTargets(schema), ...uniqueEmbedTargets(schema)])];
1681
+ const slugs = [.../* @__PURE__ */ new Set([
1682
+ ...uniqueRefTargets(schema),
1683
+ ...uniqueEmbedTargets(schema),
1684
+ ...uniqueBacklinkSources(schema)
1685
+ ])];
1364
1686
  const loaded = {};
1365
1687
  for (const slug of slugs) {
1366
1688
  const target = await loadTarget(slug, opts);
@@ -1371,16 +1693,29 @@ async function loadLinkedTargets(schema, opts) {
1371
1693
  function toRefRecords(linked) {
1372
1694
  return Object.fromEntries(Object.entries(linked).map(([slug, target]) => [slug, target.byId]));
1373
1695
  }
1696
+ /** The matching source rows for one `backlinks` field, projected to the
1697
+ * source primaryKey + `display` columns — so getItems on a
1698
+ * heavily-referenced record stays a summary, not a dump of the source
1699
+ * collection. Missing source ⇒ [] (fail-soft). The rows come from the
1700
+ * DERIVED source records (`byId`), so `display`/`filter` on a derived
1701
+ * source column (an invoice `total`) works. */
1702
+ function projectBacklinks(field, schema, enriched, linked) {
1703
+ const source = linked[field.from];
1704
+ if (!source) return [];
1705
+ return require_calendarGrid.backlinkRows(field, String(enriched[schema.primaryKey] ?? ""), Object.values(source.byId)).map((row) => require_calendarGrid.projectBacklinkRow(row, field.display, source.schema.primaryKey));
1706
+ }
1374
1707
  /** Project the computed (never-stored) field kinds onto one derived
1375
1708
  * record: `toggle` → boolean off its enum, `embed` → the target record
1376
- * (fixed `id` or per-record `idField`), or null when missing. */
1709
+ * (fixed `id` or per-record `idField`), or null when missing,
1710
+ * `backlinks` → the matching source rows (see `projectBacklinks`). */
1377
1711
  function projectComputed(schema, enriched, linked) {
1378
1712
  for (const [key, field] of Object.entries(schema.fields)) {
1379
1713
  if (field.type === "toggle" && field.field) enriched[key] = String(enriched[field.field] ?? "") === field.onValue;
1380
1714
  if (field.type === "embed" && field.to) {
1381
- const targetId = require_deriveAll.embedTargetId(field, enriched);
1715
+ const targetId = require_calendarGrid.embedTargetId(field, enriched);
1382
1716
  enriched[key] = targetId && linked[field.to]?.byId[targetId] || null;
1383
1717
  }
1718
+ if (field.type === "backlinks") enriched[key] = projectBacklinks(field, schema, enriched, linked);
1384
1719
  }
1385
1720
  return enriched;
1386
1721
  }
@@ -1392,7 +1727,7 @@ async function enrichItems(collection, items, opts = {}) {
1392
1727
  const { schema } = collection;
1393
1728
  const linked = await loadLinkedTargets(schema, opts);
1394
1729
  const refRecords = toRefRecords(linked);
1395
- return items.map((item) => projectComputed(schema, require_deriveAll.deriveAll(schema, item, refRecords), linked));
1730
+ return items.map((item) => projectComputed(schema, require_calendarGrid.deriveAll(schema, item, refRecords), linked));
1396
1731
  }
1397
1732
  //#endregion
1398
1733
  //#region src/collection/server/dynamicIcon.ts
@@ -1428,9 +1763,9 @@ async function computeCollectionIcon(collection, opts = {}) {
1428
1763
  const source = await loadCollection(spec.source.collection, opts);
1429
1764
  if (!source) return spec.fallback ?? schema.icon;
1430
1765
  const ordered = sortByPrimaryKey(await listItems(source.dataDir, { workspaceRoot: opts.workspaceRoot }), source.schema.primaryKey);
1431
- const orderBy = spec.source.orderBy ?? require_deriveAll.firstDateField(source.schema);
1766
+ const orderBy = spec.source.orderBy ?? require_calendarGrid.firstDateField(source.schema);
1432
1767
  const recordsById = buildRecordsById(ordered, source.schema.primaryKey);
1433
- return require_deriveAll.resolveIcon(require_deriveAll.selectDynamicRecord(ordered, spec.source, orderBy, recordsById), spec, schema.icon, recordsById);
1768
+ return require_calendarGrid.resolveIcon(require_calendarGrid.selectDynamicRecord(ordered, spec.source, orderBy, recordsById), spec, schema.icon, recordsById);
1434
1769
  } catch (err) {
1435
1770
  log.warn("collections", "dynamic icon compute failed, falling back", {
1436
1771
  slug: collection.slug,
@@ -1564,7 +1899,7 @@ function matchesWhen(when, schema, item) {
1564
1899
  * Discovery rejects a map that doesn't cover the enum's values, so null
1565
1900
  * here means a record that predates a map/enum edit. */
1566
1901
  function resolveEvery(every, sourceItem) {
1567
- if (!require_deriveAll.isFieldDrivenEvery(every)) return every;
1902
+ if (!require_calendarGrid.isFieldDrivenEvery(every)) return every;
1568
1903
  const raw = sourceItem[every.fromField];
1569
1904
  if (raw === void 0 || raw === null || raw === "") return null;
1570
1905
  return every.map[String(raw)] ?? null;
@@ -1604,7 +1939,7 @@ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
1604
1939
  });
1605
1940
  return;
1606
1941
  }
1607
- const fromField = require_deriveAll.isFieldDrivenEvery(every) ? every.fromField : void 0;
1942
+ const fromField = require_calendarGrid.isFieldDrivenEvery(every) ? every.fromField : void 0;
1608
1943
  log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
1609
1944
  slug,
1610
1945
  sourceId,
@@ -1917,12 +2252,6 @@ async function deleteCustomView(collection, viewId, opts = {}) {
1917
2252
  };
1918
2253
  }
1919
2254
  //#endregion
1920
- Object.defineProperty(exports, "COMPUTED_TYPES", {
1921
- enumerable: true,
1922
- get: function() {
1923
- return COMPUTED_TYPES;
1924
- }
1925
- });
1926
2255
  Object.defineProperty(exports, "CollectionSchemaZ", {
1927
2256
  enumerable: true,
1928
2257
  get: function() {
@@ -1965,12 +2294,24 @@ Object.defineProperty(exports, "buildCollectionActionSeedPrompt", {
1965
2294
  return buildCollectionActionSeedPrompt;
1966
2295
  }
1967
2296
  });
2297
+ Object.defineProperty(exports, "buildWorkspaceOntology", {
2298
+ enumerable: true,
2299
+ get: function() {
2300
+ return buildWorkspaceOntology;
2301
+ }
2302
+ });
1968
2303
  Object.defineProperty(exports, "collectionsRegistriesConfigPath", {
1969
2304
  enumerable: true,
1970
2305
  get: function() {
1971
2306
  return collectionsRegistriesConfigPath;
1972
2307
  }
1973
2308
  });
2309
+ Object.defineProperty(exports, "compileRecordZ", {
2310
+ enumerable: true,
2311
+ get: function() {
2312
+ return compileRecordZ;
2313
+ }
2314
+ });
1974
2315
  Object.defineProperty(exports, "computeCollectionIcon", {
1975
2316
  enumerable: true,
1976
2317
  get: function() {
@@ -2067,6 +2408,12 @@ Object.defineProperty(exports, "isContainedInWorkspace", {
2067
2408
  return isContainedInWorkspace;
2068
2409
  }
2069
2410
  });
2411
+ Object.defineProperty(exports, "isRegularFile", {
2412
+ enumerable: true,
2413
+ get: function() {
2414
+ return isRegularFile;
2415
+ }
2416
+ });
2070
2417
  Object.defineProperty(exports, "isTriggerDue", {
2071
2418
  enumerable: true,
2072
2419
  get: function() {
@@ -2145,6 +2492,12 @@ Object.defineProperty(exports, "readSkillTemplate", {
2145
2492
  return readSkillTemplate;
2146
2493
  }
2147
2494
  });
2495
+ Object.defineProperty(exports, "recordFieldProblem", {
2496
+ enumerable: true,
2497
+ get: function() {
2498
+ return recordFieldProblem;
2499
+ }
2500
+ });
2148
2501
  Object.defineProperty(exports, "resolveCreateItemId", {
2149
2502
  enumerable: true,
2150
2503
  get: function() {
@@ -2181,6 +2534,12 @@ Object.defineProperty(exports, "safeSlugName", {
2181
2534
  return safeSlugName;
2182
2535
  }
2183
2536
  });
2537
+ Object.defineProperty(exports, "schemaRelations", {
2538
+ enumerable: true,
2539
+ get: function() {
2540
+ return schemaRelations;
2541
+ }
2542
+ });
2184
2543
  Object.defineProperty(exports, "setCollectionChangePublisher", {
2185
2544
  enumerable: true,
2186
2545
  get: function() {
@@ -2230,4 +2589,4 @@ Object.defineProperty(exports, "writeItem", {
2230
2589
  }
2231
2590
  });
2232
2591
 
2233
- //# sourceMappingURL=server-G6GtOdaW.cjs.map
2592
+ //# sourceMappingURL=server-bsNrf2yW.cjs.map