Repository navigation
Plugin custom fields: list fields with an observed-remove merge (nxf 6j6v.4g6r) - #63
Conversation
…xf 6j6v.vbp6)
A plugin can declare `[fields.<name>] type = "datetime"`. A value must be an
RFC 3339 timestamp with a time zone; a plain date or a local time is refused.
It is stored as the same instant in UTC at whole seconds
(`2026-10-12T10:30:00+02:00` -> `2026-10-12T08:30:00Z`), so text order is
chronological order for every downstream comparison (derived ranking, hosted
fold, SQL) with nothing type-aware there. `FieldType` gains `Datetime` and
becomes #[non_exhaustive] (facade: breaking).
Tests: validator unit tests, plugin declaration, facade write path, Engine
seam, derived ranking `of = { field }`, MCP handlers, schema --json via the
file-fixture consumer, and the fold-ddb served-board round trip. Spec §5.1
and the plugins guide (EN/DE) document the type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
…(nxf 6j6v.4g6r)
A plugin can declare `[fields.<name>] list = true`: the field holds entries, each
a value of its type (text, date, datetime, enum) with an optional free-text label
(work, private, ...). Entries are written with `nxf entry add/remove`, read with
`nxf entry list`, over MCP with flow_entry_add/remove/list and on the facade with
Engine::field_entry_add/field_entry_remove/field_entries. `--set <field>=` clears
a list; a non-empty `--set` is refused. Records carry the entries under `custom`
as [{value, label?}]; `nxf schema --json` marks the field "list": true.
Merge: the value is the entry's identity; several present adds of one value are
one entry, labelled by the latest add and placed by the earliest; a remove (or a
clear) tombstones the adds its writer saw, so a concurrent add survives (add
wins). Ops share the `field` kind (op_type add/remove) and fold into one table,
field_entries(item_id, field, tag, ...), keyed with the item leftmost: one Query
per item on a hosted board, no index change. Task fold revision 3.
A list field cannot be required, a longtext, take a merge, or be read by a
ranking key (refused at load). facade: breaking (FieldDecl/SchemaField gain
`list` and become #[non_exhaustive]).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
… guard, stored-form receipts (nxf 6j6v.4g6r) - fold-ddb: the records readers refuse a stream whose watermark names another task-reducer revision than this build's (NotCurrent now names the domain); INDEX_REVISION stays 3. The DynamoDB-local cost test expects 4n Queries (five requests per ticket); cost docs in lib.rs and the E4 spec follow. - core: one entry separator, model::ENTRY_SEP. An entry add whose op id holds it is not folded, live_entry_tags skips such tags, and an entry op's field must meet the chunk-field grammar so a server's sort key stays under 1 KiB. A plugin's list field name is at most 64 characters. - facade: write::entry_add returns the stored FieldEntry, entry_remove the value as matched; entry_add_receipt / entry_remove_receipt are the one spelling the CLI and MCP share. entry_remove ignores the `on` scope and also matches a datetime's text as given. validate::datetime trims its input. - spec 5.2: tombstone growth and phantom tombstones (follow-up 6j6v.2sbr), the op-id and field-name rules, receipts, removal scope. - golden: every case must have a sandbox, asserted before trycmd runs. - tests: task revision 2 -> 3 refold (local and hosted), hostile op ids, typed entries, 16 KiB limit, on scope, Engine and MCP refusals, the real `nxf entry list` output through a probe-nxf subprocess, leap second and space separator, a relabel that really races a remove, pinned labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
Mitigation of the review (report 2026-10-12T12-02-18Z), commit 197a136
Facade changes beyond the PR's original note: Gates run locally: core |
Stacked on #61 (vbp6, datetime). Merge #61 first; until then this diff includes its commit.
What
A plugin can declare a list field:
[fields.email] type = "text" list = true. Each entry is a value of the field's type (text,date,datetime,enum, validated as a single value is) with an optional free-text label (work,private, …).nxf entry add <id> <field> <value> [--label L]nxf entry remove <id> <field> <value>nxf entry list <id> <field>flow_entry_addflow_entry_removeflow_entry_listfield_entry_addfield_entry_removefield_entriesupdate --set <field>=(empty) clears a list; a non-empty--set(orcreate --set) on a list field is refused with a pointer to the entry verbs. Records carry entries undercustomas[{"value": …, "label": …}](labelonly when set, empty list omitted).nxf schema --json/flow_schemaadd"list": truebesidekind.Merge semantics (OR-set, add wins)
(lamport, site, op id); relabel = re-add with the new label (no--label→ unlabelled).Core / hosted impact
target_kind = field,op_type = add(value + U+001F + label) /remove(observed tags, ≤1024 per op). Older binaries store-don't-fold them.field_entries(item_id, field, tag, value, label, lamport, site, removed), PK(item_id, field, tag); a remove only setsremoved = 1on the add's row, so any order converges on both appliers. Projection shared by replica and server (model::present_entries).INDEX_REVISIONstays 3), no GSI / table-contract change; the hosted records readers now also refuse a stream whose watermark names another task revision (RecordsError::NotCurrent { domain, found },domainnew) until it is refolded — no extra refold beyond the one the task bump already causes; hosted records read one more Query per item (field_entries#<item>#): active board5n + 1 + p.Refused at load
list = truewithlongtext, withmerge, or withrequired = true; a ranking keyof = { field = <list> }.Facade: breaking
plugin::FieldDeclandread::SchemaFieldgainlistand become#[non_exhaustive];ActiveTicket/ListItemgaincustom_lists(already non-exhaustive); newEngine::field_entry_add/remove/field_entries,read::field_entries,read::field_entries_to_value,write::entry_add/remove;FieldEntryre-exported. After review mitigation:Engine::field_entry_add/write::entry_addreturn the storedFieldEntry,Engine::field_entry_remove/write::entry_removereturn the matched value (String), newwrite::entry_add_receipt/entry_remove_receipt; a list field name over 64 characters is refused at plugin load. app-foundations follows with 41j0.bm81.Tests
tests/field_entries.rs(two replicas: concurrent same-value add, remove vs re-add/relabel → add wins, concurrent removes, concurrent relabels, clear vs add, remove-before-add, malformed ops, 32 shuffled orders); full core suite green.dynamodb_localagainst DynamoDB Local 3.3.1 (7/7).file-fixturegainsmirrorslist; facade surfaces, sync round trip, and the realnxfcommand tree in-process; green.tests/mcp.rssurface/annotation tests; CLIverb_seam,schema, golden (entries.trycmd);cargo xtask docs check/coverage,changelog checkok.Spec: docs/specs/plugin-custom-fields.md §5.2 (+ TB-CF-4, E1 §3.2, E4 cost). Guide EN/DE "List fields".
🤖 Generated with Claude Code
https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME