opencdd 0.1.1

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 (97) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +486 -0
  3. data/README.md +304 -0
  4. data/bin/lint-no-raw-mdc +41 -0
  5. data/lib/cdd.rb +11 -0
  6. data/lib/opencdd/alias_table.rb +52 -0
  7. data/lib/opencdd/cddal/ast.rb +151 -0
  8. data/lib/opencdd/cddal/builder.rb +374 -0
  9. data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
  10. data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
  11. data/lib/opencdd/cddal/fetcher.rb +18 -0
  12. data/lib/opencdd/cddal/generated_parser.rb +805 -0
  13. data/lib/opencdd/cddal/lexer.rb +193 -0
  14. data/lib/opencdd/cddal/parser.rb +19 -0
  15. data/lib/opencdd/cddal/resolver.rb +100 -0
  16. data/lib/opencdd/cddal/serializer.rb +210 -0
  17. data/lib/opencdd/cddal/value_serializer.rb +60 -0
  18. data/lib/opencdd/cddal.rb +63 -0
  19. data/lib/opencdd/class_tree.rb +80 -0
  20. data/lib/opencdd/class_type.rb +33 -0
  21. data/lib/opencdd/codegen/ts.rb +185 -0
  22. data/lib/opencdd/codegen.rb +7 -0
  23. data/lib/opencdd/composition_tree.rb +119 -0
  24. data/lib/opencdd/condition.rb +120 -0
  25. data/lib/opencdd/data_type.rb +143 -0
  26. data/lib/opencdd/database.rb +719 -0
  27. data/lib/opencdd/effective_properties.rb +119 -0
  28. data/lib/opencdd/entity/field_reader.rb +141 -0
  29. data/lib/opencdd/entity/field_registry.rb +99 -0
  30. data/lib/opencdd/entity/version_history.rb +62 -0
  31. data/lib/opencdd/entity.rb +255 -0
  32. data/lib/opencdd/exporters/json.rb +235 -0
  33. data/lib/opencdd/exporters/mermaid.rb +62 -0
  34. data/lib/opencdd/exporters/yaml.rb +16 -0
  35. data/lib/opencdd/exporters.rb +9 -0
  36. data/lib/opencdd/guid.rb +27 -0
  37. data/lib/opencdd/instance_rule.rb +70 -0
  38. data/lib/opencdd/irdi.rb +128 -0
  39. data/lib/opencdd/klass.rb +230 -0
  40. data/lib/opencdd/languages.rb +70 -0
  41. data/lib/opencdd/meta_class.rb +274 -0
  42. data/lib/opencdd/model/entity_store.rb +85 -0
  43. data/lib/opencdd/model/yaml_database.rb +49 -0
  44. data/lib/opencdd/model/yaml_entity.rb +196 -0
  45. data/lib/opencdd/model.rb +13 -0
  46. data/lib/opencdd/parcel/csv_reader.rb +35 -0
  47. data/lib/opencdd/parcel/csv_writer.rb +114 -0
  48. data/lib/opencdd/parcel/entity_manifest.rb +119 -0
  49. data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
  50. data/lib/opencdd/parcel/layout_detector.rb +115 -0
  51. data/lib/opencdd/parcel/metadata.rb +112 -0
  52. data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
  53. data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
  54. data/lib/opencdd/parcel/selector.rb +115 -0
  55. data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
  56. data/lib/opencdd/parcel/sheet.rb +287 -0
  57. data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
  58. data/lib/opencdd/parcel/sheet_schema.rb +253 -0
  59. data/lib/opencdd/parcel/workbook.rb +172 -0
  60. data/lib/opencdd/parcel/workbook_reader.rb +200 -0
  61. data/lib/opencdd/parcel/writer.rb +185 -0
  62. data/lib/opencdd/parcel.rb +175 -0
  63. data/lib/opencdd/parse_helpers.rb +73 -0
  64. data/lib/opencdd/property.rb +120 -0
  65. data/lib/opencdd/property_data_element_type.rb +44 -0
  66. data/lib/opencdd/property_ids.rb +202 -0
  67. data/lib/opencdd/reader.rb +103 -0
  68. data/lib/opencdd/relation.rb +88 -0
  69. data/lib/opencdd/relation_tree.rb +74 -0
  70. data/lib/opencdd/relation_type.rb +58 -0
  71. data/lib/opencdd/structured_values.rb +183 -0
  72. data/lib/opencdd/unit.rb +16 -0
  73. data/lib/opencdd/validator/class_reference_rule.rb +77 -0
  74. data/lib/opencdd/validator/condition_rule.rb +27 -0
  75. data/lib/opencdd/validator/data_type_rule.rb +26 -0
  76. data/lib/opencdd/validator/enum_rule.rb +27 -0
  77. data/lib/opencdd/validator/format_rule.rb +54 -0
  78. data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
  79. data/lib/opencdd/validator/irdi_rule.rb +57 -0
  80. data/lib/opencdd/validator/mandatory_rule.rb +25 -0
  81. data/lib/opencdd/validator/pattern_rule.rb +26 -0
  82. data/lib/opencdd/validator/reference_rule.rb +36 -0
  83. data/lib/opencdd/validator/rule.rb +65 -0
  84. data/lib/opencdd/validator/runner.rb +101 -0
  85. data/lib/opencdd/validator/set_rule.rb +41 -0
  86. data/lib/opencdd/validator/synonym_rule.rb +30 -0
  87. data/lib/opencdd/validator/type_rule.rb +102 -0
  88. data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
  89. data/lib/opencdd/validator.rb +68 -0
  90. data/lib/opencdd/value_format.rb +71 -0
  91. data/lib/opencdd/value_list.rb +42 -0
  92. data/lib/opencdd/value_term.rb +24 -0
  93. data/lib/opencdd/version.rb +5 -0
  94. data/lib/opencdd/view_control.rb +10 -0
  95. data/lib/opencdd/visitor.rb +93 -0
  96. data/lib/opencdd.rb +57 -0
  97. metadata +325 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1ea74c1b474208d733e1aedb3174ae3c088b6899f78e65a88b87cff180932b3
