@mulmoclaude/core 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/assets/helps/billing-clients-worklog.md +15 -12
  2. package/assets/helps/collection-skills.md +67 -22
  3. package/assets/helps/todo-collection.md +13 -7
  4. package/dist/{deriveAll-BQ_p5HSB.cjs → calendarGrid-CaR-q36Y.js} +352 -74
  5. package/dist/calendarGrid-CaR-q36Y.js.map +1 -0
  6. package/dist/calendarGrid-gN4sei7s.cjs +1070 -0
  7. package/dist/calendarGrid-gN4sei7s.cjs.map +1 -0
  8. package/dist/collection/core/backlinks.d.ts +20 -0
  9. package/dist/collection/core/ids.d.ts +12 -0
  10. package/dist/collection/core/recordZ.d.ts +23 -0
  11. package/dist/collection/core/schema.d.ts +72 -418
  12. package/dist/collection/core/schemaZ.d.ts +878 -0
  13. package/dist/collection/core/templatePath.d.ts +34 -0
  14. package/dist/collection/core/uiTypes.d.ts +22 -0
  15. package/dist/collection/core/where.d.ts +9 -15
  16. package/dist/collection/index.cjs +50 -330
  17. package/dist/collection/index.cjs.map +1 -1
  18. package/dist/collection/index.d.ts +2 -0
  19. package/dist/collection/index.js +14 -301
  20. package/dist/collection/index.js.map +1 -1
  21. package/dist/collection/paths.cjs +1 -1
  22. package/dist/collection/paths.cjs.map +1 -1
  23. package/dist/collection/paths.js +1 -1
  24. package/dist/collection/paths.js.map +1 -1
  25. package/dist/collection/registry/server/index.cjs +1 -1
  26. package/dist/collection/registry/server/index.js +1 -1
  27. package/dist/collection/server/discovery.d.ts +2 -244
  28. package/dist/collection/server/index.cjs +8 -2
  29. package/dist/collection/server/index.d.ts +1 -0
  30. package/dist/collection/server/index.js +3 -2
  31. package/dist/collection/server/io.d.ts +9 -0
  32. package/dist/collection/server/ontology.d.ts +35 -0
  33. package/dist/collection/server/templatePath.d.ts +1 -34
  34. package/dist/collection/server/validate.d.ts +11 -7
  35. package/dist/collection-watchers/index.cjs +5 -4
  36. package/dist/collection-watchers/index.cjs.map +1 -1
  37. package/dist/collection-watchers/index.js +3 -2
  38. package/dist/collection-watchers/index.js.map +1 -1
  39. package/dist/feeds/index.cjs +4 -4
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/ingestTypes.d.ts +15 -40
  42. package/dist/feeds/server/index.cjs +5 -5
  43. package/dist/feeds/server/index.js +3 -3
  44. package/dist/{ingestTypes-C7EheYZX.cjs → ingestTypes-DG8RExWt.cjs} +3 -3
  45. package/dist/ingestTypes-DG8RExWt.cjs.map +1 -0
  46. package/dist/{ingestTypes-CmJeOUJc.js → ingestTypes-V4upKeBx.js} +2 -2
  47. package/dist/ingestTypes-V4upKeBx.js.map +1 -0
  48. package/dist/{server-DKXXFTbw.js → server-BNTR7ySK.js} +531 -196
  49. package/dist/server-BNTR7ySK.js.map +1 -0
  50. package/dist/{server-G6GtOdaW.cjs → server-bsNrf2yW.cjs} +572 -213
  51. package/dist/server-bsNrf2yW.cjs.map +1 -0
  52. package/package.json +1 -1
  53. package/dist/deriveAll-BQ_p5HSB.cjs.map +0 -1
  54. package/dist/deriveAll-CMFXDQ_G.js +0 -506
  55. package/dist/deriveAll-CMFXDQ_G.js.map +0 -1
  56. package/dist/ingestTypes-C7EheYZX.cjs.map +0 -1
  57. package/dist/ingestTypes-CmJeOUJc.js.map +0 -1
  58. package/dist/server-DKXXFTbw.js.map +0 -1
  59. package/dist/server-G6GtOdaW.cjs.map +0 -1
@@ -103,18 +103,19 @@ Don't push for fields the user hasn't given you — leave optional fields out of
103
103
  the JSON entirely.
104
104
 
105
105
  ## What to do
106
- **Add**: derive an `id`, build the record, Write `data/clients/items/<id>.json`.
107
- List the directory first and pick a fresh slug if the file already exists — don't
108
- silently overwrite.
106
+ **Add**: derive an `id`, build the record, write via `manageCollection`
107
+ `putItems` with `mode: "create"` an id collision comes back as a `rejected`
108
+ row instead of silently overwriting; pick a fresh slug and retry.
109
109
 
