ecoportal-api-graphql 1.3.11 → 1.3.12

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/code/filter_contract_matrix.md +177 -0
  3. data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +10 -0
  4. data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +11 -0
  5. data/.ai-assistance/skills/procedural-memory/SKILL.md +319 -0
  6. data/.ai-assistance/standards-version.json +21 -20
  7. data/CHANGELOG.md +32 -0
  8. data/CLAUDE.md +10 -0
  9. data/docs/self-docs/ARCHITECTURE.md +88 -0
  10. data/docs/self-docs/CHANGES.jsonl +7 -0
  11. data/docs/self-docs/CONVENTIONS.md +74 -0
  12. data/docs/self-docs/INTEGRATIONS.md +63 -0
  13. data/docs/self-docs/OVERVIEW.md +51 -0
  14. data/docs/self-docs/STATUS.md +69 -0
  15. data/docs/self-docs/self-docs-index.json +39 -0
  16. data/docs/worklog.md +0 -23
  17. data/lib/ecoportal/api/common/graphql/model/diffable.rb +54 -54
  18. data/lib/ecoportal/api/graphql/base/action.rb +43 -43
  19. data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +67 -67
  20. data/lib/ecoportal/api/graphql/base/page/data_field/collection.rb +7 -0
  21. data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +28 -28
  22. data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +25 -25
  23. data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +24 -24
  24. data/lib/ecoportal/api/graphql/base/page/section_collection.rb +85 -79
  25. data/lib/ecoportal/api/graphql/base/preset_view.rb +17 -17
  26. data/lib/ecoportal/api/graphql/base/register.rb +18 -18
  27. data/lib/ecoportal/api/graphql/compat/filter_translator.rb +63 -28
  28. data/lib/ecoportal/api/graphql/concerns/page_compat.rb +51 -51
  29. data/lib/ecoportal/api/graphql/concerns.rb +14 -14
  30. data/lib/ecoportal/api/graphql/file_upload/client.rb +181 -181
  31. data/lib/ecoportal/api/graphql/fragment/page.rb +85 -85
  32. data/lib/ecoportal/api/graphql/input/action/update.rb +14 -14
  33. data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +41 -41
  34. data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +47 -47
  35. data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +49 -49
  36. data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +27 -27
  37. data/lib/ecoportal/api/graphql/input/search_conf.rb +436 -367
  38. data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +30 -30
  39. data/lib/ecoportal/api/graphql/logic/input.rb +26 -26
  40. data/lib/ecoportal/api/graphql_version.rb +1 -1
  41. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78e2adbcdb62b6d2fdbf2ddb5906d9d8b5b93464af6269936b7eeda2dc94cb17
4
- data.tar.gz: e6c55777dab78795abe7b81f419275cb52ec82a986aa92ee0da4affa4d5ca7e8
3
+ metadata.gz: 74b442ca224440bee94f3f88522a4233b995d187db1b70ba31a0801e0691d3eb
4
+ data.tar.gz: 5d5d05fdf46fac5a32a12c5d4c8e47c21b913332bbe04c40a3ab81beeacbab9a
5
5
  SHA512:
