@contractspec/example.versioned-knowledge-base 0.0.0-canary-20260113162409
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/.turbo/turbo-build$colon$bundle.log +47 -0
- package/.turbo/turbo-build.log +48 -0
- package/CHANGELOG.md +302 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.js +1 -0
- package/dist/docs/versioned-knowledge-base.docblock.d.ts +1 -0
- package/dist/docs/versioned-knowledge-base.docblock.js +31 -0
- package/dist/docs/versioned-knowledge-base.docblock.js.map +1 -0
- package/dist/entities/index.d.ts +2 -0
- package/dist/entities/index.js +3 -0
- package/dist/entities/models.d.ts +139 -0
- package/dist/entities/models.d.ts.map +1 -0
- package/dist/entities/models.js +151 -0
- package/dist/entities/models.js.map +1 -0
- package/dist/events.d.ts +63 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +124 -0
- package/dist/events.js.map +1 -0
- package/dist/example.d.ts +7 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +49 -0
- package/dist/example.js.map +1 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/memory.handlers.d.ts +78 -0
- package/dist/handlers/memory.handlers.d.ts.map +1 -0
- package/dist/handlers/memory.handlers.js +103 -0
- package/dist/handlers/memory.handlers.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/dist/operations/index.d.ts +2 -0
- package/dist/operations/index.js +3 -0
- package/dist/operations/kb.d.ts +267 -0
- package/dist/operations/kb.d.ts.map +1 -0
- package/dist/operations/kb.js +256 -0
- package/dist/operations/kb.js.map +1 -0
- package/dist/versioned-knowledge-base.feature.d.ts +7 -0
- package/dist/versioned-knowledge-base.feature.d.ts.map +1 -0
- package/dist/versioned-knowledge-base.feature.js +70 -0
- package/dist/versioned-knowledge-base.feature.js.map +1 -0
- package/example.ts +1 -0
- package/package.json +71 -0
- package/src/docs/index.ts +1 -0
- package/src/docs/versioned-knowledge-base.docblock.ts +29 -0
- package/src/entities/index.ts +1 -0
- package/src/entities/models.ts +75 -0
- package/src/events.ts +102 -0
- package/src/example.ts +34 -0
- package/src/handlers/index.ts +1 -0
- package/src/handlers/memory.handlers.test.ts +81 -0
- package/src/handlers/memory.handlers.ts +216 -0
- package/src/index.ts +13 -0
- package/src/operations/index.ts +1 -0
- package/src/operations/kb.ts +201 -0
- package/src/versioned-knowledge-base.feature.ts +34 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contractspec/example.versioned-knowledge-base",
|
|
3
|
+
"version": "0.0.0-canary-20260113162409",
|
|
4
|
+
"description": "Example: curated, versioned knowledge base with immutable sources, rule versions, and published snapshots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./docs": "./dist/docs/index.js",
|
|
10
|
+
"./docs/versioned-knowledge-base.docblock": "./dist/docs/versioned-knowledge-base.docblock.js",
|
|
11
|
+
"./entities": "./dist/entities/index.js",
|
|
12
|
+
"./entities/models": "./dist/entities/models.js",
|
|
13
|
+
"./events": "./dist/events.js",
|
|
14
|
+
"./example": "./dist/example.js",
|
|
15
|
+
"./handlers": "./dist/handlers/index.js",
|
|
16
|
+
"./handlers/memory.handlers": "./dist/handlers/memory.handlers.js",
|
|
17
|
+
"./operations": "./dist/operations/index.js",
|
|
18
|
+
"./operations/kb": "./dist/operations/kb.js",
|
|
19
|
+
"./versioned-knowledge-base.feature": "./dist/versioned-knowledge-base.feature.js",
|
|
20
|
+
"./*": "./*"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
24
|
+
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
25
|
+
"build": "bun build:types && bun build:bundle",
|
|
26
|
+
"build:bundle": "tsdown",
|
|
27
|
+
"build:types": "tsc --noEmit",
|
|
28
|
+
"dev": "bun build:bundle --watch",
|
|
29
|
+
"clean": "rimraf dist .turbo",
|
|
30
|
+
"lint": "bun lint:fix",
|
|
31
|
+
"lint:fix": "eslint src --fix",
|
|
32
|
+
"lint:check": "eslint src",
|
|
33
|
+
"test": "bun test"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@contractspec/lib.contracts": "0.0.0-canary-20260113162409",
|
|
37
|
+
"@contractspec/lib.schema": "0.0.0-canary-20260113162409"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@contractspec/tool.tsdown": "0.0.0-canary-20260113162409",
|
|
41
|
+
"@contractspec/tool.typescript": "0.0.0-canary-20260113162409",
|
|
42
|
+
"tsdown": "^0.19.0",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"exports": {
|
|
48
|
+
".": "./dist/index.js",
|
|
49
|
+
"./contracts": "./dist/contracts/index.js",
|
|
50
|
+
"./contracts/kb": "./dist/contracts/kb.js",
|
|
51
|
+
"./docs": "./dist/docs/index.js",
|
|
52
|
+
"./docs/versioned-knowledge-base.docblock": "./dist/docs/versioned-knowledge-base.docblock.js",
|
|
53
|
+
"./entities": "./dist/entities/index.js",
|
|
54
|
+
"./entities/models": "./dist/entities/models.js",
|
|
55
|
+
"./events": "./dist/events.js",
|
|
56
|
+
"./example": "./dist/example.js",
|
|
57
|
+
"./handlers": "./dist/handlers/index.js",
|
|
58
|
+
"./handlers/memory.handlers": "./dist/handlers/memory.handlers.js",
|
|
59
|
+
"./versioned-knowledge-base.feature": "./dist/versioned-knowledge-base.feature.js",
|
|
60
|
+
"./*": "./*"
|
|
61
|
+
},
|
|
62
|
+
"registry": "https://registry.npmjs.org/"
|
|
63
|
+
},
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "https://github.com/lssm-tech/contractspec.git",
|
|
68
|
+
"directory": "packages/examples/versioned-knowledge-base"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://contractspec.io"
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './versioned-knowledge-base.docblock';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DocBlock } from '@contractspec/lib.contracts/docs';
|
|
2
|
+
import { registerDocBlocks } from '@contractspec/lib.contracts/docs';
|
|
3
|
+
|
|
4
|
+
const docBlocks: DocBlock[] = [
|
|
5
|
+
{
|
|
6
|
+
id: 'docs.examples.versioned-knowledge-base.goal',
|
|
7
|
+
title: 'Versioned Knowledge Base — Goal',
|
|
8
|
+
summary:
|
|
9
|
+
'Curated KB with immutable sources, versioned rules, and published snapshots referenced by answers.',
|
|
10
|
+
kind: 'goal',
|
|
11
|
+
visibility: 'public',
|
|
12
|
+
route: '/docs/examples/versioned-knowledge-base/goal',
|
|
13
|
+
tags: ['knowledge', 'versioning', 'snapshots', 'traceability'],
|
|
14
|
+
body: `## Why it matters
|
|
15
|
+
- Separates raw sources from curated knowledge.\n- Ensures assistant answers cite a published snapshot.\n- Makes change review and safe regeneration possible.\n\n## Core invariants\n- Sources are immutable and content-addressed (hash).\n- Rule versions must cite at least one source.\n- Snapshots include only approved rule versions.`,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'docs.examples.versioned-knowledge-base.reference',
|
|
19
|
+
title: 'Versioned Knowledge Base — Reference',
|
|
20
|
+
summary: 'Entities, contracts, and events for the versioned KB example.',
|
|
21
|
+
kind: 'reference',
|
|
22
|
+
visibility: 'public',
|
|
23
|
+
route: '/docs/examples/versioned-knowledge-base',
|
|
24
|
+
tags: ['knowledge', 'reference'],
|
|
25
|
+
body: `## Contracts\n- kb.ingestSource\n- kb.upsertRuleVersion\n- kb.approveRuleVersion\n- kb.publishSnapshot\n- kb.search\n\n## Events\n- kb.source.ingested\n- kb.ruleVersion.created\n- kb.ruleVersion.approved\n- kb.snapshot.published`,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
registerDocBlocks(docBlocks);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './models';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ScalarTypeEnum, defineSchemaModel } from '@contractspec/lib.schema';
|
|
2
|
+
|
|
3
|
+
export const SourceDocumentModel = defineSchemaModel({
|
|
4
|
+
name: 'SourceDocument',
|
|
5
|
+
description:
|
|
6
|
+
'Immutable raw source document metadata referencing a stored file.',
|
|
7
|
+
fields: {
|
|
8
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
9
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
10
|
+
authority: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
11
|
+
title: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
12
|
+
fetchedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
13
|
+
hash: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
14
|
+
fileId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const SourceRefModel = defineSchemaModel({
|
|
19
|
+
name: 'SourceRef',
|
|
20
|
+
description: 'Reference to a source document used to justify a rule version.',
|
|
21
|
+
fields: {
|
|
22
|
+
sourceDocumentId: {
|
|
23
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
24
|
+
isOptional: false,
|
|
25
|
+
},
|
|
26
|
+
excerpt: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const RuleModel = defineSchemaModel({
|
|
31
|
+
name: 'Rule',
|
|
32
|
+
description:
|
|
33
|
+
'Curated rule (stable identity) with topic + jurisdiction scope.',
|
|
34
|
+
fields: {
|
|
35
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
36
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
37
|
+
topicKey: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const RuleVersionModel = defineSchemaModel({
|
|
42
|
+
name: 'RuleVersion',
|
|
43
|
+
description:
|
|
44
|
+
'A versioned rule content with source references and approval status.',
|
|
45
|
+
fields: {
|
|
46
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
47
|
+
ruleId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
48
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
49
|
+
topicKey: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
50
|
+
version: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
51
|
+
content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
52
|
+
sourceRefs: { type: SourceRefModel, isArray: true, isOptional: false },
|
|
53
|
+
status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }, // draft|approved|rejected
|
|
54
|
+
approvedBy: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
55
|
+
approvedAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
56
|
+
createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const KBSnapshotModel = defineSchemaModel({
|
|
61
|
+
name: 'KBSnapshot',
|
|
62
|
+
description:
|
|
63
|
+
'Published KB snapshot (as-of) referencing approved rule versions.',
|
|
64
|
+
fields: {
|
|
65
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
66
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
67
|
+
asOfDate: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
68
|
+
includedRuleVersionIds: {
|
|
69
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
70
|
+
isArray: true,
|
|
71
|
+
isOptional: false,
|
|
72
|
+
},
|
|
73
|
+
publishedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
74
|
+
},
|
|
75
|
+
});
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { defineEvent, defineSchemaModel } from '@contractspec/lib.contracts';
|
|
2
|
+
import { ScalarTypeEnum } from '@contractspec/lib.schema';
|
|
3
|
+
|
|
4
|
+
const KbSourceIngestedPayload = defineSchemaModel({
|
|
5
|
+
name: 'KbSourceIngestedPayload',
|
|
6
|
+
description: 'Emitted when a source document is ingested.',
|
|
7
|
+
fields: {
|
|
8
|
+
sourceDocumentId: {
|
|
9
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
10
|
+
isOptional: false,
|
|
11
|
+
},
|
|
12
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
13
|
+
hash: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const KbSourceIngestedEvent = defineEvent({
|
|
18
|
+
meta: {
|
|
19
|
+
key: 'kb.source.ingested',
|
|
20
|
+
version: '1.0.0',
|
|
21
|
+
description: 'Source document ingested (immutable).',
|
|
22
|
+
stability: 'experimental',
|
|
23
|
+
owners: ['@examples'],
|
|
24
|
+
tags: ['knowledge'],
|
|
25
|
+
},
|
|
26
|
+
payload: KbSourceIngestedPayload,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const KbRuleVersionCreatedPayload = defineSchemaModel({
|
|
30
|
+
name: 'KbRuleVersionCreatedPayload',
|
|
31
|
+
description: 'Emitted when a rule version draft is created.',
|
|
32
|
+
fields: {
|
|
33
|
+
ruleVersionId: {
|
|
34
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
35
|
+
isOptional: false,
|
|
36
|
+
},
|
|
37
|
+
ruleId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
38
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
39
|
+
status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const KbRuleVersionCreatedEvent = defineEvent({
|
|
44
|
+
meta: {
|
|
45
|
+
key: 'kb.ruleVersion.created',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
description: 'Rule version created (draft).',
|
|
48
|
+
stability: 'experimental',
|
|
49
|
+
owners: ['@examples'],
|
|
50
|
+
tags: ['knowledge'],
|
|
51
|
+
},
|
|
52
|
+
payload: KbRuleVersionCreatedPayload,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const KbRuleVersionApprovedPayload = defineSchemaModel({
|
|
56
|
+
name: 'KbRuleVersionApprovedPayload',
|
|
57
|
+
description: 'Emitted when a rule version is approved.',
|
|
58
|
+
fields: {
|
|
59
|
+
ruleVersionId: {
|
|
60
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
61
|
+
isOptional: false,
|
|
62
|
+
},
|
|
63
|
+
approver: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const KbRuleVersionApprovedEvent = defineEvent({
|
|
68
|
+
meta: {
|
|
69
|
+
key: 'kb.ruleVersion.approved',
|
|
70
|
+
version: '1.0.0',
|
|
71
|
+
description: 'Rule version approved (human verified).',
|
|
72
|
+
stability: 'experimental',
|
|
73
|
+
owners: ['@examples'],
|
|
74
|
+
tags: ['knowledge'],
|
|
75
|
+
},
|
|
76
|
+
payload: KbRuleVersionApprovedPayload,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const KbSnapshotPublishedPayload = defineSchemaModel({
|
|
80
|
+
name: 'KbSnapshotPublishedPayload',
|
|
81
|
+
description: 'Emitted when a KB snapshot is published.',
|
|
82
|
+
fields: {
|
|
83
|
+
snapshotId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
84
|
+
jurisdiction: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
85
|
+
includedRuleVersionsCount: {
|
|
86
|
+
type: ScalarTypeEnum.Int_unsecure(),
|
|
87
|
+
isOptional: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const KbSnapshotPublishedEvent = defineEvent({
|
|
93
|
+
meta: {
|
|
94
|
+
key: 'kb.snapshot.published',
|
|
95
|
+
version: '1.0.0',
|
|
96
|
+
description: 'KB snapshot published.',
|
|
97
|
+
stability: 'experimental',
|
|
98
|
+
owners: ['@examples'],
|
|
99
|
+
tags: ['knowledge'],
|
|
100
|
+
},
|
|
101
|
+
payload: KbSnapshotPublishedPayload,
|
|
102
|
+
});
|
package/src/example.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineExample } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
const example = defineExample({
|
|
4
|
+
meta: {
|
|
5
|
+
key: 'versioned-knowledge-base',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
title: 'Versioned Knowledge Base',
|
|
8
|
+
description:
|
|
9
|
+
'Curated KB with immutable sources, reviewable rule versions, and published snapshots.',
|
|
10
|
+
kind: 'knowledge',
|
|
11
|
+
visibility: 'public',
|
|
12
|
+
stability: 'experimental',
|
|
13
|
+
owners: ['@platform.core'],
|
|
14
|
+
tags: ['knowledge', 'versioning', 'snapshots'],
|
|
15
|
+
},
|
|
16
|
+
docs: {
|
|
17
|
+
rootDocId: 'docs.examples.versioned-knowledge-base',
|
|
18
|
+
},
|
|
19
|
+
entrypoints: {
|
|
20
|
+
packageName: '@contractspec/example.versioned-knowledge-base',
|
|
21
|
+
feature: './feature',
|
|
22
|
+
contracts: './contracts',
|
|
23
|
+
handlers: './handlers',
|
|
24
|
+
docs: './docs',
|
|
25
|
+
},
|
|
26
|
+
surfaces: {
|
|
27
|
+
templates: true,
|
|
28
|
+
sandbox: { enabled: true, modes: ['markdown', 'specs', 'builder'] },
|
|
29
|
+
studio: { enabled: true, installable: true },
|
|
30
|
+
mcp: { enabled: true },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default example;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './memory.handlers';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { createMemoryKbHandlers, createMemoryKbStore } from './memory.handlers';
|
|
4
|
+
|
|
5
|
+
describe('@contractspec/example.versioned-knowledge-base memory handlers', () => {
|
|
6
|
+
it('requires sourceRefs for rule versions (traceability)', async () => {
|
|
7
|
+
const store = createMemoryKbStore();
|
|
8
|
+
const kb = createMemoryKbHandlers(store);
|
|
9
|
+
await kb.createRule({ id: 'rule_1', jurisdiction: 'EU', topicKey: 't1' });
|
|
10
|
+
await expect(
|
|
11
|
+
kb.upsertRuleVersion({ ruleId: 'rule_1', content: 'x', sourceRefs: [] })
|
|
12
|
+
).rejects.toThrow('SOURCE_REFS_REQUIRED');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('snapshot includes only approved rule versions for that jurisdiction', async () => {
|
|
16
|
+
const store = createMemoryKbStore();
|
|
17
|
+
const kb = createMemoryKbHandlers(store);
|
|
18
|
+
|
|
19
|
+
await kb.createRule({ id: 'rule_eu', jurisdiction: 'EU', topicKey: 'tax' });
|
|
20
|
+
await kb.createRule({ id: 'rule_fr', jurisdiction: 'FR', topicKey: 'tax' });
|
|
21
|
+
|
|
22
|
+
const euDraft = await kb.upsertRuleVersion({
|
|
23
|
+
ruleId: 'rule_eu',
|
|
24
|
+
content: 'EU rule content',
|
|
25
|
+
sourceRefs: [{ sourceDocumentId: 'src1' }],
|
|
26
|
+
});
|
|
27
|
+
const frDraft = await kb.upsertRuleVersion({
|
|
28
|
+
ruleId: 'rule_fr',
|
|
29
|
+
content: 'FR rule content',
|
|
30
|
+
sourceRefs: [{ sourceDocumentId: 'src2' }],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const euApproved = await kb.approveRuleVersion({
|
|
34
|
+
ruleVersionId: euDraft.id,
|
|
35
|
+
approver: 'expert_1',
|
|
36
|
+
});
|
|
37
|
+
await kb.approveRuleVersion({
|
|
38
|
+
ruleVersionId: frDraft.id,
|
|
39
|
+
approver: 'expert_1',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const snapshot = await kb.publishSnapshot({
|
|
43
|
+
jurisdiction: 'EU',
|
|
44
|
+
asOfDate: new Date('2026-01-01T00:00:00.000Z'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(snapshot.includedRuleVersionIds).toEqual([euApproved.id]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('search is scoped to snapshot + jurisdiction', async () => {
|
|
51
|
+
const store = createMemoryKbStore();
|
|
52
|
+
const kb = createMemoryKbHandlers(store);
|
|
53
|
+
|
|
54
|
+
await kb.createRule({
|
|
55
|
+
id: 'rule_eu',
|
|
56
|
+
jurisdiction: 'EU',
|
|
57
|
+
topicKey: 'topic',
|
|
58
|
+
});
|
|
59
|
+
const euDraft = await kb.upsertRuleVersion({
|
|
60
|
+
ruleId: 'rule_eu',
|
|
61
|
+
content: 'This is about reporting obligations',
|
|
62
|
+
sourceRefs: [{ sourceDocumentId: 'src1' }],
|
|
63
|
+
});
|
|
64
|
+
await kb.approveRuleVersion({ ruleVersionId: euDraft.id, approver: 'u1' });
|
|
65
|
+
const snap = await kb.publishSnapshot({
|
|
66
|
+
jurisdiction: 'EU',
|
|
67
|
+
asOfDate: new Date('2026-01-01T00:00:00.000Z'),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await expect(
|
|
71
|
+
kb.search({ snapshotId: snap.id, jurisdiction: 'FR', query: 'reporting' })
|
|
72
|
+
).rejects.toThrow('JURISDICTION_MISMATCH');
|
|
73
|
+
|
|
74
|
+
const ok = await kb.search({
|
|
75
|
+
snapshotId: snap.id,
|
|
76
|
+
jurisdiction: 'EU',
|
|
77
|
+
query: 'reporting',
|
|
78
|
+
});
|
|
79
|
+
expect(ok.items.map((i) => i.ruleVersionId)).toEqual([euDraft.id]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
interface SourceRef {
|
|
2
|
+
sourceDocumentId: string;
|
|
3
|
+
excerpt?: string;
|
|
4
|
+
}
|
|
5
|
+
interface SourceDocument {
|
|
6
|
+
id: string;
|
|
7
|
+
jurisdiction: string;
|
|
8
|
+
authority: string;
|
|
9
|
+
title: string;
|
|
10
|
+
fetchedAt: Date;
|
|
11
|
+
hash: string;
|
|
12
|
+
fileId: string;
|
|
13
|
+
}
|
|
14
|
+
interface Rule {
|
|
15
|
+
id: string;
|
|
16
|
+
jurisdiction: string;
|
|
17
|
+
topicKey: string;
|
|
18
|
+
}
|
|
19
|
+
interface RuleVersion {
|
|
20
|
+
id: string;
|
|
21
|
+
ruleId: string;
|
|
22
|
+
jurisdiction: string;
|
|
23
|
+
topicKey: string;
|
|
24
|
+
version: string;
|
|
25
|
+
content: string;
|
|
26
|
+
sourceRefs: SourceRef[];
|
|
27
|
+
status: 'draft' | 'approved' | 'rejected';
|
|
28
|
+
approvedBy?: string;
|
|
29
|
+
approvedAt?: Date;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
}
|
|
32
|
+
interface KBSnapshot {
|
|
33
|
+
id: string;
|
|
34
|
+
jurisdiction: string;
|
|
35
|
+
asOfDate: Date;
|
|
36
|
+
includedRuleVersionIds: string[];
|
|
37
|
+
publishedAt: Date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MemoryKbStore {
|
|
41
|
+
sources: Map<string, SourceDocument>;
|
|
42
|
+
rules: Map<string, Rule>;
|
|
43
|
+
ruleVersions: Map<string, RuleVersion>;
|
|
44
|
+
snapshots: Map<string, KBSnapshot>;
|
|
45
|
+
nextRuleVersionNumberByRuleId: Map<string, number>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createMemoryKbStore(): MemoryKbStore {
|
|
49
|
+
return {
|
|
50
|
+
sources: new Map(),
|
|
51
|
+
rules: new Map(),
|
|
52
|
+
ruleVersions: new Map(),
|
|
53
|
+
snapshots: new Map(),
|
|
54
|
+
nextRuleVersionNumberByRuleId: new Map(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MemoryKbHandlers {
|
|
59
|
+
createRule(rule: Rule): Promise<Rule>;
|
|
60
|
+
ingestSource(input: Omit<SourceDocument, 'id'>): Promise<SourceDocument>;
|
|
61
|
+
upsertRuleVersion(input: {
|
|
62
|
+
ruleId: string;
|
|
63
|
+
content: string;
|
|
64
|
+
sourceRefs: SourceRef[];
|
|
65
|
+
}): Promise<RuleVersion>;
|
|
66
|
+
approveRuleVersion(input: {
|
|
67
|
+
ruleVersionId: string;
|
|
68
|
+
approver: string;
|
|
69
|
+
}): Promise<RuleVersion>;
|
|
70
|
+
publishSnapshot(input: {
|
|
71
|
+
jurisdiction: string;
|
|
72
|
+
asOfDate: Date;
|
|
73
|
+
}): Promise<KBSnapshot>;
|
|
74
|
+
search(input: {
|
|
75
|
+
snapshotId: string;
|
|
76
|
+
jurisdiction: string;
|
|
77
|
+
query: string;
|
|
78
|
+
}): Promise<{ items: { ruleVersionId: string; excerpt?: string }[] }>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stableId(prefix: string, value: string): string {
|
|
82
|
+
return `${prefix}_${value.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createMemoryKbHandlers(store: MemoryKbStore): MemoryKbHandlers {
|
|
86
|
+
async function createRule(rule: Rule): Promise<Rule> {
|
|
87
|
+
store.rules.set(rule.id, rule);
|
|
88
|
+
return rule;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function ingestSource(
|
|
92
|
+
input: Omit<SourceDocument, 'id'>
|
|
93
|
+
): Promise<SourceDocument> {
|
|
94
|
+
const id = stableId('src', `${input.jurisdiction}_${input.hash}`);
|
|
95
|
+
const doc: SourceDocument = { id, ...input };
|
|
96
|
+
store.sources.set(id, doc);
|
|
97
|
+
return doc;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function upsertRuleVersion(input: {
|
|
101
|
+
ruleId: string;
|
|
102
|
+
content: string;
|
|
103
|
+
sourceRefs: SourceRef[];
|
|
104
|
+
}): Promise<RuleVersion> {
|
|
105
|
+
if (!input.sourceRefs.length) {
|
|
106
|
+
throw new Error('SOURCE_REFS_REQUIRED');
|
|
107
|
+
}
|
|
108
|
+
const rule = store.rules.get(input.ruleId);
|
|
109
|
+
if (!rule) {
|
|
110
|
+
throw new Error('RULE_NOT_FOUND');
|
|
111
|
+
}
|
|
112
|
+
const next =
|
|
113
|
+
(store.nextRuleVersionNumberByRuleId.get(input.ruleId) ?? 0) + 1;
|
|
114
|
+
const id = stableId('rv', `${input.ruleId}_${next}`);
|
|
115
|
+
const ruleVersion: RuleVersion = {
|
|
116
|
+
id,
|
|
117
|
+
ruleId: input.ruleId,
|
|
118
|
+
jurisdiction: rule.jurisdiction,
|
|
119
|
+
topicKey: rule.topicKey,
|
|
120
|
+
version: next.toString(),
|
|
121
|
+
content: input.content,
|
|
122
|
+
sourceRefs: input.sourceRefs,
|
|
123
|
+
status: 'draft',
|
|
124
|
+
createdAt: new Date(),
|
|
125
|
+
approvedAt: undefined,
|
|
126
|
+
approvedBy: undefined,
|
|
127
|
+
};
|
|
128
|
+
store.ruleVersions.set(id, ruleVersion);
|
|
129
|
+
return ruleVersion;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function approveRuleVersion(input: {
|
|
133
|
+
ruleVersionId: string;
|
|
134
|
+
approver: string;
|
|
135
|
+
}): Promise<RuleVersion> {
|
|
136
|
+
const existing = store.ruleVersions.get(input.ruleVersionId);
|
|
137
|
+
if (!existing) {
|
|
138
|
+
throw new Error('RULE_VERSION_NOT_FOUND');
|
|
139
|
+
}
|
|
140
|
+
const approved: RuleVersion = {
|
|
141
|
+
...existing,
|
|
142
|
+
status: 'approved',
|
|
143
|
+
approvedBy: input.approver,
|
|
144
|
+
approvedAt: new Date(),
|
|
145
|
+
};
|
|
146
|
+
store.ruleVersions.set(approved.id, approved);
|
|
147
|
+
return approved;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function publishSnapshot(input: {
|
|
151
|
+
jurisdiction: string;
|
|
152
|
+
asOfDate: Date;
|
|
153
|
+
}): Promise<KBSnapshot> {
|
|
154
|
+
const approved = [...store.ruleVersions.values()].filter(
|
|
155
|
+
(rv) => rv.status === 'approved' && rv.jurisdiction === input.jurisdiction
|
|
156
|
+
);
|
|
157
|
+
if (approved.length === 0) {
|
|
158
|
+
throw new Error('NO_APPROVED_RULES');
|
|
159
|
+
}
|
|
160
|
+
const includedRuleVersionIds = approved.map((rv) => rv.id).sort();
|
|
161
|
+
const id = stableId(
|
|
162
|
+
'snap',
|
|
163
|
+
`${input.jurisdiction}_${input.asOfDate.toISOString().slice(0, 10)}_${includedRuleVersionIds.length}`
|
|
164
|
+
);
|
|
165
|
+
const snapshot: KBSnapshot = {
|
|
166
|
+
id,
|
|
167
|
+
jurisdiction: input.jurisdiction,
|
|
168
|
+
asOfDate: input.asOfDate,
|
|
169
|
+
includedRuleVersionIds,
|
|
170
|
+
publishedAt: new Date(),
|
|
171
|
+
};
|
|
172
|
+
store.snapshots.set(id, snapshot);
|
|
173
|
+
return snapshot;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function search(input: {
|
|
177
|
+
snapshotId: string;
|
|
178
|
+
jurisdiction: string;
|
|
179
|
+
query: string;
|
|
180
|
+
}): Promise<{ items: { ruleVersionId: string; excerpt?: string }[] }> {
|
|
181
|
+
const snapshot = store.snapshots.get(input.snapshotId);
|
|
182
|
+
if (!snapshot) {
|
|
183
|
+
throw new Error('SNAPSHOT_NOT_FOUND');
|
|
184
|
+
}
|
|
185
|
+
if (snapshot.jurisdiction !== input.jurisdiction) {
|
|
186
|
+
throw new Error('JURISDICTION_MISMATCH');
|
|
187
|
+
}
|
|
188
|
+
const q = input.query.toLowerCase();
|
|
189
|
+
const tokens = q
|
|
190
|
+
.split(/\s+/)
|
|
191
|
+
.map((t) => t.trim())
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
const items = snapshot.includedRuleVersionIds
|
|
194
|
+
.map((id) => store.ruleVersions.get(id))
|
|
195
|
+
.filter((rv): rv is RuleVersion => Boolean(rv))
|
|
196
|
+
.filter((rv) => {
|
|
197
|
+
if (tokens.length === 0) return true;
|
|
198
|
+
const hay = rv.content.toLowerCase();
|
|
199
|
+
return tokens.every((token) => hay.includes(token));
|
|
200
|
+
})
|
|
201
|
+
.map((rv) => ({
|
|
202
|
+
ruleVersionId: rv.id,
|
|
203
|
+
excerpt: rv.content.slice(0, 120),
|
|
204
|
+
}));
|
|
205
|
+
return { items };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
createRule,
|
|
210
|
+
ingestSource,
|
|
211
|
+
upsertRuleVersion,
|
|
212
|
+
approveRuleVersion,
|
|
213
|
+
publishSnapshot,
|
|
214
|
+
search,
|
|
215
|
+
};
|
|
216
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned Knowledge Base Example
|
|
3
|
+
*
|
|
4
|
+
* Curated KB with immutable sources, reviewable rule versions, and published snapshots.
|
|
5
|
+
*/
|
|
6
|
+
export * from './entities';
|
|
7
|
+
export * from './operations';
|
|
8
|
+
export * from './events';
|
|
9
|
+
export * from './handlers';
|
|
10
|
+
export * from './versioned-knowledge-base.feature';
|
|
11
|
+
export { default as example } from './example';
|
|
12
|
+
|
|
13
|
+
import './docs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './kb';
|