110
- **List / look up**: read `data/clients/items/`, answer from those files. Don't
110
+ **List / look up**: `manageCollection` `getItems`, answer from the rows. Don't
111
111
  recite the whole table in chat — the user sees it at `/collections/clients`. A
112
112
  one-line confirmation ("Added Acme Corp.") is enough.
113
113
 
114
- **Update**: Read merge changes Write back. Preserve fields you weren't asked
115
- to change.
114
+ **Update**: `putItems` with `mode: "merge"` and a partial row
115
+ (`{ id, <changed fields> }`) — the default upsert replaces the whole record.
116
116
 
117
- **Delete**: confirm once if the request is ambiguous, then remove the file.
117
+ **Delete**: confirm once if the request is ambiguous, then remove the file
118
+ (`manageCollection` has no delete).
118
119
 
119
120
  ## Linking to a client in chat
120
121
  Link to the collection view, not the raw JSON path:
@@ -179,18 +180,20 @@ description: A simple timesheet — log billable / non-billable hours per client
179
180
  ## clientId resolution
180
181
  `clientId` is a `ref` to the `clients` collection — write the raw client slug.
181
182
  When the user says "log 2 hours for Acme":
182
- - List `data/clients/items/` and find the slug whose `name` matches "Acme"
183
- (case-insensitive substring is fine).
183
+ - `manageCollection` `getItems` on `clients` (`fields: ["id", "name"]`) and
184
+ find the slug whose `name` matches "Acme" (case-insensitive substring is
185
+ fine).
184
186
  - If no match: ask whether to create the client first (via the `clients` skill)
185
187
  or use a literal slug they supply. Never invent a clientId that doesn't exist —
186
188
  it renders as a broken link.
187
189
 
188
190
  ## What to do
189
191
  **Log hours**: derive `id`, default `billable: true`, default `date` to today if
190
- unspecified, Write the JSON. (This skill tracks total hours per day per client —
191
- not start/end times.)
192
+ unspecified, write via `manageCollection` `putItems` (`mode: "create"`; fix any
193
+ `rejected` row from its `problem` text and retry). (This skill tracks total
194
+ hours per day per client — not start/end times.)
192
195
 
