@sourcepress/core 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/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ export { defineConfig, collection, field, relation } from "./config.js";
2
+ export { validateConfig, type ValidationResult } from "./validate.js";
3
+ export { collectionToZod } from "./schema.js";
4
+ export type {
5
+ SourcePressConfig,
6
+ CollectionDefinition,
7
+ FieldDefinition,
8
+ StringField,
9
+ BooleanField,
10
+ NumberField,
11
+ ImageField,
12
+ RelationOneField,
13
+ RelationManyField,
14
+ Provenance,
15
+ ContentFile,
16
+ KnowledgeFile,
17
+ Entity,
18
+ GraphEdge,
19
+ JobDefinition,
20
+ JobFilter,
21
+ JobStatus,
22
+ MediaRef,
23
+ MediaRegistry,
24
+ MediaUploadInput,
25
+ MediaConfig,
26
+ ContentChange,
27
+ ApprovalRequest,
28
+ ApprovalStatus,
29
+ StatusChangeHandler,
30
+ ApprovalProvider,
31
+ ApprovalRules,
32
+ } from "./types.js";
package/src/schema.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import type { CollectionDefinition, FieldDefinition } from "./types.js";
3
+
4
+ function fieldToZod(fieldDef: FieldDefinition): z.ZodType {
5
+ switch (fieldDef.type) {
6
+ case "string": {
7
+ let schema: z.ZodType = z.string();
8
+ if (fieldDef.default !== undefined)
9
+ schema = schema.pipe(z.string().default(fieldDef.default));
10
+ if (!fieldDef.required) schema = z.string().optional();
11
+ return schema;
12
+ }
13
+ case "boolean": {
14
+ if (!fieldDef.required) return z.boolean().optional();
15
+ return z.boolean();
16
+ }
17
+ case "number": {
18
+ if (!fieldDef.required) return z.number().optional();
19
+ return z.number();
20
+ }
21
+ case "image": {
22
+ if (fieldDef.multiple) {
23
+ const schema = z.array(z.string());
24
+ if (!fieldDef.required) return schema.optional();
25
+ return schema;
26
+ }
27
+ if (!fieldDef.required) return z.string().optional();
28
+ return z.string();
29
+ }
30
+ case "relation-one": {
31
+ if (!fieldDef.required) return z.string().optional();
32
+ return z.string();
33
+ }
34
+ case "relation-many": {
35
+ const schema = z.array(z.string());
36
+ if (!fieldDef.required) return schema.optional();
37
+ return schema;
38
+ }
39
+ }
40
+ }
41
+
42
+ export function collectionToZod(
43
+ collection: CollectionDefinition,
44
+ ): z.ZodObject<Record<string, z.ZodType>> {
45
+ const shape: Record<string, z.ZodType> = {};
46
+
47
+ for (const [fieldName, fieldDef] of Object.entries(collection.fields)) {
48
+ shape[fieldName] = fieldToZod(fieldDef);
49
+ }
50
+
51
+ return z.object(shape);
52
+ }
package/src/types.ts ADDED
@@ -0,0 +1,300 @@
1
+ // Content field types
2
+ export interface StringField {
3
+ type: "string";
4
+ required?: boolean;
5
+ default?: string;
6
+ }
7
+
8
+ export interface BooleanField {
9
+ type: "boolean";
10
+ required?: boolean;
11
+ default?: boolean;
12
+ }
13
+
14
+ export interface NumberField {
15
+ type: "number";
16
+ required?: boolean;
17
+ default?: number;
18
+ }
19
+
20
+ export interface ImageField {
21
+ type: "image";
22
+ required?: boolean;
23
+ multiple?: boolean;
24
+ }
25
+
26
+ export interface RelationOneField {
27
+ type: "relation-one";
28
+ collection: string;
29
+ required?: boolean;
30
+ }
31
+
32
+ export interface RelationManyField {
33
+ type: "relation-many";
34
+ collection: string;
35
+ required?: boolean;
36
+ }
37
+
38
+ export type FieldDefinition =
39
+ | StringField
40
+ | BooleanField
41
+ | NumberField
42
+ | ImageField
43
+ | RelationOneField
44
+ | RelationManyField;
45
+
46
+ // Collection definition
47
+ export interface CollectionDefinition {
48
+ name: string;
49
+ path: string;
50
+ format: "mdx" | "md" | "yaml" | "json";
51
+ fields: Record<string, FieldDefinition>;
52
+ }
53
+
54
+ // Full SourcePress config
55
+ export interface SourcePressConfig {
56
+ repository: {
57
+ owner: string;
58
+ repo: string;
59
+ branch: string;
60
+ content_path?: string;
61
+ };
62
+
63
+ ai: {
64
+ provider: "anthropic" | "openai" | "local";
65
+ model: string;
66
+ daily_limit_usd?: number;
67
+ warn_at_usd?: number;
68
+ };
69
+
70
+ collections: Record<string, CollectionDefinition>;
71
+
72
+ knowledge: {
73
+ path: string;
74
+ graph: {
75
+ backend: "local" | "vectorize" | "turso";
76
+ };
77
+ ingestion?: {
78
+ scraping?: {
79
+ respectRobotsTxt?: boolean;
80
+ rateLimitMs?: number;
81
+ };
82
+ };
83
+ };
84
+
85
+ intent: {
86
+ path: string;
87
+ };
88
+
89
+ media?: {
90
+ storage: "git" | "r2" | "s3";
91
+ path: string;
92
+ registry: string;
93
+ allowedTypes?: string[];
94
+ maxSizeMb?: number;
95
+ transform?: {
96
+ formats?: string[];
97
+ sizes?: number[];
98
+ };
99
+ };
100
+
101
+ jobs?: {
102
+ backend: "in-process" | "queue" | "durable-objects";
103
+ };
104
+
105
+ evals?: {
106
+ threshold: number;
107
+ auto_approve?: boolean;
108
+ };
109
+
110
+ approval?: ApprovalRules;
111
+
112
+ auth?: {
113
+ provider: "github" | "api-key" | "custom";
114
+ };
115
+
116
+ cache?: {
117
+ backend: "sqlite" | "d1" | "memory";
118
+ };
119
+
120
+ sync?: {
121
+ reconciliation_interval: string;
122
+ };
123
+ }
124
+
125
+ // Content change submitted for approval
126
+ export interface ContentChange {
127
+ collection: string;
128
+ slug: string;
129
+ path: string;
130
+ action: "create" | "update" | "delete";
131
+ content: string;
132
+ frontmatter: Record<string, unknown>;
133
+ provenance: Provenance;
134
+ }
135
+
136
+ // Approval request — tracks a pending change
137
+ export interface ApprovalRequest {
138
+ id: string;
139
+ change: ContentChange;
140
+ status: "pending" | "approved" | "rejected";
141
+ submitted_at: string;
142
+ submitted_by: string;
143
+ reviewed_by?: string;
144
+ reviewed_at?: string;
145
+ review_comment?: string;
146
+ pr_url?: string;
147
+ pr_number?: number;
148
+ }
149
+
150
+ // Approval status change handler
151
+ export type ApprovalStatus = "pending" | "approved" | "rejected";
152
+ export type StatusChangeHandler = (id: string, status: ApprovalStatus, by: string) => void;
153
+
154
+ // Approval provider interface — pluggable
155
+ export interface ApprovalProvider {
156
+ submit(change: ContentChange): Promise<ApprovalRequest>;
157
+ status(id: string): Promise<ApprovalStatus>;
158
+ approve(id: string, by: string, comment?: string): Promise<void>;
159
+ reject(id: string, by: string, reason: string): Promise<void>;
160
+ pending(): Promise<ApprovalRequest[]>;
161
+ onStatusChange(callback: StatusChangeHandler): void;
162
+ }
163
+
164
+ // Approval rules config
165
+ export interface ApprovalRules {
166
+ provider: "github-pr" | "api" | "auto";
167
+ rules: Record<string, "pr" | "direct">;
168
+ auto_approve?: {
169
+ enabled: boolean;
170
+ min_score: number;
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Provenance metadata for content traceability.
176
+ * Satisfies EU AI Act transparency requirements:
177
+ * - AI attribution (generated_by, prompt_version)
178
+ * - Human oversight (approved_by, approved_at)
179
+ * - Source traceability (source_files)
180
+ * - Quality assessment (eval_score)
181
+ */
182
+ export interface Provenance {
183
+ generated_by: string;
184
+ generated_at: string;
185
+ source_files: string[];
186
+ prompt_version?: string;
187
+ eval_score?: number;
188
+ approved_by?: string;
189
+ approved_at?: string;
190
+ }
191
+
192
+ // Content file representation
193
+ export interface ContentFile {
194
+ collection: string;
195
+ slug: string;
196
+ path: string;
197
+ frontmatter: Record<string, unknown>;
198
+ body: string;
199
+ provenance?: Provenance;
200
+ }
201
+
202
+ // Knowledge file representation
203
+ export interface KnowledgeFile {
204
+ path: string;
205
+ type: string;
206
+ quality: "structured" | "draft" | "thoughts";
207
+ quality_score: number;
208
+ entities: Entity[];
209
+ ingested_at: string;
210
+ source: "manual" | "url" | "document" | "transcript" | "scrape";
211
+ source_url?: string;
212
+ body: string;
213
+ }
214
+
215
+ // Entity in knowledge graph
216
+ export interface Entity {
217
+ type: string;
218
+ name: string;
219
+ aliases?: string[];
220
+ }
221
+
222
+ // Graph edge
223
+ export interface GraphEdge {
224
+ from: string;
225
+ to: string;
226
+ relation_type: string;
227
+ weight: number;
228
+ }
229
+
230
+ // Job definition — what gets enqueued
231
+ export interface JobDefinition {
232
+ type: string;
233
+ params: Record<string, unknown>;
234
+ }
235
+
236
+ // Job filter for listing
237
+ export interface JobFilter {
238
+ status?: JobStatus["status"];
239
+ type?: string;
240
+ limit?: number;
241
+ }
242
+
243
+ // Job status — tracks lifecycle of a background job
244
+ export interface JobStatus {
245
+ job_id: string;
246
+ type: string;
247
+ status: "queued" | "running" | "completed" | "failed" | "cancelled";
248
+ progress: { completed: number; total: number; failed: number };
249
+ created_at: string;
250
+ started_at?: string;
251
+ completed_at?: string;
252
+ result?: unknown;
253
+ error?: string;
254
+ }
255
+
256
+ // Media reference — stored in media.json registry
257
+ export interface MediaRef {
258
+ path: string;
259
+ content_type: string;
260
+ size_bytes: number;
261
+ hash: string;
262
+ width?: number;
263
+ height?: number;
264
+ format?: string;
265
+ alt?: string;
266
+ source: "uploaded" | "ai-generated" | "scraped" | "stock";
267
+ generated_by?: string;
268
+ prompt?: string;
269
+ uploaded_at: string;
270
+ uploaded_by: string;
271
+ variants?: Record<string, string>;
272
+ }
273
+
274
+ // Media registry — the full media.json file
275
+ export type MediaRegistry = Record<string, MediaRef>;
276
+
277
+ // Media upload input
278
+ export interface MediaUploadInput {
279
+ file: Buffer;
280
+ path: string;
281
+ content_type: string;
282
+ alt?: string;
283
+ source: MediaRef["source"];
284
+ generated_by?: string;
285
+ prompt?: string;
286
+ uploaded_by: string;
287
+ }
288
+
289
+ // Media storage config (from SourcePressConfig.media)
290
+ export interface MediaConfig {
291
+ storage: "git" | "r2" | "s3";
292
+ path: string;
293
+ registry: string;
294
+ allowedTypes?: string[];
295
+ maxSizeMb?: number;
296
+ transform?: {
297
+ formats?: string[];
298
+ sizes?: number[];
299
+ };
300
+ }
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+
3
+ export interface ValidationResult {
4
+ success: boolean;
5
+ data?: unknown;
6
+ errors: string[];
7
+ }
8
+
9
+ // Build the full Zod schema for SourcePressConfig
10
+ const repositorySchema = z.object({
11
+ owner: z.string().min(1),
12
+ repo: z.string().min(1),
13
+ branch: z.string().min(1),
14
+ content_path: z.string().optional(),
15
+ });
16
+
17
+ const aiSchema = z.object({
18
+ provider: z.enum(["anthropic", "openai", "local"]),
19
+ model: z.string().min(1),
20
+ daily_limit_usd: z.number().optional(),
21
+ warn_at_usd: z.number().optional(),
22
+ });
23
+
24
+ const fieldSchema: z.ZodType = z.lazy(() =>
25
+ z.union([
26
+ z.object({
27
+ type: z.literal("string"),
28
+ required: z.boolean().optional(),
29
+ default: z.string().optional(),
30
+ }),
31
+ z.object({
32
+ type: z.literal("boolean"),
33
+ required: z.boolean().optional(),
34
+ default: z.boolean().optional(),
35
+ }),
36
+ z.object({
37
+ type: z.literal("number"),
38
+ required: z.boolean().optional(),
39
+ default: z.number().optional(),
40
+ }),
41
+ z.object({
42
+ type: z.literal("image"),
43
+ required: z.boolean().optional(),
44
+ multiple: z.boolean().optional(),
45
+ }),
46
+ z.object({
47
+ type: z.literal("relation-one"),
48
+ collection: z.string(),
49
+ required: z.boolean().optional(),
50
+ }),
51
+ z.object({
52
+ type: z.literal("relation-many"),
53
+ collection: z.string(),
54
+ required: z.boolean().optional(),
55
+ }),
56
+ ]),
57
+ );
58
+
59
+ const collectionSchema = z.object({
60
+ name: z.string().min(1),
61
+ path: z.string().min(1),
62
+ format: z.enum(["mdx", "md", "yaml", "json"]),
63
+ fields: z.record(fieldSchema),
64
+ });
65
+
66
+ const configSchema = z.object({
67
+ repository: repositorySchema,
68
+ ai: aiSchema,
69
+ collections: z.record(collectionSchema),
70
+ knowledge: z.object({
71
+ path: z.string().min(1),
72
+ graph: z.object({
73
+ backend: z.enum(["local", "vectorize", "turso"]),
74
+ }),
75
+ ingestion: z
76
+ .object({
77
+ scraping: z
78
+ .object({
79
+ respectRobotsTxt: z.boolean().optional(),
80
+ rateLimitMs: z.number().optional(),
81
+ })
82
+ .optional(),
83
+ })
84
+ .optional(),
85
+ }),
86
+ intent: z.object({ path: z.string().min(1) }),
87
+ media: z
88
+ .object({
89
+ storage: z.enum(["git", "r2", "s3"]),
90
+ path: z.string(),
91
+ registry: z.string(),
92
+ allowedTypes: z.array(z.string()).optional(),
93
+ maxSizeMb: z.number().optional(),
94
+ transform: z
95
+ .object({
96
+ formats: z.array(z.string()).optional(),
97
+ sizes: z.array(z.number()).optional(),
98
+ })
99
+ .optional(),
100
+ })
101
+ .optional(),
102
+ jobs: z.object({ backend: z.enum(["in-process", "queue", "durable-objects"]) }).optional(),
103
+ evals: z.object({ threshold: z.number(), auto_approve: z.boolean().optional() }).optional(),
104
+ approval: z
105
+ .object({
106
+ provider: z.enum(["github-pr", "api", "auto"]),
107
+ rules: z.record(z.enum(["pr", "direct"])).default({}),
108
+ auto_approve: z
109
+ .object({
110
+ enabled: z.boolean(),
111
+ min_score: z.number().min(0).max(100),
112
+ })
113
+ .optional(),
114
+ })
115
+ .optional(),
116
+ auth: z.object({ provider: z.enum(["github", "api-key", "custom"]) }).optional(),
117
+ cache: z.object({ backend: z.enum(["sqlite", "d1", "memory"]) }).optional(),
118
+ sync: z.object({ reconciliation_interval: z.string() }).optional(),
119
+ });
120
+
121
+ export function validateConfig(raw: unknown): ValidationResult {
122
+ const result = configSchema.safeParse(raw);
123
+ if (result.success) {
124
+ return { success: true, data: result.data, errors: [] };
125
+ }
126
+ return {
127
+ success: false,
128
+ errors: result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
129
+ };
130
+ }
131
+
132
+ export { configSchema };
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/__tests__/**/*.test.ts"],
6
+ },
7
+ });