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.
- checksums.yaml +7 -0
- data/CLAUDE.md +486 -0
- data/README.md +304 -0
- data/bin/lint-no-raw-mdc +41 -0
- data/lib/cdd.rb +11 -0
- data/lib/opencdd/alias_table.rb +52 -0
- data/lib/opencdd/cddal/ast.rb +151 -0
- data/lib/opencdd/cddal/builder.rb +374 -0
- data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
- data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
- data/lib/opencdd/cddal/fetcher.rb +18 -0
- data/lib/opencdd/cddal/generated_parser.rb +805 -0
- data/lib/opencdd/cddal/lexer.rb +193 -0
- data/lib/opencdd/cddal/parser.rb +19 -0
- data/lib/opencdd/cddal/resolver.rb +100 -0
- data/lib/opencdd/cddal/serializer.rb +210 -0
- data/lib/opencdd/cddal/value_serializer.rb +60 -0
- data/lib/opencdd/cddal.rb +63 -0
- data/lib/opencdd/class_tree.rb +80 -0
- data/lib/opencdd/class_type.rb +33 -0
- data/lib/opencdd/codegen/ts.rb +185 -0
- data/lib/opencdd/codegen.rb +7 -0
- data/lib/opencdd/composition_tree.rb +119 -0
- data/lib/opencdd/condition.rb +120 -0
- data/lib/opencdd/data_type.rb +143 -0
- data/lib/opencdd/database.rb +719 -0
- data/lib/opencdd/effective_properties.rb +119 -0
- data/lib/opencdd/entity/field_reader.rb +141 -0
- data/lib/opencdd/entity/field_registry.rb +99 -0
- data/lib/opencdd/entity/version_history.rb +62 -0
- data/lib/opencdd/entity.rb +255 -0
- data/lib/opencdd/exporters/json.rb +235 -0
- data/lib/opencdd/exporters/mermaid.rb +62 -0
- data/lib/opencdd/exporters/yaml.rb +16 -0
- data/lib/opencdd/exporters.rb +9 -0
- data/lib/opencdd/guid.rb +27 -0
- data/lib/opencdd/instance_rule.rb +70 -0
- data/lib/opencdd/irdi.rb +128 -0
- data/lib/opencdd/klass.rb +230 -0
- data/lib/opencdd/languages.rb +70 -0
- data/lib/opencdd/meta_class.rb +274 -0
- data/lib/opencdd/model/entity_store.rb +85 -0
- data/lib/opencdd/model/yaml_database.rb +49 -0
- data/lib/opencdd/model/yaml_entity.rb +196 -0
- data/lib/opencdd/model.rb +13 -0
- data/lib/opencdd/parcel/csv_reader.rb +35 -0
- data/lib/opencdd/parcel/csv_writer.rb +114 -0
- data/lib/opencdd/parcel/entity_manifest.rb +119 -0
- data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
- data/lib/opencdd/parcel/layout_detector.rb +115 -0
- data/lib/opencdd/parcel/metadata.rb +112 -0
- data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
- data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
- data/lib/opencdd/parcel/selector.rb +115 -0
- data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
- data/lib/opencdd/parcel/sheet.rb +287 -0
- data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
- data/lib/opencdd/parcel/sheet_schema.rb +253 -0
- data/lib/opencdd/parcel/workbook.rb +172 -0
- data/lib/opencdd/parcel/workbook_reader.rb +200 -0
- data/lib/opencdd/parcel/writer.rb +185 -0
- data/lib/opencdd/parcel.rb +175 -0
- data/lib/opencdd/parse_helpers.rb +73 -0
- data/lib/opencdd/property.rb +120 -0
- data/lib/opencdd/property_data_element_type.rb +44 -0
- data/lib/opencdd/property_ids.rb +202 -0
- data/lib/opencdd/reader.rb +103 -0
- data/lib/opencdd/relation.rb +88 -0
- data/lib/opencdd/relation_tree.rb +74 -0
- data/lib/opencdd/relation_type.rb +58 -0
- data/lib/opencdd/structured_values.rb +183 -0
- data/lib/opencdd/unit.rb +16 -0
- data/lib/opencdd/validator/class_reference_rule.rb +77 -0
- data/lib/opencdd/validator/condition_rule.rb +27 -0
- data/lib/opencdd/validator/data_type_rule.rb +26 -0
- data/lib/opencdd/validator/enum_rule.rb +27 -0
- data/lib/opencdd/validator/format_rule.rb +54 -0
- data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
- data/lib/opencdd/validator/irdi_rule.rb +57 -0
- data/lib/opencdd/validator/mandatory_rule.rb +25 -0
- data/lib/opencdd/validator/pattern_rule.rb +26 -0
- data/lib/opencdd/validator/reference_rule.rb +36 -0
- data/lib/opencdd/validator/rule.rb +65 -0
- data/lib/opencdd/validator/runner.rb +101 -0
- data/lib/opencdd/validator/set_rule.rb +41 -0
- data/lib/opencdd/validator/synonym_rule.rb +30 -0
- data/lib/opencdd/validator/type_rule.rb +102 -0
- data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
- data/lib/opencdd/validator.rb +68 -0
- data/lib/opencdd/value_format.rb +71 -0
- data/lib/opencdd/value_list.rb +42 -0
- data/lib/opencdd/value_term.rb +24 -0
- data/lib/opencdd/version.rb +5 -0
- data/lib/opencdd/view_control.rb +10 -0
- data/lib/opencdd/visitor.rb +93 -0
- data/lib/opencdd.rb +57 -0
- 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.
|