@bradtech/ontologies 1.1.0

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 (69) hide show
  1. package/LICENSE +620 -0
  2. package/README.md +66 -0
  3. package/data/crop-covers/bare-soil.md +23 -0
  4. package/data/crop-covers/index.md +21 -0
  5. package/data/crop-covers/permanent-sod.md +23 -0
  6. package/data/crop-covers/sown-legume.md +23 -0
  7. package/data/crop-covers/spontaneous-grass.md +23 -0
  8. package/data/crops/index.md +23 -0
  9. package/data/crops/lavandula-angustifolia.md +30 -0
  10. package/data/crops/malus-domestica.md +30 -0
  11. package/data/crops/prunus-persica.md +30 -0
  12. package/data/crops/triticum-aestivum.md +30 -0
  13. package/data/crops/vitis-vinifera.md +30 -0
  14. package/data/crops/zea-mays.md +30 -0
  15. package/data/index.md +22 -0
  16. package/data/irrigations/drip.md +24 -0
  17. package/data/irrigations/gravity.md +24 -0
  18. package/data/irrigations/index.md +21 -0
  19. package/data/irrigations/none.md +24 -0
  20. package/data/irrigations/sprinkler.md +24 -0
  21. package/data/soils/clay-loam.md +29 -0
  22. package/data/soils/clay.md +29 -0
  23. package/data/soils/index.md +26 -0
  24. package/data/soils/loam.md +29 -0
  25. package/data/soils/sand.md +29 -0
  26. package/data/soils/sandy-clay-loam.md +29 -0
  27. package/data/soils/sandy-clay.md +29 -0
  28. package/data/soils/sandy-loam.md +29 -0
  29. package/data/soils/silt-loam.md +29 -0
  30. package/data/soils/silt.md +29 -0
  31. package/data/soils/silty-clay-loam.md +29 -0
  32. package/dist/build.d.ts +5 -0
  33. package/dist/build.d.ts.map +1 -0
  34. package/dist/build.js +61 -0
  35. package/dist/build.js.map +1 -0
  36. package/dist/catalog.json +1623 -0
  37. package/dist/embeddedData.d.ts +6 -0
  38. package/dist/embeddedData.d.ts.map +1 -0
  39. package/dist/embeddedData.js +906 -0
  40. package/dist/embeddedData.js.map +1 -0
  41. package/dist/extractor.d.ts +31 -0
  42. package/dist/extractor.d.ts.map +1 -0
  43. package/dist/extractor.js +161 -0
  44. package/dist/extractor.js.map +1 -0
  45. package/dist/index.d.ts +24 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +44 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/loader.d.ts +25 -0
  50. package/dist/loader.d.ts.map +1 -0
  51. package/dist/loader.js +104 -0
  52. package/dist/loader.js.map +1 -0
  53. package/dist/opendata.jsonld +268 -0
  54. package/dist/parser.d.ts +14 -0
  55. package/dist/parser.d.ts.map +1 -0
  56. package/dist/parser.js +124 -0
  57. package/dist/parser.js.map +1 -0
  58. package/dist/types.d.ts +89 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +5 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +42 -0
  63. package/src/build.ts +71 -0
  64. package/src/embeddedData.ts +910 -0
  65. package/src/extractor.ts +207 -0
  66. package/src/index.ts +98 -0
  67. package/src/loader.ts +126 -0
  68. package/src/parser.ts +137 -0
  69. package/src/types.ts +110 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Extractor & Open Data Publisher for @bradtech/ontologies.
