Skip to content

plan: !task type at genesis + haustoria-side VTODO field mapping#100

Draft
friedenberg wants to merge 12 commits intomasterfrom
plan-task-type-genesis-haustoria-fields
Draft

plan: !task type at genesis + haustoria-side VTODO field mapping#100
friedenberg wants to merge 12 commits intomasterfrom
plan-task-type-genesis-haustoria-fields

Conversation

@friedenberg
Copy link
Copy Markdown
Collaborator

Plan-only PR. Comment on docs/plans/2026-04-06-task-type-genesis-and-haustoria-fields.md directly to drive decisions.

Drops the !vtodo persisted-type plan (docs/plans/2026-04-05-vtodo-langlang-design.md) — VTODO format becomes a haustoria-side concern only. Two changes proposed:

  1. Formally commit !task into the bigbang/genesis process with status / priority / due field definitions, alongside the existing !md default type.
  2. Hardcode VTODO ↔ !task field mapping inside mike/haustoria_caldav (compile + decompile), replacing the lossy status-tags config.

Six decisions to confirm in the "Decisions to confirm" section.

🤖 Generated with Claude Code

Drops the !vtodo persisted-type plan in favor of haustoria-side
hardcoded VTODO ↔ !task field mapping. Formally commits !task into
the bigbang/genesis process so every fresh repo has it as a built-in
with status/priority/due fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