6
- metadata.gz: 2bff88bcf06b31025a011fdb2b7d5f4a209aaa92b21c4c6c09ef1439da39a0b1e76fe75a204e5d4bc3e55dda5b7d4d6b215554650007b6d5097afa388ca08537
7
- data.tar.gz: 0df6a4578b1ddbefa765f813fd97efd5f8e55fc84ad0384a7e7e6459f2ddc040dc6d8999af2493bb4e6368a638254ca7d8ed2154b7eae27bc40b965b80a94be4
6
+ metadata.gz: 7d667e7ed09fc6ed6fd200695567eb25a8bb5f4f41b3b98882e04d84acd1775c1a17dc7d424e82f26b7f89d4f826bb46c25fddc0ef5fc526e8731aaccd73193b
7
+ data.tar.gz: 324b54e96de9f7ae82e71bf1b2c0427fa6fd4ed99d0153ec836a85cf68d76d5ce8f8462865b29cddd446015caa2b4a5fcaf940c5c2866970d7814a2d0495a73b
@@ -0,0 +1,177 @@
1
+ # Filter Contract Matrix — GraphQL register/page filters
2
+
3
+ **Purpose.** Evidence-based ground-truth mapping of *ES register/page filter operations* to
4
+ their exact params, per register field type, so the compat `FilterTranslator` can be validated
5
+ against the backend rather than conjecture. Built after the Date/Select translator bug
6
+ (commit e45ba58) where the original translation table was never validated.
7
+
8
+ **Authoritative source.** `ecoPortal_master` Rails backend, `NewEp::Es` filter stack:
9
+
10
+ - **Field-type → allowed operations** (the contract a register exposes):
11
+ `app/services/new_ep/es/definitions/page.rb`
12
+ - **Operation → exact params** (constructor kwargs / `argument` declarations):
13
+ `app/services/new_ep/es/filters/pages/**` and `.../filters/global/**`
14
+ - **Operation string → class dispatch:**
15
+ `app/services/new_ep/es/filters/deserializer.rb:51-58` — the `operation` string is
16
+ `camelize`d + suffixed `Filter`, resolved **first** under `NewEp::Es::Filters::Pages::`,
17
+ then falling back to `NewEp::Es::Filters::Global::`. An operation with no matching class
18
+ raises `UnknownFilterError` (`deserializer.rb:59`). This is why `options_filter` /
19
+ `text_filter` (v2 names) are NOT valid GraphQL operations — see §"v2 → GraphQL" below.
20
+
21
+ All line numbers below are `file:line` in `ecoPortal_master` unless noted. GraphQL param names
22
+ match the backend constructor / `argument` declarations exactly.
23
+
24
+ ---
25
+
26
+ ## 1. v2 → GraphQL translator input types (what `Compat::FilterTranslator` handles)
27
+
28
+ `lib/ecoportal/api/graphql/compat/filter_translator.rb` accepts *v2-style* `type:` hashes and
29
+ emits GraphQL `{operation:, params:}` hashes. The v2 `type` names are NOT backend operation
30
+ names; the emitted `operation` names ARE. Every emitted operation below resolves to a real
31
+ backend class (verified via the deserializer dispatch).
32
+
33
+ | v2 `type` | Emitted GraphQL `operation` | Emitted params | Backend class (evidence) | Translator handles? |
34
+ |---|---|---|---|---|
35
+ | `date_filter` (system key, e.g. `created_at`) | `date_filter` | `key, time_zone, gte?(=lbound), lte?(=ubound)` | `Global::DateFilter` — `date_filter.rb:28` (`key, path, gt, gte, lt, lte`) | YES (`filter_translator.rb:110-124`) |
36
+ | `date_filter` (field key, e.g. `date.zXXXX`) | `range_date_filter` | `key, path=membranes.date, time_zone, gte?, lte?` | `Pages::RangeDateFilter` — `range_date_filter.rb:797` (`key, path, gte, lte, gt, lt`) | YES (`filter_translator.rb:110-124`) |
37
+ | `text_filter` | `match_filter` | `key, value(=query)` | `Global::MatchFilter` — `match_filter.rb:6` (`key, value, path`) | YES (`filter_translator.rb:74-82`) |
38
+ | `exact_filter` / `is_filter` | `is_filter` | `key, path?(derived membranes.<type>), value` | `Pages::IsFilter` — `is_filter.rb:258` (`key, path, value`) | YES (`filter_translator.rb:52-57`) |
39
+ | `options_filter` (system key) | `one_of_filter` / `none_of_filter` (exclude) | `key, values(=chosen)` | `Global::OneOfFilter` — `one_of_filter.rb:16` (`key, values`); `Global::NoneOfFilter` — `none_of_filter.rb:6` (`key, values`) | YES (`filter_translator.rb:90-104`) |
40
+ | `options_filter` (field key, e.g. `select_str.zXXXX`) | `has_any_filter` / `doesnt_have_any_filter` (exclude) | `key, path(=membranes.<type>), field_key='selected', values(=chosen)` | `Pages::HasAnyFilter` — `has_any_filter.rb:995` → `Pages::BaseHasAnyFilter` — `base_has_any_filter.rb:10` (`key, path, field_key, values, invert`); `Pages::DoesntHaveAnyFilter` — `doesnt_have_any_filter.rb:1061` | YES (`filter_translator.rb:90-104`) |
41
+ | `tag_filter` | *(passthrough — no rewrite)* | — | n/a (already GraphQL-shaped) | passthrough (`filter_translator.rb:37-39`) |
42
+ | `register_filter` | *(passthrough)* | — | `Pages::RegisterFilter` (`filters/pages/register_filter.rb`) | passthrough |
43
+ | `and_filter` / `or_filter` | *(passthrough)* | — | `Global::AndFilter` / `Global::OrFilter` | passthrough |
44
+ | already-`{operation:,params:}` hash | *(passthrough)* | — | — | passthrough (unmatched `type`) |
45
+ | SearchConf value object (`to_h`, not Hash) | *(passthrough)* | — | — | passthrough (`filter_translator.rb:25`) |
46
+
47
+ ### Why `options_filter` / `text_filter` are NOT emitted verbatim
48
+
49
+ - **`options_filter`**: there is NO `Pages::OptionsFilter` or `Global::OptionsFilter` class.
50
+ The closest backend class is `Pages::OptionsFieldFilter` (op `options_field_filter`,
51
+ `options_field_filter.rb:9`, takes `chosen`/`mode`) — a *different* operation the register
52
+ definitions never expose. Emitting `options_filter` verbatim would raise `UnknownFilterError`.
53
+ So the translator rewrites to the backend-blessed `has_any_filter`/`one_of_filter`.
54
+ - **`text_filter`**: a `Pages::TextFilter` class DOES exist (`text_filter.rb:1252`) but its
55
+ constructor takes `key:, query:` (not `value:`) and register definitions never expose a
56
+ `text_filter` operation — they expose `contains_filter`/`is_filter`. The translator therefore
57
+ emits `match_filter` (`Global::MatchFilter`, `match_filter.rb:6`), which is a valid operation
58
+ with `{key, value}`. NOTE: `match_filter` matches the *analyzed* value (partial/full-text);
59
+ for whole-value exact use `is_filter` (see §exact-vs-partial in `search_filters.md`).
60
+
61
+ ---
62
+
63
+ ## 2. Field-type contract matrix (register/page fields)
64
+
65
+ One row per **(field type, operation)** enumerated from `page.rb`. `key` = field definition key
66
+ (`<type>.zXXXX`); `path` = the nested membranes path the definition hardcodes. `field_key` /
67
+ `values` shown where the operation carries a subfield. "Translator?" marks whether the compat
68
+ `FilterTranslator` currently produces this operation from a v2 input.
69
+
70
+ ### 2.1 System / metadata fields (no `membranes.*` path)
71
+
72
+ | Field | v2 type | GraphQL operation(s) | params | page.rb evidence | Translator? |
73
+ |---|---|---|---|---|---|
74
+ | Created on / Updated on | `date_filter` | `date_filter`, `isnt_in_date_filter` | `key=created_at`, gte/lte/time_zone | `page.rb:31-39` | YES (system date branch) |
75
+ | External ID | `exact_filter` | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter` | `key=external_id`, value | `page.rb:65-70` | YES via `exact_filter`→`is_filter` (no path — system key) |
76
+ | Page ID (`mould_counter`) | — | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `contains_filter`, `doesnt_contain_filter` | `key=mould_counter`, value | `page.rb:72-79` | is_filter YES; contains_filter NO |
77
+ | Page name (`name`) | `text_filter`/`exact_filter` | `is_filter`, `isnt_filter`, `contains_filter`, `doesnt_contain_filter` | `key=name`, value | `page.rb:81-86` | is_filter YES; match/contains partial: `text_filter`→`match_filter` (valid but `name` uses QueryFilter internally — see NOTE) |
78
+ | Page status (`state`) | `options_filter`/`exact` | `is_filter`, `isnt_filter` | `key=state`, value(`@extras`) | `page.rb:87-99` | via `exact_filter`→`is_filter` YES |
79
+ | System ID (`id`) | `exact_filter` | `is_filter`, `isnt_filter`, `one_of_filter` | `key=id`, value / values | `page.rb:101-105` | is_filter + one_of_filter YES |
80
+ | Assigned to | — | `is_filter`, `isnt_filter`, `has_all/any_filter`, `doesnt_have_all/any_filter` | `key=assigned_to`, field_key=`assigned_to`, values | `page.rb:15-22` | NO (no v2 driver) |
81
+ | Created by | — | `is_filter`, `isnt_filter`, `has_any_filter`, `doesnt_have_any_filter` | `key=creator_id`, field_key=`creator_id` | `page.rb:24-29` | NO |
82
+ | Created via (`source`) | — | `is_filter`, `isnt_filter` | `key=source`, value | `page.rb:41-45` | via is_filter YES (if driven) |
83
+ | Custom tags (`other_tags`) | `tag_filter` | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter` | `key=other_tags`, field_key=`other_tags`, values | `page.rb:56-63` | passthrough (`tag_filter`) |
84
+ | Tags / locations (`all_location_ids`) | `tag_filter` | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter` | `key=all_location_ids`, field_key=`all_location_ids`, values | `page.rb:107-114` | passthrough (values lowercased backend-side: `has_any_filter.rb:1024`) |
85
+ | Current Stage (`active_stage_names`) | — | `has_any/all_filter`, `doesnt_have_any/all_filter` | `key=active_stage_names`, field_key=`active_stage_names`, values | `page.rb:138-143` | NO |
86
+
87
+ NOTE (`name`): `contains_filter` on `name` routes to `Global::MatchFilter` (`contains_filter.rb:453-455`)
88
+ and `is_filter` on `name` routes to `Global::ExactFilter` on `name.exact` (`is_filter.rb:283-284`).
89
+ The translator's `text_filter`→`match_filter{key:'name',value:}` therefore hits the right backend
90
+ class for partial name search.
91
+
92
+ ### 2.2 Register data fields (nested `membranes.<type>` path)
93
+
94
+ | Field type | GraphQL operation(s) | params (key/path/field_key/values) | page.rb evidence | Backend op class | Translator? |
95
+ |---|---|---|---|---|---|
96
+ | **date** (`date.zXXXX`) | `range_date_filter`, `isnt_in_range_date_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.date`; range: gte/lte/gt/lt/time_zone | `page.rb:191-209` | `Pages::RangeDateFilter` `range_date_filter.rb:797`; field_key resolved to `value` `range_date_filter.rb:822-828` | YES (`date_filter` field branch → `range_date_filter`) |
97
+ | **select_str** (`select_str.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.select_str`, field_key=`selected`, values | `page.rb:279-300` | `Pages::HasAnyFilter`→`BaseHasAnyFilter` `base_has_any_filter.rb:10`; search_key=`path.selected` `base_has_any_filter.rb:42-44` | YES (`options_filter` field branch → `has_any_filter`) |
98
+ | **select_num** (`select_num.zXXXX`) | same as select_str, path=`membranes.select_num` | key, path, field_key=`selected`, values | `page.rb:256-277` | `Pages::HasAnyFilter`→`BaseHasAnyFilter` | YES (field branch; caller passes path/`select_num` prefix) |
99
+ | **plain_text** (`plain_text.zXXXX`) | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `contains_filter`, `doesnt_contain_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.plain_text`; is_filter → `.exact` subfield | `page.rb:145-165` | `is_filter`→`Pages::IsFilter`→`BaseIsFilter` search_key=`exact` `is_filter.rb:315-316`; contains→`Pages::TextFilter` `contains_filter.rb:459-461` | is_filter YES (exact_filter→is_filter); contains NO |
100
+ | **rich_text** (`rich_text.zXXXX`) | `contains_filter`, `doesnt_contain_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.rich_text`; contains → `content` subfield | `page.rb:303-321` | `contains_filter`→`BaseContainsFilter` field_key=`content` `contains_filter.rb:477-478` | NO (no is/exact op; contains only) |
101
+ | **number** (`number.zXXXX`) | `numeric_range_filter`, `isnt_in_numeric_range_filter`, `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.number`; range: gte/lte/gt/lt on `.value` | `page.rb:168-188` | `numeric_range_filter`→`Pages::NumericRangeFilter` search_key=`path.value` `numeric_range_filter.rb:941-943` | is_filter YES; numeric_range NO |
102
+ | **gauge** (`gauge.zXXXX`) | `is_filter`, `isnt_filter`, `numeric_range_filter`, `isnt_in_numeric_range_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.gauge` | `page.rb:234-254` | numeric via `NumericRangeFilter`; is via `IsFilter` search_key=`value` `is_filter.rb:317-318` | is_filter YES; numeric_range NO |
103
+ | **file** (`file.zXXXX`) | `has_any_filter`, `doesnt_have_any_filter` (×`file_container_id`, `content_type`), `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.file`, field_key=`file_container_id`\|`content_type`, values | `page.rb:212-232` | `has_any_filter`→`Pages::File::HasAnyFilter` `has_any_filter.rb:1034-1035` | NO |
104
+ | **image_gallery** (`image_gallery.zXXXX`) | `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.image_gallery` | `page.rb:545-561` | `is_empty`→`Pages::ImageGallery::IsEmptyFilter` `is_empty_filter.rb:572-573` | NO |
105
+ | **people** (`people.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter` (×`id`/`contractor_organization_id`/`schema_id`/`prefilter_location_ids`), `is_direct_supervisor_filter`, `has_direct_supervisor_filter`, `is/isnt_contractor_administrator_filter`, `people_field_contains_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.people`, field_key varies, values | `page.rb:466-499` | `Pages::People::HasAnyFilter` `has_any_filter.rb:1030-1031` | NO |
106
+ | **contractor_entities** (`contractor_entities.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.contractor_entities`, field_key=`contractor_entities_ids`, values | `page.rb:324-344` | `BaseHasAnyFilter`; `required_data` loads ContractorEntity `base_has_any_filter.rb:27-28` | NO |
107
+ | **cross_reference / page_reference** (`cross_reference.zXXXX`, `any_reference_field`) | `has_any_filter`, `doesnt_have_any_filter`, `has_all_filter`, `doesnt_have_all_filter`, `meets_conditions_filter`(if CRR), `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.cross_references`, field_key=`id`, values=`@page_id`; meets_conditions: `register_id` | `page.rb:502-542` | `Pages::CrossReferences::HasAnyFilter` `has_any_filter.rb:1021-1022` | NO |
108
+ | **actions_list** (`actions_list.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.actions_list`, field_key=`id`, values=`@action_id` | `page.rb:347-367` | `Pages::ActionsList::HasAnyFilter` `has_any_filter.rb:1032-1033` | NO |
109
+ | **checklist** (`checklist.zXXXX`) | `all_options_ticked_filter`, `not_all_options_ticked_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.checklist` | `page.rb:445-463` | `all_options_ticked_filter` = `Pages::AllOptionsTickedFilter` (`all_options_ticked_filter.rb`) | NO |
110
+ | **geo** (`geo.zXXXX`) | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.geo`; is → `address.exact` subfield | `page.rb:370-388` | `IsFilter` search_key=`address.exact` `is_filter.rb:319-320` | is_filter YES (if driven); rest NO |
111
+ | **signature** (`signature.zXXXX`) | `is_filter`, `isnt_filter` (value=`@user_id`), `range_date_filter`, `isnt_in_range_date_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.signature`; is → `signed_by_id`; range → `date` subfield | `page.rb:391-411` | `IsFilter` search_key=`signed_by_id` `is_filter.rb:323-324`; range → `Pages::Signature::RangeDateFilter` `range_date_filter.rb:810-811` | is_filter/range partial (translator emits generic ops; subfield resolution is backend-side by key prefix) |
112
+ | **mailbox** (`mailbox.zXXXX`) | `range_date_filter`, `isnt_in_range_date_filter`, `has_any/all_filter`, `doesnt_have_any/all_filter` (×`all_recipients`/`sender_email`), `contains_filter`, `doesnt_contain_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.mailbox`, field_key varies, values | `page.rb:414-442` | `Pages::Mailbox::*` `has_any_filter.rb:1019-1020` | NO |
113
+ | **law** (`law.zXXXX`) | `has_any_filter`, `doesnt_have_any_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.law`, field_key=`snippets_ids`, values=`@law_snippet_id` | `page.rb:564-582` | `BaseHasAnyFilter`; `required_data` loads Snippet `base_has_any_filter.rb:29` | NO |
114
+
115
+ ---
116
+
117
+ ## 3. Operation → param contract (backend constructor signatures)
118
+
119
+ Cited so the guard spec asserts exact param shapes. All under
120
+ `app/services/new_ep/es/filters/`.
121
+
122
+ | Operation | Namespace / class | Constructor params (evidence) |
123
+ |---|---|---|
124
+ | `date_filter` | `Global::DateFilter` | `key, path?, gt?, gte?, lt?, lte?` — `global/date_filter.rb:28` |
125
+ | `range_date_filter` | `Pages::RangeDateFilter` | `key, path?, gte?, lte?, gt?, lt?` — `pages/range_date_filter.rb:797` |
126
+ | `is_filter` | `Pages::IsFilter` | `key, path?, value?` — `pages/is_filter.rb:258` |
127
+ | `isnt_filter` | `Pages::IsntFilter` | `key, path?, value` — `pages/isnt_filter.rb:360` |
128
+ | `match_filter` | `Global::MatchFilter` | `key, value, path?` — `global/match_filter.rb:6` |
129
+ | `contains_filter` | `Pages::ContainsFilter` | `key, path?, value` — `pages/contains_filter.rb:445` |
130
+ | `has_any_filter` | `Pages::HasAnyFilter` → `BaseHasAnyFilter` | `key, path?, field_key?, values` — `pages/has_any_filter.rb:995`, `pages/base_has_any_filter.rb:10` |
131
+ | `has_all_filter` | `Pages::HasAllFilter` → `BaseHasAllFilter` | `key, path?, field_key?, values` — `pages/has_all_filter.rb:1135`, `pages/base_has_all_filter.rb:59` |
132
+ | `doesnt_have_any_filter` | `Pages::DoesntHaveAnyFilter` | `key, path?, field_key?, values` (invert) — `pages/doesnt_have_any_filter.rb:1061` |
133
+ | `doesnt_have_all_filter` | `Pages::DoesntHaveAllFilter` | `key, path?, field_key?, values` (invert) — `pages/doesnt_have_all_filter.rb:1199` |
134
+ | `one_of_filter` | `Global::OneOfFilter` | `key, values` — `global/one_of_filter.rb:16` |
135
+ | `none_of_filter` | `Global::NoneOfFilter` | `key, values` — `global/none_of_filter.rb:6` |
136
+ | `numeric_range_filter` | `Pages::NumericRangeFilter` | `key, path?, gte?, lte?, gt?, lt?` — `pages/numeric_range_filter.rb:917` |
137
+ | `is_empty_filter` | `Pages::IsEmptyFilter` | `key, path?` (search_key by type) — `pages/is_empty_filter.rb:552` |
138
+ | `presence_filter` | `Pages::PresenceFilter` | `key, path?` — `pages/presence_filter.rb:724` |
139
+
140
+ ---
141
+
142
+ ## 4. Known-UNCOVERED field types / operations (visible gap, not silent)
143
+
144
+ The compat `FilterTranslator` intentionally translates only the v2 filter shapes that real
145
+ consumers emit (`date_filter`, `text_filter`, `exact_filter`/`is_filter`, `options_filter`,
146
+ plus passthroughs). The following register field types / operations have a backend contract
147
+ (cataloged above) but NO translator branch. This is by design — do not add speculative
148
+ translations without a consumer. The guard spec enumerates these so the gap stays visible.
149
+
150
+ **Field types with NO dedicated translator path** (any v2 filter targeting them falls through
151
+ to passthrough — the caller must pass a pre-shaped `{operation:,params:}` hash):
152
+
153
+ - `number` (numeric_range_filter), `gauge` (numeric_range_filter)
154
+ - `rich_text` (contains_filter / doesnt_contain_filter)
155
+ - `file` (has_any_filter on file_container_id / content_type)
156
+ - `image_gallery` (is_empty / presence only)
157
+ - `people`, `contractor_entities`, `cross_reference` / `page_reference`
158
+ - `actions_list`, `checklist`, `mailbox`, `law`, `signature` (range/is subfields)
159
+ - `geo` (is_filter on address.exact — reachable via exact_filter if the caller sets key `geo.zXXXX`)
160
+
161
+ **System fields with NO translator driver:** `assigned_to`, `creator_id` (Created by),
162
+ `active_stage_names` (Current Stage), `mould_counter` contains_filter.
163
+
164
+ **Operations never emitted by the translator** (must be passed pre-shaped): `has_all_filter`,
165
+ `doesnt_have_all_filter`, `numeric_range_filter`, `isnt_in_numeric_range_filter`,
166
+ `isnt_in_range_date_filter`, `contains_filter`, `doesnt_contain_filter`, `is_empty_filter`,
167
+ `isnt_empty_filter`, `presence_filter`, `not_presence_filter`, `meets_conditions_filter`,
168
+ `all_options_ticked_filter`, `people_field_contains_filter`, direct-supervisor / contractor-admin
169
+ filters.
170
+
171
+ ---
172
+
173
+ ## 5. Change log
174
+
175
+ - 2026-07-08 — Initial matrix built from `page.rb` + `filters/pages/**` + `filters/global/**`.
176
+ Guard spec `spec/ecoportal/api/graphql/compat/filter_translator_spec.rb` extended to pin every
177
+ emitted operation to its backend param contract and to enumerate the uncovered types.
@@ -4,6 +4,16 @@
4
4
  `template-diff-deploy` + `template-maintenance` projects into one plan).
