diff --git a/CHANGELOG.md b/CHANGELOG.md index 0009072..d214ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.0.0] - 2026-XX-XX +## [1.1.0] - 2026-02-22 +### Added +- `data/census.json` — full law census (10 laws, 135 provisions, jurisdiction IL) +- Dual transport in `server.json` (stdio + streamable-http via Vercel) +- Census consistency tests validating DB matches census +- `describe.skipIf` guards on all DB-dependent test suites (CI-safe) + +### Changed +- Rewrote `__tests__/contract/golden.test.ts` to golden standard pattern + - DB integrity, key law presence, provision retrieval, FTS search, negative tests + - All describe blocks skip gracefully when `data/database.db` is absent +- Updated `server.json` to `packages` format with Vercel endpoint + +## [1.0.0] - 2026-02-19 ### Added - Initial release of Israel Law MCP - `search_legislation` tool for full-text search across all Israeli statutes @@ -23,5 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - npm package with stdio transport - MCP Registry publishing -[Unreleased]: https://github.com/Ansvar-Systems/israel-law-mcp/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/Ansvar-Systems/israel-law-mcp/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/Ansvar-Systems/israel-law-mcp/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/Ansvar-Systems/israel-law-mcp/releases/tag/v1.0.0 diff --git a/__tests__/contract/golden.test.ts b/__tests__/contract/golden.test.ts index 40d73d6..9081cf6 100644 --- a/__tests__/contract/golden.test.ts +++ b/__tests__/contract/golden.test.ts @@ -1,73 +1,198 @@ /** * Golden contract tests for Israel Law MCP. + * Validates DB integrity for full official-portal ingestion. * - * Tests tool outputs against the golden-tests.json fixture file. - * These tests verify that the MCP server returns expected data - * for well-known Israeli legal provisions. + * Skipped automatically when the database file is absent (e.g. CI without artifacts). */ -import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import { join, dirname } from 'path'; +import { describe, it, expect, beforeAll } from 'vitest'; +import Database from 'better-sqlite3'; +import * as fs from 'fs'; +import * as path from 'path'; import { fileURLToPath } from 'url'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const fixturesPath = join(__dirname, '../../fixtures/golden-tests.json'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const DB_PATH = path.resolve(__dirname, '../../data/database.db'); +const CENSUS_PATH = path.resolve(__dirname, '../../data/census.json'); +const FIXTURE_PATH = path.resolve(__dirname, '../../fixtures/golden-tests.json'); -interface GoldenTest { +const DB_EXISTS = fs.existsSync(DB_PATH); + +interface CensusLaw { id: string; - category: string; - description: string; - tool: string; - input: Record; - assertions: { - result_not_empty?: boolean; - any_result_contains?: string[]; - fields_present?: string[]; - text_not_empty?: boolean; - min_results?: number; - citation_url_pattern?: string; - handles_gracefully?: boolean; - }; + title: string; + provisions: number; } -interface GoldenFixture { - version: string; - mcp_name: string; - tests: GoldenTest[]; +interface Census { + schema_version: string; + jurisdiction: string; + total_laws: number; + total_provisions: number; + laws: CensusLaw[]; } -const fixture: GoldenFixture = JSON.parse(readFileSync(fixturesPath, 'utf-8')); +let db: InstanceType; +let census: Census; -describe('Golden contract tests', () => { - it('fixture file is valid', () => { +describe.skipIf(!DB_EXISTS)('Database integrity', () => { + beforeAll(() => { + db = new Database(DB_PATH, { readonly: true }); + db.pragma('journal_mode = DELETE'); + census = JSON.parse(fs.readFileSync(CENSUS_PATH, 'utf-8')) as Census; + }); + + it('should have correct number of legal documents', () => { + const row = db.prepare('SELECT COUNT(*) as cnt FROM legal_documents').get() as { cnt: number }; + expect(row.cnt).toBe(census.total_laws); + }); + + it('should have correct total provision count', () => { + const row = db.prepare('SELECT COUNT(*) as cnt FROM legal_provisions').get() as { cnt: number }; + expect(row.cnt).toBe(census.total_provisions); + }); + + it('should have FTS index populated', () => { + const row = db.prepare( + "SELECT COUNT(*) as cnt FROM provisions_fts WHERE provisions_fts MATCH 'privacy OR פרטיות'" + ).get() as { cnt: number }; + expect(row.cnt).toBeGreaterThan(0); + }); + + it('should have db_metadata table populated', () => { + const row = db.prepare('SELECT COUNT(*) as cnt FROM db_metadata').get() as { cnt: number }; + expect(row.cnt).toBeGreaterThan(0); + }); +}); + +describe.skipIf(!DB_EXISTS)('All key laws are present', () => { + beforeAll(() => { + if (!db) { + db = new Database(DB_PATH, { readonly: true }); + db.pragma('journal_mode = DELETE'); + } + }); + + const expectedDocs = [ + 'privacy-protection-law-1981', + 'data-security-regulations-2017', + 'computer-law-1995', + 'companies-law-1999', + 'electronic-signature-law-2001', + 'credit-data-law-2002', + 'freedom-of-information-law-1998', + 'communications-law-1982', + 'basic-law-human-dignity-1992', + 'regulation-of-security-1998', + ]; + + for (const docId of expectedDocs) { + it(`should contain document: ${docId}`, () => { + const row = db.prepare('SELECT id FROM legal_documents WHERE id = ?').get(docId) as { id: string } | undefined; + expect(row).toBeDefined(); + expect(row!.id).toBe(docId); + }); + } +}); + +describe.skipIf(!DB_EXISTS)('Provision retrieval and search', () => { + beforeAll(() => { + if (!db) { + db = new Database(DB_PATH, { readonly: true }); + db.pragma('journal_mode = DELETE'); + } + }); + + it('should retrieve section 1 from Privacy Protection Law 1981', () => { + const row = db.prepare( + "SELECT content FROM legal_provisions WHERE document_id = 'privacy-protection-law-1981' AND section = '1'" + ).get() as { content: string } | undefined; + + expect(row).toBeDefined(); + expect(row!.content.length).toBeGreaterThan(20); + }); + + it('should retrieve section 1 from Computer Law 1995', () => { + const row = db.prepare( + "SELECT content FROM legal_provisions WHERE document_id = 'computer-law-1995' AND section = '1'" + ).get() as { content: string } | undefined; + + expect(row).toBeDefined(); + expect(row!.content.length).toBeGreaterThan(20); + }); + + it('should find results via FTS search for database/מאגר', () => { + const row = db.prepare( + "SELECT COUNT(*) as cnt FROM provisions_fts WHERE provisions_fts MATCH 'database OR מאגר'" + ).get() as { cnt: number }; + expect(row.cnt).toBeGreaterThan(0); + }); +}); + +describe.skipIf(!DB_EXISTS)('Census consistency', () => { + beforeAll(() => { + if (!db) { + db = new Database(DB_PATH, { readonly: true }); + db.pragma('journal_mode = DELETE'); + } + if (!census) { + census = JSON.parse(fs.readFileSync(CENSUS_PATH, 'utf-8')) as Census; + } + }); + + it('census law count matches database document count', () => { + const row = db.prepare('SELECT COUNT(*) as cnt FROM legal_documents').get() as { cnt: number }; + expect(row.cnt).toBe(census.laws.length); + }); + + it('each census law exists in database', () => { + for (const law of census.laws) { + const row = db.prepare('SELECT id FROM legal_documents WHERE id = ?').get(law.id) as { id: string } | undefined; + expect(row, `Missing law: ${law.id}`).toBeDefined(); + } + }); + + it('provision counts match census per law', () => { + for (const law of census.laws) { + const row = db.prepare( + 'SELECT COUNT(*) as cnt FROM legal_provisions WHERE document_id = ?' + ).get(law.id) as { cnt: number }; + expect(row.cnt, `Mismatch for ${law.id}`).toBe(law.provisions); + } + }); +}); + +describe.skipIf(!DB_EXISTS)('Negative tests', () => { + beforeAll(() => { + if (!db) { + db = new Database(DB_PATH, { readonly: true }); + db.pragma('journal_mode = DELETE'); + } + }); + + it('should return no results for fictional document', () => { + const row = db.prepare( + "SELECT COUNT(*) as cnt FROM legal_provisions WHERE document_id = 'fictional-law-2099'" + ).get() as { cnt: number }; + expect(row.cnt).toBe(0); + }); + + it('should return no results for invalid section', () => { + const row = db.prepare( + "SELECT COUNT(*) as cnt FROM legal_provisions WHERE document_id = 'privacy-protection-law-1981' AND section = '999ZZZ-INVALID'" + ).get() as { cnt: number }; + expect(row.cnt).toBe(0); + }); +}); + +describe('Golden fixture file validation', () => { + const HAS_FIXTURE = fs.existsSync(FIXTURE_PATH); + + it.skipIf(!HAS_FIXTURE)('fixture file is valid', () => { + const fixture = JSON.parse(fs.readFileSync(FIXTURE_PATH, 'utf-8')); expect(fixture.version).toBe('1.0'); expect(fixture.mcp_name).toBe('Israel Law MCP'); expect(fixture.tests.length).toBeGreaterThan(0); }); - - for (const test of fixture.tests) { - it(`${test.id}: ${test.description}`, () => { - // Contract tests validate the fixture structure. - // Full integration tests require a running server with a database. - // In CI, these run in CONTRACT_MODE=nightly for live assertions. - - expect(test.id).toBeTruthy(); - expect(test.tool).toBeTruthy(); - expect(test.assertions).toBeTruthy(); - - // Validate assertion structure - if (test.assertions.any_result_contains) { - expect(Array.isArray(test.assertions.any_result_contains)).toBe(true); - } - - if (test.assertions.fields_present) { - expect(Array.isArray(test.assertions.fields_present)).toBe(true); - } - - if (test.assertions.min_results !== undefined) { - expect(typeof test.assertions.min_results).toBe('number'); - } - }); - } }); diff --git a/data/census.json b/data/census.json new file mode 100644 index 0000000..bb69aab --- /dev/null +++ b/data/census.json @@ -0,0 +1,60 @@ +{ + "schema_version": "1.0", + "jurisdiction": "IL", + "portal": "knesset.gov.il + gov.il", + "generated": "2026-02-22", + "total_laws": 10, + "total_provisions": 135, + "laws": [ + { + "id": "basic-law-human-dignity-1992", + "title": "חוק יסוד: כבוד האדם וחירותו", + "provisions": 13 + }, + { + "id": "communications-law-1982", + "title": "חוק התקשורת (בזק ושידורים), תשמ\"ב-1982", + "provisions": 6 + }, + { + "id": "companies-law-1999", + "title": "חוק החברות, תשנ\"ט-1999", + "provisions": 6 + }, + { + "id": "computer-law-1995", + "title": "חוק המחשבים, תשנ\"ה-1995", + "provisions": 21 + }, + { + "id": "credit-data-law-2002", + "title": "חוק נתוני אשראי, תס\"ב-2002", + "provisions": 6 + }, + { + "id": "data-security-regulations-2017", + "title": "תקנות הגנת הפרטיות (אבטחת מידע), תשע\"ז-2017", + "provisions": 11 + }, + { + "id": "electronic-signature-law-2001", + "title": "חוק חתימה אלקטרונית, תס\"א-2001", + "provisions": 6 + }, + { + "id": "freedom-of-information-law-1998", + "title": "חוק חופש המידע, תשנ\"ח-1998", + "provisions": 6 + }, + { + "id": "privacy-protection-law-1981", + "title": "חוק הגנת הפרטיות, תשמ\"א-1981", + "provisions": 55 + }, + { + "id": "regulation-of-security-1998", + "title": "חוק הסדרת האבטחה בגופים ציבוריים, תשנ\"ח-1998", + "provisions": 5 + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index c025e15..1747b72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ansvar/israel-law-mcp", - "version": "1.0.0", + "version": "1.1.0", "mcpName": "eu.ansvar/israel-law-mcp", "description": "Israel law database covering Privacy Protection Law, Data Security Regulations, Computer Law, Companies Law, Electronic Signature Law, and Credit Data Law with full-text search", "author": "Ansvar Systems AB ", diff --git a/server.json b/server.json index 7dca8af..999bd36 100644 --- a/server.json +++ b/server.json @@ -1,20 +1,31 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "eu.ansvar/israel-law-mcp", - "description": "Israel law database covering Privacy Protection Law, Data Security Regulations, Computer Law, Companies Law, Electronic Signature Law, and Credit Data Law with full-text search", + "description": "Israel legislation via MCP — full-text search across statutes and provisions", "repository": { "url": "https://github.com/Ansvar-Systems/israel-law-mcp", "source": "github" }, "homepage": "https://ansvar.eu", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0", "packages": [ { "registryType": "npm", "identifier": "@ansvar/israel-law-mcp", - "version": "1.0.0", - "transport": { "type": "stdio" } + "version": "1.1.0", + "transport": { + "type": "stdio" + } + }, + { + "registryType": "npm", + "identifier": "@ansvar/israel-law-mcp", + "version": "1.1.0", + "transport": { + "type": "streamable-http", + "url": "https://israel-law-mcp.vercel.app/mcp" + } } ] } diff --git a/src/constants.ts b/src/constants.ts index 583aa07..2161775 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ export const SERVER_NAME = 'israel-law-mcp'; -export const SERVER_VERSION = '1.0.0'; +export const SERVER_VERSION = '1.1.0'; export const SERVER_LABEL = 'Israel Law MCP'; export const PACKAGE_NAME = '@ansvar/israel-law-mcp'; export const REPOSITORY_URL = 'https://github.com/Ansvar-Systems/israel-law-mcp';