3
+ */
4
+
5
+ import type { OntologyCatalogIndex } from './loader'
6
+ import type {
7
+ AnyOntologyItem,
8
+ FlatSelectOption,
9
+ HierarchyNode,
10
+ OntologyDomain,
11
+ OntologyLookupResult,
12
+ SupportedLanguage,
13
+ } from './types'
14
+
15
+ /**
16
+ * Returns localized label for an ontology item given a target language with fallback.
17
+ */
18
+ export function getLocalizedLabel(
19
+ item: AnyOntologyItem,
20
+ language: SupportedLanguage = 'fr',
21
+ ): string {
22
+ if (item.translations && item.translations[language]) {
23
+ return item.translations[language]!
24
+ }
25
+ if (item.translations && item.translations.fr) {
26
+ return item.translations.fr!
27
+ }
28
+ if (item.translations && item.translations.en) {
29
+ return item.translations.en!
30
+ }
31
+ return item.title
32
+ }
33
+
34
+ /**
35
+ * Extracts a flat list of options for a domain, localized to the specified language.
36
+ * Ideal for rendering dropdown select components (UI/API).
37
+ */
38
+ export function extractFlatList(
39
+ catalog: OntologyCatalogIndex,
40
+ domain: OntologyDomain,
41
+ language: SupportedLanguage = 'fr',
42
+ ): FlatSelectOption[] {
43
+ const items = catalog.byDomain.get(domain) || []
44
+ return items.map((item) => ({
45
+ id: item.id,
46
+ label: getLocalizedLabel(item, language),
47
+ description: item.description,
48
+ standards: item.standards,
49
+ }))
50
+ }
51
+
52
+ /**
53
+ * Extracts a key-value dictionary { id: label } for direct consumption by UI form definitions.
54
+ */
55
+ export function extractFlatMap(
56
+ catalog: OntologyCatalogIndex,
57
+ domain: OntologyDomain,
58
+ language: SupportedLanguage = 'fr',
59
+ ): Record<string, string> {
60
+ const list = extractFlatList(catalog, domain, language)
61
+ const map: Record<string, string> = {}
62
+ for (const opt of list) {
63
+ map[opt.id] = opt.label
64
+ }
65
+ return map
66
+ }
67
+
68
+ /**
69
+ * Extracts a hierarchical taxonomy tree for cascading selectors.
70
+ */
71
+ export function extractHierarchy(
72
+ catalog: OntologyCatalogIndex,
73
+ domain: OntologyDomain,
74
+ _language: SupportedLanguage = 'fr',
75
+ ): HierarchyNode[] {
76
+ const items = catalog.byDomain.get(domain) || []
77
+
78
+ // If items have category or hierarchy grouping (e.g. crops by category)
79
+ const categoryGroups = new Map<string, AnyOntologyItem[]>()
80
+
81
+ for (const item of items) {
82
+ const category = (item as any).category || 'general'
83
+ if (!categoryGroups.has(category)) {
84
+ categoryGroups.set(category, [])
85
+ }
86
+ categoryGroups.get(category)!.push(item)
87
+ }
88
+
89
+ const roots: HierarchyNode[] = []
90
+
91
+ for (const [category, childrenItems] of categoryGroups.entries()) {
92
+ const childrenNodes: HierarchyNode[] = childrenItems.map((child) => ({
93
+ item: child,
94
+ children: [],
95
+ }))
96
+
97
+ roots.push({
98
+ item: {
99
+ type: 'category',
100
+ id: category,
101
+ title: category.toUpperCase(),
102
+ description: `Category: ${category}`,
103
+ tags: [domain, category],
104
+ translations: { fr: category, en: category },
105
+ },
106
+ children: childrenNodes,
107
+ })
108
+ }
109
+
110
+ return roots
111
+ }
112
+
113
+ /**
114
+ * Universal lookup that resolves any input (ID, EPPO code, TelePAC code, AGROVOC URI, or title).
115
+ */
116
+ export function lookup(
117
+ catalog: OntologyCatalogIndex,
118
+ query: string,
119
+ ): OntologyLookupResult | null {
120
+ if (!query || typeof query !== 'string') return null
121
+ const trimmed = query.trim()
122
+ const upper = trimmed.toUpperCase()
123
+
124
+ // 1. Exact ID match
125
+ if (catalog.byId.has(trimmed)) {
126
+ const item = catalog.byId.get(trimmed)!
127
+ return { item, domain: resolveDomainOfItem(item), matchedBy: 'id' }
128
+ }
129
+
130
+ // 2. EPPO code match
131
+ if (catalog.byEppo.has(upper)) {
132
+ const item = catalog.byEppo.get(upper)!
133
+ return { item, domain: 'crops', matchedBy: 'eppo' }
134
+ }
135
+
136
+ // 3. TelePAC RPG code match
137
+ if (catalog.byTelepac.has(upper)) {
138
+ const item = catalog.byTelepac.get(upper)!
139
+ return { item, domain: 'crops', matchedBy: 'telepac' }
140
+ }
141
+
142
+ // 4. AGROVOC URI/Code match
143
+ if (catalog.byAgrovoc.has(trimmed)) {
144
+ const item = catalog.byAgrovoc.get(trimmed)!
145
+ return { item, domain: resolveDomainOfItem(item), matchedBy: 'agrovoc' }
146
+ }
147
+
148
+ // 5. Case-insensitive title / alias scan
149
+ for (const item of catalog.items) {
150
+ if (item.title.toLowerCase() === trimmed.toLowerCase()) {
151
+ return { item, domain: resolveDomainOfItem(item), matchedBy: 'alias' }
152
+ }
153
+ for (const trans of Object.values(item.translations || {})) {
154
+ if (trans && trans.toLowerCase() === trimmed.toLowerCase()) {
155
+ return { item, domain: resolveDomainOfItem(item), matchedBy: 'alias' }
156
+ }
157
+ }
158
+ }
159
+
160
+ return null
161
+ }
162
+
163
+ function resolveDomainOfItem(item: AnyOntologyItem): OntologyDomain {
164
+ if (item.type === 'soil') return 'soils'
165
+ if (item.type === 'irrigation') return 'irrigations'
166
+ if (item.type === 'crop') return 'crops'
167
+ return 'crop-covers'
168
+ }
169
+
170
+ /**
171
+ * Converts ontology items into an Open Data Schema.org / DCAT-AP compliant JSON-LD graph.
172
+ */
173
+ export function toOpenDataJsonLd(
174
+ catalog: OntologyCatalogIndex,
175
+ domain?: OntologyDomain,
176
+ ): Record<string, any> {
177
+ const targetItems = domain ? catalog.byDomain.get(domain) || [] : catalog.items
178
+
179
+ return {
180
+ '@context': {
181
+ '@vocab': 'https://schema.org/',
182
+ 'okf': 'https://openknowledgeformat.org/ns/',
183
+ 'eppo': 'https://gd.eppo.int/taxon/',
184
+ 'agrovoc': 'http://aims.fao.org/aos/agrovoc/',
185
+ 'wrb': 'https://www.isric.org/explore/wrb/',
186
+ },
187
+ '@type': 'DataCatalog',
188
+ 'name': 'OSFARM & Brad Agricultural Taxonomies',
189
+ 'url': 'https://lexicon.osfarm.org',
190
+ 'publisher': {
191
+ '@type': 'Organization',
192
+ 'name': 'Brad Technology SAS & OSFARM Collective',
193
+ },
194
+ 'itemListElement': targetItems.map((item) => ({
195
+ '@type': item.type === 'crop' ? 'Plant' : 'DefinedTerm',
196
+ '@id': `okf:${item.type}/${item.id}`,
197
+ 'identifier': item.id,
198
+ 'name': item.title,
199
+ 'description': item.description,
200
+ 'keywords': item.tags.join(', '),
201
+ 'sameAs': [
202
+ item.standards?.eppo ? `https://gd.eppo.int/taxon/${item.standards.eppo}` : null,
203
+ item.standards?.agrovoc ? `http://aims.fao.org/aos/agrovoc/${item.standards.agrovoc}` : null,
204
+ ].filter(Boolean),
205
+ })),
206
+ }
207
+ }
package/src/index.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @bradtech/ontologies
3
+ * Open Knowledge Format (OKF v0.1) Agricultural Lexicon & Taxonomies
4
+ */
5
+
6
+ import { EMBEDDED_ONTOLOGY_ITEMS } from './embeddedData'
7
+ import { indexOntologyItems, type OntologyCatalogIndex } from './loader'
8
+ import {
9
+ extractFlatList,
10
+ extractFlatMap,
11
+ extractHierarchy,
12
+ getLocalizedLabel,
13
+ lookup,
14
+ toOpenDataJsonLd,
15
+ } from './extractor'
16
+ import type {
17
+ AnyOntologyItem,
18
+ CropAgronomy,
19
+ CropCoverEffects,
20
+ CropCoverOntologyItem,
21
+ CropOntologyItem,
22
+ FlatSelectOption,
23
+ HierarchyNode,
24
+ IrrigationEngineering,
25
+ IrrigationOntologyItem,
26
+ LanguageMap,
27
+ OntologyDomain,
28
+ OntologyLookupResult,
29
+ OntologyStandards,
30
+ SoilOntologyItem,
31
+ SoilPhysics,
32
+ SupportedLanguage,
33
+ } from './types'
34
+
35
+ export type {
36
+ AnyOntologyItem,
37
+ CropAgronomy,
38
+ CropCoverEffects,
39
+ CropCoverOntologyItem,
40
+ CropOntologyItem,
41
+ FlatSelectOption,
42
+ HierarchyNode,
43
+ IrrigationEngineering,
44
+ IrrigationOntologyItem,
45
+ LanguageMap,
46
+ OntologyDomain,
47
+ OntologyLookupResult,
48
+ OntologyStandards,
49
+ SoilOntologyItem,
50
+ SoilPhysics,
51
+ SupportedLanguage,
52
+ OntologyCatalogIndex,
53
+ }
54
+
55
+ export * from './parser'
56
+ export * from './loader'
57
+ export * from './extractor'
58
+ export { EMBEDDED_ONTOLOGY_ITEMS } from './embeddedData'
59
+
60
+ /**
61
+ * Singleton instance of the built-in Agricultural Ontology Catalog.
62
+ * Lazily loaded on first access using statically embedded concepts (zero fs dependency).
63
+ */
64
+ let _defaultCatalog: OntologyCatalogIndex | null = null
65
+
66
+ export function getDefaultCatalog(): OntologyCatalogIndex {
67
+ if (!_defaultCatalog) {
68
+ _defaultCatalog = indexOntologyItems(EMBEDDED_ONTOLOGY_ITEMS)
69
+ }
70
+ return _defaultCatalog
71
+ }
72
+
73
+ /**
74
+ * Convenience helper methods using the default built-in catalog.
75
+ */
76
+ export const Lexicon = {
77
+ getCatalog: getDefaultCatalog,
78
+
79
+ getFlatList(domain: OntologyDomain, lang: SupportedLanguage = 'fr'): FlatSelectOption[] {
80
+ return extractFlatList(getDefaultCatalog(), domain, lang)
81
+ },
82
+
83
+ getFlatMap(domain: OntologyDomain, lang: SupportedLanguage = 'fr'): Record<string, string> {
84
+ return extractFlatMap(getDefaultCatalog(), domain, lang)
85
+ },
86
+
87
+ getHierarchy(domain: OntologyDomain, lang: SupportedLanguage = 'fr'): HierarchyNode[] {
88
+ return extractHierarchy(getDefaultCatalog(), domain, lang)
89
+ },
90
+
91
+ lookup(query: string): OntologyLookupResult | null {
92
+ return lookup(getDefaultCatalog(), query)
93
+ },
94
+
95
+ toOpenData(domain?: OntologyDomain): Record<string, any> {
96
+ return toOpenDataJsonLd(getDefaultCatalog(), domain)
97
+ },
98
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * OKF Knowledge Graph & Directory Loader for @bradtech/ontologies.
3
+ */
4
+
5
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
6
+ import { join, resolve } from 'node:path'
7
+ import { parseOkfDocument } from './parser'
8
+ import type {
9
+ AnyOntologyItem,
10
+ CropOntologyItem,
11
+ IrrigationOntologyItem,
12
+ OntologyDomain,
13
+ SoilOntologyItem,
14
+ } from './types'
15
+
16
+ export interface OntologyCatalogIndex {
17
+ items: AnyOntologyItem[]
18
+ byId: Map<string, AnyOntologyItem>
19
+ byDomain: Map<OntologyDomain, AnyOntologyItem[]>
20
+ byEppo: Map<string, CropOntologyItem>
21
+ byAgrovoc: Map<string, AnyOntologyItem>
22
+ byTelepac: Map<string, CropOntologyItem>
23
+ }
24
+
25
+ /**
26
+ * Builds an in-memory index from a list of parsed ontology items.
27
+ */
28
+ export function indexOntologyItems(rawItems: AnyOntologyItem[]): OntologyCatalogIndex {
29
+ const items: AnyOntologyItem[] = []
30
+ const byId = new Map<string, AnyOntologyItem>()
31
+ const byDomain = new Map<OntologyDomain, AnyOntologyItem[]>([
32
+ ['soils', []],
33
+ ['irrigations', []],
34
+ ['crops', []],
35
+ ['crop-covers', []],
36
+ ])
37
+ const byEppo = new Map<string, CropOntologyItem>()
38
+ const byAgrovoc = new Map<string, AnyOntologyItem>()
39
+ const byTelepac = new Map<string, CropOntologyItem>()
40
+
41
+ for (const doc of rawItems) {
42
+ if (doc.type === 'catalog' || doc.type === 'domain') {
43
+ byId.set(doc.id, doc)
44
+ continue
45
+ }
46
+
47
+ items.push(doc)
48
+ byId.set(doc.id, doc)
49
+
50
+ if (doc.type === 'soil') {
51
+ byDomain.get('soils')?.push(doc as SoilOntologyItem)
52
+ } else if (doc.type === 'irrigation') {
53
+ byDomain.get('irrigations')?.push(doc as IrrigationOntologyItem)
54
+ } else if (doc.type === 'crop') {
55
+ byDomain.get('crops')?.push(doc as CropOntologyItem)
56
+ } else if (doc.type === 'crop-cover') {
57
+ byDomain.get('crop-covers')?.push(doc)
58
+ }
59
+
60
+ if (doc.standards?.eppo && doc.type === 'crop') {
61
+ byEppo.set(doc.standards.eppo.toUpperCase(), doc as CropOntologyItem)
62
+ }
63
+ if (doc.standards?.agrovoc) {
64
+ byAgrovoc.set(String(doc.standards.agrovoc), doc)
65
+ }
66
+ if (doc.standards?.telepac_rpg && Array.isArray(doc.standards.telepac_rpg)) {
67
+ for (const code of doc.standards.telepac_rpg) {
68
+ byTelepac.set(String(code).toUpperCase(), doc as CropOntologyItem)
69
+ }
70
+ }
71
+ }
72
+
73
+ return {
74
+ items,
75
+ byId,
76
+ byDomain,
77
+ byEppo,
78
+ byAgrovoc,
79
+ byTelepac,
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Recursively scans a directory and returns all .md file paths.
85
+ */
86
+ export function scanMarkdownFiles(dirPath: string): string[] {
87
+ const results: string[] = []
88
+
89
+ function walk(current: string) {
90
+ const entries = readdirSync(current)
91
+ for (const entry of entries) {
92
+ if (entry.startsWith('.')) continue
93
+ const fullPath = join(current, entry)
94
+ const stat = statSync(fullPath)
95
+ if (stat.isDirectory()) {
96
+ walk(fullPath)
97
+ } else if (stat.isFile() && entry.endsWith('.md')) {
98
+ results.push(fullPath)
99
+ }
100
+ }
101
+ }
102
+
103
+ walk(dirPath)
104
+ return results
105
+ }
106
+
107
+ /**
108
+ * Loads and indexes all OKF documents from the specified directory.
109
+ */
110
+ export function loadOntologyCatalog(dataPath: string): OntologyCatalogIndex {
111
+ const targetDir = resolve(dataPath)
112
+ const files = scanMarkdownFiles(targetDir)
113
+ const rawItems: AnyOntologyItem[] = []
114
+
115
+ for (const filePath of files) {
116
+ const content = readFileSync(filePath, 'utf-8')
117
+ try {
118
+ const doc = parseOkfDocument(content)
119
+ rawItems.push(doc)
120
+ } catch (err) {
121
+ // Skip files without valid frontmatter
122
+ }
123
+ }
124
+
125
+ return indexOntologyItems(rawItems)
126
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Lightweight, zero-dependency OKF (Open Knowledge Format v0.1) Markdown & YAML parser.
3
+ */
4
+
5
+ import type { AnyOntologyItem, BaseOntologyItem } from './types'
6
+
7
+ /**
8
+ * Parses simple YAML frontmatter strings into a JavaScript object.
9
+ * Supports strings, numbers, booleans, flat arrays `[a, b]`, and 1-level nested maps.
10
+ */
11
+ export function parseYamlFrontmatter(yamlContent: string): Record<string, any> {
12
+ const result: Record<string, any> = {}
13
+ const lines = yamlContent.split(/\r?\n/)
14
+ let currentParentKey: string | null = null
15
+
16
+ for (let i = 0; i < lines.length; i++) {
17
+ const rawLine = lines[i]
18
+ const trimmed = rawLine.trim()
19
+
20
+ if (!trimmed || trimmed.startsWith('#')) {
21
+ continue
22
+ }
23
+
24
+ // Check indentation to determine if child of current parent
25
+ const isIndented = /^\s{2,}/.test(rawLine)
26
+
27
+ if (isIndented && currentParentKey) {
28
+ const colonIndex = trimmed.indexOf(':')
29
+ if (colonIndex !== -1) {
30
+ const subKey = trimmed.slice(0, colonIndex).trim()
31
+ const rawVal = trimmed.slice(colonIndex + 1).trim()
32
+ if (!result[currentParentKey] || typeof result[currentParentKey] !== 'object') {
33
+ result[currentParentKey] = {}
34
+ }
35
+ result[currentParentKey][subKey] = parseYamlValue(rawVal)
36
+ } else if (trimmed.startsWith('- ')) {
37
+ // Array element under parent
38
+ if (!Array.isArray(result[currentParentKey])) {
39
+ result[currentParentKey] = []
40
+ }
41
+ result[currentParentKey].push(parseYamlValue(trimmed.slice(2).trim()))
42
+ }
43
+ continue
44
+ }
45
+
46
+ // Top-level key
47
+ const colonIndex = trimmed.indexOf(':')
48
+ if (colonIndex === -1) {
49
+ continue
50
+ }
51
+
52
+ const key = trimmed.slice(0, colonIndex).trim()
53
+ const rawValue = trimmed.slice(colonIndex + 1).trim()
54
+
55
+ if (rawValue === '') {
56
+ // Parent block starting
57
+ currentParentKey = key
58
+ result[key] = {}
59
+ } else {
60
+ currentParentKey = null
61
+ result[key] = parseYamlValue(rawValue)
62
+ }
63
+ }
64
+
65
+ return result
66
+ }
67
+
68
+ function parseYamlValue(val: string): any {
69
+ if (val === '') return null
70
+ if (val === 'true' || val === 'yes') return true
71
+ if (val === 'false' || val === 'no') return false
72
+ if (val === 'null' || val === '~') return null
73
+
74
+ // Numbers
75
+ if (!isNaN(Number(val)) && !val.includes(' ') && val !== '') {
76
+ return Number(val)
77
+ }
78
+
79
+ // Arrays format: [a, b, c]
80
+ if (val.startsWith('[') && val.endsWith(']')) {
81
+ const inner = val.slice(1, -1).trim()
82
+ if (!inner) return []
83
+ return inner
84
+ .split(',')
85
+ .map((item) => item.trim())
86
+ .map((item) => {
87
+ if (item.startsWith('"') && item.endsWith('"')) return item.slice(1, -1)
88
+ if (item.startsWith("'") && item.endsWith("'")) return item.slice(1, -1)
89
+ return parseYamlValue(item)
90
+ })
91
+ }
92
+
93
+ // Strip surrounding quotes
94
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
95
+ return val.slice(1, -1)
96
+ }
97
+
98
+ return val
99
+ }
100
+
101
+ /**
102
+ * Parses an entire OKF Markdown file into an OntologyItem.
103
+ */
104
+ export function parseOkfDocument(content: string): AnyOntologyItem {
105
+ const trimmed = content.trim()
106
+
107
+ if (!trimmed.startsWith('---')) {
108
+ throw new Error('Invalid OKF document: Missing leading YAML frontmatter delimiter (---)')
109
+ }
110
+
111
+ const secondDelimiterIndex = trimmed.indexOf('\n---', 3)
112
+ if (secondDelimiterIndex === -1) {
113
+ throw new Error('Invalid OKF document: Missing closing YAML frontmatter delimiter (---)')
114
+ }
115
+
116
+ const frontmatterRaw = trimmed.slice(3, secondDelimiterIndex).trim()
117
+ const bodyMarkdown = trimmed.slice(secondDelimiterIndex + 4).trim()
118
+
119
+ const frontmatter = parseYamlFrontmatter(frontmatterRaw)
120
+
121
+ if (!frontmatter.id || !frontmatter.title) {
122
+ throw new Error(`Invalid OKF document: Mandatory "id" or "title" missing in frontmatter`)
123
+ }
124
+
125
+ return {
126
+ type: frontmatter.type || 'concept',
127
+ id: String(frontmatter.id),
128
+ title: String(frontmatter.title),
129
+ description: String(frontmatter.description || ''),
130
+ tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
131
+ translations: frontmatter.translations || {},
132
+ standards: frontmatter.standards || {},
133
+ bodyMarkdown,
134
+ timestamp: frontmatter.timestamp,
135
+ ...frontmatter,
136
+ } as AnyOntologyItem
137
+ }
package/src/types.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Core type definitions for @bradtech/ontologies (OKF v0.1 Agricultural Lexicon)
3
+ */
4
+
5
+ export type SupportedLanguage = 'fr' | 'en' | 'es' | 'it' | 'de'
6
+
7
+ export type LanguageMap = Partial<Record<SupportedLanguage, string>>
8
+
9
+ export type OntologyDomain = 'soils' | 'irrigations' | 'crops' | 'crop-covers'
10
+
11
+ export interface OntologyStandards {
12
+ eppo?: string
13
+ agrovoc?: string
14
+ inrae?: string
15
+ usda?: string
16
+ wrb?: string
17
+ icid?: string
18
+ telepac_rpg?: string[]
19
+ [key: string]: unknown
20
+ }
21
+
22
+ export interface BaseOntologyItem {
23
+ type: string
24
+ id: string
25
+ title: string
26
+ description: string
27
+ tags: string[]
28
+ translations: LanguageMap
29
+ standards?: OntologyStandards
30
+ bodyMarkdown?: string
31
+ timestamp?: string
32
+ }
33
+
34
+ export interface SoilPhysics {
35
+ fieldCapacityPoint: number // % Volumetric Soil Moisture (Capacité au champ)
36
+ temporaryWiltingPoint: number // % Volumetric Soil Moisture (Point de stress)
37
+ permanentWiltingPoint: number // % Volumetric Soil Moisture (Point de flétrissement permanent)
38
+ bulkDensityKgM3: number // kg/m³
39
+ clayPercentageApprox?: number
40
+ sandPercentageApprox?: number
41
+ siltPercentageApprox?: number
42
+ }
43
+
44
+ export interface SoilOntologyItem extends BaseOntologyItem {
45
+ type: 'soil'
46
+ physics: SoilPhysics
47
+ }
48
+
49
+ export interface IrrigationEngineering {
50
+ method: string
51
+ applicationEfficiency: number // e.g. 0.90 for drip, 0.75 for sprinkler
52
+ wettingPatternFraction: number // fraction of soil surface wetted (0.2 to 1.0)
53
+ }
54
+
55
+ export interface IrrigationOntologyItem extends BaseOntologyItem {
56
+ type: 'irrigation'
57
+ engineering: IrrigationEngineering
58
+ }
59
+
60
+ export interface CropAgronomy {
61
+ rootDepthMaxCm: number
62
+ kcInitial: number
63
+ kcMid: number
64
+ kcEnd: number
65
+ baseTemperatureGdd: number // Base temperature for Growing Degree Days (°C)
66
+ waterRequirementMmAnnual?: number
67
+ }
68
+
69
+ export interface CropOntologyItem extends BaseOntologyItem {
70
+ type: 'crop'
71
+ category: string // e.g. 'viticulture', 'aromatics', 'arboriculture', 'cereals'
72
+ scientificName?: string
73
+ agronomy: CropAgronomy
74
+ }
75
+
76
+ export interface CropCoverEffects {
77
+ soilProtectionFactor: number // 0.0 to 1.0
78
+ waterCompetitionIndex: number // 0.0 (negligible) to 1.0 (high competition)
79
+ nitrogenFixation: boolean
80
+ }
81
+
82
+ export interface CropCoverOntologyItem extends BaseOntologyItem {
83
+ type: 'crop-cover'
84
+ effects: CropCoverEffects
85
+ }
86
+
87
+ export type AnyOntologyItem =
88
+ | SoilOntologyItem
89
+ | IrrigationOntologyItem
90
+ | CropOntologyItem
91
+ | CropCoverOntologyItem
92
+ | BaseOntologyItem
93
+
94
+ export interface FlatSelectOption {
95
+ id: string
96
+ label: string
97
+ description?: string
98
+ standards?: OntologyStandards
99
+ }
100
+
101
+ export interface HierarchyNode<T = AnyOntologyItem> {
102
+ item: T
103
+ children: HierarchyNode<T>[]
104
+ }
105
+
106
+ export interface OntologyLookupResult {
107
+ item: AnyOntologyItem
108
+ domain: OntologyDomain
109
+ matchedBy: 'id' | 'eppo' | 'agrovoc' | 'telepac' | 'alias'
110
+ }