193
- **List / summarize**: read `data/worklog/items/`, answer from the files. Don't
196
+ **List / summarize**: `manageCollection` `getItems`, answer from the rows. Don't
194
197
  recite the table — point at `/collections/worklog`. For aggregates ("how many
195
198
  hours did I bill Acme last month?") group by clientId + date range and answer in
196
199
  one line.
@@ -31,7 +31,7 @@ data/skills/<slug>/ ← YOU write here (Write / Edit)
31
31
  SKILL.md · schema.json · templates/*.md
32
32
 
33
33
  data/<name>/items/ ← the records (separate from the skill dir)
34
- <id>.json ← one record per file (you write; host reads + renders)
34
+ <id>.json ← one record per file (write via manageCollection putItems)
35
35
  ```
36
36
 
37
37
  - **Author under `data/skills/<slug>/`, NEVER `.claude/skills/<slug>/`
@@ -148,7 +148,7 @@ skipped, never crashes the host):
148
148
 
149
149
  `string` · `text` (multi-line) · `email` · `number` · `date` (`YYYY-MM-DD`) ·
150
150
  `datetime` (`YYYY-MM-DDTHH:MM`) · `boolean` · `markdown` · `money` · `enum` ·
151
- `ref` · `embed` · `table` · `derived` · `image` · `file` · `toggle`
151
+ `ref` · `embed` · `backlinks` · `table` · `derived` · `image` · `file` · `toggle`
152
152
 
153
153
  Every field spec needs a `type` and a `label`. Extra keys by type:
154
154
 
@@ -179,9 +179,23 @@ Every field spec needs a `type` and a `label`. Extra keys by type:
179
179
  sibling as a dropdown picker in the editor and hides its own cell (the embed
180
180
  owns it). E.g. a multi-issuer invoice: a `ref` field `issuerId → profile`
181
181
  plus `{ "type": "embed", "to": "profile", "idField": "issuerId" }`.
182
+ - **`backlinks`** — `from: "<source-slug>"`, `via: "<ref-field-in-source>"`,
183
+ `display: ["<source-col>", ...]`, optional `filter: { "field": ..., "in": [...] }`.
184
+ The **reverse** side of a `ref`: a read-only sub-table (detail view only) of
185
+ the records in `from` whose `via` ref points at this record — each row links
186
+ to that record. **Nothing is stored** on this record (like `derived`/`embed`);
187
+ you never write it, and the rows update whenever the source records change.
188
+ `display` names the source columns to show — a derived source column works
189
+ when its formula is self-contained (e.g. an invoice `total` summing its own
190
+ line items), but one that derefs yet another collection renders em-dash;
191
+ `filter` narrows rows by a source field's value, same shape as `when`. E.g. a
192
+ client's open invoices:
193
+ `{ "type": "backlinks", "label": "Invoices", "from": "invoice", "via": "clientId", "display": ["issueDate", "total", "status"], "filter": { "field": "status", "in": ["draft", "sent"] } }`.
194
+ Resolution is fail-soft: an unknown `from` / `via` / `display` column just
195
+ renders an empty sub-table — no error, so author the `ref` side first.
182
196
  - **`table`** — `of: { <col>: <sub-field-spec>, ... }`. An array of rows. Each
183
- sub-field is a flat spec; sub-fields **cannot** be `table` or `derived`
184
- (no nested tables, no computed columns).
197
+ sub-field is a flat spec; sub-fields **cannot** be `table`, `derived`, or
198
+ `backlinks` (no nested tables, no computed columns).
185
199
  - **`derived`** — `formula: "<expr>"`, optional `display` (`number` default, or
186
200
  `money` / `string` / `date`) and `currency`. **Read-only, host-computed** —
187
201
  you NEVER write derived values into the JSON; the host recomputes them on
@@ -761,10 +775,32 @@ single source of truth and the "done" checkbox is a `toggle` field projecting it
761
775
 
762
776
  ## Records — one JSON object per file
763
777
 
764
- - Write each record to `<dataPath>/<id>.json` via the **Write** tool; the `id`
765
- field's value is the filename (no extension).
778
+ Each record is a plain file at `<dataPath>/<id>.json` (the `id` field's value is
779
+ the filename, no extension) — that is the storage model. But you read and write
780
+ records through **`manageCollection`**, not raw file I/O:
781
+
782
+ - **Create / update — `putItems`.** Every row is validated against the schema
783
+ BEFORE the write (required fields, enum membership, primaryKey = record id)
784
+ and the result reports `{ written, rejected }` — fix each rejected row from
785
+ its `problem` text and retry just those rows. Use `mode: "create"` when
786
+ adding, so an id collision is rejected instead of silently overwritten, and
787
+ `mode: "merge"` with a partial row (`{ id, <changed fields> }`) when
788
+ updating — the default upsert replaces the WHOLE record and would erase
789
+ every optional field the row omits.
790
+ - **Read / list — `getItems`.** The only way to see host-computed `derived` /
791
+ `toggle` / `embed` values (the stored JSON never contains them). Pass `ids`
792
+ / `fields` on large collections to keep the result small — e.g.
793
+ `fields: ["id"]` to check for an id collision before an add.
794
+ - **Delete** — remove the record file (`manageCollection` has no delete).
795
+ - **Cross-collection questions — `getOntology`.** Returns every collection in
796
+ the workspace with its `primaryKey`, effective `displayField`, record count,
797
+ and outbound `ref` / `embed` relations (field → target slug, including refs
798
+ inside `table` columns as `lines.clientId`). When a question spans
799
+ collections ("which clients have unpaid invoices AND unlogged hours?"),
800
+ call it first to see which collections exist and how they join, then
801
+ `getItems` only the ones involved — instead of reading every schema.json.
766
802
  - **Id charset** (enforced by `safeRecordId` in
767
- `packages/plugins/collection-plugin/src/server/paths.ts` — the single source of
803
+ `packages/core/src/collection/server/paths.ts` — the single source of
768
804
  truth; `manageCollection` rejects ids that fail it): start and end with a
769
805
  letter or digit; inside, also `-`, `_`, and `.` are allowed (so natural keys
770
806
  like a Slack ts `1718900000.123456` or a SemVer `1.2.3` work). **No** path
@@ -774,6 +810,25 @@ single source of truth and the "done" checkbox is a `toggle` field projecting it
774
810
  enforces this on every targeted read/write, so an id that only _looks_ fine in
775
811
  a full `getItems` listing but violates the rule can't be updated or deleted by
776
812
  id — fix the id, don't work around it with raw file I/O.
813
+ - **Never write `derived` fields**, and never write an `embed` or `backlinks`
814
+ field — all are display-only / host-computed (`putItems` rejects rows that
815
+ carry them).
816
+ - Leave optional fields out of the row entirely rather than writing empty
817
+ strings.
818
+ - For a `ref` field, write the raw target slug, and make sure that record
819
+ actually exists in the target collection — an invalid slug renders as a broken
820
+ link. The host enforces structure and safety; **you own semantic correctness**
821
+ (valid refs, sane values).
822
+
823
+ ### Raw file I/O on records — the escape hatch
824
+
825
+ Read / Write / Edit on the record files stays available (files are the source
826
+ of truth), but it skips `putItems`' pre-write validation — a mistake lands on
827
+ disk instead of coming back as a `rejected` row. Reach for it only when the
828
+ tool can't do the job: bulk file surgery, or repairing a file so malformed
829
+ that `manageCollection` can't address it. If you do write record files
830
+ directly:
831
+
777
832
  - **The file MUST be valid JSON.** A malformed record is **silently skipped** at
778
833
  read time (logged server-side, but invisible in the UI) — so one bad file out
779
834
  of fifteen looks like "fourteen records vanished." The #1 cause is an
@@ -782,22 +837,12 @@ single source of truth and the "done" checkbox is a `toggle` field projecting it
782
837
  (`text`, `markdown`, a long `objective`), either escape every inner ASCII quote
783
838
  as `\"`, or — better — use the language's own quotation marks (`「」`/`『』` for
784
839
  Japanese, `‘ ’`/`“ ”` or `'…'` for English) so no escaping is needed.
785
- `presentCollection` re-validates the records and reports any unreadable /
840
+ - `presentCollection` re-validates the records and reports any unreadable /
786
841
  malformed / schema-violating files back to you (a `⚠️` in its result) — so
787
- always follow a batch of writes with a `presentCollection` call and **act on
788
- any ⚠️ it returns** (Read → fix → Write), rather than assuming every record
789
- landed.
790
- - **List the directory first** and pick a fresh id rather than silently
791
- overwriting. Update = Read, merge, Write back (preserve fields you weren't
792
- asked to change). Delete = remove the file.
793
- - **Never write `derived` fields**, and never write an `embed` field — both are
794
- display-only / host-computed.
795
- - Leave optional fields out of the JSON entirely rather than writing empty
796
- strings.
797
- - For a `ref` field, write the raw target slug, and make sure that record
798
- actually exists in the target collection — an invalid slug renders as a broken
799
- link. The host enforces structure and safety; **you own semantic correctness**
800
- (valid refs, sane values).
842
+ always follow a batch of direct writes with a `presentCollection` call and
843
+ **act on any ⚠️ it returns**, rather than assuming every record landed.
844
+ (This safety net applies after `putItems` batches too, but direct writes are
845
+ where it earns its keep.)
801
846
 
802
847
  ## End-to-end: creating a new collection skill
803
848
 
@@ -80,11 +80,16 @@ description: The user's personal todo list. Use whenever they add, list, edit,
80
80
  - Do NOT write a `done` field — it's a projection of `status`.
81
81
 
82
82
  ## What to do
83
- - **Add**: derive an id, default `status: "todo"`, Write the JSON.
84
- - **List**: read `data/todos/items/`, answer from the files; point the user at
85
- `/collections/todos` rather than reciting the table.
86
- - **Mark done**: Read set `status: "done"` Write.
87
- - **Edit / Delete**: Read mutate / remove the file (preserve untouched fields).
83
+ - **Add**: derive an id, default `status: "todo"`, write via `manageCollection`
84
+ `putItems` (`mode: "create"`; a rejected row comes back with a `problem` to
85
+ fix and retry).
86
+ - **List**: `manageCollection` `getItems` (the only way to see the computed
87
+ `done` toggle); point the user at `/collections/todos` rather than reciting
88
+ the table.
89
+ - **Mark done**: `putItems` with `mode: "merge"` and `{ id, status: "done" }`.
90
+ - **Edit**: `putItems` `mode: "merge"` with a partial row — the default upsert
91
+ replaces the whole record. **Delete**: remove the file (`manageCollection`
92
+ has no delete).
88
93
  - After a change, call `presentCollection` with slug `todos` (and the id) to show
89
94
  it inline.
90
95
  ```
@@ -131,8 +136,9 @@ single array) with the columns in the sibling `columns.json`. Convert them:
131
136
  the legacy list had.
132
137
  3. **Fold `completed` into `status`.** A legacy `completed: true` item belongs in
133
138
  the done column; don't carry a separate boolean.
134
- 4. **One file per item.** Split the array into `data/todos/items/<id>.json`,
135
- preserving each legacy `id` verbatim.
139
+ 4. **One record per item.** Split the array into one `putItems` row per item
140
+ (stored as `data/todos/items/<id>.json`), preserving each legacy `id`
141
+ verbatim.
136
142
  5. **Convert `createdAt`.** The legacy value is Unix milliseconds — convert it to
137
143
  a `YYYY-MM-DD` `date` field. Carry `priority`, `dueDate`, `note` straight
138
144
  across.