5
5
  **Domain reference (read first):** `.ai-assistance/code/template_diff_pairing_domain.md`
6
6
  **Code spec:** `.ai-assistance/code/diff_pairing_engine.md`
7
+ **Governing security standard (read before wiring live creds):** `security/ai-service-account-scope`
8
+ in ep-ai-standards. This automation MUST authenticate as a dedicated per-org SERVICE ACCOUNT whose
9
+ grant covers templates/org-config only -- page records (customer data + PII) are structurally out of
10
+ scope, enforced by the credential grant, not by instruction. This is the practitioner discovery point
11
+ for that standard (the template automation is its concrete consumer). The "sandbox creds" in success
12
+ criterion #4 below must be provisioned to that scope. Platform dependency RESOLVED: templates ARE pages
13
+ (`PageUnion`), but `stage.tags` (wrongly deprecated ~21 months ago) has been restored via ecoPortal MR
14
+ !6649, so a rule scoped to template stages can structurally exclude record stages. Defining and
15
+ provisioning the concrete grant is now OUR work (this gem + agent), not a platform blocker -- see the
16
+ service-account grant task in `TODO.md`.
7
17
  **Superseded folders (pointers only now):** `template-diff-deploy/`, `template-maintenance/`.
8
18
  **Sibling projects:** `qa-services-delivery` (verify), `ooze-graphql-native-migration`,
