@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.
- package/LICENSE +620 -0
- package/README.md +66 -0
- package/data/crop-covers/bare-soil.md +23 -0
- package/data/crop-covers/index.md +21 -0
- package/data/crop-covers/permanent-sod.md +23 -0
- package/data/crop-covers/sown-legume.md +23 -0
- package/data/crop-covers/spontaneous-grass.md +23 -0
- package/data/crops/index.md +23 -0
- package/data/crops/lavandula-angustifolia.md +30 -0
- package/data/crops/malus-domestica.md +30 -0
- package/data/crops/prunus-persica.md +30 -0
- package/data/crops/triticum-aestivum.md +30 -0
- package/data/crops/vitis-vinifera.md +30 -0
- package/data/crops/zea-mays.md +30 -0
- package/data/index.md +22 -0
- package/data/irrigations/drip.md +24 -0
- package/data/irrigations/gravity.md +24 -0
- package/data/irrigations/index.md +21 -0
- package/data/irrigations/none.md +24 -0
- package/data/irrigations/sprinkler.md +24 -0
- package/data/soils/clay-loam.md +29 -0
- package/data/soils/clay.md +29 -0
- package/data/soils/index.md +26 -0
- package/data/soils/loam.md +29 -0
- package/data/soils/sand.md +29 -0
- package/data/soils/sandy-clay-loam.md +29 -0
- package/data/soils/sandy-clay.md +29 -0
- package/data/soils/sandy-loam.md +29 -0
- package/data/soils/silt-loam.md +29 -0
- package/data/soils/silt.md +29 -0
- package/data/soils/silty-clay-loam.md +29 -0
- package/dist/build.d.ts +5 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +61 -0
- package/dist/build.js.map +1 -0
- package/dist/catalog.json +1623 -0
- package/dist/embeddedData.d.ts +6 -0
- package/dist/embeddedData.d.ts.map +1 -0
- package/dist/embeddedData.js +906 -0
- package/dist/embeddedData.js.map +1 -0
- package/dist/extractor.d.ts +31 -0
- package/dist/extractor.d.ts.map +1 -0
- package/dist/extractor.js +161 -0
- package/dist/extractor.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +25 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +104 -0
- package/dist/loader.js.map +1 -0
- package/dist/opendata.jsonld +268 -0
- package/dist/parser.d.ts +14 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +124 -0
- package/dist/parser.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
- package/src/build.ts +71 -0
- package/src/embeddedData.ts +910 -0
- package/src/extractor.ts +207 -0
- package/src/index.ts +98 -0
- package/src/loader.ts +126 -0
- package/src/parser.ts +137 -0
- package/src/types.ts +110 -0
package/src/extractor.ts
ADDED
|
@@ -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
|
+
}
|