``` toml
file-extension = "task"
vim-syntax-type = "taskpaper"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be toml


[[fields]]
name = "priority"
kind = "u32"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be an enum that maps to the vtodo default priorities from tasks.org (!, !!, !!!, none -- but as text, not exclamation points)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually enum values of p0, p1, p2, p3


The new step is gated on the existing `BigBang.ExcludeDefaultType` flag --- same
flag that already gates `!md`. This means `init-workspace` and `clone` both skip
it, which matches existing behavior for default types.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default should be don't include it -- opt-in for now, and once it matures can become opt-out.

(matches the field-mutation work in `19c6f63`). Alternative: keep VTODO
casing (`needs-action` / `in-process` / `completed` / `cancelled`).

2. **`!chore` handling**: drop `!chore` entirely and have both `tasks` and
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ship both for now, but eventually would like to explore an !actionable abstract type that !task and !chore can be built on top of (less extension and more composition)

indexFields.Add(fields.Field{
Key: "status",
Value: statusValue,
TypeBlobDigest: s.taskTypeBlobDigest,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the task type blob digest should always be looked-up-fresh for each commit cycle.

also, let's confirm that this approach (setting the field on the object struct) will cause the commit cycle to update the blob as part of the field-reading/writing process

- Status enum stays as todo/in-process/done/cancelled
- Priority becomes p0/p1/p2/p3 enum (tasks.org convention)
- Ship !chore as separate built-in alongside !task; future !actionable
- New opt-in BigBang.IncludeBuiltinActionableTypes flag (default false)
- File extension toml; instances are TOML-mirrored field blobs
- Fresh type-blob-digest lookup per compile/decompile cycle
- Add field-codec-without-scripts verification test as gating step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@friedenberg
Copy link
Copy Markdown
Collaborator Author

Pushed f2c6c04 addressing review feedback. Per-comment responses:

  1. file-extension = "toml" (#discussion_r3039096235) — done. Instances of !task are now TOML blobs that mirror the field values. The haustoria reads VTODOs directly from CalDAV, so for the haustoria flow the blob is decorative; for direct dodder new !task use the user edits TOML. See §1.

  2. Priority enum p0/p1/p2/p3 (#discussion_r3039100824#discussion_r3039159731) — done. priority is now an enum with values p0 / p1 / p2 / p3 mapped to the tasks.org convention (none / ! / !! / !!!) and VTODO PRIORITY integers 0 / 9 / 5 / 1. Mapping table in §1, reverse mapping in §3.

  3. Opt-in by default (#discussion_r3039104282) — done. New BigBang.IncludeBuiltinActionableTypes flag, default false. dodder init does NOT commit !task / !chore unless the user passes the flag. Can flip to opt-out once the types stabilize. No reuse of ExcludeDefaultType. See §2.

  4. Ship !task and !chore both, future !actionable (#discussion_r3039147836) — done. Both ship as separate built-in types with the same field set. Field list is duplicated for now; future work captured as a §1a note pointing at the eventual !actionable composition target. The live workspace's tasks and chores calendars bind to their respective types via the existing per-calendar type config.

  5. Fresh type-blob lookup per cycle + confirm field-writer behavior (#discussion_r3039157787) — done on the lookup side. The haustoria now does a fresh s.supplies.ReadPrimitiveQuery for the type blob digest per calendar inside queryCheckedOutForCalendar and CheckoutOne. No caching at Initialize.

    On the field-writer behavior: I read papa/store/field_writer.go:110-112 and field_reader.go:74-77. Both early-return when the type blob has no [fields-writer] / [fields-reader] script configured. Since !task ships without scripts, the flow is:

    1. Haustoria sets daughter.Metadata.Index.Fields["status"] with TypeBlobDigest populated.
    2. tryWriteFields early-returns at fieldsWriter == nil. Daughter fields stay intact (the Reset() at line 203 only fires after the script runs successfully).
    3. tryReadFields early-returns at fieldsReader == nil. Fields still intact.
    4. Binary stream-index encoder serializes Metadata.Index.Fields verbatim (the codec change from 19c6f63).
    5. On read back, fields appear in the index without ever touching the blob.

    So the blob does NOT get updated as part of the field-writing process for !task — there's no script to project field values back into the TOML blob. The canonical source for field values is the binary stream index, fed by the haustoria on each compile cycle. The blob is decorative.

    I'm not 100% confident in step 4 — the codec was added in 19c6f63 and tests pass for organize-driven mutation, but the haustoria is a new entry point. The plan now lists TestFieldsPersistThroughCommitWithoutScripts as the first implementation step (gates everything else) to verify this assumption before any haustoria work lands. If it fails, we either ship a fields-writer script for !task (a small TOML projector) or add a "field-only" commit path that bypasses the script gate. See §3 "Field-writer / field-reader interaction" + Risks.

    Worth your eye on the assumption: do you remember whether the binary codec persists Metadata.Index.Fields unconditionally, or only when a script ran? If you already know the answer, that lets us skip the gating test.

Updated plan also tightens implementation order, drops the now-stale fixture-regeneration step (opt-in means the existing fresh-store fixtures don't change), and updates the live workspace migration to a clean re-init flow with the new flag.

🤖 Generated with Claude Code

Two BATS probes added to fields.bats to answer the open verification
question on PR #100: does the binary stream-index codec persist
Metadata.Index.Fields when no fields-reader/fields-writer scripts are
configured on the type?

field_persists_without_any_scripts: type with [[fields]] only, no
scripts. Organize-set status field is DROPPED entirely on commit. Blob
digest also disappears. Locked assertion: [one/uno !task "my task"].

field_persists_with_reader_only_no_writer: type with reader, no writer.
Organize-set status=done is silently overwritten by the reader
re-projecting status=todo from the unchanged blob, AND the field appears
TWICE in show output (status=todo status=todo).

Both findings invalidate the original plan's assumption that the
haustoria can set fields directly on metadata without a fields-writer
script. The plan needs revision: either ship a TOML field-projector
script with !task, or add a code path that bypasses the script gate.

The duplicated-field result in probe 2 looks like a separate bug to
file independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@friedenberg
Copy link
Copy Markdown
Collaborator Author

Probe results: the original plan's core assumption is wrong. Pushed cfdb846 with two BATS probes in zz-tests_bats/current_version/fields.bats.

Probe 1: field_persists_without_any_scripts

Type with [[fields]] definition only, NO [fields-reader], NO [fields-writer]. Create a !task, then organize-mutate status=done, then dodder show:

[one/uno !task "my task"]

The status field is dropped entirely. Blob digest is also gone. Organize-set field values do NOT survive the commit cycle when no writer script is configured.

Probe 2: field_persists_with_reader_only_no_writer

Type with reader script, NO writer. Create a task with status=todo in the blob, then organize to status=done, then show:

[one/uno @blake2b256-qhdgjzc945w6v4pw4j4hr66gehgzwf8hq4v9rch9e7px5lf525sqfjcuaa !task "my task" status=todo status=todo]

Two regressions in one line:

  1. The user's status=done was silently dropped — the reader re-projected status=todo from the unchanged blob and clobbered the edit.
  2. The field appears twice (status=todo status=todo) in the output.

The duplicated-field result looks like a separate bug worth filing independently of this plan.

Implication for the haustoria plan

The "haustoria sets fields directly on Metadata.Index.Fields" approach does not work as drafted. Three options:

  1. Ship a [fields-writer] script with !task (e.g. a tiny yq invocation that writes DODDER_FIELD_status into the TOML blob). The haustoria still emits a TOML blob; the writer script reads the env vars and projects them into the blob; on subsequent reads the reader script projects them back. Symmetric, no new code path needed. Cost: requires yq (or similar) at runtime, which the existing fields tests already do.

  2. Have the haustoria emit the TOML blob directly with field values baked in. The haustoria already parses VTODOs in Go; it can serialize a {status="done", priority="p3", due="..."} TOML blob and write that to the blob store, then commit a !task with [fields-reader] script. The reader projects fields out on read. No writer needed because the blob is canonical. This is the cleanest split: the haustoria owns the blob format, the reader script owns the index projection.

  3. Add a "field-only" commit path in papa/store that bypasses the script gate when fields are set explicitly. New API surface, biggest blast radius.

My recommendation: option 2. The haustoria already has all the data; emitting a 3-line TOML blob is trivial; the existing fields-reader machinery does the index projection for free; no new code paths in papa/store. The !task type ships with a [fields-reader] script (e.g. yq -p toml -o json '{\"status\": .status, \"priority\": .priority, \"due\": .due}') and no writer.

Want me to update the plan with option 2 as the chosen path?

🤖 Generated with Claude Code

Three additional BATS probes verify the "ship !task with reader+writer
scripts" path:

field_full_task_three_fields_from_blob: blob with 3 fields baked in →
reader projects all 3 → all display in show. WORKS.

field_full_task_organize_mutate_one_of_three: start with 3 fields in
blob, organize-set status=done, all 3 persist with writer rewriting the
TOML. WORKS.

field_full_task_organize_from_empty_blob: empty starting blob →
organize-set 3 fields → fields and blob digest both dropped. FAILS the
same way as field_persists_without_any_scripts.

Conclusion: option 1 + option 2 combined works. !task ships with
[[fields]] + [fields-reader] + [fields-writer]. The haustoria writes a
complete TOML blob with field values baked in (compile path). The
reader projects fields on commit; the writer projects edits back into
the blob on subsequent organize mutations. The decompile path reads
fields from metadata.Index.

The empty-blob case (probe 13) is a non-issue because the haustoria
always writes a full blob, never an empty one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@friedenberg
Copy link
Copy Markdown
Collaborator Author

Adding [fields-reader] + [fields-writer] to !task works. Three more probes in c5ebd34:

Probe 11: blob with all three fields baked in → reader projects them

[one/uno @blake2b256-vlvc52ycuyyk4vm98j9z33kceh4c0z253e9rlg3n3erp5kwc2xhssfyyn9 !task "my task" status=in-process priority=p2 due=20260415T120000Z]

✅ Reader script with yq -p toml -o json '{status: .status, priority: .priority, due: .due}' projects all three. This is the haustoria compile path.

Probe 12: organize-mutate one of three fields, others preserved

[one/uno @blake2b256-vpgtdcx47rtm8djza9acw46gjrj0a0rc7g6grxt5fqxdcyu5scss7e3xae !task "my task" status=done priority=p1 due=20260415T120000Z]

✅ Status changes from todo to done; priority=p1 and due=... are preserved. New blob digest (writer rewrote the TOML). Round-trip clean. This proves the haustoria can use dodder organize for field mutation and the changes will persist back through the writer script — the dodder side becomes the source of truth between haustoria sync cycles.

Probe 13: organize-set fields starting from EMPTY blob

[one/uno !task "my task"]

❌ Same failure as probe 1. With an empty starting blob, tryWriteFields can't run (the writer needs an existing blob to mutate via DODDER_BLOB_PATH). Fields and blob digest both drop.

This is a non-issue for the haustoria because the haustoria always writes a complete TOML blob with field values baked in (the compile path). It never tries to commit a fields-only object with an empty blob.

Revised plan: option 1 + option 2 combined

The plan now becomes:

  1. !task ships with [[fields]] + [fields-reader] + [fields-writer] scripts (using yq for the TOML projection).
  2. Compile path (CalDAV → dodder): the haustoria parses the VTODO, builds a {status, priority, due} map in Go, serializes it as a TOML blob, writes the blob to the blob store, and commits a !task object with that blob digest. The reader script projects the fields into Metadata.Index.Fields automatically. No manual field-setting needed.
  3. User edits (dodder organize): standard fields-writer flow takes over. The user can edit any of the three field values; the writer projects them back into the TOML blob.
  4. Decompile path (dodder → CalDAV): the haustoria reads Metadata.Index.Fields (which the reader has populated from the blob) and emits a VTODO with the corresponding STATUS/PRIORITY/DUE values.

Bonus benefits over the original plan

  • No new code paths in papa/store or mike/haustoria_caldav for field handling — everything goes through the existing reader/writer machinery.
  • No "fresh type blob digest lookup" needed — the reader script handles digest stamping automatically when it runs during commit.
  • dodder can be the source of truth between sync cycles. User edits via organize survive haustoria re-syncs because the dodder blob has the canonical field values.
  • The two-pass dependency on TestFieldsPersistThroughCommitWithoutScripts is gone. We don't need to verify codec-without-scripts behavior because we're using scripts.

Drawbacks

  • yq becomes a runtime dependency for !task (already a dependency for the existing fields tests, so this is a wash).
  • Three OS process invocations per object on commit (reader + writer + reader). For 1100-task workspaces this is ~3300 yq invocations on a full sync. Acceptable for an interactive command, possibly slow enough to want batching later.

Will update the plan markdown next. WDYT?

🤖 Generated with Claude Code

PR #100 probes proved the original "set fields directly on metadata"
assumption wrong. Both tryWriteFields and tryReadFields early-return on
missing scripts AND drop the daughter fields before they reach the
binary codec. The plan now uses [fields-reader] + [fields-writer]
scripts on !task instead.

- §1: !task type blob now declares both reader and writer scripts.
- §1a: !chore documented as separate built-in with same field set.
  Future !actionable abstract type captured as out-of-scope.
- §3: rewritten as blob-canonical haustoria flow. Compile path writes
  a TOML blob; reader script projects fields on commit. Decompile
  reads fields from Metadata.Index.Fields. No fresh type-blob-digest
  lookup needed; no new code paths in papa/store.
- §4: tests updated to reference the already-landed fields.bats probes
  (cfdb846, c5ebd34) instead of the dropped
  TestFieldsPersistThroughCommitWithoutScripts.
- Open verification → Resolved verification, citing issues #101 and
  #102 filed against the fields infrastructure.
- Implementation order updated to drop the gating test and add the
  buildTaskTomlBlob compile-path step.
- Out of Scope: !chore separate-type bullet replaced with !actionable
  future-work note; conflict resolution explicitly out of scope.

Also annotates docs/plans/2026-04-05-fields-doddish-organize-design.md
with #101 and #102 in the Open issues list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[[fields]]
name = "status"
kind = "enum"
values = ["todo", "in-process", "done", "cancelled"]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be "in_progress"

5 `p2` `!!`
1 `p3` `!!!`

Other PRIORITY values fall back to the closest bucket on read; on write the
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the mapping is wrong, it's reversed. !!! should be p0, and none should be P3

- Status enum: in-process -> in_progress (snake_case underscore)
- Priority mapping reversed: p0 is now HIGHEST (matches sev-0 ticket
  convention), p3 is lowest. New table:
    p0 = !!! = VTODO PRIORITY 1
    p1 = !!  = VTODO PRIORITY 5
    p2 = !   = VTODO PRIORITY 9
    p3 = (none) = VTODO PRIORITY 0/absent
- Default priority: p3 (most tasks have no priority assigned)
- Decompile inverse mapping updated accordingly
- Three BATS probes updated with new enum values + recaptured digests
- TestPriorityValueRoundTrip integers swapped to match new mapping
- caldav_round_trip_priority_field example values updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@friedenberg
Copy link
Copy Markdown
Collaborator Author

Pushed d2d585a addressing both new comments:

  1. Status enum (line 56) — in-processin_progress (snake_case underscore). Updated in §1, §3 mapping table, decision summary, and the create_task_type_full BATS helper.

  2. Priority mapping reversed (line 104) — p0 is now highest priority (sev-0 convention):

    !task priority VTODO PRIORITY tasks.org
    p0 1 !!!
    p1 5 !!
    p2 9 !
    p3 0 (or absent) (none)

    Default value flipped from p0 to p3 since most tasks have no explicit priority.

    Decompile inverse mapping in §3 updated to p0→1, p1→5, p2→9, p3→0. caldav_round_trip_priority_field BATS spec updated to PRIORITY:1p0 → organize to p2PRIORITY:9.

  3. BATS probe digests recaptured for the three probes that hardcoded the old enum values:

    • field_full_task_three_fields_from_blob: status=in_progress priority=p1 → digest sehqwrekuhk346cppprq72mxk0qug5qfe6ghdqcd0tanf4plu6lsape8ag
    • field_full_task_organize_mutate_one_of_three: status=done priority=p2 → digest j0yn4cd0fvng3daxg0xrgjrag55v9l3gfsyhnawma3pm3szxpmtsgzaxs9
    • field_full_task_organize_from_empty_blob: still drops fields (issue fields-writer should support blob-less first writes #101), assertion unchanged

All 13 probes pass with locked-in assertions.

🤖 Generated with Claude Code

Add built-in TomlV2 constructors for !task and !chore types per the
plan in docs/plans/2026-04-06-task-type-genesis-and-haustoria-fields.md.

Both types share the actionable field set (status enum todo/in_progress/
done/cancelled, priority enum p0/p1/p2/p3 with p0 highest, due string)
plus reader and writer scripts using yq for TOML projection. The shared
constructors actionableFields/Reader/Writer are private helpers in the
golf/type_blobs package.

Re-exported from hotel/type_blobs for downstream consumers.

Unit tests verify field definitions, default values, and that the
reader/writer scripts contain the expected DODDER_FIELD_* and
DODDER_BLOB_PATH variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
// DODDER_FIELD_priority, DODDER_FIELD_due env vars and writes them into the
// blob at DODDER_BLOB_PATH.
func actionableFieldsWriter() *script_config.ScriptConfig {
return &script_config.ScriptConfig{
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should turn this into a go:embed. same for the reader script

friedenberg and others added 3 commits April 7, 2026 07:53
…steps 2-3)

- Add BigBang.IncludeBuiltinActionableTypes (default false).
- prepareBuiltinActionableTypes commits both built-in types via the
  typed blob store and adds them to the genesis import plan, gated on
  the new flag.
- Expose -include-builtin-actionable-types on the dodder genesis CLI.
- Add genesis_actionable_types.bats with four probe tests:
  - default init omits !task and !chore (existing behavior preserved)
  - opt-in init commits both alongside !md
  - !task type blob round-trips through TOML with the actionable field
    set and yq reader/writer scripts (note: tommy serializes script as
    triple-quoted multiline)
  - !chore and !task have byte-identical blob digests since they share
    the same field set + scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ep 4)

Replace the lossy status-tags config + raw-DESCRIPTION blob model with
a TOML blob format that mirrors the !task field set.

mike/haustoria_caldav:
- Drop CalendarMapping.StatusTags. The status-tag tag mapping is gone.
- Replace raw-description writeBlob with buildTaskTomlBlob, which emits
  a four-key TOML document:
    status   = mapVTODOStatusToFieldValue(...)
    priority = mapVTODOPriorityToFieldValue(...)
    due      = task.Due
    notes    = task.Description
  status / priority / due project as typed fields when the !task type
  with reader script is committed (genesis -include-builtin-actionable-
  types). notes stays in the blob as free-form text — not declared as
  a [[fields]] entry per "blob-only for now" decision.
- New task_blob.go with buildTaskTomlBlob, the four mapping helpers
  (vtodo↔field for status and priority, plus inverse versions for the
  upcoming decompile path in step 5), and quoteTomlString.

echo/workspace_config_blobs:
- Drop StatusTags from CalendarConfig. v2_tommy.go regenerated.

november/env_workspace:
- Drop StatusTags from the haustoria_caldav.CalendarMapping construction.

The 10 existing CalDAV BATS tests in current_version/haustoria_caldav.bats
are now broken because their assertions encode the old blob digests (built
from raw VTODO description). Step 6 updates those alongside the new
round-trip tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The decompile path (CheckoutOne → Decompile) now reads status, priority,
due, and notes from the !task TOML blob produced by buildTaskTomlBlob.
The blob is the canonical source of truth — we don't read from
Metadata.Index.Fields, so the path works whether or not the !task type
with reader script is committed in the repo.

task_blob.go gains parseTaskTomlBlob, a constrained line-based parser
for the four-key TOML format. It uses strconv.Unquote for value parsing
(symmetric with strconv.Quote in the writer). Unknown keys are ignored;
blank lines and # comments are skipped. The parser is intentionally NOT
a full TOML implementation — the blob shape is owned by buildTaskTomlBlob
and parseTaskTomlBlob is the single consumer.

Decompile now constructs the caldav.Task with:
  Status      = mapFieldValueToVTODOStatus(values.Status)
  Priority    = mapFieldValueToVTODOPriority(values.Priority)
  Due         = values.Due
  Description = values.Notes

Replaces the previous Description = string(req.Blob) (raw text) and
Status = "NEEDS-ACTION" hardcode.

task_blob_test.go covers the round-trip:
- TestStatusValueRoundTrip: 5 VTODO STATUS values + unknowns
- TestPriorityValueRoundTrip: canonical 0/1/5/9 plus out-of-band 2,3,4,
  6,7,8 bucketing
- TestBuildTaskTomlBlobBasic: known-good TOML output for a populated task
- TestBuildTaskTomlBlobEmptyDefaults: zero-value task → todo/p3/empty
- TestParseTaskTomlBlobRoundTrip: build then parse, all four keys
  preserved including multi-line notes
- TestParseTaskTomlBlobIgnoresBlankAndCommentLines
- TestParseTaskTomlBlobUnknownKeysIgnored

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… step 6)

mike/haustoria_caldav/checked_out.go:
- Implements ReadCheckedOutFromTransacted by fetching the live VTODO
  via HTTP GET, projecting it through buildTaskTomlBlob into a fresh
  *sku.CheckedOut. Required for any commit path that sets
  options.MergeCheckedOut (organize, etc.).
- Implements UpdateCheckoutFromCheckedOut by parsing the daughter's
  TOML blob, applying inverse status/priority mappings, and PUTting a
  VTODO to CalDAV. Falls back to the internal slot's ExternalObjectId
  when ResetWithExceptFields clears the external slot's binding.
- Implements Merge as a no-op (last-write-wins toward dodder); proper
  conflict resolution belongs to #19.
- Compile-time interface assertions ensure all three optional
  store_workspace interfaces are satisfied so the env_workspace
  wrapper's type assertions don't fail at runtime.

mike/haustoria_caldav/main.go:
- CheckoutOne now passes object.GetExternalObjectId() through to
  Decompile as ExternalId. Without this fix, decompile generated a
  fresh `dodder-N@dodder` UID on every checkout, orphaning the original
  .ics resource on the server. Pre-existing bug exposed by the new
  round-trip BATS tests.

zz-tests_bats/current_version/haustoria_caldav.bats:
- bootstrap helpers now pass -include-builtin-actionable-types so the
  workspace's experimental repo commits the !task and !chore type
  definitions. Without this the reader script can't find the type and
  silently skips field projection.
- Drop status-tags blocks from the multi-calendar config; the haustoria
  no longer reads them.
- Update existing 10 tests for the new TOML blob digest layout and the
  field-column box format.
- Rename multi_calendar_status_with_status_tags →
  multi_calendar_status_field_projection.
- Rename completed_tasks_get_status_tag →
  completed_tasks_get_status_field.
- Rewrite assertions from zz-archive-task-done tags to status=done /
  status=todo field columns.
- Add put_vtodo_full and get_vtodo_ical helpers for the round-trip tests.
- Add four round-trip BATS tests:
  - caldav_round_trip_all_fields: full status/priority/due/notes
    round-trip via checkin → checkout. Doesn't use organize because of
    a pre-existing blob-digest bug filed as #104.
  - caldav_round_trip_empty_optionals: defaults flow correctly.
  - caldav_round_trip_multiline_notes: multi-line VTODO DESCRIPTION
    survives via strconv.Quote's \n escapes.
  - caldav_round_trip_out_of_band_priority: PRIORITY:3 buckets to p1.

zz-tests_bats/current_version/genesis_actionable_types.bats:
- Add genesis_dodder_new_task_projects_fields probe verifying field
  projection via dodder new (validates the standalone genesis flow).

Two follow-up issues filed:
- #103: haustoria CalDAV caching via ETag/Last-Modified
- #104: organize commit panics on Id.IsNull when daughter blob digest
  reaches haustoria UpdateCheckoutFromCheckedOut

All 14 haustoria_caldav BATS tests pass. Full test suite green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@friedenberg
Copy link
Copy Markdown
Collaborator Author

Implementation status (pause point)

Steps 1–7 complete and pushed (14 commits). Full test suite green.

What's done

  • type_blobs.DefaultTaskType() + DefaultChoreType() with reader/writer scripts (e3d6e5c)
  • BigBang.IncludeBuiltinActionableTypes opt-in genesis flag (eddb797)
  • Genesis + init-workspace BATS tests (eddb797)
  • Haustoria compile path: buildTaskTomlBlob emits TOML with status/priority/due/notes (be181c1)
  • Haustoria decompile path: parseTaskTomlBlob + inverse mappings (1c75a91)
  • Merge interface: ReadCheckedOutFromTransacted, UpdateCheckoutFromCheckedOut, Merge (2c6c402)
  • CheckoutOne UID-binding fix (pre-existing bug, exposed by round-trip tests) (2c6c402)
  • All 14 CalDAV BATS tests passing + 4 new round-trip tests
  • 7 Go unit tests for status/priority mapping + blob serialization/parsing
  • 5 genesis BATS tests
  • 13 field probe tests in fields.bats
  • Bootstrap script at ~/workspaces/dodder-haustoria-caldav/bootstrap.mjs

What's remaining

  • Step 8: Update FDR-0007 status (exploringexperimental) and add supersede note to the !vtodo plan
  • Step 9: Run bootstrap.mjs against the live CalDAV workspace
  • Commit the gofumpt cleanup diff (cosmetic, from codegen runs)

Issues filed during this PR

🤖 Generated with Claude Code

Cosmetic: trailing blank line removals and struct field alignment
changes applied by `just codemod-go-fmt` during the tommy codegen
regeneration in step 4. No behavioral changes.
friedenberg added a commit that referenced this pull request Apr 17, 2026
- Status enum stays as todo/in-process/done/cancelled
- Priority becomes p0/p1/p2/p3 enum (tasks.org convention)
- Ship !chore as separate built-in alongside !task; future !actionable
- New opt-in BigBang.IncludeBuiltinActionableTypes flag (default false)
- File extension toml; instances are TOML-mirrored field blobs
- Fresh type-blob-digest lookup per compile/decompile cycle
- Add field-codec-without-scripts verification test as gating step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
Two BATS probes added to fields.bats to answer the open verification
question on PR #100: does the binary stream-index codec persist
Metadata.Index.Fields when no fields-reader/fields-writer scripts are
configured on the type?

field_persists_without_any_scripts: type with [[fields]] only, no
scripts. Organize-set status field is DROPPED entirely on commit. Blob
digest also disappears. Locked assertion: [one/uno !task "my task"].

field_persists_with_reader_only_no_writer: type with reader, no writer.
Organize-set status=done is silently overwritten by the reader
re-projecting status=todo from the unchanged blob, AND the field appears
TWICE in show output (status=todo status=todo).

Both findings invalidate the original plan's assumption that the
haustoria can set fields directly on metadata without a fields-writer
script. The plan needs revision: either ship a TOML field-projector
script with !task, or add a code path that bypasses the script gate.

The duplicated-field result in probe 2 looks like a separate bug to
file independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
Three additional BATS probes verify the "ship !task with reader+writer
scripts" path:

field_full_task_three_fields_from_blob: blob with 3 fields baked in →
reader projects all 3 → all display in show. WORKS.

field_full_task_organize_mutate_one_of_three: start with 3 fields in
blob, organize-set status=done, all 3 persist with writer rewriting the
TOML. WORKS.

field_full_task_organize_from_empty_blob: empty starting blob →
organize-set 3 fields → fields and blob digest both dropped. FAILS the
same way as field_persists_without_any_scripts.

Conclusion: option 1 + option 2 combined works. !task ships with
[[fields]] + [fields-reader] + [fields-writer]. The haustoria writes a
complete TOML blob with field values baked in (compile path). The
reader projects fields on commit; the writer projects edits back into
the blob on subsequent organize mutations. The decompile path reads
fields from metadata.Index.

The empty-blob case (probe 13) is a non-issue because the haustoria
always writes a full blob, never an empty one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
PR #100 probes proved the original "set fields directly on metadata"
assumption wrong. Both tryWriteFields and tryReadFields early-return on
missing scripts AND drop the daughter fields before they reach the
binary codec. The plan now uses [fields-reader] + [fields-writer]
scripts on !task instead.

- §1: !task type blob now declares both reader and writer scripts.
- §1a: !chore documented as separate built-in with same field set.
  Future !actionable abstract type captured as out-of-scope.
- §3: rewritten as blob-canonical haustoria flow. Compile path writes
  a TOML blob; reader script projects fields on commit. Decompile
  reads fields from Metadata.Index.Fields. No fresh type-blob-digest
  lookup needed; no new code paths in papa/store.
- §4: tests updated to reference the already-landed fields.bats probes
  (cfdb846, c5ebd34) instead of the dropped
  TestFieldsPersistThroughCommitWithoutScripts.
- Open verification → Resolved verification, citing issues #101 and
  #102 filed against the fields infrastructure.
- Implementation order updated to drop the gating test and add the
  buildTaskTomlBlob compile-path step.
- Out of Scope: !chore separate-type bullet replaced with !actionable
  future-work note; conflict resolution explicitly out of scope.

Also annotates docs/plans/2026-04-05-fields-doddish-organize-design.md
with #101 and #102 in the Open issues list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
- Status enum: in-process -> in_progress (snake_case underscore)
- Priority mapping reversed: p0 is now HIGHEST (matches sev-0 ticket
  convention), p3 is lowest. New table:
    p0 = !!! = VTODO PRIORITY 1
    p1 = !!  = VTODO PRIORITY 5
    p2 = !   = VTODO PRIORITY 9
    p3 = (none) = VTODO PRIORITY 0/absent
- Default priority: p3 (most tasks have no priority assigned)
- Decompile inverse mapping updated accordingly
- Three BATS probes updated with new enum values + recaptured digests
- TestPriorityValueRoundTrip integers swapped to match new mapping
- caldav_round_trip_priority_field example values updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
Add built-in TomlV2 constructors for !task and !chore types per the
plan in docs/plans/2026-04-06-task-type-genesis-and-haustoria-fields.md.

Both types share the actionable field set (status enum todo/in_progress/
done/cancelled, priority enum p0/p1/p2/p3 with p0 highest, due string)
plus reader and writer scripts using yq for TOML projection. The shared
constructors actionableFields/Reader/Writer are private helpers in the
golf/type_blobs package.

Re-exported from hotel/type_blobs for downstream consumers.

Unit tests verify field definitions, default values, and that the
reader/writer scripts contain the expected DODDER_FIELD_* and
DODDER_BLOB_PATH variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
…steps 2-3)

- Add BigBang.IncludeBuiltinActionableTypes (default false).
- prepareBuiltinActionableTypes commits both built-in types via the
  typed blob store and adds them to the genesis import plan, gated on
  the new flag.
- Expose -include-builtin-actionable-types on the dodder genesis CLI.
- Add genesis_actionable_types.bats with four probe tests:
  - default init omits !task and !chore (existing behavior preserved)
  - opt-in init commits both alongside !md
  - !task type blob round-trips through TOML with the actionable field
    set and yq reader/writer scripts (note: tommy serializes script as
    triple-quoted multiline)
  - !chore and !task have byte-identical blob digests since they share
    the same field set + scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
…ep 4)

Replace the lossy status-tags config + raw-DESCRIPTION blob model with
a TOML blob format that mirrors the !task field set.

mike/haustoria_caldav:
- Drop CalendarMapping.StatusTags. The status-tag tag mapping is gone.
- Replace raw-description writeBlob with buildTaskTomlBlob, which emits
  a four-key TOML document:
    status   = mapVTODOStatusToFieldValue(...)
    priority = mapVTODOPriorityToFieldValue(...)
    due      = task.Due
    notes    = task.Description
  status / priority / due project as typed fields when the !task type
  with reader script is committed (genesis -include-builtin-actionable-
  types). notes stays in the blob as free-form text — not declared as
  a [[fields]] entry per "blob-only for now" decision.
- New task_blob.go with buildTaskTomlBlob, the four mapping helpers
  (vtodo↔field for status and priority, plus inverse versions for the
  upcoming decompile path in step 5), and quoteTomlString.

echo/workspace_config_blobs:
- Drop StatusTags from CalendarConfig. v2_tommy.go regenerated.

november/env_workspace:
- Drop StatusTags from the haustoria_caldav.CalendarMapping construction.

The 10 existing CalDAV BATS tests in current_version/haustoria_caldav.bats
are now broken because their assertions encode the old blob digests (built
from raw VTODO description). Step 6 updates those alongside the new
round-trip tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
The decompile path (CheckoutOne → Decompile) now reads status, priority,
due, and notes from the !task TOML blob produced by buildTaskTomlBlob.
The blob is the canonical source of truth — we don't read from
Metadata.Index.Fields, so the path works whether or not the !task type
with reader script is committed in the repo.

task_blob.go gains parseTaskTomlBlob, a constrained line-based parser
for the four-key TOML format. It uses strconv.Unquote for value parsing
(symmetric with strconv.Quote in the writer). Unknown keys are ignored;
blank lines and # comments are skipped. The parser is intentionally NOT
a full TOML implementation — the blob shape is owned by buildTaskTomlBlob
and parseTaskTomlBlob is the single consumer.

Decompile now constructs the caldav.Task with:
  Status      = mapFieldValueToVTODOStatus(values.Status)
  Priority    = mapFieldValueToVTODOPriority(values.Priority)
  Due         = values.Due
  Description = values.Notes

Replaces the previous Description = string(req.Blob) (raw text) and
Status = "NEEDS-ACTION" hardcode.

task_blob_test.go covers the round-trip:
- TestStatusValueRoundTrip: 5 VTODO STATUS values + unknowns
- TestPriorityValueRoundTrip: canonical 0/1/5/9 plus out-of-band 2,3,4,
  6,7,8 bucketing
- TestBuildTaskTomlBlobBasic: known-good TOML output for a populated task
- TestBuildTaskTomlBlobEmptyDefaults: zero-value task → todo/p3/empty
- TestParseTaskTomlBlobRoundTrip: build then parse, all four keys
  preserved including multi-line notes
- TestParseTaskTomlBlobIgnoresBlankAndCommentLines
- TestParseTaskTomlBlobUnknownKeysIgnored

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
… step 6)

mike/haustoria_caldav/checked_out.go:
- Implements ReadCheckedOutFromTransacted by fetching the live VTODO
  via HTTP GET, projecting it through buildTaskTomlBlob into a fresh
  *sku.CheckedOut. Required for any commit path that sets
  options.MergeCheckedOut (organize, etc.).
- Implements UpdateCheckoutFromCheckedOut by parsing the daughter's
  TOML blob, applying inverse status/priority mappings, and PUTting a
  VTODO to CalDAV. Falls back to the internal slot's ExternalObjectId
  when ResetWithExceptFields clears the external slot's binding.
- Implements Merge as a no-op (last-write-wins toward dodder); proper
  conflict resolution belongs to #19.
- Compile-time interface assertions ensure all three optional
  store_workspace interfaces are satisfied so the env_workspace
  wrapper's type assertions don't fail at runtime.

mike/haustoria_caldav/main.go:
- CheckoutOne now passes object.GetExternalObjectId() through to
  Decompile as ExternalId. Without this fix, decompile generated a
  fresh `dodder-N@dodder` UID on every checkout, orphaning the original
  .ics resource on the server. Pre-existing bug exposed by the new
  round-trip BATS tests.

zz-tests_bats/current_version/haustoria_caldav.bats:
- bootstrap helpers now pass -include-builtin-actionable-types so the
  workspace's experimental repo commits the !task and !chore type
  definitions. Without this the reader script can't find the type and
  silently skips field projection.
- Drop status-tags blocks from the multi-calendar config; the haustoria
  no longer reads them.
- Update existing 10 tests for the new TOML blob digest layout and the
  field-column box format.
- Rename multi_calendar_status_with_status_tags →
  multi_calendar_status_field_projection.
- Rename completed_tasks_get_status_tag →
  completed_tasks_get_status_field.
- Rewrite assertions from zz-archive-task-done tags to status=done /
  status=todo field columns.
- Add put_vtodo_full and get_vtodo_ical helpers for the round-trip tests.
- Add four round-trip BATS tests:
  - caldav_round_trip_all_fields: full status/priority/due/notes
    round-trip via checkin → checkout. Doesn't use organize because of
    a pre-existing blob-digest bug filed as #104.
  - caldav_round_trip_empty_optionals: defaults flow correctly.
  - caldav_round_trip_multiline_notes: multi-line VTODO DESCRIPTION
    survives via strconv.Quote's \n escapes.
  - caldav_round_trip_out_of_band_priority: PRIORITY:3 buckets to p1.

zz-tests_bats/current_version/genesis_actionable_types.bats:
- Add genesis_dodder_new_task_projects_fields probe verifying field
  projection via dodder new (validates the standalone genesis flow).

Two follow-up issues filed:
- #103: haustoria CalDAV caching via ETag/Last-Modified
- #104: organize commit panics on Id.IsNull when daughter blob digest
  reaches haustoria UpdateCheckoutFromCheckedOut

All 14 haustoria_caldav BATS tests pass. Full test suite green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
friedenberg added a commit that referenced this pull request Apr 17, 2026
…step 8)

FDR-0007 (checkout-bridges): status exploring → experimental. The
promotion criteria (CheckoutStore with Compile/Decompile + round-trip
BATS test) are met by the CalDAV haustoria with typed field
round-tripping.

Updated Implementation Status section to reflect PR #100 work:
- 14 BATS tests + 4 round-trip tests
- !task and !chore built-in types with status/priority/due fields
- Compile/decompile paths through TOML blob + reader/writer scripts
- Remaining gaps: partial property coverage, no three-way merge,
  no sub-object decomposition, no ETag caching

!vtodo langlang design: added supersede note pointing at the
task-type-genesis plan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant