Commit f57482e
feat(properties): canonicalize YAML frontmatter on write path
Context: serializeProperties at src/lib/features/properties/properties.logic.ts
emits the canonical YAML byte sequence consumed by every UI mutation path
(Properties panel, lifecycle, icons, deep-link, meta-bind). The rules
(quoting, empty handling, alias canonicalization, list flow style, key order,
nested-object drop) were functionally centralized but ungoverned: no ADR, no
machine-readable spec, no snapshot tests, yaml dep pinned with caret (^2.9.0),
and external producers (Python person-note generator at
koko.brain-os/vault/work/people/_generate.py) hand-rolled divergent quoting
heuristics. Aliases (favorite/icon/is_a) were only normalized at parse time,
so external producers writing non-canonical keys triggered silent disk diffs
on the next UI touch. Nested mappings were dropped without log trail. The
collection evaluator did not recognize is_a as a type identifier and compared
type values case-sensitively, so `type == "person"` returned zero matches
against a vault storing first-letter-uppercase type values. A Type Definition
inside the configured system folder produced a sidebar section with
definitionPath = null because TypeSidebar filtered entries before
buildTypeSections, causing right-click "Create type definition" to silently
duplicate the file at the vault root.
Solution: Nine-task plan delivered as one squash:
1. Pin yaml@2.9.0 (drop caret) to freeze canonical form.
2. Extract shouldQuoteScalar/canonicalizeScalar to yaml-quoting.logic.ts with
130 parity tests vs yaml.stringify across mapping-value + flow-item
contexts. Predicate is the documented contract + parity safety net; emitter
still delegates to yaml.Document.
3. Apply canonicalizeKey inside serializeProperties before doc.set, so write
path produces canonical keys regardless of construction path.
4. Log dropped nested mappings via appendLog('PROPERTIES', ...) so the silent
data loss is now traceable.
5. Add 12 toBe snapshot tests pinning canonical form (emails bare, URN-style
bare, wikilinks quoted, empty as "", lists flow style, reserved literals
quoted, yes/no/on/off bare, native vs string-looking bool/number, key
order preserved, realistic person-note end-to-end).
6. ADR 0029 documents canonical form end-to-end (scalar rules, empty, list
flow, key order, alias canonicalization on parse AND write, documented
drops, framing, trigger window, yaml version pin).
7. docs/specs/frontmatter-canonical-form.md normative spec with JSON rule
block + predicate pseudocode + worked-examples table for external
producers.
8-9. (External to this repo — Python generator rewrite + parity suite landed
in the nested koko.brain-os/vault/ git repo as commits 0a3f38b and
d8198dd.)
Tangential fixes bundled in:
- collection evaluator: IDENTIFIER_ALIASES map + canonicalPropertyName helper
so is_a resolves to type in resolveIdentifier and resolveMember's note./
property. branches. isTypeIdentifier AST predicate + case-insensitive
branch in evaluateBinary for == / != against type/is_a (other fields stay
case-sensitive).
- type-definitions: promote on-disk location to TypeMetadata.path field
populated in extractTypeMetadata. buildTypeSections drops typeDefPaths Map
and reads metadata.path directly, so definitionPath resolves correctly
when the Type entry was filtered out (system folder case).
Behavior: Frontmatter write path now emits canonical bytes regardless of
caller construction. Nested-mapping drops appear in session log. Pinned
yaml@2.9.0 refuses silent minor bumps. `type == "person"`,
`type == "Person"`, `is_a == "person"`, `note.type == "PERSON"` all match
the same notes; other field comparisons stay case-sensitive. Type
Definitions in system folders show the standard right-click menu (Open
/ Copy path / Change icon / Reveal in Finder) instead of "Create type
definition", eliminating the duplicate-file footgun. No user-visible
change to existing notes already in canonical form.
Files:
- package.json:74, pnpm-lock.yaml:170-172: yaml pin "^2.9.0" -> "2.9.0".
- src/lib/features/properties/yaml-quoting.logic.ts:1-167: New module with
shouldQuoteScalar/canonicalizeScalar + RESERVED_LITERALS /
MUST_QUOTE_LEADING_CHARS / SCALAR_MUST_QUOTE_SUBSTRINGS /
FLOW_ITEM_MUST_QUOTE_SUBSTRINGS exports.
- src/lib/features/properties/properties.logic.ts:1-4,132-145,192-219:
appendLog import; nested-mapping drop now logs via PROPERTIES tag;
canonicalizeKey applied before each doc.set with JSDoc rationale.
- src/lib/features/collection/expression/evaluator.ts:85-244:
IDENTIFIER_ALIASES, canonicalPropertyName, isTypeIdentifier;
resolveIdentifier + resolveMember alias lookup; evaluateBinary
case-insensitive == / != branch for type/is_a.
- src/lib/features/type-definitions/type-definitions.logic.ts:7-95:
TypeMetadata.path field (string | null); DEFAULTS.path = null;
extractTypeMetadata returns entry.path.
- src/lib/features/type-definitions/type-sidebar.logic.ts:268-305:
buildTypeSections reads metadata.path; removed typeDefPaths Map and
the second iteration over entries.
- docs/adr/0029-frontmatter-yaml-canonical-form.md:1-113, docs/adr/README.md:135:
New ADR + index row.
- docs/specs/frontmatter-canonical-form.md:1-183: New normative spec
(producer checklist, JSON rule block, predicate pseudocode, worked-
examples table, person-note snapshot).
- src/tests/lib/features/properties/yaml-quoting.logic.test.ts:1-310:
130 tests (mapping-value rules, flow-item rules, canonicalizeScalar
emission, parity vs yaml.stringify).
- src/tests/lib/features/properties/properties.logic.test.ts:1-571:
vi.mock for log.service; _resetParseFrontmatterCache import;
warn-on-drop tests; serializeProperties alias canonicalization tests;
12 canonical-form snapshot tests.
- src/tests/lib/features/collection/expression/evaluator.test.ts:198-286:
"frontmatter aliases (is_a -> type)" + "case-insensitive equality for
type / is_a" describe blocks (13 tests covering bare identifier,
note./property. member access, !=, missing-property null, control
case `name == "alice"`).
- src/tests/lib/features/type-definitions/{type-definitions.logic,type-sidebar.logic,type-definitions.service,type-definitions.store}.test.ts,
src/tests/lib/features/auto-move/type-lifecycle-rules.test.ts:
meta() factories add path field; new tests for path-from-entry +
fallback null + system-folder regression scenario.
- help/documentation/{12-collection,25-types-and-relationships}.md:
Documented is_a alias + case-insensitive type equality.
- tasks/done/frontmatter-canonical-form.md, tasks/done/filter-expression-type-ergonomics.md:
Plan trackers moved to done with task SHAs + nested-repo notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 07fdc02 commit f57482e
22 files changed
Lines changed: 1280 additions & 17 deletions
File tree
- docs
- adr
- specs
- help/documentation
- src
- lib/features
- collection/expression
- properties
- type-definitions
- tests/lib/features
- auto-move
- collection/expression
- properties
- type-definitions
- tasks/done
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| 136 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
0 commit comments