9
19
  `template-csv-pipeline` (memory).
@@ -90,6 +90,17 @@ emission layer: an ordered `WorkflowCommand` batch with `placeholderId` threadin
90
90
  ---
91
91
 
92
92
  ## Cross-cutting
93
+ - [ ] **Service-account grant (gates live use; governed by ep-ai-standards `security/ai-service-account-scope`).**
94
+ This automation must run as a dedicated per-org SERVICE ACCOUNT scoped to templates/org-config only --
95
+ page records out of scope, enforced by the grant (structural PII/compliance boundary, not instruction).
96
+ Platform enabler is in place: `stage.tags` (wrongly deprecated ~21 months ago) restored via ecoPortal
97
+ MR !6649, so a rule scoped to template stages can exclude record stages. OUR remaining work:
98
+ (a) enumerate in-scope object types (templates; and which of registers / stages / tag defs / workflow +
99
+ automation config / custom-field defs -- explicit in/out per type, each a potential path back to records);
100
+ (b) write the concrete `stage.tags` rule and confirm no record stage shares those tags;
101
+ (c) fix the read/write surface per type (edit templates? read-only org-config?);
102
+ (d) provision the per-org account and confirm its `canXXX` set carries NO page-record capability.
103
+ Feeds the "sandbox creds" needed by Phase B2 / Phase 5. Reference: INTENT header + the standard.
93
104
  - [ ] Tighten `ecoportal-qa` to consume the gem's page-model instead of its own `TemplateModel`.