4
+ data.tar.gz: ea3d59ec075cd72cb7182955936dc577fb6be2b53bacd45045a5409cd002df8a
5
+ SHA512:
6
+ metadata.gz: f9efe7bf576ed24dc6f124a234ea12c3a2a8a70872e38e6e274550b3b77c78847434992f47250b18718cd3f5719103b2182afcacb517a2e5568ccce0588bcc12
7
+ data.tar.gz: 32499575fc889ae2b278ed5d767b0ad75e91cdd339ccecea4cc02b008edab66b87c123963132e307ed932553cbfdded8c0e047773cbcbb113963b03585ce89eb
data/CLAUDE.md ADDED
@@ -0,0 +1,486 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this repository is
6
+
7
+ OpenCDD is an open Common Data Dictionary online resource that republishes IEC CDD
8
+ data plus additional models built on the same schema. Backed by
9
+ ISO/TC 184/SC 4/JWG 24 and IEC/SC 3D.
10
+
11
+ CDD is an ontology language that is **not** OWL/RDF or UML. Its defining feature is
12
+ **power-type modelling**: classes can be specialized by instances, and those
13
+ instances can themselves be used as classes. Any model the library builds must
14
+ preserve that two-level capability — instances-as-classes is not an edge case,
15
+ it is the central abstraction.
16
+
17
+ The **Parcel** format is the canonical exchange format for CDD, embodied as an
18
+ Excel workbook with a fixed sheet structure. The library targets Parcel as its
19
+ primary import/export surface.
20
+
21
+ ## What's in this repo
22
+
23
+ The Ruby `cdd` gem is shipped — full ParcelMaker 5.2.1 functional parity
24
+ in the library layer, 572 specs green. See `README.md` for the API overview
25
+ and `TODO.full-cdd/` for per-phase plans.
26
+
27
+ The repo also contains:
28
+
29
+ - `reference-docs/` — authoritative CDD exports + ParcelMaker manuals (source
30
+ material, do not delete).
31
+ - `downloads/` — scraped `.xls` files (one dir per dictionary).
32
+ - `harvest/` — Python scraper ecosystem (`download.py`, `discover.py`,
33
+ `probe_*.py`, cached `tree_form_*.html`, `pages/`, `verify/iec-*.txt`).
34
+ Fetches from `cdd.iec.ch` using headful Playwright. See
35
+ [`harvest/README.md`](harvest/README.md) and [`DATA.md`](DATA.md).
36
+ - `specs/parcelmaker/` — the behavioral spec driving the gem.
37
+ - `TODO.full-cdd/` and `TODO.cdd-editor/` — engineering plans.
38
+
39
+ ## Reference formats (in `reference-docs/`)
40
+
41
+ Two distinct layouts exist; the library must understand both.
42
+
43
+ **ParcelMaker `.xlsx`** — single workbook, 10 sheets, the canonical Parcel shape:
44
+
45
+ ```
46
+ Project metadata
47
+ sheetmap sheet ↔ entity-type mapping
48
+ pcls_LOCAL local class-id bridge
49
+ <PROJ>_CLASS class definitions
50
+ <PROJ>_PROPERTY property definitions
51
+ <PROJ>_ENUM enumerations / value lists
52
+ <PROJ>_TERMINOLOGY value terms (multilingual labels)
53
+ <PROJ>_UoM units of measurement
54
+ <PROJ>_RELATION relationships between classes
55
+ <PROJ>_VIEWCONTROL view-control metadata
56
+ ```
57
+
58
+ Real example: `export_CDD_IEC62683 in ParcelMaker format.xlsx` (project IEC62683).
59
+
60
+ **Legacy "EXCEL format" `.xls`** — OLE-compound BIFF, **one file per entity
61
+ type**, 6 files per dictionary: `export_{CLASS,PROPERTY,RELATION,UNIT,
62
+ VALUELIST,VALUETERMS}_<CODE>.xls`. Real example: the `export_CDD_IEC62368 ...`
63
+ folder. Per-class downloads in `downloads/` only fetch 4 of these variants
64
+ (CLASS, PROPERTY, VALUELIST, VALUETERMS) — that is a scrape-time decision, not
65
+ a format constraint.
66
+
67
+ The IRDI (International Registration Data Identifier) is the canonical key for
68
+ every entity. Forms observed: `0112/2///61360_4#AAA001` (full) and `AAA001`
69
+ (short class code). The same entity can be referenced either way.
70
+
71
+ ## Harvesting from cdd.iec.ch
72
+
73
+ The CDD backend is Lotus Notes Domino. There is no parcel server, no JSON
74
+ endpoint, no bulk dump. Pages are `.nsf/<form>/<UNID>` and attachments are
75
+ `.nsf/0/<UNID>/$file/<filename>`. The site is gated by AWS WAF behind CloudFront.
76
+
77
+ Hard constraints baked into `harvest/download.py`:
78
+
79
+ - **Headful Chromium only.** Headless gets 403 from WAF. Do not "optimize" by
80
+ switching to headless.
81
+ - **In-page navigation only.** `page.request.get()` and out-of-page HTTP get 403.
82
+ The WAF token is bound to the issuing browser client.
83
+ - **Tokens are per-page, per-class.** Each class page renders export buttons as
84
+ `<input id="export7" onclick="return _doClick('<UNID-body-token>', ...)">`.
85
+ The flow is: fetch class page → parse token → `?OpenDocument&Click=<token>`
86
+ → read `<a id="download" href="...">` → follow href to capture the `.xls`.
87
+ - **Tree enumeration** comes from `Tree?ReadForm&ongletactif=1`, which returns
88
+ inline `d.add(id, parent_id, 'label', 'TU0/<IRDI>')` calls. Parse those to
89
+ get every class IRDI and its place in the hierarchy.
90
+ - **Must run on the user's machine.** The WAF token is bound to the issuing
91
+ client IP and TLS fingerprint; cannot be transferred.
92
+
93
+ If you change `harvest/download.py`, run it against `iec63213` (26 classes) as
94
+ a smoke test before pointing at the larger dictionaries.
95
+
96
+ ## The `cdd` gem
97
+
98
+ The library is at full ParcelMaker parity. Five layers in `lib/cdd/`:
99
+
100
+ 1. **Identifiers & registries** — `irdi.rb`, `alias_table.rb`,
101
+ `property_ids.rb`, `meta_class.rb`. Single source of truth for every
102
+ property ID (`Cdd::PropertyIds::REGISTRY`) and meta-class IRDI
103
+ (`Cdd::MetaClasses::REGISTRY`). No raw `"MDC_P###"` / `"MDC_C###"`
104
+ string literals outside these files.
105
+ 2. **Domain entities** — `entity.rb`, `klass.rb`, `property.rb`,
106
+ `relation.rb`, `unit.rb`, `value_list.rb`, `value_term.rb`,
107
+ `view_control.rb`. Frozen value objects. (Never name a class `Class`;
108
+ the IEC entity type is `Cdd::Klass`.)
109
+ 3. **Database & traversal** — `database.rb` (in-memory store + finalize!),
110
+ `effective_properties.rb` (powertype-aware walker),
111
+ `class_tree.rb`, `composition_tree.rb`, `relation_tree.rb`.
112
+ 4. **Formats** — `parcel/` (all Parcel-format readers + writer + CSV:
113
+ `WorkbookReader` for single .xlsx, `FlatDirReader` for flat .xls
114
+ directories, `ShardedDirReader` for per-class sharded subdirs),
115
+ `cddal/` (plain-text canonical format with racc-generated parser).
116
+ The `.xls` format IS Parcel (IEC 62656-1) — there is no separate
117
+ `Excel` namespace.
118
+ 5. **Validation & utilities** — `validator/` (R01–R15 rules),
119
+ `instance_rule.rb` (M1→M0 expansion), `guid.rb`, `structured_values.rb`,
120
+ `exporters/` (JSON/YAML/Mermaid).
121
+
122
+ ### Conventions that override any default instinct
123
+
124
+ - Ruby `autoload` declared in the immediate parent namespace's file.
125
+ `lib/cdd/parcel.rb` declares `autoload :WorkbookReader,
126
+ "cdd/parcel/workbook_reader"` etc. No `require_relative` inside
127
+ library code.
128
+ - One concern per class — MECE. Adding a new entity type means adding a new
129
+ model class + reader, not editing a switch statement.
130
+ - Persistence is downstream — the in-memory model is canonical.
131
+ - Do not delete or "clean up" any file in `reference-docs/` or `downloads/` —
132
+ those are source material, not derived artifacts.
133
+
134
+ ### Commands
135
+
136
+ ```bash
137
+ bundle exec rspec # full suite (607 specs)
138
+ bundle exec rspec spec/database_spec.rb # one file
139
+ bundle exec rspec spec/ -e "drop_dictionary" # one example
140
+ bundle exec rake build # build pkg/cdd-*.gem
141
+ bundle exec racc lib/cdd/cddal/cddal.y -o lib/cdd/cddal/generated_parser.rb
142
+ ```
143
+
144
+ ### Audit checklist (Phase 2)
145
+
146
+ Every ParcelMaker feature F01–F30 maps to either ✅ (shipped in the gem),
147
+ ✅+Editor (shipped in Ruby, planned for the OpenCDD Editor web app), or
148
+ ❌ (explicitly out of scope). See `TODO.full-cdd/13-audit-and-docs.md` for
149
+ the full matrix.
150
+
151
+ The OpenCDD Editor (Phase 3) is the next deliverable — a static-site
152
+ TypeScript web app that mirrors the Ruby gem's model and exposes
153
+ ParcelMaker's interactive flows in a browser. Plans are in `TODO.cdd-editor/`.
154
+
155
+ **Phase 3 status (2026-06-24)**:
156
+ - 3.1 project scaffold — shipped (Vite + React + TS + Tailwind + Vitest).
157
+ - 3.2 TS model port — shipped (IRDI, Entity, Klass, Property, Unit,
158
+ ValueList, ValueTerm, Relation, ViewControl, AliasTable, MetaClass,
159
+ ClassType and PropertyDataTypeElement value objects).
160
+ - 3.3 CDDAL port — shipped (Lexer, Parser, AST, Serializer, Builder,
161
+ DatabaseSerializer). Round-trips the OceanRunner fixture.
162
+ - 3.4 Validator port — shipped (Runner + all 13 Ruby rules R01–R14,
163
+ including Database-dependent R02/R08/R10/R14, and the R14
164
+ composition-cycle check via `compositionHierarchyAcyclic`).
165
+ - Database shipped (`editor/src/models/Database.ts`) with idempotent
166
+ `finalize()` (guarded by `finalized` flag), `merge()`, `renameEntity()`,
167
+ `removeEntity()`, `propertiesOf()`, semantic equality. `addEntity`
168
+ keeps code/type indexes in sync when overwriting a duplicate IRDI.
169
+ - 3.5 Parcel sheet schema model — shipped (`canonicalParcelId`,
170
+ `ParcelMetadata`, `SheetSchema`, `Sheet`, `Workbook`). Pure-TS, no
171
+ SheetJS yet — that's Phase 3.5b.
172
+ - 3.5+ Exporters — shipped (`JsonExporter`, `YamlExporter`,
173
+ `MermaidExporter`, `CsvWriter`). Browser-friendly ports of the Ruby
174
+ exporters; hand-rolled minimal YAML emitter (no js-yaml runtime dep).
175
+ - 3.6 Zustand stores shipped — `databaseStore`, `historyStore`,
176
+ `selectionStore`, `validationStore`, `uiStore`, `fileStore`. Mutation
177
+ command pattern; single-owner rule (only `databaseStore` mutates
178
+ Database). localStorage-backed `uiStore`.
179
+ - 3.8a tree walker ports — shipped: `ClassTree`, `RelationTree`,
180
+ `CompositionTree`, `EffectiveProperties`. All with specs.
181
+ - 3.8b utility ports — shipped: `Guid`, `InstanceRule`,
182
+ `StructuredValues` (7 parser/serializer pairs). All with specs.
183
+ - Audit fixes from the deep architecture review:
184
+ - F1–F6: Builder DRY, AliasTable collision warn, Entity.type cache,
185
+ finalize idempotency, shared `codePropertyIdFor`, addEntity index splice.
186
+ - F11–F15: shared `referenceKinds` module (memoized
187
+ REFERENCE_VALUE_KINDS), typed-collection return types on Database,
188
+ SSOT literals (no raw `"MDC_P###"` / `"MDC_C###"` outside the
189
+ registry), `asEntityIrdi`/`asPropertyIrdi` helpers.
190
+ - F29–F35: `parseStringList` delimiter unwrap, shared
191
+ `entityConstructors` map, `ClassType` value object.
192
+ - F36–F40: exporter parity fixes — `JsonExporter.compact()` no
193
+ longer strips empty arrays (Ruby `Hash#compact` parity);
194
+ PropertyNode emits `condition` and `data_element_type`; new
195
+ `PropertyDataTypeElement` value object; `Property.condition` /
196
+ `Property.dataElementType` accessors. See
197
+ `TODO.cdd-editor/26-exporter-parity-fixes.md`.
198
+ - F43–F48 (round-5): validator message parity via shared
199
+ `rubyInspect` helper (Ruby `.inspect` semantics — `"nil"`,
200
+ `'"foo"'`); memoized `Property.condition` /
201
+ `Property.dataElementType`; shared `byEntityCode` sort helper
202
+ deduplicating Json.ts and Mermaid.ts; new `ValueFormat` value
203
+ object + 7th StructuredValues parser/serializer pair; new
204
+ StructuredValues round-trip invariant suite. See
205
+ `TODO.cdd-editor/27-validator-parity-round5.md`.
206
+ - Phase 3.14 static browser site shipped (3.14a–c) + Tier 1 audit
207
+ fixes (B1,B2,B3,B5,B6,B7,B8,B11,B12,B15) + Tier 2 audit fixes
208
+ (B19–B24) + Tier 3 audit fixes
209
+ (B27,B28,B30,B31,B33,B37–B39,B42) + Tier 4 audit fixes
210
+ (B47–B55: URL `?tab=` sync, document title, ErrorBoundary,
211
+ NotFoundPage, dropped `view_control` route segment, component
212
+ tests, `aria-current`, two-pane persistent-tree layout, modern
213
+ design-system overhaul) + Tier 5 audit fixes
214
+ (B56–B65: `/` keyboard shortcut to focus search via
215
+ `useSlashToFocus`, tree-node tooltip via `title=`, skip-to-content
216
+ link via `SkipToContent` (WCAG 2.4.1), `prefers-reduced-motion`
217
+ audit, one-click copy IRDI via `CopyButton` + `IrdiPill`
218
+ (`aria-live="polite"` feedback), section deep-linking via
219
+ `sectionSlug` + `SectionTitle anchor` + `useScrollToHash`,
220
+ `EntityHero` redesign with gradient backdrop + IRDI pill +
221
+ version/revision/dates pills + definition subtitle, SearchBar
222
+ clear (×) button with re-focus, PropertyTable sticky header,
223
+ search match highlighting via `highlightMatches` wrapping matches
224
+ in `<mark>`) + Tier 6 audit fixes
225
+ (B66–B78: per-entity-type `EntityIcon` glyphs, WCAG tree keyboard
226
+ navigation via `useTreeKeyboardNav` with roving tabindex, sticky
227
+ `TableOfContents` with `IntersectionObserver` scroll-spy,
228
+ `DictionaryBundle.propertiesForUnit` reverse index + unit-detail
229
+ "Used by" section, expand-all/collapse-all tree bulk controls,
230
+ `/d/:dict/about` metadata page, about-page heading hierarchy fix
231
+ (h1→h2 to avoid duplicate h1), about-page counts derived from
232
+ `bundle.byType` runtime ground truth instead of `registry.counts`
233
+ build-time snapshot) + Tier 7 audit fixes
234
+ (B79–B89: **entity-type metadata SSOT**
235
+ (`data/entityTypeMeta.ts` — one record per type drives labels,
236
+ route segments, badge tones, and tab membership; replaces five
237
+ parallel declarations across `routes.ts`, `entityTabs.ts`,
238
+ `primitives.tsx`, `EntityCountChips.tsx`, and
239
+ `DictionaryAboutPage.tsx`), **bundle-derived count chips**
240
+ (`EntityCountChips` accepts an optional `bundle` prop and reads
241
+ `bundle.byType` when present — completes B78's SSOT fix for the
242
+ layout header), **value-list term lookup bug fix** (B89:
243
+ `ValueListBody` was round-tripping through `codeFromIrdi` +
244
+ `byCode`; now uses `entities.get(irdi)` directly like every other
245
+ IRDI reference), **shared `EntityLinkList` component** (DRYs the
246
+ repeated link-list pattern across six call sites in detail pages),
247
+ **`EntityLink` bundle self-resolution** (optional `bundle` prop
248
+ auto-resolves `type` and `fallbackLabel` from the bundle)) + Tier 8
249
+ audit fixes
250
+ (B91–B94: **property ↔ value-list cross-linkage** (B92: Ruby
251
+ exporter emits `value_list` IRDI on `PropertyNode`; fixed
252
+ `Database#value_list_of` to resolve via
253
+ `parsed_data_type.value_list_identifier` — was broken for
254
+ CDDAL-sourced data; browser renders "Value list" link on property
255
+ detail + "Used by (N)" reverse section on value-list detail via
256
+ new `propertiesForValueList` index), **declared-property reverse
257
+ index** (B93: `propertiesByClassIrdi` pre-built index replaces
258
+ O(N) scan per class page), **bundle plumbing to
259
+ RelationBody/ValueTermBody** (B94: round-7's `EntityLink`
260
+ self-resolution now fires on relation domain lists and value-term
261
+ → value-list links), **dead shim file cleanup** (B91: deleted
262
+ three untracked files from earlier rounds — `routes.ts`,
263
+ `entityTabs.ts`, `DictionaryPage.tsx`)) + Tier 9 audit fixes
264
+ (B98: global cross-type search via `DictionaryBundle.search`
265
+ linear scan + `useGlobalSearch` debounced hook (120ms) +
266
+ `GlobalSearch` WAI-ARIA header combobox (ArrowUp/Down, Enter,
267
+ Escape, `aria-activedescendant`); dictionary-scoped via new
268
+ `ActiveDictionaryContext` consumed by the Header;
269
+ B99: pretty data-type chip via `parseDataType` discriminated
270
+ union (`simple` / `measure` / `enum` / `class_reference` /
271
+ `unknown`) + `DataTypeChip` component that hyperlinks the
272
+ value-list name inside `ENUM_*_TYPE(...)` and the class code
273
+ inside `CLASS_REFERENCE(...)` through three-step resolution
274
+ (preferredIrdi → parsed identifier → byCode fallback, with type
275
+ filter to prevent wrong-typed links);
276
+ B100: `EntityHero` accepts optional `sourceDocument`, renders as
277
+ accent-tone Badge, auto-wraps URLs in `<a target="_blank"
278
+ rel="noreferrer noopener">`;
279
+ B101: `relationsForCodomainClass` reverse index + "Referenced
280
+ as codomain" section on ClassDetailPage (symmetric to the
281
+ existing Relations section, reusing a shared `RelationRows`
282
+ helper);
283
+ B107: `UsedByField` shared component DRYs the `Used by (N)`
284
+ Field + EntityLinkList pattern across PropertyBody /
285
+ ValueListBody / UnitBody;
286
+ B102: `useDictionary` exposes a stable `retry` callback backed
287
+ by an `attempt` counter that re-runs the effect; PageShell
288
+ renders a "Try again" button on the error card when `onRetry`
289
+ is provided) + Tier 10 audit fixes
290
+ (B108: dedicated `ValueTermTable` component — 3-column
291
+ scannable table (enumeration_code | preferred_name |
292
+ definition) matching `PropertyTable`'s visual treatment;
293
+ value-list detail lifts terms out of the Details Card into
294
+ their own deep-linkable `#terms` section via the new
295
+ `renderAfter` slot on `EntityDetailPage` (DRY-safe:
296
+ `EntityDetailBody` now accepts an optional `afterCard`
297
+ ReactNode);
298
+ B109: `GlobalSearch` active-row auto-scroll — `useEffect`
299
+ watches `activeIndex` and calls
300
+ `scrollIntoView({ block: "nearest" })` via the existing
301
+ deterministic `resultId(listboxId, i)` row id; mirrors the
302
+ pattern already used in `ClassTree.tsx` for tree focus;
303
+ B110: `tests/GlobalSearch.test.tsx` — WAI-ARIA combobox
304
+ behavior suite (disabled state, results render, no-matches
305
+ status, ArrowDown advances active row, Enter navigates,
306
+ Escape closes, clear button resets + re-focuses); wraps the
307
+ component in `ActiveDictionaryContext.Provider` +
308
+ `MemoryRouter` so the suite owns both dependencies;
309
+ B111: `tests/helpers/factories.ts` — builder-pattern test
310
+ factories (`makeClass`, `makeProperty`, `makeValueList`,
311
+ `makeValueTerm`, `makeUnit`, `makeRelation`, `resetFactories`,
312
+ `makeBundle`, `makeBundleSlice`); each builder accepts
313
+ `Partial<T>` overrides applied over the type-required minimum;
314
+ convention: IRDIs use `"test#CODE"` so `codeFromIrdi` extracts
315
+ the expected short code; `makeBundle` constructs a real
316
+ `DictionaryBundle` (full reverse-index surface);
317
+ `makeBundleSlice` returns a shallow `Pick<DictionaryBundle,
318
+ "entities" | "byCode">` for tests that only need lookup) +
319
+ Tier 11 audit fixes
320
+ (B112: **Cmd+K / Ctrl+K shortcut to focus global search** —
321
+ the industry convention every modern web app ships; new
322
+ generic `useKeyboardShortcut(matcher, handler, { enabled,
323
+ ignoreEditable })` hook centralizes editable-element detection
324
+ (INPUT/TEXTAREA/SELECT/contentEditable) and `defaultPrevented`
325
+ guard; `useSlashToFocus` rewritten as a thin wrapper over the
326
+ new hook; new `usePlatform` SSOT module exporting
327
+ `isApplePlatform()` + `platformModifierLabel()` (returns `⌘`
328
+ on Apple, `Ctrl` elsewhere) — used by `Header` to render both
329
+ `⌘K` primary hint and `/` secondary hint with platform-aware
330
+ label;
331
+ B113: **print stylesheet** layered via Tailwind `print:`
332
+ variants on chrome (header/footer/sidebar get `print:hidden`;
333
+ `Card` gets `print:break-inside-avoid`; copy buttons and
334
+ `SectionTitle`'s anchor `#` link get `print:hidden`;
335
+ `EntityHero` gradient flattens via `print:bg-none
336
+ print:shadow-none`; `PageShell`'s main gets
337
+ `print:max-w-none print:px-0 print:py-0`); small `@media print`
338
+ block in `styles/index.css` for the three rules Tailwind
339
+ variants can't express — visible link URLs via `a[href]:after
340
+ { content: " (" attr(href) ")" }` (skipped for internal
341
+ anchors), `h1/h2/h3 { break-after: avoid }`, color-scheme
342
+ override;
343
+ B114: **tree property count badges** — subtle count badge at
344
+ the right edge of each tree row when the class has at least
345
+ one declared property; new
346
+ `DictionaryBundle.declaredPropertyCount(classIrdi): number`
347
+ reads the existing `declaredPropertiesByClassIrdi` index
348
+ without materializing the array; `ClassTree` accepts an
349
+ optional `propertyCounts?: (irdi: string) => number` prop,
350
+ renders nothing when count is 0 or prop is absent (no layout
351
+ shift); `DictionaryLayout` threads a stable
352
+ `propertyCounts={(irdi) => bundle.declaredPropertyCount(irdi)}`
353
+ callback;
354
+ B115: **shared `useScrollActiveIntoView` hook** DRYs the
355
+ `scrollIntoView({ block: "nearest" })` pattern shared by
356
+ ClassTree and GlobalSearch; `GlobalSearch` migrates to the
357
+ hook (replacing the inline round-10 scroll effect);
358
+ `ClassTree` intentionally keeps its inline focus-and-scroll
359
+ effect because focus and scroll are two distinct concerns
360
+ there — migrating would force the scroll-only hook to take a
361
+ second side-effect, breaking single-responsibility;
362
+ B116: **spec coverage expansion** — `useKeyboardShortcut.test.tsx`
363
+ (5 specs: matcher fires, not when target is editable, not
364
+ when `enabled` is false, fires from editable when
365
+ `ignoreEditable` is false, supports modifier chords),
366
+ `useScrollActiveIntoView.test.tsx` (4 specs: scrolls on
367
+ activeId change, noop when disabled, noop when resolver
368
+ returns null, noop when activeId is null/undefined),
369
+ `ClassTree.test.tsx` extended with 3 specs for property count
370
+ badges: renders badge when count > 0, omits when 0, omits
371
+ when `propertyCounts` prop absent) —
372
+ read-only static-hosted CDD browser in
373
+ `browser/`, rebuilt with a 2026-grade modern UX rather than a
374
+ cdd.iec.ch look-alike. Vite + React + TS + Tailwind; shares the
375
+ editor's model layer via a path alias (`@opencdd/models`).
376
+ **Architecture**: `DictionaryLayout` owns the dictionary fetch and
377
+ renders a persistent two-pane shell (sticky `ClassTree` sidebar +
378
+ `<section aria-label="Dictionary content">` with an `<Outlet />`).
379
+ Detail pages consume the bundle via `useDictionaryContext()`
380
+ (wraps `useOutletContext`) so the tree stays mounted across
381
+ detail navigation. URL is the source of truth for both tab
382
+ (`?tab=`) and highlighted class (derived from
383
+ `useMatch("/d/:dict/c/:code")`).
384
+ **Design system**: custom Tailwind tokens (`ink-*` warm dark,
385
+ `sand-*` warm light, `accent-*` indigo), `Card` / `SectionTitle`
386
+ / `Badge` / `EntityBadge` / `Skeleton` / `EmptyState` /
387
+ `CopyButton` / `IrdiPill` / `EntityHero` / `SkipToContent`
388
+ primitives in `components/ui/primitives.tsx`. Foundational text
389
+ helpers (`sectionSlug`, `highlightMatches`) in
390
+ `components/text.tsx`. Skeleton loaders, ErrorBoundary
391
+ with `getDerivedStateFromError`, real 404 surface, fade-in /
392
+ slide-up animations, sticky sidebar, mobile tree toggle,
393
+ `prefers-reduced-motion` override.
394
+ Data pipeline is `rake browser:sample` /
395
+ `rake browser:build[<dict>]` → static JSON under
396
+ `browser/public/data/`. `DictionaryBundle` owns derived indexes
397
+ (`subclassesOf`, `classesDeclaringProperty`, `instancesOf`,
398
+ `relationsForClass`, `relationsForCodomainClass`,
399
+ `propertiesByClassIrdi`, `propertiesForValueList`,
400
+ `propertiesForUnit`) — all built by a single generic
401
+ `buildReverseIndex` helper — plus an in-memory `search(query)`
402
+ method for the header global-search combobox, and cycle-safe
403
+ walkers (`ancestorChainOf`, `effectivePropertiesOf` — ports
404
+ Ruby's `Cdd::EffectiveProperties`). `PageShell` unifies
405
+ loading/error/not-found scaffolding across all pages including
406
+ the dictionary index. `MetadataFields` renders the shared
407
+ cdd.iec.ch metadata surface (synonyms, note, remark, description,
408
+ example, source_document, guid, version, revision, time_stamp,
409
+ dates). Class detail page surfaces ancestor-chain breadcrumb
410
+ (Root › Parent › Self), powertype Instances (reverse of
411
+ `is_case_of`), Composition (`sub_class_selection`), Relations
412
+ (where the class is in `domain`), source attribution on Inherited
413
+ properties, and a `?expand=<code>` back-link that auto-expands
414
+ every ancestor of the target class in the tree. `EntityCountChips`
415
+ renders all 7 entity-type counts on the dictionary index.
416
+ `codeFromIrdi` in `browser/src/data/irdi.ts` is the SSOT for
417
+ short-code extraction. Ruby `Cdd::Exporters::Json` extended with
418
+ the shared `entity_payload` helper. 301 browser tests passing. See
419
+ `TODO.cdd-editor/28-static-browser-site.md`,
420
+ `TODO.cdd-editor/29-browser-site-audit-findings.md`,
421
+ `TODO.cdd-editor/30-browser-audit-round-2.md`,
422
+ `TODO.cdd-editor/31-browser-audit-round-3.md`,
423
+ `TODO.cdd-editor/32-browser-audit-round-4.md`,
424
+ `TODO.cdd-editor/33-browser-audit-round-5.md`,
425
+ `TODO.cdd-editor/34-browser-audit-round-6.md`, and
426
+ `TODO.cdd-editor/35-browser-audit-round-7.md`, and
427
+ `TODO.cdd-editor/36-browser-audit-round-8.md`.
428
+ - Codegen: `rake generate_ts` regenerates `PropertyIds.generated.ts`
429
+ and `MetaClasses.generated.ts` from the Ruby REGISTRY. These files
430
+ are checked in.
431
+ - 607 Ruby specs + 578 editor tests + 301 browser tests passing
432
+ (1486 total). Phase 1 import pipeline shipped: `Cdd::Parcel.aggregate`
433
+ (merge multiple parcel paths), `Cdd::Parcel.split` (partition by
434
+ `:entity_type`, `:root_class`, `:each_class`, or Proc),
435
+ `Cdd::Parcel::Selector` (entity selection with class-hierarchy
436
+ closure), `Cdd::Database#apply_change_request` (upsert + removals).
437
+ Back-compat aliases removed — `Cdd::Excel` namespace eliminated,
438
+ readers renamed to MECE names (`WorkbookReader`, `FlatDirReader`,
439
+ `ShardedDirReader`). CDDAL serializer fixed for non-ASCII UTF-8
440
+ values, control-character escaping, and backslash quoting.
441
+ Phases 3.5b, 3.7, 3.8 UI, 3.9, 3.10, 3.11, 3.12,
442
+ 3.13, 3.14d–f are pending — see
443
+ `TODO.cdd-editor/20-remaining-work-master.md` for the prioritized
444
+ roadmap and audit register.
445
+ - **2026-06-25 demo data**: `rake browser:build[<dict>]` builds any
446
+ scraped dictionary into the static browser. Currently populated:
447
+ oceanrunner (40) + iec63213 (26) + iec61360-7 (52) + iec63508 (151)
448
+ + iec61360 (574) + iec62683 (1855) + iec61987 (11831) =
449
+ **14,529 entities** across 7 dictionaries. Run `cd browser && npm run dev`
450
+ to serve at `http://localhost:5173`.
451
+ - **Phase 2 (lutaml-model migration) NOT started**: tracked in
452
+ `TODO.full-cdd/16-lutaml-model-migration.md`. This is the major
453
+ remaining architectural work — migrate entities to `Lutaml::Model`
454
+ classes, add per-item YAML/JSON file layout, replace hand-rolled
455
+ CDDAL serializer. Blocked on user decision re: file-format default
456
+ (YAML vs JSON) and multilingual field shape (nested vs flat).
457
+ - **Audit findings 2026-06-25**: see
458
+ `TODO.full-cdd/17-architecture-audit-2026-06-25.md`. Key items:
459
+ A1 (Condition grammar rejects class-reference sets — interim
460
+ workaround in `Property#condition`), A2 (dead `format_set` in
461
+ CDDAL serializer), A3 (Parcel writer normalizes "001"→"1"),
462
+ A4 (entity.rb vs REGISTRY property ID discrepancies — needs
463
+ user decision).
464
+
465
+ ## Working with the Python scrapers
466
+
467
+ The scraper ecosystem lives in `harvest/` (see
468
+ [`harvest/README.md`](harvest/README.md) for the full guide). Quick reference:
469
+
470
+ - `harvest/download.py` is the main harvester. `--dictionary <key>` selects
471
+ from `DICTIONARIES`. Idempotent: existing files on disk are skipped,
472
+ progress is in `downloads/<dict>/_state.json`.
473
+ - `harvest/discover.py` and `harvest/probe_*.py` are one-off investigation
474
+ scripts; keep them but don't extend them.
475
+ - `harvest/tree_form_*.html`, `harvest/discovery.json`, `harvest/pages/*.html`,
476
+ and the per-class `downloads/<dict>/<CLASS>/_page.html` files are diagnostic
477
+ captures — useful for figuring out token shapes without re-hitting the server.
478
+ - `harvest/verify/iec-*.txt` are user-authored authoritative class lists used
479
+ to verify scrape coverage.
480
+
481
+ ## Things that look unused but are not
482
+
483
+ `harvest/tree_form_*.html`, `harvest/discovery.json`, `harvest/pages/*.html`,
484
+ and the per-class `_page.html` files are diagnostic source material for
485
+ reverse-engineering the Domino page shapes. They are not generated artifacts.
486
+ Do not delete them.