@mulmoclaude/core 4.0.0 → 4.1.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 (35) hide show
  1. package/assets/helps/collection-skills.md +6 -1
  2. package/assets/helps/error-recovery.md +56 -19
  3. package/dist/collection/core/draft.d.ts +9 -1
  4. package/dist/collection/core/fieldDefaults.d.ts +28 -0
  5. package/dist/collection/core/schemaZ.d.ts +3 -0
  6. package/dist/collection/index.cjs +18 -7
  7. package/dist/collection/index.cjs.map +1 -1
  8. package/dist/collection/index.d.ts +1 -0
  9. package/dist/collection/index.js +16 -8
  10. package/dist/collection/index.js.map +1 -1
  11. package/dist/collection/registry/server/index.cjs +2 -2
  12. package/dist/collection/registry/server/index.js +2 -2
  13. package/dist/collection/server/index.cjs +2 -2
  14. package/dist/collection/server/index.js +2 -2
  15. package/dist/collection-watchers/index.cjs +3 -3
  16. package/dist/collection-watchers/index.js +3 -3
  17. package/dist/{discovery-C_3gXoin.cjs → discovery-B9EESjHP.cjs} +15 -3
  18. package/dist/{discovery-C_3gXoin.cjs.map → discovery-B9EESjHP.cjs.map} +1 -1
  19. package/dist/{discovery-CQp0wi-n.js → discovery-D88x2DmW.js} +15 -3
  20. package/dist/{discovery-CQp0wi-n.js.map → discovery-D88x2DmW.js.map} +1 -1
  21. package/dist/feeds/server/index.cjs +2 -2
  22. package/dist/feeds/server/index.js +2 -2
  23. package/dist/google/index.cjs +1 -1
  24. package/dist/google/index.js +1 -1
  25. package/dist/{promptSafety-CSdyp7RF.js → promptSafety-CJ-sC1pX.js} +43 -2
  26. package/dist/promptSafety-CJ-sC1pX.js.map +1 -0
  27. package/dist/{promptSafety-CWqYq-NS.cjs → promptSafety-CuczAN9d.cjs} +60 -1
  28. package/dist/promptSafety-CuczAN9d.cjs.map +1 -0
  29. package/dist/{server-CF_tGCGP.cjs → server-DSrRdPvf.cjs} +11 -5
  30. package/dist/{server-CF_tGCGP.cjs.map → server-DSrRdPvf.cjs.map} +1 -1
  31. package/dist/{server-BIewQYpC.js → server-dijB4BYv.js} +11 -5
  32. package/dist/{server-BIewQYpC.js.map → server-dijB4BYv.js.map} +1 -1
  33. package/package.json +1 -1
  34. package/dist/promptSafety-CSdyp7RF.js.map +0 -1
  35. package/dist/promptSafety-CWqYq-NS.cjs.map +0 -1
@@ -200,7 +200,12 @@ Every field spec needs a `type` and a `label`. Extra keys by type:
200
200
  "date column + separate time column" shape, keep `date` and point
201
201
  `calendarTimeField` at the time string instead.
202
202
  - **`enum`** — `values: ["draft", "sent", "paid"]` (non-empty strings). Renders
203
- a `<select>`; stored as a plain string.
203
+ a `<select>`; stored as a plain string. Optional `default: "draft"` pre-fills
204
+ a NEW record — the Add form opens on it, and a `putItems` row in `create` mode
205
+ that omits the field gets it. The value must be one of `values` (`putSchema`
206
+ refuses otherwise). `default` pairs with `required: true`: the requirement is
207
+ already satisfied, so the user just saves. It is NOT re-applied by `upsert` or
208
+ `merge` — those edit a record that already carries an answer.
204
209
  - **`money`** — `currency: "USD"` (ISO 4217, defaults to USD). Stored as a plain
205
210
  decimal; currency is display-only.
206
211
  - **`ref`** — `to: "<target-slug>"`. Stores the target record's primary-key
@@ -800,40 +800,77 @@ shipped builders produce, and asserts `handlePermission` comes back over the MCP
800
800
 
801
801
  ### Cause
802
802
 
803
- `Dockerfile.sandbox` installs the CLI unpinned (`RUN npm install -g @anthropic-ai/claude-code
804
- tsx`), so the image freezes whatever was latest when it was built. Two mechanisms then keep it
805
- frozen:
803
+ The image freezes whichever CLI was current when it was built, and
804
+ `ensureSandboxImage()` (`server/system/docker.ts`) rebuilds only when the **Dockerfile's SHA**
805
+ changes. A new CLI release upstream doesn't change that SHA, so nothing retriggers the build.
806
806
 
807
- 1. `ensureSandboxImage()` (`server/system/docker.ts`) rebuilds only when the **Dockerfile's
808
- SHA** changes. A new CLI release upstream doesn't change that SHA, so nothing retriggers.
809
- 2. Deleting the image does not delete the **build cache**. The rebuild reuses the cached
810
- `npm install -g` layer (it reports `CACHED` in 0.0s) and reinstalls nothing.
807
+ ### Fix
811
808
 
812
- So the usual "remove it and let it rebuild" reflex genuinely does nothing here, which is why
813
- this one burns time.
809
+ Read it off the boot log — since #2842 the server records the version at build time as an image
810
+ label and prints it on every start, so no container has to be run to find out:
814
811
 
815
- ### Fix
812
+ ```
813
+ INFO [sandbox] sandbox image claudeCodeVersion=2.1.220 ageDays=3
814
+ ```
816
815
 
817
- Check the version inside the image — the host's `claude --version` says nothing about it, and
818
- the entrypoint override is required because the image's ENTRYPOINT is the sandbox script:
816
+ `claudeCodeVersion=unrecorded` means the image predates the label (or was built with npm
817
+ unreachable). In that case ask the image directly — the entrypoint override is required because
818
+ the image's ENTRYPOINT is the sandbox script, and the host's own `claude --version` says nothing
819
+ about what is inside:
819
820
 
820
821
  ```bash
821
822
  docker run --rm --entrypoint claude mulmoclaude-sandbox --version
822
823
  ```
823
824
 
824
- If it's behind, drop the image AND the build cache, then let the next run rebuild:
825
+ If it's behind, drop the image and let the next run rebuild:
825
826
 
826
827
  ```bash
827
828
  yarn sandbox:remove # docker rmi mulmoclaude-sandbox
828
- docker builder prune -a -f # the step that actually invalidates the npm layer
829
829
  ```
830
830
 