94
105
  - [ ] Emit `stageId`/`sectionId` back-refs on diff-side `addField`/`addSection` (BUILD already does;
95
106
  the diff synthesizer's structural adds still emit only label/placeholder — then threading covers them).
@@ -0,0 +1,319 @@
1
+ ---
2
+ name: procedural-memory
3
+ category: governance
4
+ version: 0.2.0
5
+ status: draft
6
+ description: >
7
+ Distills accumulated session signals into stored `procedural` memory entries
8
+ (recurring routines: a trigger condition + the action that reliably follows) and
9
+ surfaces them as likely next steps. Invoke on a periodic distill pass or when a
10
+ developer asks what patterns you have noticed in how they work.
11
+ triggers:
12
+ - what patterns have you noticed
13
+ - distill my routines
14
+ - procedural memory
15
+ - how do I work
16
+ - learn my routines
17
+ - weekly distill
18
+ - modus operandi
19
+ applicable_to:
20
+ - any
21
+ ---
22
+
23
+ # procedural-memory
24
+
25
+ ## Purpose
26
+
27
+ Turn the substrate of accumulated session history into stored **`procedural`** memory --
28
+ recurring routines, each expressed as a **trigger condition** and the **inferred action**
29
+ that reliably follows it -- and surface those routines (plus a short "likely next steps"
30
+ list) so the next session starts proactively shaped instead of from scratch.
31
+
32
+ This skill operationalizes the **Distill** stage of the Observe -> Distill -> Act loop, plus
33
+ the human-facing part of the **Act** stage (surfacing). It implements
34
+ `standards/workflows/procedural-memory.md`. Observation (the `Stop`/`SessionEnd` logging
35
+ substrate) and autonomous execution (the auto-worker) are out of scope here -- this skill
36
+ reads the Observe substrate and writes the inferred routines; it never acts on them itself.
37
+
38
+ > **v0.2.0 -- draft-aligned, blockers resolved.** This skill tracks
39
+ > `standards/workflows/procedural-memory.md` at v0.2.0 (`status: draft`, ready for consolidation
40
+ > review). It implements the now-decided **decay/retirement lifecycle** (D1) and **Observe-stage
41
+ > privacy policy** (D2). The distill cadence, the minimum-observation threshold, the promotion
42
+ > rule, and the lifecycle numbers (confidence steps, retirement floor, staleness/cool-off
43
+ > windows) are all **provisional** and will firm up when the standard reaches `status: reviewed`.
44
+ > Treat the numbers below as conservative defaults, not settled policy.
45
+
46
+ ---
47
+
48
+ ## When to invoke
49
+
50
+ - **On a periodic distill pass** -- e.g. weekly, or as part of an end-of-week wrap-up. This is
51
+ the primary mode: a scheduled mining pass over accumulated session history.
52
+ - **On demand** -- when a developer asks "what patterns have you noticed in how I work?",
53
+ "distill my routines", or similar. Run the same procedure, scoped to the data available.
54
+
55
+ Do not invoke this skill to *perform* a routine -- it only distils and surfaces. Acting on a
56
+ surfaced routine is gated separately (see HARD CONSTRAINTS).
57
+
58
+ ---
59
+
60
+ ## On invocation
61
+
62
+ Before any other output, do both of the following:
63
+
64
+ 1. Print to the user:
65
+ `[skill: procedural-memory] <one-line description of what you are about to do>`
66
+ Examples:
67
+ - Periodic pass: `[skill: procedural-memory] weekly distill — mining usage log for recurring routines`
68
+ - On demand: `[skill: procedural-memory] reviewing session history for patterns in how you work`
69
+
70
+ 2. Append a usage record to `.ai-assistance/local/kpi/usage-<YYYY-WNN>.jsonl`
71
+ (ISO week format, e.g. `2026-W26`):
72
+ ```json
73
+ {"component": "skill/procedural-memory", "action": "invoked", "detail": "<same one-liner>", "ts": "<ISO timestamp>", "session_id": "<if known>"}
74
+ ```
75
+ Before writing, run the scrub/redaction pass (see Privacy below) over the record. If the file
76
+ or directory does not exist, create it. If writing fails, continue silently.
77
+
78
+ ### Consent check (do this first)
79
+
80
+ The Observe log is **opt-in**. Before reading any `usage-*.jsonl` to mine routines, confirm the
81
+ developer has enabled Observe (`procedural_memory.observe: true` in local settings, or the
82
+ equivalent Observe-hook entry). If consent is not set, report that the Observe stage is not
83
+ enabled, do NOT read the log, and stop. (Writing this skill's own invoked/completed usage
84
+ records is the skill announcing itself and is unaffected by the Observe consent flag.)
85
+
86
+ ---
87
+
88
+ ## The procedure
89
+
90
+ ### Step 1 -- Read the Observe substrate
91
+
92
+ The Observe stage is append-only and machine-local; this skill only reads it. Read only after
93
+ the consent check above has passed.
94
+
95
+ - Read `.ai-assistance/local/kpi/usage-<YYYY-WNN>.jsonl` across the distill window (default: the
96
+ current and previous 3 ISO weeks). These records carry only low-sensitivity signal: command/
97
+ tool names (not arguments), file paths/types touched, components invoked, event types and
98
+ `action` values, and coarse durations/counts -- never full arguments, file contents, or
99
+ prompt/response text (see Privacy below). Parse defensively: skip malformed/corrupt JSON lines
100
+ rather than failing the whole pass.
101
+ - Optionally read `docs/worklog.md` for `<!-- LEARNING: ... -->` markers. These are the
102
+ human-authored analogue of a `procedural` entry and may seed the distill pass
103
+ (`standards/workflows/session-handoff.md`).
104
+
105
+ If no usage log exists, report that the Observe substrate is not yet populated and stop --
106
+ there is nothing to distil. Do not invent routines from a single session.
107
+
108
+ ### Step 2 -- Mine for recurring patterns
109
+
110
+ A pattern is a **trigger condition** plus the **action that reliably follows it** within the
111
+ same or the next session. Look for:
112
+
113
+ - An action that consistently follows a specific event (e.g. a code-spec regeneration that
114
+ follows every merge to `main`).
115
+ - A consistent ordering of actions (e.g. tests -> lint -> commit).
116
+ - A deferral that is consistently picked up at a predictable later point.
117
+
118
+ **Minimum-observation threshold.** A pattern is only written if it was **observed in >= 3
119
+ distinct sessions** in the window. This default is deliberately conservative to stop a one-off
120
+ coincidence becoming a stored routine; the standard leaves the exact number open, so treat
121
+ this as **provisional**. Below threshold: note it as a *candidate* in the report, but do not
122
+ write a memory entry.
123
+
124
+ ### Step 3 -- Write each confirmed pattern as a `procedural` memory entry
125
+
126
+ Write each pattern at or above threshold into the repo-scoped memory directory
127
+ `~/.claude/projects/<project>/memory/` (auto memory -- repo-scoped and machine-local), as a
128
+ memory entry of **type `procedural`**. The body carries **Trigger:** and **Inferred action:**
129
+ lines, mirroring how `feedback` entries use **Why:** / **How to apply:**:
130
+
131
+ ```markdown
132
+ ## [Short routine name]
133
+
134
+ **Trigger:** [the observed condition that recurs]
135
+ **Inferred action:** [what the developer reliably does next]
136
+ **Confidence:** 0.6
137
+ **First-observed:** [YYYY-MM-DD]
138
+ **Last-observed:** [YYYY-MM-DD]
139
+
140
+ Observed in X of Y opportunities across N sessions (source: usage-<YYYY-WNN> ... usage-<YYYY-WNN>).
141
+ ```
142
+
143
+ New entries start at **Confidence: 0.6** (provisional) -- above the retirement floor, below full
144
+ trust, so a freshly inferred routine starts on probation. **First-observed** and **Last-observed**
145
+ are both set to the current distill-pass date on first write. These three lines are required: they
146
+ drive the decay/retirement lifecycle in Step 3b.
147
+
148
+ Then add the index pointer line to `~/.claude/projects/<project>/memory/MEMORY.md`, matching
149
+ the existing index style:
150
+
151
+ ```markdown
152
+ - [Procedural: <short routine name>](<filename>.md) — <one-line trigger → action summary>
153
+ ```
154
+
155
+ Mark the entry clearly as `procedural` so it stays distinguishable from `user`/`project`
156
+ memory (see HARD CONSTRAINTS).
157
+
158
+ ### Step 3b -- Re-evaluate, decay, and retire existing routines
159
+
160
+ On every distill pass, before reporting, walk each existing `procedural` entry and update its
161
+ lifecycle metadata (standard D1). Honour forced markers first, then apply the arithmetic.
162
+
163
+ 1. **Forced markers (one-shot, applied first).** Scan `docs/worklog.md` and the memory files for:
164
+ - `<!-- RETIRE: <short routine name> -->` -- retire that routine immediately (see step 4
165
+ below), regardless of confidence or recency.
166
+ - `<!-- CONFIRM: <short routine name> -->` -- set its **Confidence** to `1.0` and refresh
167
+ **Last-observed** to today.
168
+ Record that the marker was applied so it is not re-applied on the next pass.
169
+
170
+ 2. **Re-evaluate against this window.** For each routine whose **Trigger** occurred at least once
171
+ in the window:
172
+ - trigger fired AND inferred action followed: refresh **Last-observed** to today and
173
+ **increment Confidence by +0.1**, capped at `1.0`;
174
+ - trigger fired but action did NOT follow: **decrement Confidence by -0.2** (contradiction
175
+ costs more than a single confirmation earns -- intentional asymmetry);
176
+ - trigger did not fire at all: leave Confidence unchanged; do NOT refresh Last-observed.
177
+
178
+ 3. **Retire** a routine when EITHER its **Confidence < 0.3** (floor) OR its **Last-observed** is
179
+ **older than 90 days** (staleness window). All thresholds provisional.
180
+
181
+ 4. **Archive on retirement -- never silent-delete.** Move the entry file to
182
+ `~/.claude/projects/<project>/memory/retired/`, appending the retirement date and reason
183
+ (`confidence floor` / `staleness` / `forced`). Remove its line from `MEMORY.md`. Keep a
184
+ tombstone (routine name + retirement date + reason). When mining (Step 2), if a re-detected
185
+ Trigger/Inferred-action pair matches a tombstone retired **within the last 30 days**
186
+ (cool-off, provisional), do NOT auto-write it -- surface it as a *candidate* for the developer
187
+ to re-confirm. After the cool-off it may be written fresh at the initial confidence.
188
+
189
+ ### Step 4 -- Promotion (propose only)
190
+
191
+ A routine observed consistently **across multiple repos** is a candidate cross-project trait
192
+ for user-global memory in `~/.claude/CLAUDE.md`. **Propose it -- do not auto-promote.** Present
193
+ the candidate and the evidence; the developer decides. The exact promotion threshold is an
194
+ open question in the standard and is provisional here.
195
+
196
+ ### Step 5 -- Act / surface
197
+
198
+ Present the result to the developer:
199
+
200
+ 1. The distilled routines (newly written and pre-existing) for this repo.
201
+ 2. A short **"likely next steps"** list inferred from the current state and the routines.
202
+ 3. For routines whose inferred action is mechanically actionable, **PROPOSE** (never auto-add)
203
+ an `[auto]` tag so the auto-worker could pre-stage it -- subject to the developer confirming
204
+ the tag. The `[auto]` tag is the authorization boundary; eligibility rules in
205
+ `standards/tooling/auto-worker-eligibility.md` still apply, and this skill never tags
206
+ anything itself.
207
+
208
+ After surfacing, append a completion usage record:
209
+ ```json
210
+ {"component": "skill/procedural-memory", "action": "completed", "detail": "<N routines written, M candidates, K promotions proposed>", "ts": "<ISO timestamp>", "session_id": "<if known>"}
211
+ ```
212
+
213
+ ---
214
+
215
+ ## HARD CONSTRAINTS
216
+
217
+ From `standards/workflows/procedural-memory.md` -- these are not optional:
218
+
219
+ - **A `procedural` entry is an inference, not an asserted fact.** It must remain visibly
220
+ distinguishable from `user`/`project` memory (which record things the developer stated), so
221
+ a wrong inference can be retired without eroding trust in asserted facts.
222
+ - **Suggest, never act.** A distilled routine may *suggest* a next step or an `[auto]` tag, but
223
+ it must not drive an autonomous action without a human-in-the-loop confirmation. Any routine
224
+ that would trigger an irreversible or shared-system action is subject to
225
+ `standards/workflows/hitl-gates.md` -- inference grants no authority to act.
226
+ - **Observe logging is opt-in, scrubbed, and machine-local (standard D2).** The Observe hook is
227
+ off by default; do not read the usage log to mine routines unless consent is set (see Consent
228
+ check). Capture only the documented allow-list -- command/tool names, file paths/types, event
229
+ types, durations -- and NEVER full arguments, file contents, diffs, or prompt/response text.
230
+ Run the scrub/redaction pass (regex for tokens/keys, emails, obvious PII; replace with typed
231
+ placeholders; drop a record that cannot be confidently scrubbed) BEFORE writing any record.
232
+ Never copy the log off the machine, into a versioned file, or into a `procedural`/promoted
233
+ entry -- entries store only the distilled Trigger/Inferred-action plus aggregate evidence.
234
+ - **Decay and retirement are mandatory (standard D1).** Every `procedural` entry carries
235
+ **Confidence**, **First-observed**, and **Last-observed**. Each pass re-evaluates them, retires
236
+ entries below the floor or past the staleness window, honours `<!-- RETIRE: -->` /
237
+ `<!-- CONFIRM: -->` markers, and archives (never silently deletes) retired entries with a
238
+ cool-off tombstone so a retired routine does not silently reappear.
239
+ - **Repo-relative paths only** in any versioned output. Use `~/.claude/...` solely as the
240
+ canonical memory location, never a developer's absolute filesystem path.
241
+
242
+ ### Privacy -- what the Observe log captures, redacts, and where it lives
243
+
244
+ - **Captured:** command/tool names; file paths/types touched (repo-relative); event types and
245
+ `action` values; coarse durations and counts; timestamps, ISO-week, session id.
246
+ - **Never captured:** full command arguments; file contents or diffs; prompt/response text; the
247
+ literal text of corrections/redirects (only that one occurred, and its coarse type); secrets,
248
+ tokens, credentials, env values.
249
+ - **Redaction:** scrub-before-write -- regex detectors for tokens/keys (high-entropy strings,
250
+ `sk-`/`ghp_`/AWS-style prefixes), emails, obvious PII; matches replaced with `<REDACTED:...>`;
251
+ unscrubabble records dropped.
252
+ - **Storage/consent:** lives only at `.ai-assistance/local/kpi/usage-<YYYY-WNN>.jsonl`
253
+ (machine-local, gitignored, never committed). Enable via `procedural_memory.observe: true` in
254
+ local settings; disable by removing it; the developer may delete the log at any time.
255
+
256
+ ---
257
+
258
+ ## Example -- one mined pattern -> resulting `procedural` entry
259
+
260
+ **Observed.** Over four ISO weeks the usage log shows that in 4 of 4 sessions ending with a
261
+ merge to `main`, the same or next session regenerates code-specs and bumps the changelog
262
+ before any new branch is opened. 4 sessions >= the threshold of 3, so it is written.
263
+
264
+ Resulting file `~/.claude/projects/<project>/memory/procedural-merge-followup.md`:
265
+
266
+ ```markdown
267
+ ## Post-merge spec + changelog routine
268
+
269
+ **Trigger:** a feature branch is merged to `main`.
270
+ **Inferred action:** regenerate code-specs and bump the changelog before opening the next branch.
271
+ **Confidence:** 0.6
272
+ **First-observed:** 2026-06-28
273
+ **Last-observed:** 2026-06-28
274
+
275
+ Observed in 4 of 4 opportunities across 4 sessions (source: usage-2026-W22 ... usage-2026-W25).
276
+ ```
277
+
278
+ On a later pass, if a merge to `main` is NOT followed by the spec+changelog action, Confidence
279
+ drops by 0.2; sustained contradiction (or a `<!-- RETIRE: Post-merge spec + changelog routine -->`
280
+ marker, or 90 days without observation) retires it to `memory/retired/`.
281
+
282
+ Index line added to `MEMORY.md`:
283
+
284
+ ```markdown
285
+ - [Procedural: Post-merge spec + changelog routine](procedural-merge-followup.md) — merge to main → regenerate code-specs + bump changelog
286
+ ```
287
+
288
+ Surfaced at the next post-merge session as a likely next step, with a *proposal* (awaiting
289
+ confirmation) to tag "regenerate code-specs `[auto]`".
290
+
291
+ ---
292
+
293
+ ## What this skill does NOT do
294
+
295
+ - Does not run the Observe stage -- it reads the usage log; it does not write session signals.
296
+ - Does not execute any routine, autonomously or otherwise -- surfacing only.
297
+ - Does not add an `[auto]` tag -- it proposes; the developer confirms.
298
+ - Does not auto-promote a routine to `~/.claude/CLAUDE.md` -- it proposes; the developer decides.
299
+ - Does not write a routine below the minimum-observation threshold.
300
+ - Does not read the Observe log without consent set (Observe is opt-in, off by default).
301
+ - Does not write any record to the usage log without first running the scrub/redaction pass.
302
+ - Does not capture command arguments, file contents, diffs, or prompt/response text.
303
+ - Does not copy machine-local log contents into any versioned or promoted file.
304
+ - Does not silently delete a retired routine -- it archives to `memory/retired/` with a tombstone.
305
+ - Does not re-write a routine retired within the cool-off window -- it surfaces it as a candidate.
306
+ - Does not modify `user`/`project`/`feedback`/`reference` memory entries -- it only adds and
307
+ maintains `procedural` entries.
308
+
309
+ ---
310
+
311
+ ## Version note
312
+
313
+ v0.2.0 is aligned with `standards/workflows/procedural-memory.md` at v0.2.0 (`status: draft`,
314
+ ready for consolidation review). It implements the now-decided decay/retirement lifecycle (D1)
315
+ and Observe-stage privacy policy (D2). The distill cadence (weekly placeholder), the
316
+ minimum-observation threshold (`>= 3 sessions`), the cross-repo promotion rule, and the lifecycle
317
+ numbers (initial confidence 0.6, +0.1/-0.2 steps, 0.3 floor, 90-day staleness, 30-day cool-off)
318
+ are all provisional. They will be revisited and firmed up when the standard reaches
319
+ `status: reviewed`; this skill should be re-versioned to match.
@@ -1,21 +1,22 @@
1
- {
2
- "ep-ai-standards-version": "1.5.0",
3
- "applied-at": "2026-06-13",
4
- "project-type": [
5
- "ruby"
6
- ],
7
- "deferred": [],
8
- "installed-components": {
9
- "skills/code-specs": "0.0.0",
10
- "skills/ep-ai-manager": "2.1.0",
11
- "skills/dep-graph": "0.0.0",
12
- "skills/corporate-policies": "1.0.0",
13
- "skills/ai-instructions": "0.0.0",
14
- "skills/spec-generation": "0.0.0",
15
- "skills/gemini-assist": "0.0.0",
16
- "skills/ruby-scripting": "0.1.0",
17
- "skills/refactor": "0.0.0",
18
- "skills/project-cycle": "0.0.0",
19
- "skills/graphql-schema-analysis": "0.0.0"
20
- }
1
+ {
2
+ "ep-ai-standards-version": "1.5.0",
3
+ "applied-at": "2026-06-13",
4
+ "project-type": [
5
+ "ruby"
6
+ ],
7
+ "deferred": [],
8
+ "installed-components": {
9
+ "skills/code-specs": "0.0.0",
10
+ "skills/ep-ai-manager": "2.1.0",
11
+ "skills/dep-graph": "0.0.0",
12
+ "skills/corporate-policies": "1.0.0",
13
+ "skills/ai-instructions": "0.0.0",
14
+ "skills/spec-generation": "0.0.0",
15
+ "skills/gemini-assist": "0.0.0",
16
+ "skills/ruby-scripting": "0.1.0",
17
+ "skills/refactor": "0.0.0",
18
+ "skills/project-cycle": "0.0.0",
19
+ "skills/graphql-schema-analysis": "0.0.0",
20
+ "skills/procedural-memory": "0.2.0"
21
+ }
21
22
  }
data/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.3.12] - 2026-07-09
6
+
7
+ Compat-layer correctness for register **field filters** + the native `SearchConf` field-filter DSL.
8
+ **Backwards-compatible** — bug fixes (broken field filters returned 0 results) + additive methods;
9
+ no public return type changed. (The `get_by_name`→collection contract change is intentionally **not**
10
+ in this release — it is a breaking change parked on `feature/model-input-fixes` for a deliberate,
11
+ signed-off future decision.)
12
+
13
+ ### Fixed
14
+
15
+ - **`Compat::FilterTranslator`** now routes register **FIELD** filters to the backend-correct ES
16
+ operation (previously emitted the system-key operation → 0 results):
17
+ - Date FIELD (key `date.zXXXX`) → `range_date_filter` + `path: membranes.date` (system date keeps
18
+ `date_filter`).
19
+ - Select FIELD (key `select_str.zXXXX`) → `has_any_filter` / `doesnt_have_any_filter` +
20
+ `path: membranes.select_str` + `field_key: 'selected'` (system option key keeps
21
+ `one_of_filter`/`none_of_filter`).
22
+ - `exact_filter` no longer derives a `membranes.<type>` path for **system** keys
23
+ (`external_id`/`id`/`state`) — a spurious path produced term `membranes.external_id.external_id`
24
+ → 0 results (silent miss → dup-on-upsert). Register FIELD keys still get the nested path.
25
+ - Verified against the backend filter definitions; catalogued in
26
+ `.ai-assistance/code/filter_contract_matrix.md`.
27
+
28
+ ### Added
29
+
30
+ - **`SearchConf` field-filter DSL:** `sc[:'date.zXXXX'].between/since/before` now emits
31
+ `range_date_filter` + `membranes.date` for register Date FIELDs; new `FieldRef#any_of` / `#none_of`
32
+ → `has_any_filter` / `doesnt_have_any_filter` (path + `field_key`) for register field value
33
+ membership (distinct from `one_of`, which ORs `exact_filter` for system keys).
34
+ - **`get_by_id`** on `DataField::Collection` and `SectionCollection` (v2-compat find-by-id) — fixes
35
+ `NoMethodError` in consumers that resolve a field/section by id.
36
+
5
37
  ## [1.3.11] - 2026-07-04
6
38
 
7
39
  Template automatic **build & maintenance** engine (backwards-compatible — a self-contained new