@isparling/engram-cli 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/LICENSE +187 -0
- package/README.md +41 -0
- package/bin/engram +14 -0
- package/package.json +32 -0
- package/release/engram-release.ts +1493 -0
- package/src/atomicWrite.ts +80 -0
- package/src/candidate.ts +229 -0
- package/src/classify.ts +275 -0
- package/src/cli.ts +709 -0
- package/src/contentHash.ts +54 -0
- package/src/deepFreeze.ts +13 -0
- package/src/diff.ts +81 -0
- package/src/guardedRetrieval.ts +128 -0
- package/src/guardedRetrievalInternal.ts +321 -0
- package/src/knowledgeRecord.ts +256 -0
- package/src/knowledgeRetrieval.ts +564 -0
- package/src/knowledgeRollup.ts +479 -0
- package/src/knowledgeTransaction.ts +683 -0
- package/src/knowledgeTypes.ts +249 -0
- package/src/knowledgeValidation.ts +269 -0
- package/src/markdownRecord.ts +265 -0
- package/src/packLoader.ts +188 -0
- package/src/packTypes.ts +12 -0
- package/src/presentation.ts +673 -0
- package/src/qmdConfigGuard.ts +245 -0
- package/src/qmdRunner.ts +392 -0
- package/src/realPath.ts +47 -0
- package/src/spaceBinding.ts +37 -0
- package/src/spaceRegistry.ts +1139 -0
- package/src/submit.ts +228 -0
- package/src/symlinkGuard.ts +71 -0
- package/src/transactionLock.ts +188 -0
- package/src/types.ts +38 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Generic knowledge boundary. This file contains only the
|
|
2
|
+
// envelope and pack contract types; it deliberately has no filesystem, qmd,
|
|
3
|
+
// registry, hashing, or transaction imports.
|
|
4
|
+
|
|
5
|
+
export const KNOWLEDGE_KINDS = [
|
|
6
|
+
"evidence",
|
|
7
|
+
"claim",
|
|
8
|
+
"interpretation",
|
|
9
|
+
"decision",
|
|
10
|
+
"recommendation",
|
|
11
|
+
] as const;
|
|
12
|
+
export type KnowledgeKind = (typeof KNOWLEDGE_KINDS)[number];
|
|
13
|
+
|
|
14
|
+
export const KNOWLEDGE_STATUSES = ["candidate", "active", "contested", "retired"] as const;
|
|
15
|
+
export type KnowledgeStatus = (typeof KNOWLEDGE_STATUSES)[number];
|
|
16
|
+
|
|
17
|
+
export const KNOWLEDGE_DISPOSITIONS = ["new", "support", "contradict", "refine", "supersede", "no-change"] as const;
|
|
18
|
+
export type KnowledgeDisposition = (typeof KNOWLEDGE_DISPOSITIONS)[number];
|
|
19
|
+
|
|
20
|
+
export const RELATIONSHIP_KINDS = ["supports", "contradicts", "refines", "supersedes"] as const;
|
|
21
|
+
export type RelationshipKind = (typeof RELATIONSHIP_KINDS)[number];
|
|
22
|
+
|
|
23
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
24
|
+
export type JsonObject = { [key: string]: JsonValue };
|
|
25
|
+
|
|
26
|
+
export type KnowledgePackRef = {
|
|
27
|
+
id: string;
|
|
28
|
+
version: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type KnowledgeScope = {
|
|
32
|
+
space: string;
|
|
33
|
+
subjects: string[];
|
|
34
|
+
topics: string[];
|
|
35
|
+
contexts: string[];
|
|
36
|
+
dimensions: Record<string, string[]>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type KnowledgeSource = {
|
|
40
|
+
type: string;
|
|
41
|
+
ref: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type HostSessionProvenance = {
|
|
45
|
+
id: string;
|
|
46
|
+
host: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type KnowledgeEnvelope = {
|
|
50
|
+
id: string;
|
|
51
|
+
kind: KnowledgeKind;
|
|
52
|
+
status: KnowledgeStatus;
|
|
53
|
+
statement: string;
|
|
54
|
+
details: JsonObject;
|
|
55
|
+
scope: KnowledgeScope;
|
|
56
|
+
pack: KnowledgePackRef;
|
|
57
|
+
sources: KnowledgeSource[];
|
|
58
|
+
session: HostSessionProvenance;
|
|
59
|
+
submittedAt: string;
|
|
60
|
+
disposition: KnowledgeDisposition;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type KnowledgeRelationships = {
|
|
64
|
+
supports: string[];
|
|
65
|
+
contradicts: string[];
|
|
66
|
+
refines: string[];
|
|
67
|
+
supersedes: string[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type KnowledgeHistoryEntry = {
|
|
71
|
+
event: string;
|
|
72
|
+
relatedId: string;
|
|
73
|
+
submittedAt: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type KnowledgeRecord = KnowledgeEnvelope & {
|
|
77
|
+
schemaVersion: 0;
|
|
78
|
+
relationships: KnowledgeRelationships;
|
|
79
|
+
history: KnowledgeHistoryEntry[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type KnowledgeErrorKind =
|
|
83
|
+
| "validation"
|
|
84
|
+
| "retrieval"
|
|
85
|
+
| "plan"
|
|
86
|
+
| "approval"
|
|
87
|
+
| "transaction"
|
|
88
|
+
| "lock"
|
|
89
|
+
| "authorization"
|
|
90
|
+
| "presentation"
|
|
91
|
+
| "artifact";
|
|
92
|
+
|
|
93
|
+
export type KnowledgeError = {
|
|
94
|
+
kind: KnowledgeErrorKind;
|
|
95
|
+
code: string;
|
|
96
|
+
field?: string;
|
|
97
|
+
message: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type KnowledgeResult<T> = { ok: true; value: T } | { ok: false; errors: KnowledgeError[] };
|
|
101
|
+
|
|
102
|
+
export type RelatedKnowledgeRecord = {
|
|
103
|
+
record: KnowledgeRecord;
|
|
104
|
+
relativePath: string;
|
|
105
|
+
sourceUri: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type PackReconcileInput = {
|
|
109
|
+
candidate: KnowledgeEnvelope;
|
|
110
|
+
related: readonly KnowledgeRecord[];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type PackMutation = {
|
|
114
|
+
action: "create" | "update";
|
|
115
|
+
record: KnowledgeRecord;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type PackReconciliation = {
|
|
119
|
+
disposition: KnowledgeDisposition;
|
|
120
|
+
summary: string;
|
|
121
|
+
mutations: PackMutation[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type KnowledgePack = {
|
|
125
|
+
id: string;
|
|
126
|
+
version: string;
|
|
127
|
+
validateEnvelope: (envelope: KnowledgeEnvelope) => KnowledgeResult<void>;
|
|
128
|
+
relatedQuery: (envelope: KnowledgeEnvelope) => string;
|
|
129
|
+
reconcile: (input: PackReconcileInput) => KnowledgeResult<PackReconciliation>;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type QueryStrategyInput = {
|
|
133
|
+
query: string;
|
|
134
|
+
viewId?: string;
|
|
135
|
+
requestedSourceClasses: readonly string[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type SourceClassPolicy = {
|
|
139
|
+
allowedSourceClasses: readonly string[];
|
|
140
|
+
queryStrategy: (input: QueryStrategyInput) => string;
|
|
141
|
+
classifySource: (source: KnowledgeSource) => string;
|
|
142
|
+
relevanceThreshold: number | null;
|
|
143
|
+
isEligible: (record: KnowledgeRecord) => boolean;
|
|
144
|
+
includePresentations: false;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type SemanticProjection = {
|
|
148
|
+
title: string;
|
|
149
|
+
summary: string;
|
|
150
|
+
facts: string[];
|
|
151
|
+
requiredFacts: string[];
|
|
152
|
+
uncertainty: string[];
|
|
153
|
+
actions: string[];
|
|
154
|
+
recommendationIds: string[];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type PresentationDraft = {
|
|
158
|
+
title: string;
|
|
159
|
+
summary: string;
|
|
160
|
+
facts: string[];
|
|
161
|
+
uncertainty: string[];
|
|
162
|
+
actions: string[];
|
|
163
|
+
recommendationIds: string[];
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type ViewDefinition = {
|
|
167
|
+
id: string;
|
|
168
|
+
version: number;
|
|
169
|
+
// "search" runs the pack's declared query strategy over ranked qmd
|
|
170
|
+
// results. "space" enumerates every Markdown record under the active
|
|
171
|
+
// space's records root instead: no query, no ranking, no relevance
|
|
172
|
+
// threshold. A profile view is an enumeration; guarded retrieval over a
|
|
173
|
+
// search is a different operation, and no literal query stands in for
|
|
174
|
+
// "every active record" reliably. Required, not defaulted, so a view
|
|
175
|
+
// author cannot omit the most consequential thing about their view.
|
|
176
|
+
scope: "search" | "space";
|
|
177
|
+
retrievalQuery: (requestedQuery?: string) => string;
|
|
178
|
+
project: (records: readonly KnowledgeRecord[]) => SemanticProjection;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type AudienceAdaptationInput = {
|
|
182
|
+
projection: SemanticProjection;
|
|
183
|
+
delivery: DeliveryDefinition;
|
|
184
|
+
records: readonly KnowledgeRecord[];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type AudienceDefinition = {
|
|
188
|
+
id: string;
|
|
189
|
+
version: number;
|
|
190
|
+
authorize: (record: KnowledgeRecord) => boolean;
|
|
191
|
+
adapt: (input: AudienceAdaptationInput) => PresentationDraft;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type DeliveryDefinition = {
|
|
195
|
+
id: string;
|
|
196
|
+
version: number;
|
|
197
|
+
format: "markdown" | "plain" | "json";
|
|
198
|
+
maxWords: number;
|
|
199
|
+
retain: boolean;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type TurnToolCall = {
|
|
203
|
+
tool: string;
|
|
204
|
+
input: Record<string, unknown>;
|
|
205
|
+
result: unknown;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type TurnContext = {
|
|
209
|
+
session: HostSessionProvenance;
|
|
210
|
+
turnIndex: number;
|
|
211
|
+
timestamp: string;
|
|
212
|
+
narrative: string;
|
|
213
|
+
toolCalls: TurnToolCall[];
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export type LlmHelper = {
|
|
217
|
+
complete(prompt: string, options?: { system?: string }): Promise<string>;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type PackHelpers = {
|
|
221
|
+
llm?: LlmHelper;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Async, LLM-backed candidate extraction from raw session transcripts.
|
|
226
|
+
*
|
|
227
|
+
* Called by the host integration at a lifecycle-triggered capture point,
|
|
228
|
+
* once the host determines the session (or turn) is complete.
|
|
229
|
+
* This is separate from the synchronous KnowledgePack interface used by
|
|
230
|
+
* the transaction pipeline — extraction is non-deterministic and may use
|
|
231
|
+
* LLM inference.
|
|
232
|
+
*
|
|
233
|
+
* External pack repos implement this interface. The pack is injected via
|
|
234
|
+
* the binding's `installed_packs[].from` path.
|
|
235
|
+
*/
|
|
236
|
+
export type KnowledgeExtractor = {
|
|
237
|
+
id: string;
|
|
238
|
+
version: string;
|
|
239
|
+
extractCandidates(turn: TurnContext, helpers: PackHelpers): Promise<Record<string, unknown>[]>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export type PresentationPack = {
|
|
243
|
+
id: string;
|
|
244
|
+
version: string;
|
|
245
|
+
retrievalPolicy: SourceClassPolicy;
|
|
246
|
+
views: readonly ViewDefinition[];
|
|
247
|
+
audiences: readonly AudienceDefinition[];
|
|
248
|
+
deliveries: readonly DeliveryDefinition[];
|
|
249
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KNOWLEDGE_DISPOSITIONS,
|
|
3
|
+
KNOWLEDGE_KINDS,
|
|
4
|
+
KNOWLEDGE_STATUSES,
|
|
5
|
+
type HostSessionProvenance,
|
|
6
|
+
type JsonObject,
|
|
7
|
+
type JsonValue,
|
|
8
|
+
type KnowledgeDisposition,
|
|
9
|
+
type KnowledgeEnvelope,
|
|
10
|
+
type KnowledgeError,
|
|
11
|
+
type KnowledgePackRef,
|
|
12
|
+
type KnowledgeResult,
|
|
13
|
+
type KnowledgeScope,
|
|
14
|
+
type KnowledgeSource,
|
|
15
|
+
} from "./knowledgeTypes.ts";
|
|
16
|
+
|
|
17
|
+
const ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
18
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
19
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
|
|
21
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function error(code: string, message: string, field?: string): KnowledgeError {
|
|
26
|
+
return field === undefined
|
|
27
|
+
? { kind: "validation", code, message }
|
|
28
|
+
: { kind: "validation", code, field, message };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function unknownKeys(value: Record<string, unknown>, allowed: readonly string[]): string[] {
|
|
32
|
+
const allowedSet = new Set(allowed);
|
|
33
|
+
return Object.keys(value).filter((key) => !allowedSet.has(key));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
37
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
38
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
39
|
+
if (Array.isArray(value)) return value.every((item) => isJsonValue(item));
|
|
40
|
+
if (!isObject(value)) return false;
|
|
41
|
+
return Object.values(value).every((item) => isJsonValue(item));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findNewline(value: JsonValue, field: string, errors: KnowledgeError[]): void {
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
if (/[\r\n]/.test(value)) errors.push(error("newline_forbidden", `${field} must not contain newlines`, field));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
value.forEach((item, index) => findNewline(item, `${field}[${index}]`, errors));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (value !== null && typeof value === "object") {
|
|
54
|
+
for (const [key, item] of Object.entries(value)) findNewline(item, `${field}.${key}`, errors);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function nonEmptySingleLine(value: unknown, field: string, errors: KnowledgeError[], pattern?: RegExp): string | undefined {
|
|
59
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
60
|
+
errors.push(error("field_invalid", `${field} must be a non-empty string`, field));
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (/[\r\n]/.test(value)) {
|
|
64
|
+
errors.push(error("newline_forbidden", `${field} must not contain newlines`, field));
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (pattern !== undefined && !pattern.test(value)) {
|
|
68
|
+
errors.push(error("field_invalid", `${field} has an invalid structure`, field));
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function enumValue<T extends string>(value: unknown, allowed: readonly T[], field: string, errors: KnowledgeError[], code: string): T | undefined {
|
|
75
|
+
if (typeof value !== "string") {
|
|
76
|
+
errors.push(error(code, `${field} must be one of ${allowed.join(", ")}`, field));
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const selected = allowed.find((item) => item === value);
|
|
80
|
+
if (selected === undefined) {
|
|
81
|
+
errors.push(error(code, `${field} must be one of ${allowed.join(", ")}`, field));
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return selected;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stringArray(value: unknown, field: string, errors: KnowledgeError[]): string[] | undefined {
|
|
88
|
+
if (!Array.isArray(value)) {
|
|
89
|
+
errors.push(error("array_invalid", `${field} must be an array of strings`, field));
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const result: string[] = [];
|
|
93
|
+
for (let index = 0; index < value.length; index++) {
|
|
94
|
+
const parsed = nonEmptySingleLine(value[index], `${field}[${index}]`, errors);
|
|
95
|
+
if (parsed !== undefined) result.push(parsed);
|
|
96
|
+
}
|
|
97
|
+
return result.length === value.length ? result : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parsePackRef(value: unknown, field: string, errors: KnowledgeError[]): KnowledgePackRef | undefined {
|
|
101
|
+
if (!isObject(value)) {
|
|
102
|
+
errors.push(error("pack_invalid", `${field} must be an object`, field));
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
for (const key of unknownKeys(value, ["id", "version"])) {
|
|
106
|
+
errors.push(error("unknown_field", `${field} contains unknown field ${key}`, `${field}.${key}`));
|
|
107
|
+
}
|
|
108
|
+
const id = nonEmptySingleLine(value.id, `${field}.id`, errors, ID_PATTERN);
|
|
109
|
+
const version = nonEmptySingleLine(value.version, `${field}.version`, errors);
|
|
110
|
+
return id !== undefined && version !== undefined ? { id, version } : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseScope(value: unknown, errors: KnowledgeError[]): KnowledgeScope | undefined {
|
|
114
|
+
if (!isObject(value)) {
|
|
115
|
+
errors.push(error("scope_invalid", "scope must be an object", "scope"));
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
for (const key of unknownKeys(value, ["space", "subjects", "topics", "contexts", "dimensions"])) {
|
|
119
|
+
errors.push(error("unknown_field", `scope contains unknown field ${key}`, `scope.${key}`));
|
|
120
|
+
}
|
|
121
|
+
const space = nonEmptySingleLine(value.space, "scope.space", errors, ID_PATTERN);
|
|
122
|
+
const subjects = stringArray(value.subjects, "scope.subjects", errors);
|
|
123
|
+
const topics = stringArray(value.topics, "scope.topics", errors);
|
|
124
|
+
const contexts = stringArray(value.contexts, "scope.contexts", errors);
|
|
125
|
+
const dimensions: Record<string, string[]> = {};
|
|
126
|
+
if (!isObject(value.dimensions)) {
|
|
127
|
+
errors.push(error("scope_invalid", "scope.dimensions must be an object of string arrays", "scope.dimensions"));
|
|
128
|
+
} else {
|
|
129
|
+
for (const [key, raw] of Object.entries(value.dimensions)) {
|
|
130
|
+
if (!ID_PATTERN.test(key)) {
|
|
131
|
+
errors.push(error("scope_invalid", `scope.dimensions key ${key} is not a safe identifier`, `scope.dimensions.${key}`));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const parsed = stringArray(raw, `scope.dimensions.${key}`, errors);
|
|
135
|
+
if (parsed !== undefined) dimensions[key] = parsed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (space === undefined || subjects === undefined || topics === undefined || contexts === undefined) return undefined;
|
|
139
|
+
return { space, subjects, topics, contexts, dimensions };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseSources(value: unknown, errors: KnowledgeError[]): KnowledgeSource[] | undefined {
|
|
143
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
144
|
+
errors.push(error("sources_invalid", "sources must be a non-empty array", "sources"));
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const sources: KnowledgeSource[] = [];
|
|
148
|
+
for (let index = 0; index < value.length; index++) {
|
|
149
|
+
const raw = value[index];
|
|
150
|
+
if (!isObject(raw)) {
|
|
151
|
+
errors.push(error("sources_invalid", `sources[${index}] must be an object`, `sources[${index}]`));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const key of unknownKeys(raw, ["type", "ref"])) {
|
|
155
|
+
errors.push(error("unknown_field", `sources[${index}] contains unknown field ${key}`, `sources[${index}].${key}`));
|
|
156
|
+
}
|
|
157
|
+
const type = nonEmptySingleLine(raw.type, `sources[${index}].type`, errors);
|
|
158
|
+
const ref = nonEmptySingleLine(raw.ref, `sources[${index}].ref`, errors);
|
|
159
|
+
if (type !== undefined && ref !== undefined) sources.push({ type, ref });
|
|
160
|
+
}
|
|
161
|
+
return sources.length === value.length ? sources : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseSession(value: unknown, errors: KnowledgeError[]): HostSessionProvenance | undefined {
|
|
165
|
+
if (!isObject(value)) {
|
|
166
|
+
errors.push(error("session_invalid", "session must be an object", "session"));
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
for (const key of unknownKeys(value, ["id", "host"])) {
|
|
170
|
+
errors.push(error("unknown_field", `session contains unknown field ${key}`, `session.${key}`));
|
|
171
|
+
}
|
|
172
|
+
const before = errors.length;
|
|
173
|
+
const id = nonEmptySingleLine(value.id, "session.id", errors, SESSION_ID_PATTERN);
|
|
174
|
+
const host = nonEmptySingleLine(value.host, "session.host", errors, ID_PATTERN);
|
|
175
|
+
for (let index = before; index < errors.length; index++) {
|
|
176
|
+
const current = errors[index];
|
|
177
|
+
if (current !== undefined && current.field?.startsWith("session.")) errors[index] = { ...current, code: "session_invalid" };
|
|
178
|
+
}
|
|
179
|
+
return id !== undefined && host !== undefined ? { id, host } : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function validateKnowledgeEnvelope(raw: unknown): KnowledgeResult<KnowledgeEnvelope> {
|
|
183
|
+
if (!isObject(raw)) return { ok: false, errors: [error("envelope_invalid", "knowledge envelope must be an object")] };
|
|
184
|
+
const errors: KnowledgeError[] = [];
|
|
185
|
+
const allowed = [
|
|
186
|
+
"id",
|
|
187
|
+
"kind",
|
|
188
|
+
"status",
|
|
189
|
+
"statement",
|
|
190
|
+
"details",
|
|
191
|
+
"scope",
|
|
192
|
+
"pack",
|
|
193
|
+
"sources",
|
|
194
|
+
"session",
|
|
195
|
+
"submitted_at",
|
|
196
|
+
"disposition",
|
|
197
|
+
];
|
|
198
|
+
for (const key of unknownKeys(raw, allowed)) errors.push(error("unknown_field", `envelope contains unknown field ${key}`, key));
|
|
199
|
+
|
|
200
|
+
const id = nonEmptySingleLine(raw.id, "id", errors, ID_PATTERN);
|
|
201
|
+
const kind = enumValue(raw.kind, KNOWLEDGE_KINDS, "kind", errors, "kind_invalid");
|
|
202
|
+
const status = enumValue(raw.status, KNOWLEDGE_STATUSES, "status", errors, "status_invalid");
|
|
203
|
+
const statement = nonEmptySingleLine(raw.statement, "statement", errors);
|
|
204
|
+
|
|
205
|
+
let details: JsonObject | undefined;
|
|
206
|
+
if (!isObject(raw.details)) {
|
|
207
|
+
errors.push(error("details_invalid", "details must be a JSON object", "details"));
|
|
208
|
+
} else {
|
|
209
|
+
const parsedDetails: JsonObject = {};
|
|
210
|
+
let validDetails = true;
|
|
211
|
+
for (const [key, item] of Object.entries(raw.details)) {
|
|
212
|
+
if (!isJsonValue(item)) validDetails = false;
|
|
213
|
+
else parsedDetails[key] = item;
|
|
214
|
+
}
|
|
215
|
+
if (!validDetails) errors.push(error("details_invalid", "details must be a JSON object", "details"));
|
|
216
|
+
else {
|
|
217
|
+
details = parsedDetails;
|
|
218
|
+
findNewline(details, "details", errors);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const scope = parseScope(raw.scope, errors);
|
|
223
|
+
const pack = parsePackRef(raw.pack, "pack", errors);
|
|
224
|
+
const sources = parseSources(raw.sources, errors);
|
|
225
|
+
const session = parseSession(raw.session, errors);
|
|
226
|
+
const submittedAt = nonEmptySingleLine(raw.submitted_at, "submitted_at", errors);
|
|
227
|
+
if (submittedAt !== undefined && !DATE_PATTERN.test(submittedAt)) {
|
|
228
|
+
errors.push(error("date_invalid", "submitted_at must match YYYY-MM-DD", "submitted_at"));
|
|
229
|
+
}
|
|
230
|
+
const disposition = enumValue(raw.disposition, KNOWLEDGE_DISPOSITIONS, "disposition", errors, "disposition_invalid");
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
errors.length > 0 ||
|
|
234
|
+
id === undefined ||
|
|
235
|
+
kind === undefined ||
|
|
236
|
+
status === undefined ||
|
|
237
|
+
statement === undefined ||
|
|
238
|
+
details === undefined ||
|
|
239
|
+
scope === undefined ||
|
|
240
|
+
pack === undefined ||
|
|
241
|
+
sources === undefined ||
|
|
242
|
+
session === undefined ||
|
|
243
|
+
submittedAt === undefined ||
|
|
244
|
+
disposition === undefined
|
|
245
|
+
) {
|
|
246
|
+
return { ok: false, errors };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
value: {
|
|
252
|
+
id,
|
|
253
|
+
kind,
|
|
254
|
+
status,
|
|
255
|
+
statement,
|
|
256
|
+
details,
|
|
257
|
+
scope,
|
|
258
|
+
pack,
|
|
259
|
+
sources,
|
|
260
|
+
session,
|
|
261
|
+
submittedAt,
|
|
262
|
+
disposition,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function validationError(code: string, message: string, field?: string): KnowledgeError {
|
|
268
|
+
return error(code, message, field);
|
|
269
|
+
}
|