831
- `docker builder prune -a -f` clears the builder cache for the whole daemon, not just this
832
- image, so unrelated builds are slower once afterwards. That is the cost of the fix, not a
833
- sign something went wrong.
831
+ Two warns fire on their own when this is worth acting on: `sandbox image is stale` past 30 days,
832
+ and `Claude CLI in the sandbox image is older than the version our MCP config needs` below
833
+ 2.1.121 (the floor for `alwaysLoad`).
834
+
835
+ > **Older than #2842?** Back then the rebuild reused the cached `npm install -g` layer (reported
836
+ > `CACHED` in 0.0s) and reinstalled nothing, so `yarn sandbox:remove` alone genuinely did nothing
837
+ > and `docker builder prune -a -f` was needed. The Dockerfile now installs an exact
838
+ > `@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}` resolved on the host, so a moved CLI changes
839
+ > the layer's own command string and invalidates it. Don't reach for `builder prune` first — it
840
+ > clears the cache for the whole daemon.
841
+
842
+ The CLI is deliberately left unpinned by default (#2202), so this can recur whenever an upstream
843
+ fix matters to you — the log line above is the way to rule it in or out.
844
+
845
+ ## A turn dies on `handlePermission not found` — read the broker lines before anything else
846
+
847
+ ### Symptoms
848
+
849
+ The same `MCP tool mcp__mulmoclaude__handlePermission ... not found` as the three sections
850
+ above, and you can't yet tell WHICH of them you are in.
851
+
852
+ ### Cause
853
+
854
+ They are genuinely different failures — a permanent load failure, a startup race, a frozen CLI —
855
+ and guessing between them is what makes this expensive. Since #2842 the log answers it directly,
856
+ so read these three before forming a theory:
857
+
858
+ | Log line | What it tells you |
859
+ |---|---|
860
+ | `spawning agent … broker=tsx` | This install is on the SLOW path: the bundle is missing, so the broker is transcoded from source on every spawn (seconds to tens of seconds over a Windows/macOS bind mount). A `broker bundle missing` warn accompanies it once per process. |
861
+ | `[mcp] broker ready bootMs=… initializeMs=…` | The broker DID connect, and how long it took. `broker cold boot is slow` replaces it past 5 s. |
862
+ | `brokerEverReady=false` on the retry warn | No beacon ever arrived for that chat — the broker did not come up at all, so the automatic 3 s replay will reproduce the same error and only double the wait. |
863
+
864
+ ### Fix
834
865
 
835
- The CLI is deliberately left unpinned (#2202), so this can recur whenever an upstream fix
836
- matters to you — the check above is the way to rule it in or out.
866
+ - `broker=tsx` → this is the cold-boot cost, not the connect-wait ceiling. In a dev checkout run
867
+ `yarn build:mcp-broker` (or plain `yarn build`); on an npm install, update `mulmoclaude` —
868
+ published launchers have shipped the bundle since 1.9.0.
869
+ - `broker ready` present with a small `initializeMs`, failing anyway → it is the startup race,
870
+ not the boot. See the scheduled-run section above.
871
+ - `brokerEverReady=false` with no `broker ready` line anywhere → the broker never started. That
872
+ is the permanent load failure; check the `Cannot find module` section.
873
+ - Old or unrecorded `claudeCodeVersion` alongside any of these → rule out the frozen CLI first.
837
874
 
838
875
  ---
839
876
 
@@ -4,7 +4,15 @@ import { EditState, TableRowDraft } from './uiTypes';
4
4
  export declare function emptyRow(subFields: Record<string, FieldSpec>): TableRowDraft;
5
5
  /** Build a row draft from an existing persisted row. */
6
6
  export declare function rowFromItem(item: Record<string, unknown>, subFields: Record<string, FieldSpec>): TableRowDraft;
7
- /** Convert a full edit draft to the record to persist. */
7
+ /** Convert a full edit draft to the record to persist.
8
+ *
9
+ * Collected as entries rather than assigned into an accumulator: a field may
10
+ * be named `__proto__` (JSON.parse hands that over as an own key, so it
11
+ * reaches here like any other), and `record[key] = value` would run the
12
+ * prototype setter for that one name — the user's value would vanish between
13
+ * the form and the save, with nothing to see. `fromEntries` defines an own
14
+ * property whatever the name is. The read side of this file already guards the
15
+ * same hazard with `ownFlag` (Codex review on #2910). */
8
16
  export declare function draftToRecord(state: EditState, schema: CollectionSchema): CollectionItem;
9
17
  /** Normalise a raw inline-edit input (table-cell checkbox/select) to its
10
18
  * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty
@@ -0,0 +1,28 @@
1
+ import { CollectionFieldSpec, CollectionSchema } from './schema.js';
2
+ /** The starting value for a field, or null when it declares none.
3
+ *
4
+ * A `default` outside `values` resolves to null rather than being handed on.
5
+ * `putSchema` refuses to write one, but the key was silently ignored before
6
+ * #2839, so a stale value may already sit in a file that discovery still
7
+ * loads — and putting an impossible value into a form yields a rejected save
8
+ * the author cannot explain from what they see. */
9
+ export declare function fieldDefaultValue(field: CollectionFieldSpec): string | null;
10
+ /** Every applicable default in a schema, keyed by field.
11
+ *
12
+ * Built through `Object.fromEntries` rather than by assigning into an
13
+ * accumulator, because a field may legitimately be named `__proto__` —
14
+ * `JSON.parse` hands that over as an OWN key, so it survives into the parsed
15
+ * schema (the same reason the primary-field resolver reaches for
16
+ * `Object.hasOwn`). `out[key] = value` on a plain object would run the
17
+ * prototype setter for that one name and drop the default with no error
18
+ * anywhere; `fromEntries` defines an own property either way (Codex review on
19
+ * #2910). */
20
+ export declare function schemaDefaults(schema: CollectionSchema): Record<string, string>;
21
+ /** The first `default` that names something `values` does not offer, for the
22
+ * WRITE path to refuse. Kept out of the parse so a file already carrying one
23
+ * keeps loading — see `fieldDefaultValue`. */
24
+ export declare function firstUnknownDefault(schema: CollectionSchema): {
25
+ key: string;
26
+ value: string;
27
+ values: string[];
28
+ } | null;
@@ -125,6 +125,7 @@ export declare const FieldSpecZ: z.ZodDiscriminatedUnion<[z.ZodObject<{
125
125
  type: z.ZodLiteral<"money">;
126
126
  }, z.core.$strip>, z.ZodObject<{
127
127
  values: z.ZodArray<z.ZodString>;
128
+ default: z.ZodOptional<z.ZodString>;
128
129
  label: z.ZodString;
129
130
  primary: z.ZodOptional<z.ZodBoolean>;
130
131
  required: z.ZodOptional<z.ZodBoolean>;
@@ -749,6 +750,7 @@ declare const CollectionObjectZ: z.ZodObject<{
749
750
  type: z.ZodLiteral<"money">;
750
751
  }, z.core.$strip>, z.ZodObject<{
751
752
  values: z.ZodArray<z.ZodString>;
753
+ default: z.ZodOptional<z.ZodString>;
752
754
  label: z.ZodString;
753
755
  primary: z.ZodOptional<z.ZodBoolean>;
754
756
  required: z.ZodOptional<z.ZodBoolean>;
@@ -1230,6 +1232,7 @@ export declare const CollectionSchemaZ: z.ZodPreprocess<z.ZodObject<{
1230
1232
  type: z.ZodLiteral<"money">;
1231
1233
  }, z.core.$strip>, z.ZodObject<{
1232
1234
  values: z.ZodArray<z.ZodString>;
1235
+ default: z.ZodOptional<z.ZodString>;
1233
1236
  label: z.ZodString;
1234
1237
  primary: z.ZodOptional<z.ZodBoolean>;
1235
1238
  required: z.ZodOptional<z.ZodBoolean>;
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_dist = require("../dist-CsgSfWwR.cjs");
3
3
  const require_itemId = require("../itemId-Dzrr3vs5.cjs");
4
+ const require_promptSafety = require("../promptSafety-CuczAN9d.cjs");
4
5
  const require_project = require("../project-C1ep9pvo.cjs");
5
- const require_promptSafety = require("../promptSafety-CWqYq-NS.cjs");
6
6
  //#region src/collection/core/presentCollection.ts
7
7
  var TOOL_NAME = "presentCollection";
8
8
  /** Stamp a host's opaque project scope onto a card payload. The ONLY way a
@@ -269,24 +269,32 @@ function rowDraftToRecord(rowDraft, subFields) {
269
269
  }
270
270
  return row;
271
271
  }
272
- /** Convert a full edit draft to the record to persist. */
272
+ /** Convert a full edit draft to the record to persist.
273
+ *
274
+ * Collected as entries rather than assigned into an accumulator: a field may
275
+ * be named `__proto__` (JSON.parse hands that over as an own key, so it
276
+ * reaches here like any other), and `record[key] = value` would run the
277
+ * prototype setter for that one name — the user's value would vanish between
278
+ * the form and the save, with nothing to see. `fromEntries` defines an own
279
+ * property whatever the name is. The read side of this file already guards the
280
+ * same hazard with `ownFlag` (Codex review on #2910). */
273
281
  function draftToRecord(state, schema) {
274
- const record = {};
282
+ const entries = [];
275
283
  for (const [key, field] of Object.entries(schema.fields)) {
276
284
  if (require_itemId.COMPUTED_TYPES.has(field.type)) continue;
277
285
  if (field.type === "boolean") {
278
- if (shouldEmitBoolean(state, key, field)) record[key] = ownFlag(state.bool, key);
286
+ if (shouldEmitBoolean(state, key, field)) entries.push([key, ownFlag(state.bool, key)]);
279
287
  continue;
280
288
  }
281
289
  if (field.type === "table" && field.of) {
282
290
  const subFields = field.of;
283
- record[key] = (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields));
291
+ entries.push([key, (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields))]);
284
292
  continue;
285
293
  }
286
294
  const value = scalarDraftToValue(state.text[key], field.type);
287
- if (value !== void 0) record[key] = value;
295
+ if (value !== void 0) entries.push([key, value]);
288
296
  }
289
- return record;
297
+ return Object.fromEntries(entries);
290
298
  }
291
299
  /** Normalise a raw inline-edit input (table-cell checkbox/select) to its
292
300
  * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty
@@ -673,11 +681,13 @@ exports.enumValueIndex = enumValueIndex;
673
681
  exports.errorMessage = require_dist.errorMessage;
674
682
  exports.evaluateDerived = require_promptSafety.evaluateDerived;
675
683
  exports.executePresentCollection = executePresentCollection;
684
+ exports.fieldDefaultValue = require_promptSafety.fieldDefaultValue;
676
685
  exports.fieldText = require_itemId.fieldText;
677
686
  exports.fieldTextOrNull = require_itemId.fieldTextOrNull;
678
687
  exports.fieldVisible = require_promptSafety.fieldVisible;
679
688
  exports.firstDateField = require_promptSafety.firstDateField;
680
689
  exports.firstMissingRequiredField = firstMissingRequiredField;
690
+ exports.firstUnknownDefault = require_promptSafety.firstUnknownDefault;
681
691
  exports.flagFieldValue = require_promptSafety.flagFieldValue;
682
692
  exports.generateUniqueId = require_itemId.generateUniqueId;
683
693
  exports.isFieldDrivenEvery = require_itemId.isFieldDrivenEvery;
@@ -717,6 +727,7 @@ exports.rowIdOf = rowIdOf;
717
727
  exports.sameCollectionCard = sameCollectionCard;
718
728
  exports.sameCollectionKey = require_itemId.sameCollectionKey;
719
729
  exports.scalarSortValue = scalarSortValue;
730
+ exports.schemaDefaults = require_promptSafety.schemaDefaults;
720
731
  exports.selectDynamicRecord = require_promptSafety.selectDynamicRecord;
721
732
  exports.sharedCollectionKey = require_itemId.sharedCollectionKey;
722
733
  exports.shortHexId = require_itemId.shortHexId;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/collection/core/presentCollection.ts","../../src/collection/core/enumColors.ts","../../src/collection/core/draft.ts","../../src/collection/core/chatSeed.ts","../../src/collection/core/sortItems.ts","../../src/collection/core/sortValueOf.ts","../../src/collection/core/textSearch.ts","../../src/collection/core/recordKeys.ts","../../src/collection/core/itemLabel.ts","../../src/collection/core/ontologyGraph.ts"],"sourcesContent":["// presentCollection tool — definition + pure server-side executor.\n//\n// Isomorphic (no Vue, no Node): bundled to the browser as the plugin's\n// `execute`, and run server-side via the host's plugin dispatch route. The\n// executor only validates + echoes the addressing; the live schema + items\n// are fetched client-side by the View through the host's /api/collections\n// routes, so a bad slug surfaces as the View's \"not found\" state.\n\nimport type { ToolContext, ToolDefinition, ToolResult } from \"gui-chat-protocol\";\n\nexport const TOOL_NAME = \"presentCollection\";\n\n/** Render payload carried in the tool result's `data` field; the View mounts\n * off these. Same shape as the tool args. */\nexport interface PresentCollectionData {\n /** Slug of the collection to display (e.g. \"clients\", \"invoices\"). */\n collectionSlug: string;\n /** Optional primary-key value of a single item to open on mount. */\n itemId?: string | undefined;\n /** Which ROOT this card's collection lives in, as a host-opaque scope token.\n *\n * A collection's identity is `(root, slug)`, but a card payload named only\n * the slug — so the host re-resolved it through whatever binding was current\n * when the card RENDERED, which in a multi-root host may be a different\n * project than the one the card was made in. A card built in project A could\n * then read project B's data.\n *\n * **Stamped by the HOST, never accepted from tool arguments.** The tool args\n * are a model-controlled channel: a model that could name the scope could\n * name another project's, and the card — and any token minted for it — would\n * address a project the user never opened in this conversation. So\n * `executePresentCollection` DROPS any `scope` it is handed, and a host adds\n * its own with {@link withCardScope} when it applies the result, where it\n * already knows which binding produced the call.\n *\n * Host-opaque: pass an opaque project id, NEVER an absolute path — this\n * payload reaches the browser and, through a view, an LLM-authored iframe.\n *\n * Absent — the single-workspace case — the payload and every reconciliation\n * decision are exactly what they were before this field existed. */\n scope?: string | undefined;\n}\n\n/** Stamp a host's opaque project scope onto a card payload. The ONLY way a\n * scope gets in: the executor refuses to take one from tool arguments (see\n * `PresentCollectionData.scope`). `undefined` returns the payload untouched,\n * so a single-workspace host calling this unconditionally still produces a\n * payload with no `scope` property at all. */\nexport function withCardScope(data: PresentCollectionData, scope: string | undefined): PresentCollectionData {\n const trimmed = typeof scope === \"string\" ? scope.trim() : \"\";\n return trimmed.length > 0 ? { ...data, scope: trimmed } : data;\n}\n\n/** A card's identity for reconciliation. Two cards match only when they name\n * the same collection IN THE SAME SCOPE, so two projects' `tasks` cards stay\n * two cards. With no scope on either side this is the slug, unchanged. */\nexport function collectionCardKey(data: Pick<PresentCollectionData, \"collectionSlug\" | \"scope\">): string {\n return data.scope === undefined ? data.collectionSlug : `${data.scope}\\u0000${data.collectionSlug}`;\n}\n\n/** Do two card payloads address the same card? The reconciliation predicate a\n * host should use instead of comparing slugs. */\nexport function sameCollectionCard(\n one: Pick<PresentCollectionData, \"collectionSlug\" | \"scope\">,\n other: Pick<PresentCollectionData, \"collectionSlug\" | \"scope\">,\n): boolean {\n return collectionCardKey(one) === collectionCardKey(other);\n}\n\nexport type PresentCollectionArgs = PresentCollectionData;\n\nexport const TOOL_DEFINITION: ToolDefinition = {\n type: \"function\",\n name: TOOL_NAME,\n description:\n \"Display a schema-driven collection inline in the chat as an interactive, editable card. Shows the collection's list of records. Pass `itemId` to open one specific record on mount.\",\n parameters: {\n type: \"object\",\n properties: {\n collectionSlug: {\n type: \"string\",\n description: \"The slug of the collection to display (e.g. 'clients', 'invoices', 'contacts').\",\n },\n itemId: {\n type: \"string\",\n description: \"Optional primary-key value of a single record to open in detail view on mount. Omit to show the full list.\",\n },\n },\n required: [\"collectionSlug\"],\n },\n prompt: `After making changes to schema-driven collections, use ${TOOL_NAME} to present either the collection or the item`,\n};\n\n/** A trimmed non-empty string, or undefined. The tool's args arrive from an\n * LLM (and `scope` from the host), so every field is validated the same way\n * rather than trusted. */\nconst cleaned = (value: unknown): string | undefined => (typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined);\n\n/** Normalise the tool args into the card payload, or `null` when the required\n * slug is missing. Pure and exported so the addressing rules are testable\n * without going through a `ToolResult`.\n *\n * `scope` is deliberately NOT read from the args — see\n * `PresentCollectionData.scope`. Everything here comes from a model-controlled\n * channel, and the scope decides which project the card reads; a host stamps\n * it afterwards with `withCardScope`.\n *\n * Keys are added one at a time rather than spread with `undefined` values, so\n * a payload with no `itemId` OMITS the property entirely — which is what keeps\n * a single-workspace card byte-identical to what it has always been, in both\n * `deepEqual` and JSON. */\nexport const toPresentCollectionData = (args: PresentCollectionArgs): PresentCollectionData | null => {\n const collectionSlug = cleaned(args?.collectionSlug);\n if (!collectionSlug) return null;\n const itemId = cleaned(args.itemId);\n return { collectionSlug, ...(itemId ? { itemId } : {}) };\n};\n\nexport const executePresentCollection = async (\n _context: ToolContext,\n args: PresentCollectionArgs,\n): Promise<ToolResult<PresentCollectionData, PresentCollectionData>> => {\n const data = toPresentCollectionData(args);\n if (!data) {\n return {\n message: \"presentCollection error: collectionSlug is required\",\n instructions: \"Tell the user you couldn't display the collection because no collection was specified, and ask which collection they mean.\",\n };\n }\n const { collectionSlug, itemId } = data;\n const target = itemId ? `${collectionSlug} / ${itemId}` : collectionSlug;\n return {\n message: `Presented collection ${target}`,\n // `data` is the view's source (also the host's render-eligibility signal);\n // `jsonData` is what the LLM sees. Same payload, two audiences.\n data,\n jsonData: data,\n // Phrased to stay correct for BOTH storage kinds without loading the\n // schema (this executor is deliberately pure/isomorphic): a writable\n // collection offers edit/create/delete in the card; a read-only\n // `dataSource` collection hides those and changes flow through its\n // data file instead.\n instructions:\n \"The collection has been presented to the user as an interactive card. They can browse and open records directly; on a writable collection they can also edit, create, and delete (a read-only dataSource collection shows no edit controls — its records change by editing the backing data file). No further action is needed unless they ask.\",\n };\n};\n","// A standard, ordered colour palette for `enum` field values, shared by every\n// collection surface (list, calendar, kanban). Each value in an\n// enum's `values` array is assigned the palette entry at its index — cycling\n// when an enum declares more values than the palette holds — so colouring is\n// automatic and consistent without any per-value schema config.\n//\n// Tailwind only detects class names that appear as complete string literals,\n// so every surface's classes are spelled out per colour below rather than\n// built from a colour name at runtime.\n//\n// One enum is special: the field a schema's `notifyWhen` targets (the\n// \"notification enum\"). Its flagged values read the notification severity\n// colours — the first flagged value (most urgent) red, the rest amber — and\n// every other value reads neutral grey, mirroring the notification bell\n// (red = urgent, amber = nudge) rather than the rotating palette.\n// `resolveEnumColor` encapsulates that rule.\n\nimport { fieldText, fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionSchema } from \"./schema\";\n\nexport interface EnumColorClasses {\n /** Stat-card style: border + fill + text + hover. */\n card: string;\n /** Small status dot (kanban column header). */\n dot: string;\n /** Pill / badge / inline `<select>` fill + text (no border width). */\n badge: string;\n /** Border colour, paired with a `border` width class by the caller. */\n border: string;\n}\n\n// The palette deliberately EXCLUDES the warm warning band (red / orange /\n// amber): those are reserved for the notification-enum severity colours\n// (`ENUM_ALERT` red, `ENUM_NUDGE` amber), so a normal enum value can never\n// draw a colour that reads like a notification. Eight well-separated cool /\n// green / magenta hues keep enough variety for wide enums.\nconst PALETTE: readonly EnumColorClasses[] = [\n {\n card: \"border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100\",\n dot: \"bg-indigo-500\",\n badge: \"bg-indigo-100 text-indigo-700\",\n border: \"border-indigo-200\",\n },\n { card: \"border-sky-200 bg-sky-50 text-sky-600 hover:bg-sky-100\", dot: \"bg-sky-500\", badge: \"bg-sky-100 text-sky-700\", border: \"border-sky-200\" },\n { card: \"border-cyan-200 bg-cyan-50 text-cyan-600 hover:bg-cyan-100\", dot: \"bg-cyan-500\", badge: \"bg-cyan-100 text-cyan-700\", border: \"border-cyan-200\" },\n { card: \"border-teal-200 bg-teal-50 text-teal-600 hover:bg-teal-100\", dot: \"bg-teal-500\", badge: \"bg-teal-100 text-teal-700\", border: \"border-teal-200\" },\n {\n card: \"border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100\",\n dot: \"bg-emerald-500\",\n badge: \"bg-emerald-100 text-emerald-700\",\n border: \"border-emerald-200\",\n },\n { card: \"border-lime-200 bg-lime-50 text-lime-600 hover:bg-lime-100\", dot: \"bg-lime-500\", badge: \"bg-lime-100 text-lime-700\", border: \"border-lime-200\" },\n {\n card: \"border-violet-200 bg-violet-50 text-violet-600 hover:bg-violet-100\",\n dot: \"bg-violet-500\",\n badge: \"bg-violet-100 text-violet-700\",\n border: \"border-violet-200\",\n },\n {\n card: \"border-fuchsia-200 bg-fuchsia-50 text-fuchsia-600 hover:bg-fuchsia-100\",\n dot: \"bg-fuchsia-500\",\n badge: \"bg-fuchsia-100 text-fuchsia-700\",\n border: \"border-fuchsia-200\",\n },\n];\n\n/** Neutral styling for the empty / Uncategorized bucket — never a palette\n * colour, so an unset or unknown value reads grey across every surface. */\nexport const ENUM_NEUTRAL: EnumColorClasses = {\n card: \"border-slate-200 bg-slate-50 text-slate-500 hover:bg-slate-100\",\n dot: \"bg-slate-300\",\n badge: \"bg-slate-100 text-slate-500\",\n border: \"border-slate-200\",\n};\n\n/** The urgent notification colour (red), matching the bell's `urgent`\n * severity. The first value a schema's `notifyWhen` flags reads this. */\nexport const ENUM_ALERT: EnumColorClasses = {\n card: \"border-red-200 bg-red-50 text-red-600 hover:bg-red-100\",\n dot: \"bg-red-500\",\n badge: \"bg-red-100 text-red-700\",\n border: \"border-red-200\",\n};\n\n/** The nudge notification colour (amber), matching the bell's `nudge`\n * severity. Flagged `notifyWhen` values after the first read this. */\nexport const ENUM_NUDGE: EnumColorClasses = {\n card: \"border-amber-200 bg-amber-50 text-amber-600 hover:bg-amber-100\",\n dot: \"bg-amber-500\",\n badge: \"bg-amber-100 text-amber-700\",\n border: \"border-amber-200\",\n};\n\n/** Classes for the enum value at `index` in its field's `values` array. A\n * negative index (value unset or not among the declared values) reads\n * neutral. */\nexport function enumColorClasses(index: number): EnumColorClasses {\n if (index < 0) return ENUM_NEUTRAL;\n return PALETTE[index % PALETTE.length] ?? ENUM_NEUTRAL;\n}\n\n/** Index of `value` within an enum field's declared `values`, or -1 when the\n * value is empty / unknown (→ neutral). */\nexport function enumValueIndex(values: readonly string[] | undefined, value: unknown): number {\n // No text form (array/object) means no enum member — \"[object Object]\" must\n // not index into the palette.\n const text = fieldTextOrNull(value);\n if (text === null || text === \"\") return -1;\n return values?.indexOf(text) ?? -1;\n}\n\n/** The flagged values when `fieldKey` is the schema's `notifyWhen` target (the\n * \"notification enum\"); undefined for every other field. */\nfunction notifyValuesFor(schema: CollectionSchema, fieldKey: string): readonly string[] | undefined {\n const spec = schema.notifyWhen;\n return spec && spec.field === fieldKey ? spec.in : undefined;\n}\n\n/** Resolve a value's colour for enum field `fieldKey`:\n * - Notification enum (`notifyWhen` targets it): the first flagged value (in\n * `notifyWhen.in` order, the most urgent) reads notification red, the rest\n * amber, and every non-flagged value neutral grey.\n * - Any other enum: the standard palette by the value's declared index. */\nexport function resolveEnumColor(schema: CollectionSchema, fieldKey: string, value: unknown): EnumColorClasses {\n const notifyValues = notifyValuesFor(schema, fieldKey);\n if (notifyValues) {\n const str = fieldText(value);\n const rank = notifyValues.indexOf(str);\n if (rank < 0) return ENUM_NEUTRAL;\n return rank === 0 ? ENUM_ALERT : ENUM_NUDGE;\n }\n const spec = schema.fields[fieldKey];\n return enumColorClasses(enumValueIndex(spec?.type === \"enum\" ? spec.values : undefined, value));\n}\n","// Pure edit-draft logic for the collection record form, extracted from\n// CollectionView.vue so the parent (build draft / save / validate) and\n// the extracted CollectionRecordPanel (per-row table mutators) share one\n// implementation. No Vue, no I/O — every function maps a draft + schema\n// to a value, so the omission/validation semantics are unit-testable.\n\nimport { fieldText } from \"./fieldText\";\nimport { fieldVisible } from \"./actionVisible\";\nimport { COMPUTED_TYPES } from \"./schema\";\nimport type { CollectionFieldSpec as FieldSpec, CollectionFieldType as FieldType, CollectionItem, CollectionSchema } from \"./schema\";\nimport type { EditState, TableRowDraft } from \"./uiTypes\";\n\n/** Build a row draft, reading each sub-field's persisted value via\n * `readSub`. An empty row passes a reader that always returns\n * `undefined`; a row-from-item reads the source record. Boolean\n * presence uses `typeof raw === \"boolean\"` so an existing explicit\n * `false` is recorded as present and round-trips on a no-op save. */\nfunction buildTableRowDraft(subFields: Record<string, FieldSpec>, readSub: (subKey: string) => unknown): TableRowDraft {\n const text: Record<string, string> = {};\n const bool: Record<string, boolean> = {};\n const boolOriginallyPresent: Record<string, boolean> = {};\n const boolTouched: Record<string, boolean> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n const raw = readSub(subKey);\n if (subField.type === \"boolean\") {\n bool[subKey] = raw === true;\n boolOriginallyPresent[subKey] = typeof raw === \"boolean\";\n boolTouched[subKey] = false;\n } else {\n text[subKey] = fieldText(raw);\n }\n }\n return { text, bool, boolOriginallyPresent, boolTouched };\n}\n\n/** A fresh, empty row draft for a `table` field's sub-schema. */\nexport function emptyRow(subFields: Record<string, FieldSpec>): TableRowDraft {\n return buildTableRowDraft(subFields, () => undefined);\n}\n\n/** Build a row draft from an existing persisted row. */\nexport function rowFromItem(item: Record<string, unknown>, subFields: Record<string, FieldSpec>): TableRowDraft {\n return buildTableRowDraft(subFields, (subKey) => item[subKey]);\n}\n\n/** Own-property boolean read: a field named `toString` / `constructor` /\n * `__proto__` must read `false` here, not inherit a truthy Object.prototype\n * member and force an untouched boolean into every saved record. */\nfunction ownFlag(flags: Record<string, boolean>, key: string): boolean {\n return Object.hasOwn(flags, key) && flags[key] === true;\n}\n\n/** Decide whether a boolean field's draft value should be emitted (vs.\n * omitted so a downstream default applies). */\nfunction shouldEmitBoolean(state: EditState, key: string, field: FieldSpec): boolean {\n return ownFlag(state.boolOriginallyPresent, key) || ownFlag(state.boolTouched, key) || Boolean(field.required);\n}\n\n/** Convert a scalar draft slot to its persisted form. `undefined` = omit. */\nfunction scalarDraftToValue(raw: string | undefined, fieldType: FieldType): unknown {\n if (raw === undefined || raw === \"\") return undefined;\n if (fieldType === \"number\" || fieldType === \"money\") {\n const num = Number(raw);\n return Number.isFinite(num) ? num : raw;\n }\n return raw;\n}\n\n/** Convert one table row draft to its persisted record. */\nfunction rowDraftToRecord(rowDraft: TableRowDraft, subFields: Record<string, FieldSpec>): Record<string, unknown> {\n const row: Record<string, unknown> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n if (subField.type === \"boolean\") {\n const value = ownFlag(rowDraft.bool, subKey);\n if (ownFlag(rowDraft.boolOriginallyPresent, subKey) || ownFlag(rowDraft.boolTouched, subKey) || value || subField.required) row[subKey] = value;\n continue;\n }\n const value = scalarDraftToValue(rowDraft.text[subKey], subField.type);\n if (value !== undefined) row[subKey] = value;\n }\n return row;\n}\n\n/** Convert a full edit draft to the record to persist. */\nexport function draftToRecord(state: EditState, schema: CollectionSchema): CollectionItem {\n const record: CollectionItem = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n if (COMPUTED_TYPES.has(field.type)) continue; // never persisted (toggle projects an enum field)\n if (field.type === \"boolean\") {\n if (shouldEmitBoolean(state, key, field)) record[key] = ownFlag(state.bool, key);\n continue;\n }\n if (field.type === \"table\" && field.of) {\n const subFields = field.of;\n record[key] = (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields));\n continue;\n }\n const value = scalarDraftToValue(state.text[key], field.type);\n if (value !== undefined) record[key] = value;\n }\n return record;\n}\n\n/** Normalise a raw inline-edit input (table-cell checkbox/select) to its\n * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty\n * enum selection (the placeholder option) clears the field via `undefined`. */\nexport function coerceInlineValue(field: FieldSpec, raw: boolean | string): unknown {\n if (field.type === \"boolean\") return raw === true;\n return raw === \"\" ? undefined : raw;\n}\n\n/** Build the full record to PUT for a single-cell inline edit, without\n * mutating `item`. A `undefined` value omits the key (enum cleared),\n * matching `draftToRecord`'s omission semantics. */\nexport function buildUpdatedRecord(item: CollectionItem, key: string, value: unknown): CollectionItem {\n if (value === undefined) {\n const { [key]: __omit, ...rest } = item;\n return rest;\n }\n return { ...item, [key]: value };\n}\n\n/** Empty for required-field validation — NOT a truthiness check (a\n * numeric `0` is a filled value). */\nfunction isMissingDraftValue(value: unknown): boolean {\n return value === undefined || value === null || value === \"\";\n}\n\n/** Label of the first required table sub-field empty in any row, else null. */\nfunction firstMissingTableSubField(field: FieldSpec, rows: TableRowDraft[] | undefined): string | null {\n if (field.type !== \"table\" || !rows) return null;\n for (const [rowIdx, row] of rows.entries()) {\n for (const [subKey, subField] of Object.entries(field.of)) {\n if (!subField.required || subField.type === \"boolean\") continue;\n if (isMissingDraftValue(row.text[subKey])) return `${field.label} #${rowIdx + 1}: ${subField.label}`;\n }\n }\n return null;\n}\n\n/** Field types the required check never flags: booleans always hold a\n * value, and the computed/projected kinds (COMPUTED_TYPES — the shared\n * set, so a future computed type can't drift past this check) have no\n * draft input at all. */\nconst NEVER_MISSING_TYPES: ReadonlySet<FieldType> = new Set<FieldType>([\"boolean\", ...COMPUTED_TYPES]);\n\nfunction validateOneField(key: string, field: FieldSpec, draft: EditState, record: CollectionItem): string | null {\n // A `when`-hidden field has no input the user can fill — never missing.\n if (!fieldVisible(field, record)) return null;\n if (field.type === \"table\" && field.of) {\n const rows = draft.table[key];\n if (field.required && (!rows || rows.length === 0)) return field.label;\n return firstMissingTableSubField(field, rows);\n }\n if (!field.required) return null;\n if (draft.mode === \"create\" && field.primary === true) return null; // server auto-generates id\n if (NEVER_MISSING_TYPES.has(field.type)) return null;\n return isMissingDraftValue(draft.text[key]) ? field.label : null;\n}\n\n/** Human-readable label of the first missing required field, or null. */\nexport function firstMissingRequiredField(draft: EditState, schema: CollectionSchema): string | null {\n const record = draftToRecord(draft, schema);\n for (const [key, field] of Object.entries(schema.fields)) {\n const missing = validateOneField(key, field, draft, record);\n if (missing) return missing;\n }\n return null;\n}\n","// The slash-command chat seed a collection view sends to the agent when the\n// user starts a chat from a skill-backed collection or one of its records.\n// Skill-backed collections have a `/<slug>` command, so seed that. (Feeds are\n// skill-less and seeded differently; that branch stays in the view because it\n// is i18n-driven.)\n\n/** `/<slug> <message>`, or `/<slug> id=<itemId> <message>` when a specific\n * record is addressed. The `id=` selector goes BEFORE the free-text message so\n * the skill's argument parser reads it as a flag rather than as prose. */\nexport function skillCommandSeed(slug: string, message: string, itemId?: string): string {\n return itemId ? `/${slug} id=${itemId} ${message}` : `/${slug} ${message}`;\n}\n","// Sorting for the collection list table. The header sort toggle (next to\n// each field title) cycles a single active column through none → asc →\n// desc → none; this module turns that state plus a per-field value\n// extractor into an ordered copy of the rows.\n//\n// Two invariants the comparator guarantees:\n// 1. Rows with an empty/missing value for the sorted field always sink\n// to the bottom, regardless of direction.\n// 2. Ties (and the empty group) keep their original order — the sort is\n// stable via the captured source index.\n//\n// Field-type → comparable mapping (see `isSortableField`):\n// string/text/email → string · number/money → numeric ·\n// date/datetime → epoch-ms · enum → declared-index · boolean/toggle/flag →\n// false<true · ref → display label · derived → its display type.\n// markdown/table/image/file/embed get no sort button.\n\nimport { fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionItem, CollectionFieldSpec, CollectionFieldType } from \"./schema\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n /** Field key of the single active sort column. */\n field: string;\n direction: SortDirection;\n}\n\n/** A row's comparable value for the active field. Exactly one of `num` /\n * `str` is set when not empty; `empty` rows always sort last. */\nexport interface SortValue {\n empty: boolean;\n num?: number;\n str?: string;\n}\n\nconst EMPTY: SortValue = { empty: true };\n\n/** Field types that render no value text in the table, so offer no sort. */\nconst NON_SORTABLE: ReadonlySet<CollectionFieldType> = new Set<CollectionFieldType>([\"markdown\", \"table\", \"image\", \"file\", \"embed\", \"backlinks\", \"rollup\"]);\n\nexport function isSortableField(field: CollectionFieldSpec): boolean {\n return !NON_SORTABLE.has(field.type);\n}\n\n/** Cycle one column's state: none → asc → desc → none. */\nexport function nextSortDirection(current: SortDirection | null): SortDirection | null {\n if (current === null) return \"asc\";\n if (current === \"asc\") return \"desc\";\n return null;\n}\n\n// ── SortValue constructors (one per comparable kind) ────────────────\n\nexport function numericSortValue(raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const num = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(num) ? { empty: false, num } : EMPTY;\n}\n\nexport function stringSortValue(raw: unknown): SortValue {\n // An array/object has no sort key — treat it as empty (sorts last) instead of\n // grouping every such record together under \"[object Object]\".\n const str = fieldTextOrNull(raw);\n if (str === null) return EMPTY;\n return str.trim() === \"\" ? EMPTY : { empty: false, str };\n}\n\nexport function dateSortValue(raw: unknown): SortValue {\n const text = fieldTextOrNull(raw);\n if (text === null || text === \"\") return EMPTY;\n const epoch = Date.parse(text);\n // Unparseable dates fall back to a lexical compare rather than vanishing.\n return Number.isNaN(epoch) ? stringSortValue(raw) : { empty: false, num: epoch };\n}\n\n/** Enum sorts by the value's index in the declared `values` list. A value\n * outside the list (or unset) is treated as empty → last. */\nexport function enumSortValue(values: readonly string[] | undefined, raw: unknown): SortValue {\n const text = fieldTextOrNull(raw);\n if (text === null || text === \"\") return EMPTY;\n const idx = values ? values.indexOf(text) : -1;\n return idx < 0 ? EMPTY : { empty: false, num: idx };\n}\n\n/** Boolean / toggle: false < true, never empty (unset reads as false). */\nexport function boolSortValue(checked: boolean): SortValue {\n return { empty: false, num: checked ? 1 : 0 };\n}\n\n// ── Comparator + driver ─────────────────────────────────────────────\n\nexport function compareSortValues(left: SortValue, right: SortValue): number {\n if (left.num !== undefined && right.num !== undefined) return left.num - right.num;\n const leftStr = left.str ?? String(left.num ?? \"\");\n const rightStr = right.str ?? String(right.num ?? \"\");\n return leftStr.localeCompare(rightStr);\n}\n\n/** Stable sort of `items` by `valueOf`. Empties always last; ties hold\n * source order. Returns a new array (does not mutate `items`). */\nexport function sortItems(items: readonly CollectionItem[], direction: SortDirection, valueOf: (item: CollectionItem) => SortValue): CollectionItem[] {\n const dir = direction === \"asc\" ? 1 : -1;\n return items\n .map((item, index) => ({ item, index, sv: valueOf(item) }))\n .sort((left, right) => {\n if (left.sv.empty || right.sv.empty) {\n if (left.sv.empty && right.sv.empty) return left.index - right.index;\n return left.sv.empty ? 1 : -1;\n }\n const base = compareSortValues(left.sv, right.sv);\n return base !== 0 ? base * dir : left.index - right.index;\n })\n .map((decorated) => decorated.item);\n}\n","// Field-type → comparable value for the collection list-table sort. Splits\n// the header sort's per-row value extraction out of the Vue view so the\n// dispatch (which field type maps to which SortValue constructor) is testable\n// on its own. The comparators live in `./sortItems`; this module only routes.\n//\n// A few field kinds can't be sorted from the raw cell alone — `toggle` and\n// `flag` are projected booleans, `derived` is a formula, and `ref` sorts by\n// its resolved display label. Those readers live in the view's rendering\n// composable, so they are passed in (dependency injection) rather than\n// imported, keeping this module pure and framework-free.\n\nimport { numericSortValue, stringSortValue, dateSortValue, enumSortValue, boolSortValue, type SortValue } from \"./sortItems\";\nimport type { CollectionItem, CollectionFieldSpec as FieldSpec } from \"./schema\";\n\n/** Resolve a `ref` cell to its human display label (target slug + item id). */\nexport type RefDisplayResolver = (targetSlug: string, itemSlug: string) => string;\n\n/** The row readers the view supplies for the field kinds that can't sort off\n * the raw cell value. All come from the rendering composable. */\nexport interface SortValueDeps {\n toggleChecked: (item: CollectionItem, field: FieldSpec) => boolean;\n flagValueOf: (key: string, item: CollectionItem) => boolean;\n evaluateDerived: (field: FieldSpec, key: string, item: CollectionItem) => number | null;\n deriveRecord: (item: CollectionItem) => CollectionItem;\n resolveRefDisplay: RefDisplayResolver;\n}\n\n/** Comparable value for scalar fields that key off the raw cell value. */\nexport function scalarSortValue(field: FieldSpec, raw: unknown, resolveRefDisplay: RefDisplayResolver): SortValue {\n switch (field.type) {\n case \"number\":\n case \"money\":\n return numericSortValue(raw);\n case \"date\":\n case \"datetime\":\n return dateSortValue(raw);\n case \"enum\":\n return enumSortValue(field.values, raw);\n case \"boolean\":\n return boolSortValue(raw === true);\n case \"ref\":\n return field.to && typeof raw === \"string\" && raw ? stringSortValue(resolveRefDisplay(field.to, raw)) : stringSortValue(raw);\n default:\n return stringSortValue(raw);\n }\n}\n\n/** Derived rows sort by their display type: money/number → numeric,\n * date → epoch, anything else → the enriched value as a string. */\nexport function derivedSortValue(\n field: FieldSpec,\n key: string,\n item: CollectionItem,\n deps: Pick<SortValueDeps, \"evaluateDerived\" | \"deriveRecord\">,\n): SortValue {\n const display = field.type === \"derived\" ? field.display : undefined;\n if (display === undefined || display === \"number\" || display === \"money\") {\n return numericSortValue(deps.evaluateDerived(field, key, item));\n }\n const enriched = deps.deriveRecord(item);\n if (display === \"date\") return dateSortValue(enriched[key]);\n return stringSortValue(enriched[key]);\n}\n\n/** Comparable value for one row under the active field. Toggle, flag, and\n * derived need the whole record; every other type keys off the raw cell. */\nexport function sortValueOf(field: FieldSpec, key: string, item: CollectionItem, deps: SortValueDeps): SortValue {\n if (field.type === \"toggle\") return boolSortValue(deps.toggleChecked(item, field));\n if (field.type === \"flag\") return boolSortValue(deps.flagValueOf(key, item));\n if (field.type === \"derived\") return derivedSortValue(field, key, item, deps);\n return scalarSortValue(field, item[key], deps.resolveRefDisplay);\n}\n","// Free-text record matching for the collection list search box. Pure and\n// framework-free so the view's `filteredItems` computed just calls it.\n\nimport { fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionItem } from \"./schema\";\n\n/** Case-insensitive substring match across an item's scalar fields.\n * Object/array-valued fields (table rows, nested records) have no text\n * form (`fieldTextOrNull` → `null`) and are skipped — they don't render as\n * searchable text in the list table.\n *\n * `query` is matched against each cell's lower-cased text but is NOT\n * lower-cased here: callers pass an already-normalised (trimmed +\n * lower-cased) query, so an upper-case `query` intentionally matches\n * nothing. Keeping the normalisation at the call site avoids re-lowering\n * the same query once per row. */\nexport function itemMatchesQuery(item: CollectionItem, query: string): boolean {\n // An empty query matches every record — otherwise a row whose only fields are\n // object/array-valued (no scalar cell reaches `includes(\"\")`) would vanish\n // when the search box is cleared. The list caller already short-circuits the\n // empty case, so this only guards a future caller that doesn't.\n if (query === \"\") return true;\n return Object.values(item).some((value) => {\n const text = fieldTextOrNull(value);\n return text !== null && text.toLowerCase().includes(query);\n });\n}\n","// Composite cell keys for the list table's per-cell state maps, plus the\n// load-time \"which enum cells were empty\" snapshot the inline enum dropdown\n// reads. Pure and framework-free so the view keeps only the reactive refs.\n\nimport { fieldText } from \"./fieldText\";\nimport type { CollectionItem } from \"./schema\";\n\n/** The schema slice `snapshotEmptyEnums` reads — minimal structural shape so\n * both the client and server collection-schema types satisfy it as-is. */\nexport interface EnumSnapshotSchema {\n fields: Record<string, { type: string }>;\n primaryKey: string;\n}\n\n/** Stable key for one cell (`<rowId>:<fieldKey>`) in the per-cell state maps. */\nexport function cellKey(rowId: string, fieldKey: string): string {\n return `${rowId}:${fieldKey}`;\n}\n\n/** A row's stable identity: the primary-key value as text (`\"\"` when the schema\n * has no primary key, or the value is absent). Every per-row / per-cell state\n * map keys off this, and it is the SAME derivation `snapshotEmptyEnums` uses\n * for its record ids (`fieldText`), so `cellKey(rowIdOf(...), field)` and the\n * empty-enum snapshot always agree. A real primary key is a scalar, so this\n * matches its plain string form. */\nexport function rowIdOf(primaryKey: string | undefined, item: CollectionItem): string {\n return primaryKey ? fieldText(item[primaryKey]) : \"\";\n}\n\n/** The set of enum cells that were empty in the freshly-fetched records — the\n * only cells whose inline dropdown offers an empty option. */\nexport function snapshotEmptyEnums(schema: EnumSnapshotSchema, records: CollectionItem[]): Set<string> {\n const empty = new Set<string>();\n const enumKeys = Object.entries(schema.fields)\n .filter(([, field]) => field.type === \"enum\")\n .map(([fieldKey]) => fieldKey);\n if (enumKeys.length === 0) return empty;\n for (const record of records) {\n const recordId = fieldText(record[schema.primaryKey]);\n for (const fieldKey of enumKeys) {\n if (record[fieldKey] == null || record[fieldKey] === \"\") empty.add(cellKey(recordId, fieldKey));\n }\n }\n return empty;\n}\n","// Shared chip/label resolution for the collection calendar surfaces (month\n// grid + day view), so both label a record identically. Pure, type-only\n// dependency on the schema shape.\n\nimport { fieldText } from \"./fieldText\";\nimport type { CollectionItem, CollectionSchema } from \"./schema\";\n\n// Text-like field types that make a sensible human-readable label.\nconst LABEL_FIELD_TYPES = new Set([\"string\", \"text\", \"markdown\", \"email\"]);\n\n/** Which field labels a record: the schema's explicit `displayField`, else the\n * first non-primary text-like field (so a collection without a displayField\n * shows e.g. the title rather than the opaque primaryKey), else null → the\n * caller falls back to the primary-key value. */\nexport function labelFieldFor(schema: CollectionSchema): string | null {\n if (schema.displayField) return schema.displayField;\n for (const [key, spec] of Object.entries(schema.fields)) {\n if (key !== schema.primaryKey && LABEL_FIELD_TYPES.has(spec.type)) return key;\n }\n return null;\n}\n\n/** A record's primary-key value as a string. */\nexport function itemIdOf(item: CollectionItem, schema: CollectionSchema): string {\n return fieldText(item[schema.primaryKey]);\n}\n\n/** A record's display label: the resolved `labelField` value, else the\n * primary key. Pass the result of `labelFieldFor(schema)` as `labelField`\n * (compute it once per render rather than per item). */\nexport function itemLabelOf(item: CollectionItem, schema: CollectionSchema, labelField: string | null): string {\n if (labelField) {\n const value = item[labelField];\n // Accept any primitive (string / number / boolean) so a numeric or boolean\n // display field still labels the record; objects/arrays fall through to the\n // id rather than rendering as \"[object Object]\".\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n const text = String(value);\n if (text.length > 0) return text;\n }\n }\n return itemIdOf(item, schema);\n}\n","// Pure builder for the workspace ontology graph — the `/collections` Map\n// panel (phase 2 of plans/done/collection-ontology.md step ①; plan:\n// plans/done/feat-collection-ontology-graph.md). Consumes the per-collection\n// ontology entries `buildWorkspaceOntology` derives server-side and turns\n// them into a node/edge structure a graph view can render directly.\n// Browser-safe (no `node:*` imports) so the Vue plugin builds the same\n// graph from the raw entries — same parity discipline as `deriveAll` and\n// the sibling `wiki/graph.ts`.\n\n/** One relationship a schema declares: a `ref` (the record stores the\n * target's primaryKey slug) or `embed` (display-only pull) pointing at\n * collection `to`, or `backlinks` / `rollup` (display-only REVERSE\n * refs — `to` is the relation's source collection, i.e. its `from`,\n * and `via` names the exact ref field there being reversed).\n * A `ref` column inside a `table` field is reported with a dotted path\n * (`lines.clientId`). Whether `to` exists is NOT checked — resolution\n * is fail-soft at render. */\nexport interface OntologyRelation {\n field: string;\n kind: \"ref\" | \"embed\" | \"backlinks\" | \"rollup\";\n to: string;\n via?: string;\n}\n\nexport interface CollectionOntologyEntry {\n slug: string;\n title: string;\n icon: string;\n primaryKey: string;\n /** The effective display field: the schema's `displayField`, falling\n * back to the primaryKey exactly as render-time labelling does. */\n displayField: string;\n /** Records in the collection, or `null` when the backend couldn't be\n * reached to count them (an engine that failed to load, a closed\n * session). Deliberately not 0: an unreachable collection reported as\n * empty invites \"restoring\" data that is intact but out of reach. */\n recordCount: number | null;\n relations: OntologyRelation[];\n}\n\nexport interface OntologyGraphNode {\n slug: string;\n title: string;\n icon: string;\n recordCount: number | null;\n /** No discovered collection has this slug — the node exists only\n * because a relation points at it. Rendered as a ghost so the graph\n * doubles as a broken-ref lint surface (lint, not lock). */\n missing?: boolean;\n}\n\nexport interface OntologyGraphEdge {\n /** Tail: the collection whose records store (`ref`) or pull (`embed`)\n * the link — or, for an uncollapsed reverse relation, the collection\n * whose refs the `backlinks`/`rollup` field aggregates. */\n from: string;\n /** Head: the collection the link points at. */\n to: string;\n /** Field declaring the relation — on `from` for `ref`/`embed`, on\n * `to` for an uncollapsed `backlinks`/`rollup`. */\n field: string;\n kind: OntologyRelation[\"kind\"];\n /** `backlinks`/`rollup` field names on `to` that declare the reverse\n * of this `ref` edge, collapsed here so one link never draws twice. */\n reverseFields?: string[];\n}\n\nexport interface OntologyGraph {\n nodes: OntologyGraphNode[];\n edges: OntologyGraphEdge[];\n}\n\nconst isForwardKind = (kind: OntologyRelation[\"kind\"]): boolean => kind === \"ref\" || kind === \"embed\";\n\n/** Fold a reverse relation (`backlinks`/`rollup` declared on `owner`)\n * into the edge list: collapse onto the forward `ref` edge whose field\n * is the relation's declared `via` (several refs can link the same\n * pair — matches.homeTeam vs matches.awayTeam — so the pair alone is\n * ambiguous), else emit its own edge oriented in true data direction\n * (refs' source → owner). A `via` naming no forward edge stays\n * uncollapsed — fail-soft, like every other dangling reference. */\nconst addReverseRelation = (edges: OntologyGraphEdge[], owner: string, rel: OntologyRelation): void => {\n const forward = edges.find((edge) => edge.kind === \"ref\" && edge.from === rel.to && edge.to === owner && (rel.via === undefined || edge.field === rel.via));\n if (forward) {\n forward.reverseFields = [...(forward.reverseFields ?? []), rel.field];\n return;\n }\n edges.push({ from: rel.to, to: owner, field: rel.field, kind: rel.kind });\n};\n\n/** Two passes so every forward `ref` edge exists before any reverse\n * relation tries to collapse onto one — entry order must not matter. */\nconst collectEdges = (entries: CollectionOntologyEntry[]): OntologyGraphEdge[] => {\n const edges: OntologyGraphEdge[] = [];\n for (const entry of entries) {\n for (const rel of entry.relations) {\n if (isForwardKind(rel.kind)) edges.push({ from: entry.slug, to: rel.to, field: rel.field, kind: rel.kind });\n }\n }\n for (const entry of entries) {\n for (const rel of entry.relations) {\n if (!isForwardKind(rel.kind)) addReverseRelation(edges, entry.slug, rel);\n }\n }\n return edges;\n};\n\n/** Slugs referenced by an edge endpoint but backed by no discovered\n * collection, slug-sorted for deterministic node order. */\nconst ghostSlugs = (edges: OntologyGraphEdge[], known: ReadonlySet<string>): string[] => {\n const ghosts = new Set<string>();\n for (const edge of edges) {\n if (!known.has(edge.from)) ghosts.add(edge.from);\n if (!known.has(edge.to)) ghosts.add(edge.to);\n }\n return [...ghosts].sort();\n};\n\n/** Build the renderable graph: one node per discovered collection (in\n * the entries' slug-sorted order) plus ghost nodes for dangling\n * targets, one edge per underlying link. */\nexport const buildOntologyGraph = (entries: CollectionOntologyEntry[]): OntologyGraph => {\n const edges = collectEdges(entries);\n const known = new Set(entries.map((entry) => entry.slug));\n const nodes: OntologyGraphNode[] = entries.map(({ slug, title, icon, recordCount }) => ({ slug, title, icon, recordCount }));\n for (const slug of ghostSlugs(edges, known)) {\n // A ghost node has no collection behind it at all, so it genuinely holds\n // no records — 0, not the \"couldn't count\" null.\n nodes.push({ slug, title: slug, icon: \"\", recordCount: 0, missing: true });\n }\n return { nodes, edges };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/collection/core/presentCollection.ts","../../src/collection/core/enumColors.ts","../../src/collection/core/draft.ts","../../src/collection/core/chatSeed.ts","../../src/collection/core/sortItems.ts","../../src/collection/core/sortValueOf.ts","../../src/collection/core/textSearch.ts","../../src/collection/core/recordKeys.ts","../../src/collection/core/itemLabel.ts","../../src/collection/core/ontologyGraph.ts"],"sourcesContent":["// presentCollection tool — definition + pure server-side executor.\n//\n// Isomorphic (no Vue, no Node): bundled to the browser as the plugin's\n// `execute`, and run server-side via the host's plugin dispatch route. The\n// executor only validates + echoes the addressing; the live schema + items\n// are fetched client-side by the View through the host's /api/collections\n// routes, so a bad slug surfaces as the View's \"not found\" state.\n\nimport type { ToolContext, ToolDefinition, ToolResult } from \"gui-chat-protocol\";\n\nexport const TOOL_NAME = \"presentCollection\";\n\n/** Render payload carried in the tool result's `data` field; the View mounts\n * off these. Same shape as the tool args. */\nexport interface PresentCollectionData {\n /** Slug of the collection to display (e.g. \"clients\", \"invoices\"). */\n collectionSlug: string;\n /** Optional primary-key value of a single item to open on mount. */\n itemId?: string | undefined;\n /** Which ROOT this card's collection lives in, as a host-opaque scope token.\n *\n * A collection's identity is `(root, slug)`, but a card payload named only\n * the slug — so the host re-resolved it through whatever binding was current\n * when the card RENDERED, which in a multi-root host may be a different\n * project than the one the card was made in. A card built in project A could\n * then read project B's data.\n *\n * **Stamped by the HOST, never accepted from tool arguments.** The tool args\n * are a model-controlled channel: a model that could name the scope could\n * name another project's, and the card — and any token minted for it — would\n * address a project the user never opened in this conversation. So\n * `executePresentCollection` DROPS any `scope` it is handed, and a host adds\n * its own with {@link withCardScope} when it applies the result, where it\n * already knows which binding produced the call.\n *\n * Host-opaque: pass an opaque project id, NEVER an absolute path — this\n * payload reaches the browser and, through a view, an LLM-authored iframe.\n *\n * Absent — the single-workspace case — the payload and every reconciliation\n * decision are exactly what they were before this field existed. */\n scope?: string | undefined;\n}\n\n/** Stamp a host's opaque project scope onto a card payload. The ONLY way a\n * scope gets in: the executor refuses to take one from tool arguments (see\n * `PresentCollectionData.scope`). `undefined` returns the payload untouched,\n * so a single-workspace host calling this unconditionally still produces a\n * payload with no `scope` property at all. */\nexport function withCardScope(data: PresentCollectionData, scope: string | undefined): PresentCollectionData {\n const trimmed = typeof scope === \"string\" ? scope.trim() : \"\";\n return trimmed.length > 0 ? { ...data, scope: trimmed } : data;\n}\n\n/** A card's identity for reconciliation. Two cards match only when they name\n * the same collection IN THE SAME SCOPE, so two projects' `tasks` cards stay\n * two cards. With no scope on either side this is the slug, unchanged. */\nexport function collectionCardKey(data: Pick<PresentCollectionData, \"collectionSlug\" | \"scope\">): string {\n return data.scope === undefined ? data.collectionSlug : `${data.scope}\\u0000${data.collectionSlug}`;\n}\n\n/** Do two card payloads address the same card? The reconciliation predicate a\n * host should use instead of comparing slugs. */\nexport function sameCollectionCard(\n one: Pick<PresentCollectionData, \"collectionSlug\" | \"scope\">,\n other: Pick<PresentCollectionData, \"collectionSlug\" | \"scope\">,\n): boolean {\n return collectionCardKey(one) === collectionCardKey(other);\n}\n\nexport type PresentCollectionArgs = PresentCollectionData;\n\nexport const TOOL_DEFINITION: ToolDefinition = {\n type: \"function\",\n name: TOOL_NAME,\n description:\n \"Display a schema-driven collection inline in the chat as an interactive, editable card. Shows the collection's list of records. Pass `itemId` to open one specific record on mount.\",\n parameters: {\n type: \"object\",\n properties: {\n collectionSlug: {\n type: \"string\",\n description: \"The slug of the collection to display (e.g. 'clients', 'invoices', 'contacts').\",\n },\n itemId: {\n type: \"string\",\n description: \"Optional primary-key value of a single record to open in detail view on mount. Omit to show the full list.\",\n },\n },\n required: [\"collectionSlug\"],\n },\n prompt: `After making changes to schema-driven collections, use ${TOOL_NAME} to present either the collection or the item`,\n};\n\n/** A trimmed non-empty string, or undefined. The tool's args arrive from an\n * LLM (and `scope` from the host), so every field is validated the same way\n * rather than trusted. */\nconst cleaned = (value: unknown): string | undefined => (typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined);\n\n/** Normalise the tool args into the card payload, or `null` when the required\n * slug is missing. Pure and exported so the addressing rules are testable\n * without going through a `ToolResult`.\n *\n * `scope` is deliberately NOT read from the args — see\n * `PresentCollectionData.scope`. Everything here comes from a model-controlled\n * channel, and the scope decides which project the card reads; a host stamps\n * it afterwards with `withCardScope`.\n *\n * Keys are added one at a time rather than spread with `undefined` values, so\n * a payload with no `itemId` OMITS the property entirely — which is what keeps\n * a single-workspace card byte-identical to what it has always been, in both\n * `deepEqual` and JSON. */\nexport const toPresentCollectionData = (args: PresentCollectionArgs): PresentCollectionData | null => {\n const collectionSlug = cleaned(args?.collectionSlug);\n if (!collectionSlug) return null;\n const itemId = cleaned(args.itemId);\n return { collectionSlug, ...(itemId ? { itemId } : {}) };\n};\n\nexport const executePresentCollection = async (\n _context: ToolContext,\n args: PresentCollectionArgs,\n): Promise<ToolResult<PresentCollectionData, PresentCollectionData>> => {\n const data = toPresentCollectionData(args);\n if (!data) {\n return {\n message: \"presentCollection error: collectionSlug is required\",\n instructions: \"Tell the user you couldn't display the collection because no collection was specified, and ask which collection they mean.\",\n };\n }\n const { collectionSlug, itemId } = data;\n const target = itemId ? `${collectionSlug} / ${itemId}` : collectionSlug;\n return {\n message: `Presented collection ${target}`,\n // `data` is the view's source (also the host's render-eligibility signal);\n // `jsonData` is what the LLM sees. Same payload, two audiences.\n data,\n jsonData: data,\n // Phrased to stay correct for BOTH storage kinds without loading the\n // schema (this executor is deliberately pure/isomorphic): a writable\n // collection offers edit/create/delete in the card; a read-only\n // `dataSource` collection hides those and changes flow through its\n // data file instead.\n instructions:\n \"The collection has been presented to the user as an interactive card. They can browse and open records directly; on a writable collection they can also edit, create, and delete (a read-only dataSource collection shows no edit controls — its records change by editing the backing data file). No further action is needed unless they ask.\",\n };\n};\n","// A standard, ordered colour palette for `enum` field values, shared by every\n// collection surface (list, calendar, kanban). Each value in an\n// enum's `values` array is assigned the palette entry at its index — cycling\n// when an enum declares more values than the palette holds — so colouring is\n// automatic and consistent without any per-value schema config.\n//\n// Tailwind only detects class names that appear as complete string literals,\n// so every surface's classes are spelled out per colour below rather than\n// built from a colour name at runtime.\n//\n// One enum is special: the field a schema's `notifyWhen` targets (the\n// \"notification enum\"). Its flagged values read the notification severity\n// colours — the first flagged value (most urgent) red, the rest amber — and\n// every other value reads neutral grey, mirroring the notification bell\n// (red = urgent, amber = nudge) rather than the rotating palette.\n// `resolveEnumColor` encapsulates that rule.\n\nimport { fieldText, fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionSchema } from \"./schema\";\n\nexport interface EnumColorClasses {\n /** Stat-card style: border + fill + text + hover. */\n card: string;\n /** Small status dot (kanban column header). */\n dot: string;\n /** Pill / badge / inline `<select>` fill + text (no border width). */\n badge: string;\n /** Border colour, paired with a `border` width class by the caller. */\n border: string;\n}\n\n// The palette deliberately EXCLUDES the warm warning band (red / orange /\n// amber): those are reserved for the notification-enum severity colours\n// (`ENUM_ALERT` red, `ENUM_NUDGE` amber), so a normal enum value can never\n// draw a colour that reads like a notification. Eight well-separated cool /\n// green / magenta hues keep enough variety for wide enums.\nconst PALETTE: readonly EnumColorClasses[] = [\n {\n card: \"border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100\",\n dot: \"bg-indigo-500\",\n badge: \"bg-indigo-100 text-indigo-700\",\n border: \"border-indigo-200\",\n },\n { card: \"border-sky-200 bg-sky-50 text-sky-600 hover:bg-sky-100\", dot: \"bg-sky-500\", badge: \"bg-sky-100 text-sky-700\", border: \"border-sky-200\" },\n { card: \"border-cyan-200 bg-cyan-50 text-cyan-600 hover:bg-cyan-100\", dot: \"bg-cyan-500\", badge: \"bg-cyan-100 text-cyan-700\", border: \"border-cyan-200\" },\n { card: \"border-teal-200 bg-teal-50 text-teal-600 hover:bg-teal-100\", dot: \"bg-teal-500\", badge: \"bg-teal-100 text-teal-700\", border: \"border-teal-200\" },\n {\n card: \"border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100\",\n dot: \"bg-emerald-500\",\n badge: \"bg-emerald-100 text-emerald-700\",\n border: \"border-emerald-200\",\n },\n { card: \"border-lime-200 bg-lime-50 text-lime-600 hover:bg-lime-100\", dot: \"bg-lime-500\", badge: \"bg-lime-100 text-lime-700\", border: \"border-lime-200\" },\n {\n card: \"border-violet-200 bg-violet-50 text-violet-600 hover:bg-violet-100\",\n dot: \"bg-violet-500\",\n badge: \"bg-violet-100 text-violet-700\",\n border: \"border-violet-200\",\n },\n {\n card: \"border-fuchsia-200 bg-fuchsia-50 text-fuchsia-600 hover:bg-fuchsia-100\",\n dot: \"bg-fuchsia-500\",\n badge: \"bg-fuchsia-100 text-fuchsia-700\",\n border: \"border-fuchsia-200\",\n },\n];\n\n/** Neutral styling for the empty / Uncategorized bucket — never a palette\n * colour, so an unset or unknown value reads grey across every surface. */\nexport const ENUM_NEUTRAL: EnumColorClasses = {\n card: \"border-slate-200 bg-slate-50 text-slate-500 hover:bg-slate-100\",\n dot: \"bg-slate-300\",\n badge: \"bg-slate-100 text-slate-500\",\n border: \"border-slate-200\",\n};\n\n/** The urgent notification colour (red), matching the bell's `urgent`\n * severity. The first value a schema's `notifyWhen` flags reads this. */\nexport const ENUM_ALERT: EnumColorClasses = {\n card: \"border-red-200 bg-red-50 text-red-600 hover:bg-red-100\",\n dot: \"bg-red-500\",\n badge: \"bg-red-100 text-red-700\",\n border: \"border-red-200\",\n};\n\n/** The nudge notification colour (amber), matching the bell's `nudge`\n * severity. Flagged `notifyWhen` values after the first read this. */\nexport const ENUM_NUDGE: EnumColorClasses = {\n card: \"border-amber-200 bg-amber-50 text-amber-600 hover:bg-amber-100\",\n dot: \"bg-amber-500\",\n badge: \"bg-amber-100 text-amber-700\",\n border: \"border-amber-200\",\n};\n\n/** Classes for the enum value at `index` in its field's `values` array. A\n * negative index (value unset or not among the declared values) reads\n * neutral. */\nexport function enumColorClasses(index: number): EnumColorClasses {\n if (index < 0) return ENUM_NEUTRAL;\n return PALETTE[index % PALETTE.length] ?? ENUM_NEUTRAL;\n}\n\n/** Index of `value` within an enum field's declared `values`, or -1 when the\n * value is empty / unknown (→ neutral). */\nexport function enumValueIndex(values: readonly string[] | undefined, value: unknown): number {\n // No text form (array/object) means no enum member — \"[object Object]\" must\n // not index into the palette.\n const text = fieldTextOrNull(value);\n if (text === null || text === \"\") return -1;\n return values?.indexOf(text) ?? -1;\n}\n\n/** The flagged values when `fieldKey` is the schema's `notifyWhen` target (the\n * \"notification enum\"); undefined for every other field. */\nfunction notifyValuesFor(schema: CollectionSchema, fieldKey: string): readonly string[] | undefined {\n const spec = schema.notifyWhen;\n return spec && spec.field === fieldKey ? spec.in : undefined;\n}\n\n/** Resolve a value's colour for enum field `fieldKey`:\n * - Notification enum (`notifyWhen` targets it): the first flagged value (in\n * `notifyWhen.in` order, the most urgent) reads notification red, the rest\n * amber, and every non-flagged value neutral grey.\n * - Any other enum: the standard palette by the value's declared index. */\nexport function resolveEnumColor(schema: CollectionSchema, fieldKey: string, value: unknown): EnumColorClasses {\n const notifyValues = notifyValuesFor(schema, fieldKey);\n if (notifyValues) {\n const str = fieldText(value);\n const rank = notifyValues.indexOf(str);\n if (rank < 0) return ENUM_NEUTRAL;\n return rank === 0 ? ENUM_ALERT : ENUM_NUDGE;\n }\n const spec = schema.fields[fieldKey];\n return enumColorClasses(enumValueIndex(spec?.type === \"enum\" ? spec.values : undefined, value));\n}\n","// Pure edit-draft logic for the collection record form, extracted from\n// CollectionView.vue so the parent (build draft / save / validate) and\n// the extracted CollectionRecordPanel (per-row table mutators) share one\n// implementation. No Vue, no I/O — every function maps a draft + schema\n// to a value, so the omission/validation semantics are unit-testable.\n\nimport { fieldText } from \"./fieldText\";\nimport { fieldVisible } from \"./actionVisible\";\nimport { COMPUTED_TYPES } from \"./schema\";\nimport type { CollectionFieldSpec as FieldSpec, CollectionFieldType as FieldType, CollectionItem, CollectionSchema } from \"./schema\";\nimport type { EditState, TableRowDraft } from \"./uiTypes\";\n\n/** Build a row draft, reading each sub-field's persisted value via\n * `readSub`. An empty row passes a reader that always returns\n * `undefined`; a row-from-item reads the source record. Boolean\n * presence uses `typeof raw === \"boolean\"` so an existing explicit\n * `false` is recorded as present and round-trips on a no-op save. */\nfunction buildTableRowDraft(subFields: Record<string, FieldSpec>, readSub: (subKey: string) => unknown): TableRowDraft {\n const text: Record<string, string> = {};\n const bool: Record<string, boolean> = {};\n const boolOriginallyPresent: Record<string, boolean> = {};\n const boolTouched: Record<string, boolean> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n const raw = readSub(subKey);\n if (subField.type === \"boolean\") {\n bool[subKey] = raw === true;\n boolOriginallyPresent[subKey] = typeof raw === \"boolean\";\n boolTouched[subKey] = false;\n } else {\n text[subKey] = fieldText(raw);\n }\n }\n return { text, bool, boolOriginallyPresent, boolTouched };\n}\n\n/** A fresh, empty row draft for a `table` field's sub-schema. */\nexport function emptyRow(subFields: Record<string, FieldSpec>): TableRowDraft {\n return buildTableRowDraft(subFields, () => undefined);\n}\n\n/** Build a row draft from an existing persisted row. */\nexport function rowFromItem(item: Record<string, unknown>, subFields: Record<string, FieldSpec>): TableRowDraft {\n return buildTableRowDraft(subFields, (subKey) => item[subKey]);\n}\n\n/** Own-property boolean read: a field named `toString` / `constructor` /\n * `__proto__` must read `false` here, not inherit a truthy Object.prototype\n * member and force an untouched boolean into every saved record. */\nfunction ownFlag(flags: Record<string, boolean>, key: string): boolean {\n return Object.hasOwn(flags, key) && flags[key] === true;\n}\n\n/** Decide whether a boolean field's draft value should be emitted (vs.\n * omitted so a downstream default applies). */\nfunction shouldEmitBoolean(state: EditState, key: string, field: FieldSpec): boolean {\n return ownFlag(state.boolOriginallyPresent, key) || ownFlag(state.boolTouched, key) || Boolean(field.required);\n}\n\n/** Convert a scalar draft slot to its persisted form. `undefined` = omit. */\nfunction scalarDraftToValue(raw: string | undefined, fieldType: FieldType): unknown {\n if (raw === undefined || raw === \"\") return undefined;\n if (fieldType === \"number\" || fieldType === \"money\") {\n const num = Number(raw);\n return Number.isFinite(num) ? num : raw;\n }\n return raw;\n}\n\n/** Convert one table row draft to its persisted record. */\nfunction rowDraftToRecord(rowDraft: TableRowDraft, subFields: Record<string, FieldSpec>): Record<string, unknown> {\n const row: Record<string, unknown> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n if (subField.type === \"boolean\") {\n const value = ownFlag(rowDraft.bool, subKey);\n if (ownFlag(rowDraft.boolOriginallyPresent, subKey) || ownFlag(rowDraft.boolTouched, subKey) || value || subField.required) row[subKey] = value;\n continue;\n }\n const value = scalarDraftToValue(rowDraft.text[subKey], subField.type);\n if (value !== undefined) row[subKey] = value;\n }\n return row;\n}\n\n/** Convert a full edit draft to the record to persist.\n *\n * Collected as entries rather than assigned into an accumulator: a field may\n * be named `__proto__` (JSON.parse hands that over as an own key, so it\n * reaches here like any other), and `record[key] = value` would run the\n * prototype setter for that one name — the user's value would vanish between\n * the form and the save, with nothing to see. `fromEntries` defines an own\n * property whatever the name is. The read side of this file already guards the\n * same hazard with `ownFlag` (Codex review on #2910). */\nexport function draftToRecord(state: EditState, schema: CollectionSchema): CollectionItem {\n const entries: [string, unknown][] = [];\n for (const [key, field] of Object.entries(schema.fields)) {\n if (COMPUTED_TYPES.has(field.type)) continue; // never persisted (toggle projects an enum field)\n if (field.type === \"boolean\") {\n if (shouldEmitBoolean(state, key, field)) entries.push([key, ownFlag(state.bool, key)]);\n continue;\n }\n if (field.type === \"table\" && field.of) {\n const subFields = field.of;\n entries.push([key, (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields))]);\n continue;\n }\n const value = scalarDraftToValue(state.text[key], field.type);\n if (value !== undefined) entries.push([key, value]);\n }\n return Object.fromEntries(entries);\n}\n\n/** Normalise a raw inline-edit input (table-cell checkbox/select) to its\n * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty\n * enum selection (the placeholder option) clears the field via `undefined`. */\nexport function coerceInlineValue(field: FieldSpec, raw: boolean | string): unknown {\n if (field.type === \"boolean\") return raw === true;\n return raw === \"\" ? undefined : raw;\n}\n\n/** Build the full record to PUT for a single-cell inline edit, without\n * mutating `item`. A `undefined` value omits the key (enum cleared),\n * matching `draftToRecord`'s omission semantics. */\nexport function buildUpdatedRecord(item: CollectionItem, key: string, value: unknown): CollectionItem {\n if (value === undefined) {\n const { [key]: __omit, ...rest } = item;\n return rest;\n }\n return { ...item, [key]: value };\n}\n\n/** Empty for required-field validation — NOT a truthiness check (a\n * numeric `0` is a filled value). */\nfunction isMissingDraftValue(value: unknown): boolean {\n return value === undefined || value === null || value === \"\";\n}\n\n/** Label of the first required table sub-field empty in any row, else null. */\nfunction firstMissingTableSubField(field: FieldSpec, rows: TableRowDraft[] | undefined): string | null {\n if (field.type !== \"table\" || !rows) return null;\n for (const [rowIdx, row] of rows.entries()) {\n for (const [subKey, subField] of Object.entries(field.of)) {\n if (!subField.required || subField.type === \"boolean\") continue;\n if (isMissingDraftValue(row.text[subKey])) return `${field.label} #${rowIdx + 1}: ${subField.label}`;\n }\n }\n return null;\n}\n\n/** Field types the required check never flags: booleans always hold a\n * value, and the computed/projected kinds (COMPUTED_TYPES — the shared\n * set, so a future computed type can't drift past this check) have no\n * draft input at all. */\nconst NEVER_MISSING_TYPES: ReadonlySet<FieldType> = new Set<FieldType>([\"boolean\", ...COMPUTED_TYPES]);\n\nfunction validateOneField(key: string, field: FieldSpec, draft: EditState, record: CollectionItem): string | null {\n // A `when`-hidden field has no input the user can fill — never missing.\n if (!fieldVisible(field, record)) return null;\n if (field.type === \"table\" && field.of) {\n const rows = draft.table[key];\n if (field.required && (!rows || rows.length === 0)) return field.label;\n return firstMissingTableSubField(field, rows);\n }\n if (!field.required) return null;\n if (draft.mode === \"create\" && field.primary === true) return null; // server auto-generates id\n if (NEVER_MISSING_TYPES.has(field.type)) return null;\n return isMissingDraftValue(draft.text[key]) ? field.label : null;\n}\n\n/** Human-readable label of the first missing required field, or null. */\nexport function firstMissingRequiredField(draft: EditState, schema: CollectionSchema): string | null {\n const record = draftToRecord(draft, schema);\n for (const [key, field] of Object.entries(schema.fields)) {\n const missing = validateOneField(key, field, draft, record);\n if (missing) return missing;\n }\n return null;\n}\n","// The slash-command chat seed a collection view sends to the agent when the\n// user starts a chat from a skill-backed collection or one of its records.\n// Skill-backed collections have a `/<slug>` command, so seed that. (Feeds are\n// skill-less and seeded differently; that branch stays in the view because it\n// is i18n-driven.)\n\n/** `/<slug> <message>`, or `/<slug> id=<itemId> <message>` when a specific\n * record is addressed. The `id=` selector goes BEFORE the free-text message so\n * the skill's argument parser reads it as a flag rather than as prose. */\nexport function skillCommandSeed(slug: string, message: string, itemId?: string): string {\n return itemId ? `/${slug} id=${itemId} ${message}` : `/${slug} ${message}`;\n}\n","// Sorting for the collection list table. The header sort toggle (next to\n// each field title) cycles a single active column through none → asc →\n// desc → none; this module turns that state plus a per-field value\n// extractor into an ordered copy of the rows.\n//\n// Two invariants the comparator guarantees:\n// 1. Rows with an empty/missing value for the sorted field always sink\n// to the bottom, regardless of direction.\n// 2. Ties (and the empty group) keep their original order — the sort is\n// stable via the captured source index.\n//\n// Field-type → comparable mapping (see `isSortableField`):\n// string/text/email → string · number/money → numeric ·\n// date/datetime → epoch-ms · enum → declared-index · boolean/toggle/flag →\n// false<true · ref → display label · derived → its display type.\n// markdown/table/image/file/embed get no sort button.\n\nimport { fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionItem, CollectionFieldSpec, CollectionFieldType } from \"./schema\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n /** Field key of the single active sort column. */\n field: string;\n direction: SortDirection;\n}\n\n/** A row's comparable value for the active field. Exactly one of `num` /\n * `str` is set when not empty; `empty` rows always sort last. */\nexport interface SortValue {\n empty: boolean;\n num?: number;\n str?: string;\n}\n\nconst EMPTY: SortValue = { empty: true };\n\n/** Field types that render no value text in the table, so offer no sort. */\nconst NON_SORTABLE: ReadonlySet<CollectionFieldType> = new Set<CollectionFieldType>([\"markdown\", \"table\", \"image\", \"file\", \"embed\", \"backlinks\", \"rollup\"]);\n\nexport function isSortableField(field: CollectionFieldSpec): boolean {\n return !NON_SORTABLE.has(field.type);\n}\n\n/** Cycle one column's state: none → asc → desc → none. */\nexport function nextSortDirection(current: SortDirection | null): SortDirection | null {\n if (current === null) return \"asc\";\n if (current === \"asc\") return \"desc\";\n return null;\n}\n\n// ── SortValue constructors (one per comparable kind) ────────────────\n\nexport function numericSortValue(raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const num = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(num) ? { empty: false, num } : EMPTY;\n}\n\nexport function stringSortValue(raw: unknown): SortValue {\n // An array/object has no sort key — treat it as empty (sorts last) instead of\n // grouping every such record together under \"[object Object]\".\n const str = fieldTextOrNull(raw);\n if (str === null) return EMPTY;\n return str.trim() === \"\" ? EMPTY : { empty: false, str };\n}\n\nexport function dateSortValue(raw: unknown): SortValue {\n const text = fieldTextOrNull(raw);\n if (text === null || text === \"\") return EMPTY;\n const epoch = Date.parse(text);\n // Unparseable dates fall back to a lexical compare rather than vanishing.\n return Number.isNaN(epoch) ? stringSortValue(raw) : { empty: false, num: epoch };\n}\n\n/** Enum sorts by the value's index in the declared `values` list. A value\n * outside the list (or unset) is treated as empty → last. */\nexport function enumSortValue(values: readonly string[] | undefined, raw: unknown): SortValue {\n const text = fieldTextOrNull(raw);\n if (text === null || text === \"\") return EMPTY;\n const idx = values ? values.indexOf(text) : -1;\n return idx < 0 ? EMPTY : { empty: false, num: idx };\n}\n\n/** Boolean / toggle: false < true, never empty (unset reads as false). */\nexport function boolSortValue(checked: boolean): SortValue {\n return { empty: false, num: checked ? 1 : 0 };\n}\n\n// ── Comparator + driver ─────────────────────────────────────────────\n\nexport function compareSortValues(left: SortValue, right: SortValue): number {\n if (left.num !== undefined && right.num !== undefined) return left.num - right.num;\n const leftStr = left.str ?? String(left.num ?? \"\");\n const rightStr = right.str ?? String(right.num ?? \"\");\n return leftStr.localeCompare(rightStr);\n}\n\n/** Stable sort of `items` by `valueOf`. Empties always last; ties hold\n * source order. Returns a new array (does not mutate `items`). */\nexport function sortItems(items: readonly CollectionItem[], direction: SortDirection, valueOf: (item: CollectionItem) => SortValue): CollectionItem[] {\n const dir = direction === \"asc\" ? 1 : -1;\n return items\n .map((item, index) => ({ item, index, sv: valueOf(item) }))\n .sort((left, right) => {\n if (left.sv.empty || right.sv.empty) {\n if (left.sv.empty && right.sv.empty) return left.index - right.index;\n return left.sv.empty ? 1 : -1;\n }\n const base = compareSortValues(left.sv, right.sv);\n return base !== 0 ? base * dir : left.index - right.index;\n })\n .map((decorated) => decorated.item);\n}\n","// Field-type → comparable value for the collection list-table sort. Splits\n// the header sort's per-row value extraction out of the Vue view so the\n// dispatch (which field type maps to which SortValue constructor) is testable\n// on its own. The comparators live in `./sortItems`; this module only routes.\n//\n// A few field kinds can't be sorted from the raw cell alone — `toggle` and\n// `flag` are projected booleans, `derived` is a formula, and `ref` sorts by\n// its resolved display label. Those readers live in the view's rendering\n// composable, so they are passed in (dependency injection) rather than\n// imported, keeping this module pure and framework-free.\n\nimport { numericSortValue, stringSortValue, dateSortValue, enumSortValue, boolSortValue, type SortValue } from \"./sortItems\";\nimport type { CollectionItem, CollectionFieldSpec as FieldSpec } from \"./schema\";\n\n/** Resolve a `ref` cell to its human display label (target slug + item id). */\nexport type RefDisplayResolver = (targetSlug: string, itemSlug: string) => string;\n\n/** The row readers the view supplies for the field kinds that can't sort off\n * the raw cell value. All come from the rendering composable. */\nexport interface SortValueDeps {\n toggleChecked: (item: CollectionItem, field: FieldSpec) => boolean;\n flagValueOf: (key: string, item: CollectionItem) => boolean;\n evaluateDerived: (field: FieldSpec, key: string, item: CollectionItem) => number | null;\n deriveRecord: (item: CollectionItem) => CollectionItem;\n resolveRefDisplay: RefDisplayResolver;\n}\n\n/** Comparable value for scalar fields that key off the raw cell value. */\nexport function scalarSortValue(field: FieldSpec, raw: unknown, resolveRefDisplay: RefDisplayResolver): SortValue {\n switch (field.type) {\n case \"number\":\n case \"money\":\n return numericSortValue(raw);\n case \"date\":\n case \"datetime\":\n return dateSortValue(raw);\n case \"enum\":\n return enumSortValue(field.values, raw);\n case \"boolean\":\n return boolSortValue(raw === true);\n case \"ref\":\n return field.to && typeof raw === \"string\" && raw ? stringSortValue(resolveRefDisplay(field.to, raw)) : stringSortValue(raw);\n default:\n return stringSortValue(raw);\n }\n}\n\n/** Derived rows sort by their display type: money/number → numeric,\n * date → epoch, anything else → the enriched value as a string. */\nexport function derivedSortValue(\n field: FieldSpec,\n key: string,\n item: CollectionItem,\n deps: Pick<SortValueDeps, \"evaluateDerived\" | \"deriveRecord\">,\n): SortValue {\n const display = field.type === \"derived\" ? field.display : undefined;\n if (display === undefined || display === \"number\" || display === \"money\") {\n return numericSortValue(deps.evaluateDerived(field, key, item));\n }\n const enriched = deps.deriveRecord(item);\n if (display === \"date\") return dateSortValue(enriched[key]);\n return stringSortValue(enriched[key]);\n}\n\n/** Comparable value for one row under the active field. Toggle, flag, and\n * derived need the whole record; every other type keys off the raw cell. */\nexport function sortValueOf(field: FieldSpec, key: string, item: CollectionItem, deps: SortValueDeps): SortValue {\n if (field.type === \"toggle\") return boolSortValue(deps.toggleChecked(item, field));\n if (field.type === \"flag\") return boolSortValue(deps.flagValueOf(key, item));\n if (field.type === \"derived\") return derivedSortValue(field, key, item, deps);\n return scalarSortValue(field, item[key], deps.resolveRefDisplay);\n}\n","// Free-text record matching for the collection list search box. Pure and\n// framework-free so the view's `filteredItems` computed just calls it.\n\nimport { fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionItem } from \"./schema\";\n\n/** Case-insensitive substring match across an item's scalar fields.\n * Object/array-valued fields (table rows, nested records) have no text\n * form (`fieldTextOrNull` → `null`) and are skipped — they don't render as\n * searchable text in the list table.\n *\n * `query` is matched against each cell's lower-cased text but is NOT\n * lower-cased here: callers pass an already-normalised (trimmed +\n * lower-cased) query, so an upper-case `query` intentionally matches\n * nothing. Keeping the normalisation at the call site avoids re-lowering\n * the same query once per row. */\nexport function itemMatchesQuery(item: CollectionItem, query: string): boolean {\n // An empty query matches every record — otherwise a row whose only fields are\n // object/array-valued (no scalar cell reaches `includes(\"\")`) would vanish\n // when the search box is cleared. The list caller already short-circuits the\n // empty case, so this only guards a future caller that doesn't.\n if (query === \"\") return true;\n return Object.values(item).some((value) => {\n const text = fieldTextOrNull(value);\n return text !== null && text.toLowerCase().includes(query);\n });\n}\n","// Composite cell keys for the list table's per-cell state maps, plus the\n// load-time \"which enum cells were empty\" snapshot the inline enum dropdown\n// reads. Pure and framework-free so the view keeps only the reactive refs.\n\nimport { fieldText } from \"./fieldText\";\nimport type { CollectionItem } from \"./schema\";\n\n/** The schema slice `snapshotEmptyEnums` reads — minimal structural shape so\n * both the client and server collection-schema types satisfy it as-is. */\nexport interface EnumSnapshotSchema {\n fields: Record<string, { type: string }>;\n primaryKey: string;\n}\n\n/** Stable key for one cell (`<rowId>:<fieldKey>`) in the per-cell state maps. */\nexport function cellKey(rowId: string, fieldKey: string): string {\n return `${rowId}:${fieldKey}`;\n}\n\n/** A row's stable identity: the primary-key value as text (`\"\"` when the schema\n * has no primary key, or the value is absent). Every per-row / per-cell state\n * map keys off this, and it is the SAME derivation `snapshotEmptyEnums` uses\n * for its record ids (`fieldText`), so `cellKey(rowIdOf(...), field)` and the\n * empty-enum snapshot always agree. A real primary key is a scalar, so this\n * matches its plain string form. */\nexport function rowIdOf(primaryKey: string | undefined, item: CollectionItem): string {\n return primaryKey ? fieldText(item[primaryKey]) : \"\";\n}\n\n/** The set of enum cells that were empty in the freshly-fetched records — the\n * only cells whose inline dropdown offers an empty option. */\nexport function snapshotEmptyEnums(schema: EnumSnapshotSchema, records: CollectionItem[]): Set<string> {\n const empty = new Set<string>();\n const enumKeys = Object.entries(schema.fields)\n .filter(([, field]) => field.type === \"enum\")\n .map(([fieldKey]) => fieldKey);\n if (enumKeys.length === 0) return empty;\n for (const record of records) {\n const recordId = fieldText(record[schema.primaryKey]);\n for (const fieldKey of enumKeys) {\n if (record[fieldKey] == null || record[fieldKey] === \"\") empty.add(cellKey(recordId, fieldKey));\n }\n }\n return empty;\n}\n","// Shared chip/label resolution for the collection calendar surfaces (month\n// grid + day view), so both label a record identically. Pure, type-only\n// dependency on the schema shape.\n\nimport { fieldText } from \"./fieldText\";\nimport type { CollectionItem, CollectionSchema } from \"./schema\";\n\n// Text-like field types that make a sensible human-readable label.\nconst LABEL_FIELD_TYPES = new Set([\"string\", \"text\", \"markdown\", \"email\"]);\n\n/** Which field labels a record: the schema's explicit `displayField`, else the\n * first non-primary text-like field (so a collection without a displayField\n * shows e.g. the title rather than the opaque primaryKey), else null → the\n * caller falls back to the primary-key value. */\nexport function labelFieldFor(schema: CollectionSchema): string | null {\n if (schema.displayField) return schema.displayField;\n for (const [key, spec] of Object.entries(schema.fields)) {\n if (key !== schema.primaryKey && LABEL_FIELD_TYPES.has(spec.type)) return key;\n }\n return null;\n}\n\n/** A record's primary-key value as a string. */\nexport function itemIdOf(item: CollectionItem, schema: CollectionSchema): string {\n return fieldText(item[schema.primaryKey]);\n}\n\n/** A record's display label: the resolved `labelField` value, else the\n * primary key. Pass the result of `labelFieldFor(schema)` as `labelField`\n * (compute it once per render rather than per item). */\nexport function itemLabelOf(item: CollectionItem, schema: CollectionSchema, labelField: string | null): string {\n if (labelField) {\n const value = item[labelField];\n // Accept any primitive (string / number / boolean) so a numeric or boolean\n // display field still labels the record; objects/arrays fall through to the\n // id rather than rendering as \"[object Object]\".\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n const text = String(value);\n if (text.length > 0) return text;\n }\n }\n return itemIdOf(item, schema);\n}\n","// Pure builder for the workspace ontology graph — the `/collections` Map\n// panel (phase 2 of plans/done/collection-ontology.md step ①; plan:\n// plans/done/feat-collection-ontology-graph.md). Consumes the per-collection\n// ontology entries `buildWorkspaceOntology` derives server-side and turns\n// them into a node/edge structure a graph view can render directly.\n// Browser-safe (no `node:*` imports) so the Vue plugin builds the same\n// graph from the raw entries — same parity discipline as `deriveAll` and\n// the sibling `wiki/graph.ts`.\n\n/** One relationship a schema declares: a `ref` (the record stores the\n * target's primaryKey slug) or `embed` (display-only pull) pointing at\n * collection `to`, or `backlinks` / `rollup` (display-only REVERSE\n * refs — `to` is the relation's source collection, i.e. its `from`,\n * and `via` names the exact ref field there being reversed).\n * A `ref` column inside a `table` field is reported with a dotted path\n * (`lines.clientId`). Whether `to` exists is NOT checked — resolution\n * is fail-soft at render. */\nexport interface OntologyRelation {\n field: string;\n kind: \"ref\" | \"embed\" | \"backlinks\" | \"rollup\";\n to: string;\n via?: string;\n}\n\nexport interface CollectionOntologyEntry {\n slug: string;\n title: string;\n icon: string;\n primaryKey: string;\n /** The effective display field: the schema's `displayField`, falling\n * back to the primaryKey exactly as render-time labelling does. */\n displayField: string;\n /** Records in the collection, or `null` when the backend couldn't be\n * reached to count them (an engine that failed to load, a closed\n * session). Deliberately not 0: an unreachable collection reported as\n * empty invites \"restoring\" data that is intact but out of reach. */\n recordCount: number | null;\n relations: OntologyRelation[];\n}\n\nexport interface OntologyGraphNode {\n slug: string;\n title: string;\n icon: string;\n recordCount: number | null;\n /** No discovered collection has this slug — the node exists only\n * because a relation points at it. Rendered as a ghost so the graph\n * doubles as a broken-ref lint surface (lint, not lock). */\n missing?: boolean;\n}\n\nexport interface OntologyGraphEdge {\n /** Tail: the collection whose records store (`ref`) or pull (`embed`)\n * the link — or, for an uncollapsed reverse relation, the collection\n * whose refs the `backlinks`/`rollup` field aggregates. */\n from: string;\n /** Head: the collection the link points at. */\n to: string;\n /** Field declaring the relation — on `from` for `ref`/`embed`, on\n * `to` for an uncollapsed `backlinks`/`rollup`. */\n field: string;\n kind: OntologyRelation[\"kind\"];\n /** `backlinks`/`rollup` field names on `to` that declare the reverse\n * of this `ref` edge, collapsed here so one link never draws twice. */\n reverseFields?: string[];\n}\n\nexport interface OntologyGraph {\n nodes: OntologyGraphNode[];\n edges: OntologyGraphEdge[];\n}\n\nconst isForwardKind = (kind: OntologyRelation[\"kind\"]): boolean => kind === \"ref\" || kind === \"embed\";\n\n/** Fold a reverse relation (`backlinks`/`rollup` declared on `owner`)\n * into the edge list: collapse onto the forward `ref` edge whose field\n * is the relation's declared `via` (several refs can link the same\n * pair — matches.homeTeam vs matches.awayTeam — so the pair alone is\n * ambiguous), else emit its own edge oriented in true data direction\n * (refs' source → owner). A `via` naming no forward edge stays\n * uncollapsed — fail-soft, like every other dangling reference. */\nconst addReverseRelation = (edges: OntologyGraphEdge[], owner: string, rel: OntologyRelation): void => {\n const forward = edges.find((edge) => edge.kind === \"ref\" && edge.from === rel.to && edge.to === owner && (rel.via === undefined || edge.field === rel.via));\n if (forward) {\n forward.reverseFields = [...(forward.reverseFields ?? []), rel.field];\n return;\n }\n edges.push({ from: rel.to, to: owner, field: rel.field, kind: rel.kind });\n};\n\n/** Two passes so every forward `ref` edge exists before any reverse\n * relation tries to collapse onto one — entry order must not matter. */\nconst collectEdges = (entries: CollectionOntologyEntry[]): OntologyGraphEdge[] => {\n const edges: OntologyGraphEdge[] = [];\n for (const entry of entries) {\n for (const rel of entry.relations) {\n if (isForwardKind(rel.kind)) edges.push({ from: entry.slug, to: rel.to, field: rel.field, kind: rel.kind });\n }\n }\n for (const entry of entries) {\n for (const rel of entry.relations) {\n if (!isForwardKind(rel.kind)) addReverseRelation(edges, entry.slug, rel);\n }\n }\n return edges;\n};\n\n/** Slugs referenced by an edge endpoint but backed by no discovered\n * collection, slug-sorted for deterministic node order. */\nconst ghostSlugs = (edges: OntologyGraphEdge[], known: ReadonlySet<string>): string[] => {\n const ghosts = new Set<string>();\n for (const edge of edges) {\n if (!known.has(edge.from)) ghosts.add(edge.from);\n if (!known.has(edge.to)) ghosts.add(edge.to);\n }\n return [...ghosts].sort();\n};\n\n/** Build the renderable graph: one node per discovered collection (in\n * the entries' slug-sorted order) plus ghost nodes for dangling\n * targets, one edge per underlying link. */\nexport const buildOntologyGraph = (entries: CollectionOntologyEntry[]): OntologyGraph => {\n const edges = collectEdges(entries);\n const known = new Set(entries.map((entry) => entry.slug));\n const nodes: OntologyGraphNode[] = entries.map(({ slug, title, icon, recordCount }) => ({ slug, title, icon, recordCount }));\n for (const slug of ghostSlugs(edges, known)) {\n // A ghost node has no collection behind it at all, so it genuinely holds\n // no records — 0, not the \"couldn't count\" null.\n nodes.push({ slug, title: slug, icon: \"\", recordCount: 0, missing: true });\n }\n return { nodes, edges };\n};\n"],"mappings":"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"}
@@ -3,6 +3,7 @@ export type { CollectionQuery, CollectionQueryAggregate, CollectionQueryOrder, C
3
3
  export * from './core/ids';
4
4
  export * from './core/collectionKey';
5
5
  export * from './core/fieldText';
6
+ export * from './core/fieldDefaults';
6
7
  export * from './core/project';
7
8
  export * from './core/uiTypes';
8
9
  export * from './core/presentCollection';
@@ -1,7 +1,7 @@
1
1
  import { t as errorMessage } from "../dist-D8zokgGo.js";
2
2
  import { A as isSafeSlug, B as fieldText, C as parseCollectionKeyId, D as SAFE_SLUG_PATTERN, E as SAFE_RECORD_ID_PATTERN, F as INGEST_KINDS, I as embedTargetId, L as isFieldDrivenEvery, M as AGENT_INGEST_KIND, N as COMPUTED_TYPES, O as generateUniqueId, P as FEED_SCHEDULES, R as isReadOnlySchema, S as localCollectionKeyOf, T as sharedCollectionKey, V as fieldTextOrNull, _ as collectionKeyId, a as bucketRecords, b as isSharedCollectionKey, c as dateOf, d as parseIsoDate, f as parseIsoDateTime, g as ymdKey, h as spanCoversDay, i as assignLanes, j as nextUniqueItemId, k as isSafeRecordId, l as daySlice, m as recordSpan, n as shortHexId, o as buildMonthGrid, p as parseTimeRange, r as MINUTES_PER_DAY, s as compareYmd, t as newItemId, u as monthAnchorDate, v as collectionKeyName, w as sameCollectionKey, x as isValidCollectionName, y as isLocalCollectionKey, z as storageKindFor } from "../itemId-DfTm08jm.js";
3
+ import { C as actionVisible, D as fieldDefaultValue, E as whenMatches, O as firstUnknownDefault, S as viaMatches, T as fieldVisible, _ as uniqueRefTargets, a as evaluateDerived, b as projectBacklinkRow, c as selectDynamicRecord, d as flagFieldValue, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, k as schemaDefaults, l as chipMatches, m as matchesWhere, n as deriveAll, o as firstDateField, p as toggleChecked, r as resolveRowRefs, s as resolveIcon, t as defangForPrompt, u as completionCoveredByFieldChip, v as backlinkRows, w as agentActionRunKey, x as rollupValue, y as coerceNumeric } from "../promptSafety-CJ-sC1pX.js";
3
4
  import { n as projectRecordFields, t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
4
- import { C as actionVisible, E as whenMatches, S as viaMatches, T as fieldVisible, _ as uniqueRefTargets, a as evaluateDerived, b as projectBacklinkRow, c as selectDynamicRecord, d as flagFieldValue, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, l as chipMatches, m as matchesWhere, n as deriveAll, o as firstDateField, p as toggleChecked, r as resolveRowRefs, s as resolveIcon, t as defangForPrompt, u as completionCoveredByFieldChip, v as backlinkRows, w as agentActionRunKey, x as rollupValue, y as coerceNumeric } from "../promptSafety-CSdyp7RF.js";
5
5
  //#region src/collection/core/presentCollection.ts
6
6
  var TOOL_NAME = "presentCollection";
7
7
  /** Stamp a host's opaque project scope onto a card payload. The ONLY way a
@@ -268,24 +268,32 @@ function rowDraftToRecord(rowDraft, subFields) {
268
268
  }
269
269
  return row;
270
270
  }
271
- /** Convert a full edit draft to the record to persist. */
271
+ /** Convert a full edit draft to the record to persist.
272
+ *
273
+ * Collected as entries rather than assigned into an accumulator: a field may
274
+ * be named `__proto__` (JSON.parse hands that over as an own key, so it
275
+ * reaches here like any other), and `record[key] = value` would run the
276
+ * prototype setter for that one name — the user's value would vanish between
277
+ * the form and the save, with nothing to see. `fromEntries` defines an own
278
+ * property whatever the name is. The read side of this file already guards the
279
+ * same hazard with `ownFlag` (Codex review on #2910). */
272
280
  function draftToRecord(state, schema) {
273
- const record = {};
281
+ const entries = [];
274
282
  for (const [key, field] of Object.entries(schema.fields)) {
275
283
  if (COMPUTED_TYPES.has(field.type)) continue;
276
284
  if (field.type === "boolean") {
277
- if (shouldEmitBoolean(state, key, field)) record[key] = ownFlag(state.bool, key);
285
+ if (shouldEmitBoolean(state, key, field)) entries.push([key, ownFlag(state.bool, key)]);
278
286
  continue;
279
287
  }
280
288
  if (field.type === "table" && field.of) {
281
289
  const subFields = field.of;
282
- record[key] = (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields));
290
+ entries.push([key, (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields))]);
283
291
  continue;
284
292
  }
285
293
  const value = scalarDraftToValue(state.text[key], field.type);
286
- if (value !== void 0) record[key] = value;
294
+ if (value !== void 0) entries.push([key, value]);
287
295
  }
288
- return record;
296
+ return Object.fromEntries(entries);
289
297
  }
290
298
  /** Normalise a raw inline-edit input (table-cell checkbox/select) to its
291
299
  * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty
@@ -626,6 +634,6 @@ var buildOntologyGraph = (entries) => {
626
634
  };
627
635
  };
628
636
  //#endregion
629
- export { AGENT_INGEST_KIND, COMPUTED_TYPES, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, SAFE_RECORD_ID_PATTERN, SAFE_SLUG_PATTERN, TOOL_DEFINITION, TOOL_NAME, actionVisible, agentActionRunKey, assignLanes, backlinkRows, boolSortValue, bucketRecords, buildMonthGrid, buildOntologyGraph, buildUpdatedRecord, cellKey, chipMatches, coerceInlineValue, coerceNumeric, collectionCardKey, collectionKeyId, collectionKeyName, compareSortValues, compareYmd, completionCoveredByFieldChip, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, derivedSortValue, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldText, fieldTextOrNull, fieldVisible, firstDateField, firstMissingRequiredField, flagFieldValue, generateUniqueId, isFieldDrivenEvery, isLocalCollectionKey, isReadOnlySchema, isSafeRecordId, isSafeSlug, isSharedCollectionKey, isSortableField, isValidCollectionName, itemIdOf, itemIsDone, itemLabelOf, itemMatchesQuery, labelFieldFor, localCollectionKeyOf, matchesWhere, mergeIntoExisting, monthAnchorDate, newItemId, nextSortDirection, nextUniqueItemId, numericSortValue, parseCollectionKeyId, parseIsoDate, parseIsoDateTime, parseTimeRange, projectBacklinkRow, projectRecordFields, recordSpan, resolveEnumColor, resolveIcon, resolveRowRefs, rollupValue, rowFromItem, rowIdOf, sameCollectionCard, sameCollectionKey, scalarSortValue, selectDynamicRecord, sharedCollectionKey, shortHexId, skillCommandSeed, snapshotEmptyEnums, sortItems, sortValueOf, spanCoversDay, storageKindFor, stringSortValue, toPresentCollectionData, toggleChecked, uniqueBacklinkSources, uniqueEmbedTargets, uniqueRefTargets, viaMatches, whenMatches, withCardScope, ymdKey };
637
+ export { AGENT_INGEST_KIND, COMPUTED_TYPES, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, SAFE_RECORD_ID_PATTERN, SAFE_SLUG_PATTERN, TOOL_DEFINITION, TOOL_NAME, actionVisible, agentActionRunKey, assignLanes, backlinkRows, boolSortValue, bucketRecords, buildMonthGrid, buildOntologyGraph, buildUpdatedRecord, cellKey, chipMatches, coerceInlineValue, coerceNumeric, collectionCardKey, collectionKeyId, collectionKeyName, compareSortValues, compareYmd, completionCoveredByFieldChip, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, derivedSortValue, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldDefaultValue, fieldText, fieldTextOrNull, fieldVisible, firstDateField, firstMissingRequiredField, firstUnknownDefault, flagFieldValue, generateUniqueId, isFieldDrivenEvery, isLocalCollectionKey, isReadOnlySchema, isSafeRecordId, isSafeSlug, isSharedCollectionKey, isSortableField, isValidCollectionName, itemIdOf, itemIsDone, itemLabelOf, itemMatchesQuery, labelFieldFor, localCollectionKeyOf, matchesWhere, mergeIntoExisting, monthAnchorDate, newItemId, nextSortDirection, nextUniqueItemId, numericSortValue, parseCollectionKeyId, parseIsoDate, parseIsoDateTime, parseTimeRange, projectBacklinkRow, projectRecordFields, recordSpan, resolveEnumColor, resolveIcon, resolveRowRefs, rollupValue, rowFromItem, rowIdOf, sameCollectionCard, sameCollectionKey, scalarSortValue, schemaDefaults, selectDynamicRecord, sharedCollectionKey, shortHexId, skillCommandSeed, snapshotEmptyEnums, sortItems, sortValueOf, spanCoversDay, storageKindFor, stringSortValue, toPresentCollectionData, toggleChecked, uniqueBacklinkSources, uniqueEmbedTargets, uniqueRefTargets, viaMatches, whenMatches, withCardScope, ymdKey };
630
638
 
631
639
  //# sourceMappingURL=index.js.map