@ansvar/ch-organic-regen-mcp 0.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/.github/workflows/check-freshness.yml +49 -0
- package/.github/workflows/ci.yml +21 -0
- package/.github/workflows/codeql.yml +25 -0
- package/.github/workflows/ghcr-build.yml +45 -0
- package/.github/workflows/gitleaks.yml +18 -0
- package/.github/workflows/ingest.yml +59 -0
- package/.github/workflows/publish.yml +24 -0
- package/CHANGELOG.md +15 -0
- package/CODEOWNERS +1 -0
- package/COVERAGE.md +47 -0
- package/DISCLAIMER.md +67 -0
- package/Dockerfile +26 -0
- package/LICENSE +17 -0
- package/PRIVACY.md +23 -0
- package/README.md +117 -0
- package/SECURITY.md +25 -0
- package/TOOLS.md +141 -0
- package/data/coverage.json +22 -0
- package/data/database.db +0 -0
- package/data/sources.yml +36 -0
- package/dist/db.d.ts +25 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +184 -0
- package/dist/db.js.map +1 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +263 -0
- package/dist/http-server.js.map +1 -0
- package/dist/jurisdiction.d.ts +18 -0
- package/dist/jurisdiction.d.ts.map +1 -0
- package/dist/jurisdiction.js +16 -0
- package/dist/jurisdiction.js.map +1 -0
- package/dist/metadata.d.ts +11 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +31 -0
- package/dist/metadata.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +209 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/about.d.ts +15 -0
- package/dist/tools/about.d.ts.map +1 -0
- package/dist/tools/about.js +27 -0
- package/dist/tools/about.js.map +1 -0
- package/dist/tools/check-freshness.d.ts +15 -0
- package/dist/tools/check-freshness.d.ts.map +1 -0
- package/dist/tools/check-freshness.js +26 -0
- package/dist/tools/check-freshness.js.map +1 -0
- package/dist/tools/get-approved-inputs.d.ts +26 -0
- package/dist/tools/get-approved-inputs.d.ts.map +1 -0
- package/dist/tools/get-approved-inputs.js +28 -0
- package/dist/tools/get-approved-inputs.js.map +1 -0
- package/dist/tools/get-conversion-requirements.d.ts +28 -0
- package/dist/tools/get-conversion-requirements.d.ts.map +1 -0
- package/dist/tools/get-conversion-requirements.js +32 -0
- package/dist/tools/get-conversion-requirements.js.map +1 -0
- package/dist/tools/get-organic-standards.d.ts +27 -0
- package/dist/tools/get-organic-standards.d.ts.map +1 -0
- package/dist/tools/get-organic-standards.js +31 -0
- package/dist/tools/get-organic-standards.js.map +1 -0
- package/dist/tools/get-organic-subsidies.d.ts +28 -0
- package/dist/tools/get-organic-subsidies.d.ts.map +1 -0
- package/dist/tools/get-organic-subsidies.js +32 -0
- package/dist/tools/get-organic-subsidies.js.map +1 -0
- package/dist/tools/get-soil-health-guidance.d.ts +25 -0
- package/dist/tools/get-soil-health-guidance.d.ts.map +1 -0
- package/dist/tools/get-soil-health-guidance.js +27 -0
- package/dist/tools/get-soil-health-guidance.js.map +1 -0
- package/dist/tools/list-sources.d.ts +18 -0
- package/dist/tools/list-sources.d.ts.map +1 -0
- package/dist/tools/list-sources.js +61 -0
- package/dist/tools/list-sources.js.map +1 -0
- package/dist/tools/search-certification-guidance.d.ts +24 -0
- package/dist/tools/search-certification-guidance.d.ts.map +1 -0
- package/dist/tools/search-certification-guidance.js +24 -0
- package/dist/tools/search-certification-guidance.js.map +1 -0
- package/dist/tools/search-organic-rules.d.ts +25 -0
- package/dist/tools/search-organic-rules.d.ts.map +1 -0
- package/dist/tools/search-organic-rules.js +26 -0
- package/dist/tools/search-organic-rules.js.map +1 -0
- package/docker-compose.yml +12 -0
- package/eslint.config.js +26 -0
- package/package.json +54 -0
- package/scripts/ingest.ts +963 -0
- package/server.json +16 -0
- package/src/db.ts +225 -0
- package/src/http-server.ts +302 -0
- package/src/jurisdiction.ts +30 -0
- package/src/metadata.ts +45 -0
- package/src/server.ts +239 -0
- package/src/tools/about.ts +28 -0
- package/src/tools/check-freshness.ts +42 -0
- package/src/tools/get-approved-inputs.ts +44 -0
- package/src/tools/get-conversion-requirements.ts +50 -0
- package/src/tools/get-organic-standards.ts +48 -0
- package/src/tools/get-organic-subsidies.ts +50 -0
- package/src/tools/get-soil-health-guidance.ts +42 -0
- package/src/tools/list-sources.ts +75 -0
- package/src/tools/search-certification-guidance.ts +41 -0
- package/src/tools/search-organic-rules.ts +35 -0
- package/tests/db.test.ts +96 -0
- package/tests/helpers/seed-db.ts +145 -0
- package/tests/jurisdiction.test.ts +35 -0
- package/tests/tools/about.test.ts +22 -0
- package/tests/tools/check-freshness.test.ts +57 -0
- package/tests/tools/list-sources.test.ts +55 -0
- package/tests/tools/search-organic-rules.test.ts +56 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { buildMeta } from '../metadata.js';
|
|
2
|
+
import { validateJurisdiction } from '../jurisdiction.js';
|
|
3
|
+
import { ftsSearch, type Database } from '../db.js';
|
|
4
|
+
|
|
5
|
+
interface SearchArgs {
|
|
6
|
+
query: string;
|
|
7
|
+
topic?: string;
|
|
8
|
+
jurisdiction?: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleSearchOrganicRules(db: Database, args: SearchArgs) {
|
|
13
|
+
const jv = validateJurisdiction(args.jurisdiction);
|
|
14
|
+
if (!jv.valid) return jv.error;
|
|
15
|
+
|
|
16
|
+
const limit = Math.min(args.limit ?? 20, 50);
|
|
17
|
+
let results = ftsSearch(db, args.query, limit);
|
|
18
|
+
|
|
19
|
+
if (args.topic) {
|
|
20
|
+
results = results.filter(r => r.topic.toLowerCase().includes(args.topic!.toLowerCase()));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
query: args.query,
|
|
25
|
+
jurisdiction: jv.jurisdiction,
|
|
26
|
+
results_count: results.length,
|
|
27
|
+
results: results.map(r => ({
|
|
28
|
+
title: r.title,
|
|
29
|
+
body: r.body,
|
|
30
|
+
topic: r.topic,
|
|
31
|
+
relevance_rank: r.rank,
|
|
32
|
+
})),
|
|
33
|
+
_meta: buildMeta(),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/tests/db.test.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createDatabase, type Database } from '../src/db.js';
|
|
3
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const TEST_DB = 'tests/test-database.db';
|
|
6
|
+
|
|
7
|
+
describe('database layer', () => {
|
|
8
|
+
let db: Database;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
db = createDatabase(TEST_DB);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(() => {
|
|
15
|
+
db.close();
|
|
16
|
+
if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('creates database with db_metadata table', () => {
|
|
20
|
+
const row = db.get<{ value: string }>(
|
|
21
|
+
'SELECT value FROM db_metadata WHERE key = ?',
|
|
22
|
+
['schema_version']
|
|
23
|
+
);
|
|
24
|
+
expect(row?.value).toBe('1.0');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('creates organic_standards table', () => {
|
|
28
|
+
const result = db.all<{ name: string }>(
|
|
29
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='organic_standards'"
|
|
30
|
+
);
|
|
31
|
+
expect(result).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('creates conversion_requirements table', () => {
|
|
35
|
+
const result = db.all<{ name: string }>(
|
|
36
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='conversion_requirements'"
|
|
37
|
+
);
|
|
38
|
+
expect(result).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('creates approved_inputs table', () => {
|
|
42
|
+
const result = db.all<{ name: string }>(
|
|
43
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='approved_inputs'"
|
|
44
|
+
);
|
|
45
|
+
expect(result).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('creates organic_subsidies table', () => {
|
|
49
|
+
const result = db.all<{ name: string }>(
|
|
50
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='organic_subsidies'"
|
|
51
|
+
);
|
|
52
|
+
expect(result).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('creates soil_health table', () => {
|
|
56
|
+
const result = db.all<{ name: string }>(
|
|
57
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='soil_health'"
|
|
58
|
+
);
|
|
59
|
+
expect(result).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('creates certification_guidance table', () => {
|
|
63
|
+
const result = db.all<{ name: string }>(
|
|
64
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='certification_guidance'"
|
|
65
|
+
);
|
|
66
|
+
expect(result).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('FTS5 search_index exists', () => {
|
|
70
|
+
const result = db.all<{ name: string }>(
|
|
71
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='search_index'"
|
|
72
|
+
);
|
|
73
|
+
expect(result).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('journal mode is DELETE', () => {
|
|
77
|
+
const row = db.get<{ journal_mode: string }>('PRAGMA journal_mode');
|
|
78
|
+
expect(row?.journal_mode).toBe('delete');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('mcp_name metadata is set', () => {
|
|
82
|
+
const row = db.get<{ value: string }>(
|
|
83
|
+
'SELECT value FROM db_metadata WHERE key = ?',
|
|
84
|
+
['mcp_name']
|
|
85
|
+
);
|
|
86
|
+
expect(row?.value).toContain('Organic');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('jurisdiction metadata is CH', () => {
|
|
90
|
+
const row = db.get<{ value: string }>(
|
|
91
|
+
'SELECT value FROM db_metadata WHERE key = ?',
|
|
92
|
+
['jurisdiction']
|
|
93
|
+
);
|
|
94
|
+
expect(row?.value).toBe('CH');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createDatabase, type Database } from '../../src/db.js';
|
|
2
|
+
|
|
3
|
+
export function createSeededDatabase(dbPath: string): Database {
|
|
4
|
+
const db = createDatabase(dbPath);
|
|
5
|
+
|
|
6
|
+
// Organic standards — Knospe, Bio-Verordnung, Demeter
|
|
7
|
+
db.run(
|
|
8
|
+
`INSERT INTO organic_standards (id, production_type, standard, rule, description, jurisdiction)
|
|
9
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
10
|
+
[1, 'futtermittel', 'knospe', '100% Bio-Futter, davon mindestens 90% Knospe-Qualitaet',
|
|
11
|
+
'Knospe-Betriebe muessen 100% biologisches Futter einsetzen, davon mindestens 90% aus Knospe-zertifizierter Produktion.', 'CH']
|
|
12
|
+
);
|
|
13
|
+
db.run(
|
|
14
|
+
`INSERT INTO organic_standards (id, production_type, standard, rule, description, jurisdiction)
|
|
15
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
16
|
+
[2, 'futtermittel', 'bio_verordnung', 'Mindestens 95% Bio-Futter',
|
|
17
|
+
'Gemaess Bio-Verordnung SR 910.18 muessen mindestens 95% des Futters aus biologischer Produktion stammen.', 'CH']
|
|
18
|
+
);
|
|
19
|
+
db.run(
|
|
20
|
+
`INSERT INTO organic_standards (id, production_type, standard, rule, description, jurisdiction)
|
|
21
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
22
|
+
[3, 'tierhaltung', 'demeter', 'Biodynamische Tierhaltung mit Horntraegern',
|
|
23
|
+
'Demeter verlangt horntragende Rinder und biodynamische Futterpraeparate (Praeparate 500-508).', 'CH']
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Conversion requirements
|
|
27
|
+
db.run(
|
|
28
|
+
`INSERT INTO conversion_requirements (id, farm_type, current_system, timeline_years, requirements, support_measures, jurisdiction)
|
|
29
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
30
|
+
[1, 'ackerbau', 'konventionell', 2,
|
|
31
|
+
'Gesamtbetriebliche Umstellung, Pflanzenschutz nach FiBL-Liste, Fruchtfolge mindestens 4 Kulturen',
|
|
32
|
+
'FiBL Umstellungsberatung, kantonale Beratung, AGRIDEA Merkblaetter', 'CH']
|
|
33
|
+
);
|
|
34
|
+
db.run(
|
|
35
|
+
`INSERT INTO conversion_requirements (id, farm_type, current_system, timeline_years, requirements, support_measures, jurisdiction)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
37
|
+
[2, 'milchwirtschaft', 'oeln', 1,
|
|
38
|
+
'Futter 100% Bio (90% Knospe), Weidepflicht, Laufstall oder Auslauf taeglich',
|
|
39
|
+
'Bio Suisse Beratung, kantonale Fachstelle, Umstellungsbeitrag BLW', 'CH']
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Approved inputs
|
|
43
|
+
db.run(
|
|
44
|
+
`INSERT INTO approved_inputs (id, input_type, product_name, description, restrictions, source, jurisdiction)
|
|
45
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
46
|
+
[1, 'duenger', 'Kompost (Knospe-konform)',
|
|
47
|
+
'Gruengutkompost und Hofduenger-Kompost fuer Naehrstoffversorgung und Humusaufbau',
|
|
48
|
+
'Maximal 2.5 DGVE/ha, keine Klaerschlammbeimischung',
|
|
49
|
+
'FiBL Betriebsmittelliste', 'CH']
|
|
50
|
+
);
|
|
51
|
+
db.run(
|
|
52
|
+
`INSERT INTO approved_inputs (id, input_type, product_name, description, restrictions, source, jurisdiction)
|
|
53
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
54
|
+
[2, 'pflanzenschutz', 'Kupferpraeparat',
|
|
55
|
+
'Kupferhydroxid zur Bekaempfung von Pilzkrankheiten im Rebbau und Obstbau',
|
|
56
|
+
'Maximal 4 kg Reinkupfer/ha/Jahr (Knospe), 6 kg/ha (Bio-Verordnung)',
|
|
57
|
+
'FiBL Betriebsmittelliste', 'CH']
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Organic subsidies
|
|
61
|
+
db.run(
|
|
62
|
+
`INSERT INTO organic_subsidies (id, subsidy_type, zone, rate_chf_ha, conditions, stacking_rules, jurisdiction)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
64
|
+
[1, 'bio_beitrag', 'talzone', 1600,
|
|
65
|
+
'Gesamtbetriebliche Bioproduktion, Kontrolle durch anerkannte Inspektionsstelle',
|
|
66
|
+
'Kumulierbar mit Extenso, BFF-Beitraegen, Tierwohl-Beitraegen', 'CH']
|
|
67
|
+
);
|
|
68
|
+
db.run(
|
|
69
|
+
`INSERT INTO organic_subsidies (id, subsidy_type, zone, rate_chf_ha, conditions, stacking_rules, jurisdiction)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
71
|
+
[2, 'bio_beitrag', 'bergzone_ii', 1100,
|
|
72
|
+
'Gesamtbetriebliche Bioproduktion, Kontrolle durch anerkannte Inspektionsstelle',
|
|
73
|
+
'Kumulierbar mit Extenso, BFF-Beitraegen, Tierwohl-Beitraegen, Soemmerungsbeitrag', 'CH']
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Soil health guidance
|
|
77
|
+
db.run(
|
|
78
|
+
`INSERT INTO soil_health (id, topic, guidance, technique, benefits, jurisdiction)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
80
|
+
[1, 'kompost',
|
|
81
|
+
'Qualitaetskompost foerdert Bodenstruktur, Wasserhaltefaehigkeit und Bodenbiologie',
|
|
82
|
+
'Flaechen- oder Reihenkompostierung, 3-6 Monate Rottedauer, C/N-Verhaeltnis 25-30:1',
|
|
83
|
+
'Humusaufbau, Naehrstoffversorgung, Krankheitsunterdrueckung', 'CH']
|
|
84
|
+
);
|
|
85
|
+
db.run(
|
|
86
|
+
`INSERT INTO soil_health (id, topic, guidance, technique, benefits, jurisdiction)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
88
|
+
[2, 'gruenduengung',
|
|
89
|
+
'Gruenduengung mit Leguminosen fixiert Stickstoff und schuetzt den Boden vor Erosion',
|
|
90
|
+
'Alexandrinerklee, Inkarnatklee, Luzerne oder Mischungen nach FiBL-Empfehlung',
|
|
91
|
+
'Stickstoff-Fixierung, Erosionsschutz, Bodenstrukturverbesserung', 'CH']
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Certification guidance
|
|
95
|
+
db.run(
|
|
96
|
+
`INSERT INTO certification_guidance (id, step, description, inspector, frequency, cost_notes, jurisdiction)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
98
|
+
[1, 'anmeldung',
|
|
99
|
+
'Anmeldung bei Bio Suisse und einer anerkannten Inspektionsstelle (bio.inspecta oder Bio Test Agro)',
|
|
100
|
+
'bio.inspecta / Bio Test Agro',
|
|
101
|
+
'einmalig',
|
|
102
|
+
'Anmeldegebuehr ca. CHF 200-400', 'CH']
|
|
103
|
+
);
|
|
104
|
+
db.run(
|
|
105
|
+
`INSERT INTO certification_guidance (id, step, description, inspector, frequency, cost_notes, jurisdiction)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
107
|
+
[2, 'kontrolle',
|
|
108
|
+
'Jaehrliche Betriebskontrolle mit unangekuendigten Stichprobenkontrollen',
|
|
109
|
+
'bio.inspecta / Bio Test Agro',
|
|
110
|
+
'jaehrlich',
|
|
111
|
+
'Kontrollkosten ca. CHF 500-1200 je nach Betriebsgroesse', 'CH']
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// FTS5 search index
|
|
115
|
+
db.run(
|
|
116
|
+
`INSERT INTO search_index (title, body, topic, jurisdiction) VALUES (?, ?, ?, ?)`,
|
|
117
|
+
['Knospe Futtermittel-Anforderungen',
|
|
118
|
+
'Knospe-Betriebe muessen 100% biologisches Futter einsetzen, davon mindestens 90% aus Knospe-zertifizierter Produktion. Maximal 10% EU-Bio.',
|
|
119
|
+
'futtermittel', 'CH']
|
|
120
|
+
);
|
|
121
|
+
db.run(
|
|
122
|
+
`INSERT INTO search_index (title, body, topic, jurisdiction) VALUES (?, ?, ?, ?)`,
|
|
123
|
+
['Umstellung Ackerbau auf Bio',
|
|
124
|
+
'Umstellungsdauer 2 Jahre fuer Ackerbau bei konventioneller Bewirtschaftung. Pflanzenschutz nach FiBL-Betriebsmittelliste. Fruchtfolge mindestens 4 Kulturen.',
|
|
125
|
+
'umstellung', 'CH']
|
|
126
|
+
);
|
|
127
|
+
db.run(
|
|
128
|
+
`INSERT INTO search_index (title, body, topic, jurisdiction) VALUES (?, ?, ?, ?)`,
|
|
129
|
+
['Kompost fuer Bodenfruchtbarkeit',
|
|
130
|
+
'Qualitaetskompost foerdert Bodenstruktur und Humusaufbau. Flaechen- oder Reihenkompostierung mit C/N-Verhaeltnis 25-30:1.',
|
|
131
|
+
'bodenfruchtbarkeit', 'CH']
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// db_metadata for freshness
|
|
135
|
+
db.run(
|
|
136
|
+
`INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('last_ingest', ?)`,
|
|
137
|
+
[new Date().toISOString().split('T')[0]]
|
|
138
|
+
);
|
|
139
|
+
db.run(
|
|
140
|
+
`INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('build_date', ?)`,
|
|
141
|
+
[new Date().toISOString().split('T')[0]]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return db;
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { validateJurisdiction, SUPPORTED_JURISDICTIONS } from '../src/jurisdiction.js';
|
|
3
|
+
|
|
4
|
+
describe('jurisdiction validation', () => {
|
|
5
|
+
test('accepts CH', () => {
|
|
6
|
+
const result = validateJurisdiction('CH');
|
|
7
|
+
expect(result).toEqual({ valid: true, jurisdiction: 'CH' });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('defaults to CH when undefined', () => {
|
|
11
|
+
const result = validateJurisdiction(undefined);
|
|
12
|
+
expect(result).toEqual({ valid: true, jurisdiction: 'CH' });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('rejects unsupported jurisdiction', () => {
|
|
16
|
+
const result = validateJurisdiction('DE');
|
|
17
|
+
expect(result).toEqual({
|
|
18
|
+
valid: false,
|
|
19
|
+
error: {
|
|
20
|
+
error: 'jurisdiction_not_supported',
|
|
21
|
+
supported: ['CH'],
|
|
22
|
+
message: 'This server currently covers Switzerland. More jurisdictions are planned.',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('normalises lowercase input', () => {
|
|
28
|
+
const result = validateJurisdiction('ch');
|
|
29
|
+
expect(result).toEqual({ valid: true, jurisdiction: 'CH' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('SUPPORTED_JURISDICTIONS contains CH', () => {
|
|
33
|
+
expect(SUPPORTED_JURISDICTIONS).toContain('CH');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { handleAbout } from '../../src/tools/about.js';
|
|
3
|
+
|
|
4
|
+
describe('about tool', () => {
|
|
5
|
+
test('returns server metadata', () => {
|
|
6
|
+
const result = handleAbout();
|
|
7
|
+
expect(result.name).toContain('Organic');
|
|
8
|
+
expect(result.description).toContain('Bio Suisse');
|
|
9
|
+
expect(result.jurisdiction).toEqual(['CH']);
|
|
10
|
+
expect(result.tools_count).toBe(10);
|
|
11
|
+
expect(result.links).toHaveProperty('homepage');
|
|
12
|
+
expect(result.links).toHaveProperty('repository');
|
|
13
|
+
expect(result._meta).toHaveProperty('disclaimer');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('lists data sources', () => {
|
|
17
|
+
const result = handleAbout();
|
|
18
|
+
expect(result.data_sources).toContain('Bio Suisse Richtlinien (Knospe)');
|
|
19
|
+
expect(result.data_sources).toContain('FiBL Betriebsmittelliste');
|
|
20
|
+
expect(result.data_sources).toContain('Demeter Schweiz Richtlinien');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { handleCheckFreshness } from '../../src/tools/check-freshness.js';
|
|
3
|
+
import { createDatabase, type Database } from '../../src/db.js';
|
|
4
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
const TEST_DB = 'tests/test-check-freshness.db';
|
|
7
|
+
|
|
8
|
+
describe('check_data_freshness tool', () => {
|
|
9
|
+
let db: Database;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
db = createDatabase(TEST_DB);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
db.close();
|
|
17
|
+
if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns unknown when no ingest date', () => {
|
|
21
|
+
const result = handleCheckFreshness(db);
|
|
22
|
+
expect(result.status).toBe('unknown');
|
|
23
|
+
expect(result.days_since_ingest).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('returns fresh when recently ingested', () => {
|
|
27
|
+
const today = new Date().toISOString().split('T')[0];
|
|
28
|
+
db.run("INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('last_ingest', ?)", [today]);
|
|
29
|
+
|
|
30
|
+
const result = handleCheckFreshness(db);
|
|
31
|
+
expect(result.status).toBe('fresh');
|
|
32
|
+
expect(result.days_since_ingest).toBeLessThanOrEqual(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('returns stale when old ingest', () => {
|
|
36
|
+
db.run("INSERT OR REPLACE INTO db_metadata (key, value) VALUES ('last_ingest', '2025-01-01')", []);
|
|
37
|
+
|
|
38
|
+
const result = handleCheckFreshness(db);
|
|
39
|
+
expect(result.status).toBe('stale');
|
|
40
|
+
expect(result.days_since_ingest).toBeGreaterThan(90);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('includes refresh command', () => {
|
|
44
|
+
const result = handleCheckFreshness(db);
|
|
45
|
+
expect(result.refresh_command).toContain('gh workflow run');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('includes schema_version', () => {
|
|
49
|
+
const result = handleCheckFreshness(db);
|
|
50
|
+
expect(result.schema_version).toBe('1.0');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('staleness threshold is 90 days', () => {
|
|
54
|
+
const result = handleCheckFreshness(db);
|
|
55
|
+
expect(result.staleness_threshold_days).toBe(90);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { handleListSources } from '../../src/tools/list-sources.js';
|
|
3
|
+
import { createDatabase, type Database } from '../../src/db.js';
|
|
4
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
const TEST_DB = 'tests/test-list-sources.db';
|
|
7
|
+
|
|
8
|
+
describe('list_sources tool', () => {
|
|
9
|
+
let db: Database;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
db = createDatabase(TEST_DB);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
db.close();
|
|
17
|
+
if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns 5 data sources', () => {
|
|
21
|
+
const result = handleListSources(db);
|
|
22
|
+
expect(result.sources).toHaveLength(5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('each source has required fields', () => {
|
|
26
|
+
const result = handleListSources(db);
|
|
27
|
+
for (const source of result.sources) {
|
|
28
|
+
expect(source).toHaveProperty('name');
|
|
29
|
+
expect(source).toHaveProperty('authority');
|
|
30
|
+
expect(source).toHaveProperty('official_url');
|
|
31
|
+
expect(source).toHaveProperty('license');
|
|
32
|
+
expect(source).toHaveProperty('update_frequency');
|
|
33
|
+
expect(source).toHaveProperty('coverage');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('includes Bio Suisse as a source', () => {
|
|
38
|
+
const result = handleListSources(db);
|
|
39
|
+
const bioSuisse = result.sources.find(s => s.name.includes('Bio Suisse'));
|
|
40
|
+
expect(bioSuisse).toBeDefined();
|
|
41
|
+
expect(bioSuisse!.authority).toBe('Bio Suisse');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('includes FiBL as a source', () => {
|
|
45
|
+
const result = handleListSources(db);
|
|
46
|
+
const fibl = result.sources.find(s => s.name.includes('FiBL'));
|
|
47
|
+
expect(fibl).toBeDefined();
|
|
48
|
+
expect(fibl!.authority).toContain('FiBL');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('includes _meta', () => {
|
|
52
|
+
const result = handleListSources(db);
|
|
53
|
+
expect(result._meta).toHaveProperty('disclaimer');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { handleSearchOrganicRules } from '../../src/tools/search-organic-rules.js';
|
|
3
|
+
import { createSeededDatabase } from '../helpers/seed-db.js';
|
|
4
|
+
import type { Database } from '../../src/db.js';
|
|
5
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
const TEST_DB = 'tests/test-search-organic.db';
|
|
8
|
+
|
|
9
|
+
describe('search_organic_rules tool', () => {
|
|
10
|
+
let db: Database;
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
db = createSeededDatabase(TEST_DB);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
db.close();
|
|
18
|
+
if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('returns results for Knospe query', () => {
|
|
22
|
+
const result = handleSearchOrganicRules(db, { query: 'Knospe Futter' });
|
|
23
|
+
expect(result).toHaveProperty('results_count');
|
|
24
|
+
expect((result as { results_count: number }).results_count).toBeGreaterThan(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('returns results for Umstellung query', () => {
|
|
28
|
+
const result = handleSearchOrganicRules(db, { query: 'Umstellung' });
|
|
29
|
+
expect(result).toHaveProperty('results_count');
|
|
30
|
+
expect((result as { results_count: number }).results_count).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('respects topic filter', () => {
|
|
34
|
+
const result = handleSearchOrganicRules(db, { query: 'Knospe', topic: 'futtermittel' });
|
|
35
|
+
const typed = result as { results: { topic: string }[] };
|
|
36
|
+
for (const r of typed.results) {
|
|
37
|
+
expect(r.topic.toLowerCase()).toContain('futtermittel');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('respects limit parameter', () => {
|
|
42
|
+
const result = handleSearchOrganicRules(db, { query: 'Bio', limit: 1 });
|
|
43
|
+
const typed = result as { results: unknown[] };
|
|
44
|
+
expect(typed.results.length).toBeLessThanOrEqual(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('rejects unsupported jurisdiction', () => {
|
|
48
|
+
const result = handleSearchOrganicRules(db, { query: 'Bio', jurisdiction: 'FR' });
|
|
49
|
+
expect(result).toHaveProperty('error', 'jurisdiction_not_supported');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('includes _meta in response', () => {
|
|
53
|
+
const result = handleSearchOrganicRules(db, { query: 'Kompost' });
|
|
54
|
+
expect(result).toHaveProperty('_meta');
|
|
55
|
+
});
|
|
56
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "tests", "scripts"]
|
|
19
|
+
}
|