Skip to content

feat(capture): hierarchical capture/* tags + schema-ensure + option (d) + path-collision fix (0.3.15-rc.8) - #132

Merged
unforced merged 1 commit into
mainfrom
ag-unforced-dev
May 13, 2026
Merged

feat(capture): hierarchical capture/* tags + schema-ensure + option (d) + path-collision fix (0.3.15-rc.8)#132
unforced merged 1 commit into
mainfrom
ag-unforced-dev

Conversation

@unforced

Copy link
Copy Markdown
Contributor

feat(capture): hierarchical capture/* tags + schema-ensure + option (d) + path-collision fix (0.3.15-rc.8)

Closes notes#126 (reshaped scope per Aaron's design conversation).
Builds on rc.7's quickPath() pre-fill (PR #130) with the confirmed
capture/* classification model plus three reviewer follow-ups bundled.

What landed

Hierarchical capture tags (notes#126 reshape):

  • New file src/lib/vault/schema.ts declaring NOTES_REQUIRED_SCHEMA:
    • capture (parent, no parent_names)
    • capture/text (parent_names: ["capture"])
    • capture/voice (parent_names: ["capture"])
      First instance of patterns#57 (surface-declares-required-schema).
  • DEFAULT_TAG_ROLES.captureText"capture/text",
    DEFAULT_TAG_ROLES.captureVoice"capture/voice". Existing
    vaults preserve their stored values — no force-migrate.

Idempotent schema-ensure:

  • New VaultClient.updateTag(name, { description, parent_names })
    PUT /api/tags/:name, field-merged vault-side.
  • New src/lib/vault/schema-ensure.ts exposes ensureNotesSchema().
    Per-vault per-session module-level ref guard so repeated captures
    don't hammer the vault. Failure rolls back the guard so next
    capture retries; success is silent (it's plumbing, not user-
    actionable — notes#129 will add the audit UI).
  • Capture wires ensureNotesSchema(activeVault.id, client) as a
    fire-and-forget call inside save(). Doesn't block the save or
    surface failures.

Option (d) bundled (closed PR #131 absorbed here):

  • pathOverride.trim() || generatedPathRef.current resolves to
    the mount-time generated value when input is empty. Never falls
    back to vault-picks. The rc.6 memoPath() audio-only fallback
    is unreachable and dropped — one canonical Notes-side rule.
  • Placeholder text updated: "(blank → vault picks)" → "(blank →
    uses generated path)".

Path-collision fix (raised in #130 review):

  • reset() after successful save now regenerates quickPath() and
    updates the input — but only when the operator hasn't manually
    edited (pathEditedRef). User-typed paths survive across captures
    on the same mount; auto-generated paths roll forward in time.

Behavior changes

  • Text captures: default tag is capture/text instead of quick.
  • Voice captures: default tag is capture/voice instead of voice.
  • Existing vaults with stored values are NOT migrated — their
    quick/voice choices stand. Schema-ensure still writes the
    parent + parent_names rows so hierarchy queries work either way.
  • Audio captures with cleared path no longer go to Memos/ — they
    land under Notes/ like text captures (single canonical rule).
  • Two captures within the same wall-clock second no longer collide
    on the auto-generated path.

Tests

  • 6 new in schema-ensure.test.ts: declaration-order PUTs,
    parent-before-children, per-session per-vault idempotence,
    multi-vault independence, retry-after-failure, swallow-doesn't-
    throw.
  • 2 new in Capture.test.tsx: regen-on-reset-when-unedited (with
    fake-timer setSystemTime + shouldAdvanceTime for waitFor),
    preserve-user-edit-across-reset.
  • 4 existing tests flipped for the default rename + option (d):
    • "quick"/"voice""capture/text"/"capture/voice" in tag
      assertions.
    • "Empty path → omits path" → "Empty path reverts to generated".
    • "Audio-only cleared path → memoPath" → "Audio-only cleared
      path → generated path".
  • Capture.test.tsx adds vi.mock("@/lib/vault/schema-ensure") so
    capture tests don't hit the real PUT (covered by the dedicated
    schema-ensure tests).

Not in scope

  • Full Settings audit UI + connect-time banner → notes#129.
  • Per-vault path-template customization → notes#128.

Gates: 82 test files / 749 tests pass (+8 from rc.7's 741). typecheck
clean, lint clean, build green.

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

…d) + path-collision fix (0.3.15-rc.8)

Closes notes#126 (reshaped scope per Aaron's design conversation).
Builds on rc.7's `quickPath()` pre-fill (PR #130) with the confirmed
`capture/*` classification model plus three reviewer follow-ups bundled.

## What landed

**Hierarchical capture tags (notes#126 reshape):**

- New file `src/lib/vault/schema.ts` declaring `NOTES_REQUIRED_SCHEMA`:
  - `capture` (parent, no parent_names)
  - `capture/text` (parent_names: ["capture"])
  - `capture/voice` (parent_names: ["capture"])
  First instance of patterns#57 (surface-declares-required-schema).
- `DEFAULT_TAG_ROLES.captureText` → `"capture/text"`,
  `DEFAULT_TAG_ROLES.captureVoice` → `"capture/voice"`. Existing
  vaults preserve their stored values — no force-migrate.

**Idempotent schema-ensure:**

- New `VaultClient.updateTag(name, { description, parent_names })` —
  PUT /api/tags/:name, field-merged vault-side.
- New `src/lib/vault/schema-ensure.ts` exposes `ensureNotesSchema()`.
  Per-vault per-session module-level ref guard so repeated captures
  don't hammer the vault. Failure rolls back the guard so next
  capture retries; success is silent (it's plumbing, not user-
  actionable — notes#129 will add the audit UI).
- Capture wires `ensureNotesSchema(activeVault.id, client)` as a
  fire-and-forget call inside save(). Doesn't block the save or
  surface failures.

**Option (d) bundled** (closed PR #131 absorbed here):

- `pathOverride.trim() || generatedPathRef.current` resolves to
  the mount-time generated value when input is empty. Never falls
  back to vault-picks. The rc.6 `memoPath()` audio-only fallback
  is unreachable and dropped — one canonical Notes-side rule.
- Placeholder text updated: "(blank → vault picks)" → "(blank →
  uses generated path)".

**Path-collision fix** (raised in #130 review):

- `reset()` after successful save now regenerates `quickPath()` and
  updates the input — but only when the operator hasn't manually
  edited (`pathEditedRef`). User-typed paths survive across captures
  on the same mount; auto-generated paths roll forward in time.

## Behavior changes

- Text captures: default tag is `capture/text` instead of `quick`.
- Voice captures: default tag is `capture/voice` instead of `voice`.
- Existing vaults with stored values are NOT migrated — their
  `quick`/`voice` choices stand. Schema-ensure still writes the
  parent + parent_names rows so hierarchy queries work either way.
- Audio captures with cleared path no longer go to `Memos/` — they
  land under `Notes/` like text captures (single canonical rule).
- Two captures within the same wall-clock second no longer collide
  on the auto-generated path.

## Tests

- 6 new in `schema-ensure.test.ts`: declaration-order PUTs,
  parent-before-children, per-session per-vault idempotence,
  multi-vault independence, retry-after-failure, swallow-doesn't-
  throw.
- 2 new in `Capture.test.tsx`: regen-on-reset-when-unedited (with
  fake-timer `setSystemTime` + shouldAdvanceTime for waitFor),
  preserve-user-edit-across-reset.
- 4 existing tests flipped for the default rename + option (d):
  - `"quick"`/`"voice"` → `"capture/text"`/`"capture/voice"` in tag
    assertions.
  - "Empty path → omits path" → "Empty path reverts to generated".
  - "Audio-only cleared path → memoPath" → "Audio-only cleared
    path → generated path".
- `Capture.test.tsx` adds `vi.mock("@/lib/vault/schema-ensure")` so
  capture tests don't hit the real PUT (covered by the dedicated
  schema-ensure tests).

## Not in scope

- Full Settings audit UI + connect-time banner → notes#129.
- Per-vault path-template customization → notes#128.

Gates: 82 test files / 749 tests pass (+8 from rc.7's 741). typecheck
clean, lint clean, build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@unforced
unforced merged commit 611f2f3 into main May 13, 2026
@unforced
unforced deleted the ag-unforced-dev branch May 13, 2026 03:14
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