@agent-facets/core 0.1.1

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.
@@ -0,0 +1,89 @@
1
+ import { type } from 'arktype'
2
+ import type { FacetManifest } from '../schemas/facet-manifest.ts'
3
+ import type { ValidationError } from '../types.ts'
4
+
5
+ // --- Known platform schemas ---
6
+
7
+ /** OpenCode platform config schema */
8
+ const OpenCodePlatformSchema = type({
9
+ 'tools?': type.Record('string', 'boolean'),
10
+ 'model?': 'string',
11
+ })
12
+
13
+ /** Claude Code platform config schema */
14
+ const ClaudeCodePlatformSchema = type({
15
+ 'tools?': type.Record('string', 'boolean'),
16
+ 'permissions?': type.Record('string', 'boolean'),
17
+ })
18
+
19
+ /** Map of known platform names to their ArkType validators */
20
+ const KNOWN_PLATFORMS: Record<string, (data: unknown) => unknown> = {
21
+ opencode: (data) => OpenCodePlatformSchema(data),
22
+ 'claude-code': (data) => ClaudeCodePlatformSchema(data),
23
+ }
24
+
25
+ export interface PlatformValidationResult {
26
+ errors: ValidationError[]
27
+ warnings: string[]
28
+ }
29
+
30
+ /**
31
+ * Validates platform configuration for all assets that declare `platforms`.
32
+ * Known platforms are validated against their schema — invalid config is an error.
33
+ * Unknown platforms produce a warning but do not cause failure.
34
+ */
35
+ export function validatePlatformConfigs(manifest: FacetManifest): PlatformValidationResult {
36
+ const errors: ValidationError[] = []
37
+ const warnings: string[] = []
38
+
39
+ // Check skills
40
+ if (manifest.skills) {
41
+ for (const [name, skill] of Object.entries(manifest.skills)) {
42
+ if (skill.platforms) {
43
+ validateAssetPlatforms(`skills.${name}`, skill.platforms, errors, warnings)
44
+ }
45
+ }
46
+ }
47
+
48
+ // Check agents
49
+ if (manifest.agents) {
50
+ for (const [name, agent] of Object.entries(manifest.agents)) {
51
+ if (agent.platforms) {
52
+ validateAssetPlatforms(`agents.${name}`, agent.platforms, errors, warnings)
53
+ }
54
+ }
55
+ }
56
+
57
+ // Commands don't have platforms in the current schema, but if they ever do,
58
+ // they'd be validated here uniformly.
59
+
60
+ return { errors, warnings }
61
+ }
62
+
63
+ function validateAssetPlatforms(
64
+ assetPath: string,
65
+ platforms: Record<string, unknown>,
66
+ errors: ValidationError[],
67
+ warnings: string[],
68
+ ): void {
69
+ for (const [platformName, config] of Object.entries(platforms)) {
70
+ const validator = KNOWN_PLATFORMS[platformName]
71
+
72
+ if (!validator) {
73
+ warnings.push(`${assetPath}: unknown platform "${platformName}" — config will not be validated`)
74
+ continue
75
+ }
76
+
77
+ const result = validator(config)
78
+ if (result instanceof type.errors) {
79
+ for (const err of result) {
80
+ errors.push({
81
+ path: `${assetPath}.platforms.${platformName}.${err.path.join('.')}`,
82
+ message: `Invalid platform config for "${platformName}" on ${assetPath}: ${err.message}`,
83
+ expected: err.expected,
84
+ actual: String(err.actual),
85
+ })
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,34 @@
1
+ import { mkdir, rm } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import type { BuildManifest } from '../schemas/build-manifest.ts'
4
+ import type { BuildResult } from './pipeline.ts'
5
+
6
+ const DIST_DIR = 'dist'
7
+ const BUILD_MANIFEST_FILE = 'build-manifest.json'
8
+
9
+ /**
10
+ * Writes the build output to dist/.
11
+ *
12
+ * - Cleans (removes and recreates) the dist/ directory
13
+ * - Writes the .facet archive (gzip-compressed tar)
14
+ * - Writes build-manifest.json with integrity hash and per-asset hashes
15
+ */
16
+ export async function writeBuildOutput(result: BuildResult, rootDir: string): Promise<void> {
17
+ const distDir = join(rootDir, DIST_DIR)
18
+
19
+ // Clean previous output
20
+ await rm(distDir, { recursive: true, force: true })
21
+ await mkdir(distDir, { recursive: true })
22
+
23
+ // Write the .facet archive
24
+ await Bun.write(join(distDir, result.archiveFilename), result.archiveBytes)
25
+
26
+ // Write build manifest
27
+ const manifest: BuildManifest = {
28
+ facetVersion: 1,
29
+ archive: result.archiveFilename,
30
+ integrity: result.integrity,
31
+ assets: result.assetHashes,
32
+ }
33
+ await Bun.write(join(distDir, BUILD_MANIFEST_FILE), JSON.stringify(manifest, null, 2))
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // types
2
+
3
+ export type { ArchiveEntry } from './build/content-hash.ts'
4
+ export {
5
+ assembleTar,
6
+ collectArchiveEntries,
7
+ compressArchive,
8
+ computeAssetHashes,
9
+ computeContentHash,
10
+ } from './build/content-hash.ts'
11
+ export { detectNamingCollisions } from './build/detect-collisions.ts'
12
+ export type { BuildFailure, BuildProgress, BuildResult } from './build/pipeline.ts'
13
+ // build pipeline
14
+ export { runBuildPipeline } from './build/pipeline.ts'
15
+ export { validateCompactFacets } from './build/validate-facets.ts'
16
+ export type { PlatformValidationResult } from './build/validate-platforms.ts'
17
+ export { validatePlatformConfigs } from './build/validate-platforms.ts'
18
+ export { writeBuildOutput } from './build/write-output.ts'
19
+ export type { ResolvedFacetManifest } from './loaders/facet.ts'
20
+ // loaders
21
+ export { FACET_MANIFEST_FILE, loadManifest, resolvePrompts } from './loaders/facet.ts'
22
+ export { loadServerManifest } from './loaders/server.ts'
23
+ export type { BuildManifest } from './schemas/build-manifest.ts'
24
+ export { BuildManifestSchema } from './schemas/build-manifest.ts'
25
+ export type { FacetManifest } from './schemas/facet-manifest.ts'
26
+ // schemas
27
+ export {
28
+ checkFacetManifestConstraints,
29
+ FacetManifestSchema,
30
+ } from './schemas/facet-manifest.ts'
31
+ export type { Lockfile } from './schemas/lockfile.ts'
32
+ export { LockfileSchema } from './schemas/lockfile.ts'
33
+ export type { ServerManifest } from './schemas/server-manifest.ts'
34
+ export { ServerManifestSchema } from './schemas/server-manifest.ts'
35
+ export type { Result, ValidationError } from './types.ts'
@@ -0,0 +1,180 @@
1
+ import { join } from 'node:path'
2
+ import { type } from 'arktype'
3
+ import { checkFacetManifestConstraints, type FacetManifest, FacetManifestSchema } from '../schemas/facet-manifest.ts'
4
+ import type { Result, ValidationError } from '../types.ts'
5
+ import { mapArkErrors, parseJson, readFile } from './validate.ts'
6
+
7
+ export const FACET_MANIFEST_FILE = 'facet.json'
8
+
9
+ /**
10
+ * Loads and validates a facet manifest from the specified directory.
11
+ *
12
+ * Reads the facet manifest, parses JSON, validates against the schema, and checks
13
+ * business-rule constraints. Returns a discriminated result — either the
14
+ * validated manifest or structured errors.
15
+ */
16
+ export async function loadManifest(dir: string): Promise<Result<FacetManifest>> {
17
+ const filePath = join(dir, FACET_MANIFEST_FILE)
18
+
19
+ // Phase 0: Read the file
20
+ const fileResult = await readFile(filePath)
21
+ if (!fileResult.ok) {
22
+ return fileResult
23
+ }
24
+
25
+ // Phase 1: Parse JSON
26
+ const jsonResult = parseJson(fileResult.content)
27
+ if (!jsonResult.ok) {
28
+ return jsonResult
29
+ }
30
+
31
+ // Phase 2: Schema validation
32
+ const validated = FacetManifestSchema(jsonResult.data)
33
+ if (validated instanceof type.errors) {
34
+ return { ok: false, errors: mapArkErrors(validated) }
35
+ }
36
+
37
+ // Phase 3: Business-rule constraints
38
+ const constraintErrors = checkFacetManifestConstraints(validated)
39
+ if (constraintErrors.length > 0) {
40
+ return { ok: false, errors: constraintErrors }
41
+ }
42
+
43
+ return { ok: true, data: validated }
44
+ }
45
+
46
+ /**
47
+ * A manifest with all prompts resolved to their string content.
48
+ * File paths are derived from convention: `<type>/<name>.md`.
49
+ */
50
+ export interface ResolvedFacetManifest {
51
+ name: string
52
+ version: string
53
+ description?: string
54
+ author?: string
55
+ skills?: Record<
56
+ string,
57
+ {
58
+ description: string
59
+ prompt: string
60
+ platforms?: Record<string, unknown>
61
+ }
62
+ >
63
+ agents?: Record<
64
+ string,
65
+ {
66
+ description: string
67
+ prompt: string
68
+ platforms?: Record<string, unknown>
69
+ }
70
+ >
71
+ commands?: Record<
72
+ string,
73
+ {
74
+ description: string
75
+ prompt: string
76
+ }
77
+ >
78
+ facets?: FacetManifest['facets']
79
+ servers?: FacetManifest['servers']
80
+ }
81
+
82
+ /**
83
+ * Resolves prompt content for all skills, agents, and commands by reading
84
+ * files at conventional paths relative to the facet root directory.
85
+ *
86
+ * The convention is `<type>/<name>.md` — for example, a skill named
87
+ * "code-review" resolves to `skills/code-review.md`.
88
+ *
89
+ * This also serves as file existence verification for all three asset types —
90
+ * if an expected file doesn't exist, resolution fails with an error identifying
91
+ * the asset and the expected file path.
92
+ *
93
+ * Returns a new manifest with all prompts resolved to strings, or an error
94
+ * result identifying which prompt failed and why.
95
+ */
96
+ export async function resolvePrompts(manifest: FacetManifest, rootDir: string): Promise<Result<ResolvedFacetManifest>> {
97
+ const errors: ValidationError[] = []
98
+
99
+ // Resolve skill prompts from skills/<name>.md
100
+ let resolvedSkills: ResolvedFacetManifest['skills'] | undefined
101
+ if (manifest.skills) {
102
+ resolvedSkills = {}
103
+ for (const [name, skill] of Object.entries(manifest.skills)) {
104
+ const resolvedPrompt = await resolveAssetPrompt('skills', name, rootDir)
105
+ if (typeof resolvedPrompt === 'string') {
106
+ resolvedSkills[name] = { ...skill, prompt: resolvedPrompt }
107
+ } else {
108
+ errors.push(resolvedPrompt)
109
+ }
110
+ }
111
+ }
112
+
113
+ // Resolve agent prompts from agents/<name>.md
114
+ let resolvedAgents: ResolvedFacetManifest['agents'] | undefined
115
+ if (manifest.agents) {
116
+ resolvedAgents = {}
117
+ for (const [name, agent] of Object.entries(manifest.agents)) {
118
+ const resolvedPrompt = await resolveAssetPrompt('agents', name, rootDir)
119
+ if (typeof resolvedPrompt === 'string') {
120
+ resolvedAgents[name] = { ...agent, prompt: resolvedPrompt }
121
+ } else {
122
+ errors.push(resolvedPrompt)
123
+ }
124
+ }
125
+ }
126
+
127
+ // Resolve command prompts from commands/<name>.md
128
+ let resolvedCommands: ResolvedFacetManifest['commands'] | undefined
129
+ if (manifest.commands) {
130
+ resolvedCommands = {}
131
+ for (const [name, command] of Object.entries(manifest.commands)) {
132
+ const resolvedPrompt = await resolveAssetPrompt('commands', name, rootDir)
133
+ if (typeof resolvedPrompt === 'string') {
134
+ resolvedCommands[name] = { ...command, prompt: resolvedPrompt }
135
+ } else {
136
+ errors.push(resolvedPrompt)
137
+ }
138
+ }
139
+ }
140
+
141
+ if (errors.length > 0) {
142
+ return { ok: false, errors }
143
+ }
144
+
145
+ const resolved: ResolvedFacetManifest = {
146
+ name: manifest.name,
147
+ version: manifest.version,
148
+ ...(manifest.description !== undefined && { description: manifest.description }),
149
+ ...(manifest.author !== undefined && { author: manifest.author }),
150
+ ...(resolvedSkills !== undefined && { skills: resolvedSkills }),
151
+ ...(resolvedAgents !== undefined && { agents: resolvedAgents }),
152
+ ...(resolvedCommands !== undefined && { commands: resolvedCommands }),
153
+ ...(manifest.facets !== undefined && { facets: manifest.facets }),
154
+ ...(manifest.servers !== undefined && { servers: manifest.servers }),
155
+ }
156
+
157
+ return { ok: true, data: resolved }
158
+ }
159
+
160
+ /**
161
+ * Resolves prompt content for a single asset by reading <type>/<name>.md.
162
+ * Returns the file content as a string, or a ValidationError if the file doesn't exist.
163
+ */
164
+ async function resolveAssetPrompt(assetType: string, name: string, rootDir: string): Promise<string | ValidationError> {
165
+ const relativePath = `${assetType}/${name}.md`
166
+ const filePath = join(rootDir, relativePath)
167
+ const file = Bun.file(filePath)
168
+ const exists = await file.exists()
169
+
170
+ if (!exists) {
171
+ return {
172
+ path: `${assetType}.${name}`,
173
+ message: `Prompt file not found: ${relativePath} (resolved to ${filePath})`,
174
+ expected: 'file to exist',
175
+ actual: 'file not found',
176
+ }
177
+ }
178
+
179
+ return file.text()
180
+ }
@@ -0,0 +1,37 @@
1
+ import { join } from 'node:path'
2
+ import { type } from 'arktype'
3
+ import { type ServerManifest, ServerManifestSchema } from '../schemas/server-manifest.ts'
4
+ import type { Result } from '../types.ts'
5
+ import { mapArkErrors, parseJson, readFile } from './validate.ts'
6
+
7
+ const SERVER_MANIFEST_FILE = 'server.json'
8
+
9
+ /**
10
+ * Loads and validates a server manifest from the specified directory.
11
+ *
12
+ * Reads the server manifest, parses JSON, validates against the schema, and returns
13
+ * a discriminated result — either the validated manifest or structured errors.
14
+ */
15
+ export async function loadServerManifest(dir: string): Promise<Result<ServerManifest>> {
16
+ const filePath = join(dir, SERVER_MANIFEST_FILE)
17
+
18
+ // Phase 0: Read the file
19
+ const fileResult = await readFile(filePath)
20
+ if (!fileResult.ok) {
21
+ return fileResult
22
+ }
23
+
24
+ // Phase 1: Parse JSON
25
+ const jsonResult = parseJson(fileResult.content)
26
+ if (!jsonResult.ok) {
27
+ return jsonResult
28
+ }
29
+
30
+ // Phase 2: Schema validation
31
+ const validated = ServerManifestSchema(jsonResult.data)
32
+ if (validated instanceof type.errors) {
33
+ return { ok: false, errors: mapArkErrors(validated) }
34
+ }
35
+
36
+ return { ok: true, data: validated }
37
+ }
@@ -0,0 +1,64 @@
1
+ import type { type } from 'arktype'
2
+ import type { ValidationError } from '../types.ts'
3
+
4
+ /**
5
+ * Maps ArkType errors to our public ValidationError type.
6
+ * Decouples the public API from ArkType internals.
7
+ */
8
+ export function mapArkErrors(errors: InstanceType<typeof type.errors>): ValidationError[] {
9
+ return errors.map((err) => ({
10
+ path: err.path.join('.'),
11
+ message: err.message,
12
+ expected: err.expected ?? 'unknown',
13
+ actual: err.actual ?? 'unknown',
14
+ }))
15
+ }
16
+
17
+ /**
18
+ * Parses a JSON string. Returns the parsed data or a ValidationError array.
19
+ */
20
+ export function parseJson(jsonContent: string): { ok: true; data: unknown } | { ok: false; errors: ValidationError[] } {
21
+ try {
22
+ const parsed = JSON.parse(jsonContent)
23
+ return { ok: true, data: parsed }
24
+ } catch (err) {
25
+ const message = err instanceof SyntaxError ? err.message : 'Unknown JSON parse error'
26
+ return {
27
+ ok: false,
28
+ errors: [
29
+ {
30
+ path: '',
31
+ message: `JSON syntax error: ${message}`,
32
+ expected: 'valid JSON',
33
+ actual: 'malformed JSON',
34
+ },
35
+ ],
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Reads a file from disk. Returns the text content or a ValidationError array.
42
+ */
43
+ export async function readFile(
44
+ filePath: string,
45
+ ): Promise<{ ok: true; content: string } | { ok: false; errors: ValidationError[] }> {
46
+ const file = Bun.file(filePath)
47
+ const exists = await file.exists()
48
+ if (!exists) {
49
+ return {
50
+ ok: false,
51
+ errors: [
52
+ {
53
+ path: '',
54
+ message: `File not found: ${filePath}`,
55
+ expected: 'file to exist',
56
+ actual: 'file not found',
57
+ },
58
+ ],
59
+ }
60
+ }
61
+
62
+ const content = await file.text()
63
+ return { ok: true, content }
64
+ }
@@ -0,0 +1,15 @@
1
+ import { type } from 'arktype'
2
+
3
+ /**
4
+ * Schema for the build manifest (build-manifest.json).
5
+ * Written by `facet build` alongside the .facet archive.
6
+ */
7
+ export const BuildManifestSchema = type({
8
+ facetVersion: 'number',
9
+ archive: 'string',
10
+ integrity: /^sha256:[a-f0-9]{64}$/,
11
+ assets: type.Record('string', 'string'),
12
+ })
13
+
14
+ /** Inferred TypeScript type for a validated build manifest */
15
+ export type BuildManifest = typeof BuildManifestSchema.infer
@@ -0,0 +1,113 @@
1
+ import { type } from 'arktype'
2
+
3
+ // --- Sub-schemas ---
4
+
5
+ /** Skill descriptor — description is required, prompt resolved from skills/<name>.md */
6
+ const SkillDescriptor = type({
7
+ description: 'string',
8
+ 'platforms?': type.Record('string', 'unknown'),
9
+ })
10
+
11
+ /** Agent descriptor — description is required, prompt resolved from agents/<name>.md */
12
+ const AgentDescriptor = type({
13
+ description: 'string',
14
+ 'platforms?': type.Record('string', 'unknown'),
15
+ })
16
+
17
+ /** Command descriptor — description is required, prompt resolved from commands/<name>.md */
18
+ const CommandDescriptor = type({
19
+ description: 'string',
20
+ })
21
+
22
+ /** Selective facets entry — cherry-pick specific assets from another facet */
23
+ const SelectiveFacetsEntry = type({
24
+ name: 'string',
25
+ version: 'string',
26
+ 'skills?': 'string[]',
27
+ 'agents?': 'string[]',
28
+ 'commands?': 'string[]',
29
+ })
30
+
31
+ /** Facets entry: compact string ("name@version") or selective object */
32
+ const FacetsEntry = type('string').or(SelectiveFacetsEntry)
33
+
34
+ /** Server reference: source-mode (floor version string) or ref-mode (OCI image object) */
35
+ const ServerReference = type('string').or({ image: 'string' })
36
+
37
+ // --- Main schema ---
38
+
39
+ /**
40
+ * The structural schema for the facet manifest — validates shape only.
41
+ * Custom constraints (at least one text asset, selective entry must select at least one type)
42
+ * are checked post-validation by checkFacetManifestConstraints().
43
+ */
44
+ export const FacetManifestSchema = type({
45
+ name: 'string',
46
+ version: 'string',
47
+ 'description?': 'string',
48
+ 'author?': 'string',
49
+ 'skills?': type.Record('string', SkillDescriptor),
50
+ 'agents?': type.Record('string', AgentDescriptor),
51
+ 'commands?': type.Record('string', CommandDescriptor),
52
+ 'facets?': FacetsEntry.array(),
53
+ 'servers?': type.Record('string', ServerReference),
54
+ })
55
+
56
+ /** Inferred TypeScript type for a validated facet manifest */
57
+ export type FacetManifest = typeof FacetManifestSchema.infer
58
+
59
+ // --- Custom validation ---
60
+
61
+ export interface FacetManifestError {
62
+ path: string
63
+ message: string
64
+ expected: string
65
+ actual: string
66
+ }
67
+
68
+ /**
69
+ * Checks business-rule constraints that ArkType's structural validation cannot express:
70
+ * 1. At least one text asset must be present (skills, agents, commands, or facets)
71
+ * 2. Selective facets entries must include at least one asset type
72
+ */
73
+ export function checkFacetManifestConstraints(manifest: FacetManifest): FacetManifestError[] {
74
+ const errors: FacetManifestError[] = []
75
+
76
+ // Constraint 1: at least one text asset
77
+ const hasSkills = manifest.skills && Object.keys(manifest.skills).length > 0
78
+ const hasAgents = manifest.agents && Object.keys(manifest.agents).length > 0
79
+ const hasCommands = manifest.commands && Object.keys(manifest.commands).length > 0
80
+ const hasFacets = manifest.facets && manifest.facets.length > 0
81
+
82
+ if (!hasSkills && !hasAgents && !hasCommands && !hasFacets) {
83
+ errors.push({
84
+ path: '',
85
+ message: 'Manifest must include at least one text asset (skills, agents, commands, or facets)',
86
+ expected: 'at least one of: skills, agents, commands, facets',
87
+ actual: 'none present',
88
+ })
89
+ }
90
+
91
+ // Constraint 2: selective facets entries must select at least one asset type
92
+ if (manifest.facets) {
93
+ for (let i = 0; i < manifest.facets.length; i++) {
94
+ const entry = manifest.facets[i]
95
+ if (typeof entry === 'object') {
96
+ const hasSelectedSkills = entry.skills && entry.skills.length > 0
97
+ const hasSelectedAgents = entry.agents && entry.agents.length > 0
98
+ const hasSelectedCommands = entry.commands && entry.commands.length > 0
99
+
100
+ if (!hasSelectedSkills && !hasSelectedAgents && !hasSelectedCommands) {
101
+ errors.push({
102
+ path: `facets[${i}]`,
103
+ message: 'Selective facets entry must include at least one asset type (skills, agents, or commands)',
104
+ expected: 'at least one of: skills, agents, commands',
105
+ actual: 'none selected',
106
+ })
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ return errors
113
+ }
@@ -0,0 +1,37 @@
1
+ import { type } from 'arktype'
2
+
3
+ /** Facet identity section of the lockfile */
4
+ const LockfileFacet = type({
5
+ name: 'string',
6
+ version: 'string',
7
+ integrity: 'string',
8
+ })
9
+
10
+ /** Source-mode server entry — resolved from the facets registry */
11
+ const SourceModeServerEntry = type({
12
+ version: 'string',
13
+ integrity: 'string',
14
+ api_surface: 'string',
15
+ })
16
+
17
+ /** Ref-mode server entry — resolved from an OCI registry */
18
+ const RefModeServerEntry = type({
19
+ image: 'string',
20
+ digest: 'string',
21
+ api_surface: 'string',
22
+ })
23
+
24
+ /** A lockfile server entry is either source-mode or ref-mode */
25
+ const ServerEntry = SourceModeServerEntry.or(RefModeServerEntry)
26
+
27
+ /**
28
+ * Schema for facets.lock — the lockfile recording resolved installation state.
29
+ * Matches the shape defined in ADR-003.
30
+ */
31
+ export const LockfileSchema = type({
32
+ facet: LockfileFacet,
33
+ 'servers?': type.Record('string', ServerEntry),
34
+ })
35
+
36
+ /** Inferred TypeScript type for a validated lockfile */
37
+ export type Lockfile = typeof LockfileSchema.infer
@@ -0,0 +1,17 @@
1
+ import { type } from 'arktype'
2
+
3
+ /**
4
+ * Schema for the server manifest (server.json).
5
+ * Matches the shape defined in ADR-005.
6
+ */
7
+ export const ServerManifestSchema = type({
8
+ name: 'string',
9
+ version: 'string',
10
+ runtime: 'string',
11
+ entry: 'string',
12
+ 'description?': 'string',
13
+ 'author?': 'string',
14
+ })
15
+
16
+ /** Inferred TypeScript type for a validated server manifest */
17
+ export type ServerManifest = typeof ServerManifestSchema.infer
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A structured validation error decoupled from ArkType internals.
3
+ * Used by all loaders to report schema and parsing failures.
4
+ */
5
+ export interface ValidationError {
6
+ /** Dot-separated path to the invalid field (e.g., "agents.reviewer.prompt") */
7
+ path: string
8
+ /** Human-readable error message */
9
+ message: string
10
+ /** What was expected at this location */
11
+ expected: string
12
+ /** What was actually found */
13
+ actual: string
14
+ }
15
+
16
+ /**
17
+ * Discriminated result type returned by all loaders.
18
+ * Callers check `ok` to determine success or failure.
19
+ */
20
+ export type Result<T> = { ok: true; data: T } | { ok: false; errors: ValidationError[] }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src"]
4
+ }