@vibe-agent-toolkit/resources 0.1.34 → 0.1.36
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/dist/frontmatter-link-validator.d.ts +42 -0
- package/dist/frontmatter-link-validator.d.ts.map +1 -0
- package/dist/frontmatter-link-validator.js +86 -0
- package/dist/frontmatter-link-validator.js.map +1 -0
- package/dist/link-parser.d.ts +21 -1
- package/dist/link-parser.d.ts.map +1 -1
- package/dist/link-parser.js +6 -2
- package/dist/link-parser.js.map +1 -1
- package/dist/resource-registry.d.ts +6 -0
- package/dist/resource-registry.d.ts.map +1 -1
- package/dist/resource-registry.js +44 -10
- package/dist/resource-registry.js.map +1 -1
- package/dist/schema-uri-walker.d.ts +39 -0
- package/dist/schema-uri-walker.d.ts.map +1 -0
- package/dist/schema-uri-walker.js +154 -0
- package/dist/schema-uri-walker.js.map +1 -0
- package/dist/schemas/project-config.d.ts +157 -25
- package/dist/schemas/project-config.d.ts.map +1 -1
- package/dist/schemas/project-config.js +19 -0
- package/dist/schemas/project-config.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +21 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +55 -0
- package/dist/utils.js.map +1 -1
- package/package.json +5 -3
- package/src/frontmatter-link-validator.ts +112 -0
- package/src/link-parser.ts +6 -2
- package/src/resource-registry.ts +77 -13
- package/src/schema-uri-walker.ts +222 -0
- package/src/schemas/project-config.ts +20 -0
- package/src/types.ts +8 -0
- package/src/utils.ts +54 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure schema/data traversal that captures every string value sitting at a
|
|
3
|
+
* JSON Schema position whose `format` is in the URI family.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - `properties` (recursion)
|
|
7
|
+
* - `items` (single-schema and tuple)
|
|
8
|
+
* - `oneOf` / `anyOf` / `allOf` (every branch walked) AND sibling keywords
|
|
9
|
+
* in the same node (JSON Schema AND semantics)
|
|
10
|
+
* - `$ref` (resolved against schema root via JSON Pointer; cycle-protected)
|
|
11
|
+
* - `definitions` and `$defs` as ref targets
|
|
12
|
+
*
|
|
13
|
+
* Does NOT handle (intentional, see spec §"Non-Goals"):
|
|
14
|
+
* - `if`/`then`/`else`, `dependentSchemas`
|
|
15
|
+
* - `patternProperties`, schema-form `additionalProperties`
|
|
16
|
+
* - `prefixItems` (JSON Schema 2020-12)
|
|
17
|
+
*
|
|
18
|
+
* Captures are deduplicated by `(pointer, value)` before return so that
|
|
19
|
+
* multiple matching composite branches don't produce duplicate issues.
|
|
20
|
+
*
|
|
21
|
+
* No I/O. No side effects.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { decodeJsonPointerSegment, encodeJsonPointerSegment, formatJsonPointerAsDotted } from './utils.js';
|
|
25
|
+
|
|
26
|
+
const URI_FAMILY_FORMATS = new Set<UriFamilyFormat>([
|
|
27
|
+
'uri-reference',
|
|
28
|
+
'uri',
|
|
29
|
+
'iri-reference',
|
|
30
|
+
'iri',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export type UriFamilyFormat = 'uri' | 'uri-reference' | 'iri' | 'iri-reference';
|
|
34
|
+
|
|
35
|
+
export interface FrontmatterUriCapture {
|
|
36
|
+
/** Raw string value from frontmatter */
|
|
37
|
+
value: string;
|
|
38
|
+
/** RFC 6901 JSON Pointer to the value within the frontmatter document */
|
|
39
|
+
pointer: string;
|
|
40
|
+
/** Developer-friendly dotted form (e.g., adr-citations[0].adr) */
|
|
41
|
+
dottedPath: string;
|
|
42
|
+
/** The URI-family format keyword present on the schema node */
|
|
43
|
+
format: UriFamilyFormat;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SchemaNode {
|
|
47
|
+
type?: string | string[];
|
|
48
|
+
format?: string;
|
|
49
|
+
properties?: Record<string, SchemaNode>;
|
|
50
|
+
items?: SchemaNode | SchemaNode[];
|
|
51
|
+
oneOf?: SchemaNode[];
|
|
52
|
+
anyOf?: SchemaNode[];
|
|
53
|
+
allOf?: SchemaNode[];
|
|
54
|
+
$ref?: string;
|
|
55
|
+
// $defs / definitions / etc. are arbitrary root-level keys reached via $ref.
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Walk a frontmatter document against a JSON Schema and return every value
|
|
61
|
+
* whose schema position has a URI-family `format` keyword.
|
|
62
|
+
*/
|
|
63
|
+
export function walkFrontmatterUriReferences(
|
|
64
|
+
data: unknown,
|
|
65
|
+
schema: object,
|
|
66
|
+
): FrontmatterUriCapture[] {
|
|
67
|
+
if (data === undefined || data === null) return [];
|
|
68
|
+
const captures: FrontmatterUriCapture[] = [];
|
|
69
|
+
walk(data, schema as SchemaNode, schema as SchemaNode, [], new Set<string>(), captures);
|
|
70
|
+
return dedupe(captures);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walkComposites(
|
|
74
|
+
data: unknown,
|
|
75
|
+
node: SchemaNode,
|
|
76
|
+
root: SchemaNode,
|
|
77
|
+
pointerSegments: string[],
|
|
78
|
+
visitedRefs: Set<string>,
|
|
79
|
+
captures: FrontmatterUriCapture[],
|
|
80
|
+
): void {
|
|
81
|
+
for (const branchList of [node.oneOf, node.anyOf, node.allOf]) {
|
|
82
|
+
if (Array.isArray(branchList)) {
|
|
83
|
+
for (const branch of branchList) {
|
|
84
|
+
walk(data, branch, root, pointerSegments, visitedRefs, captures);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkProperties(
|
|
91
|
+
data: unknown,
|
|
92
|
+
node: SchemaNode,
|
|
93
|
+
root: SchemaNode,
|
|
94
|
+
pointerSegments: string[],
|
|
95
|
+
visitedRefs: Set<string>,
|
|
96
|
+
captures: FrontmatterUriCapture[],
|
|
97
|
+
): void {
|
|
98
|
+
if (!node.properties || data === null || typeof data !== 'object' || Array.isArray(data)) return;
|
|
99
|
+
const dataObj = data as Record<string, unknown>;
|
|
100
|
+
for (const [key, propSchema] of Object.entries(node.properties)) {
|
|
101
|
+
if (key in dataObj) {
|
|
102
|
+
walk(
|
|
103
|
+
dataObj[key],
|
|
104
|
+
propSchema,
|
|
105
|
+
root,
|
|
106
|
+
[...pointerSegments, encodeJsonPointerSegment(key)],
|
|
107
|
+
visitedRefs,
|
|
108
|
+
captures,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function walkItems(
|
|
115
|
+
data: unknown,
|
|
116
|
+
node: SchemaNode,
|
|
117
|
+
root: SchemaNode,
|
|
118
|
+
pointerSegments: string[],
|
|
119
|
+
visitedRefs: Set<string>,
|
|
120
|
+
captures: FrontmatterUriCapture[],
|
|
121
|
+
): void {
|
|
122
|
+
if (!node.items || !Array.isArray(data)) return;
|
|
123
|
+
if (Array.isArray(node.items)) {
|
|
124
|
+
const tupleSchemas = node.items;
|
|
125
|
+
for (const [i, itemValue] of data.entries()) {
|
|
126
|
+
if (i >= tupleSchemas.length) break;
|
|
127
|
+
walk(itemValue, tupleSchemas[i] as SchemaNode, root, [...pointerSegments, String(i)], visitedRefs, captures);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
for (const [i, itemValue] of data.entries()) {
|
|
131
|
+
walk(itemValue, node.items, root, [...pointerSegments, String(i)], visitedRefs, captures);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function walk(
|
|
137
|
+
data: unknown,
|
|
138
|
+
node: SchemaNode,
|
|
139
|
+
root: SchemaNode,
|
|
140
|
+
pointerSegments: string[],
|
|
141
|
+
visitedRefs: Set<string>,
|
|
142
|
+
captures: FrontmatterUriCapture[],
|
|
143
|
+
): void {
|
|
144
|
+
if (!node || typeof node !== 'object') return;
|
|
145
|
+
|
|
146
|
+
// Resolve $ref against schema root. Cycle protection: skip if already on the
|
|
147
|
+
// recursion stack. Pop after recursion.
|
|
148
|
+
if (typeof node.$ref === 'string') {
|
|
149
|
+
if (visitedRefs.has(node.$ref)) return;
|
|
150
|
+
const resolved = resolveRef(node.$ref, root);
|
|
151
|
+
if (!resolved) return;
|
|
152
|
+
visitedRefs.add(node.$ref);
|
|
153
|
+
walk(data, resolved, root, pointerSegments, visitedRefs, captures);
|
|
154
|
+
visitedRefs.delete(node.$ref);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Composite schemas: walk every branch. CRITICAL: do NOT short-circuit;
|
|
159
|
+
// sibling `properties`/`items` are AND-combined with the composite.
|
|
160
|
+
walkComposites(data, node, root, pointerSegments, visitedRefs, captures);
|
|
161
|
+
|
|
162
|
+
// URI-family format leaf
|
|
163
|
+
if (
|
|
164
|
+
typeof node.format === 'string' &&
|
|
165
|
+
URI_FAMILY_FORMATS.has(node.format as UriFamilyFormat) &&
|
|
166
|
+
typeof data === 'string'
|
|
167
|
+
) {
|
|
168
|
+
const pointer = pointerSegments.length === 0 ? '' : '/' + pointerSegments.join('/');
|
|
169
|
+
captures.push({
|
|
170
|
+
value: data,
|
|
171
|
+
pointer,
|
|
172
|
+
dottedPath: formatJsonPointerAsDotted(pointer),
|
|
173
|
+
format: node.format as UriFamilyFormat,
|
|
174
|
+
});
|
|
175
|
+
// Fall through — schemas with both `format` and sibling object/array
|
|
176
|
+
// structure are unusual but possible; let recursion proceed.
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Object recursion
|
|
180
|
+
walkProperties(data, node, root, pointerSegments, visitedRefs, captures);
|
|
181
|
+
|
|
182
|
+
// Array recursion
|
|
183
|
+
walkItems(data, node, root, pointerSegments, visitedRefs, captures);
|
|
184
|
+
|
|
185
|
+
// Intentionally NOT handled: if/then/else, dependentSchemas, patternProperties,
|
|
186
|
+
// schema-form additionalProperties, prefixItems (2020-12). See spec §"Non-Goals".
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve a local $ref (e.g., "#/$defs/Foo") against the schema root using a
|
|
191
|
+
* generic JSON Pointer walk. Returns null for unresolvable refs or non-local
|
|
192
|
+
* refs (no cross-file support in v1).
|
|
193
|
+
*/
|
|
194
|
+
function resolveRef(ref: string, root: SchemaNode): SchemaNode | null {
|
|
195
|
+
if (!ref.startsWith('#/')) return null;
|
|
196
|
+
// eslint-disable-next-line local/no-hardcoded-path-split -- RFC 6901 JSON Pointer segment splitting, not a file path
|
|
197
|
+
const segments = ref.slice(2).split('/').map(decodeJsonPointerSegment);
|
|
198
|
+
let cursor: unknown = root;
|
|
199
|
+
for (const seg of segments) {
|
|
200
|
+
if (cursor === null || typeof cursor !== 'object') return null;
|
|
201
|
+
cursor = (cursor as Record<string, unknown>)[seg];
|
|
202
|
+
if (cursor === undefined) return null;
|
|
203
|
+
}
|
|
204
|
+
return (cursor as SchemaNode) ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Remove duplicate captures by (pointer, value). Multiple matching branches
|
|
209
|
+
* of `oneOf`/`anyOf`/`allOf` can produce duplicates; users should see one
|
|
210
|
+
* issue per field, not one per branch.
|
|
211
|
+
*/
|
|
212
|
+
function dedupe(captures: FrontmatterUriCapture[]): FrontmatterUriCapture[] {
|
|
213
|
+
const seen = new Set<string>();
|
|
214
|
+
const out: FrontmatterUriCapture[] = [];
|
|
215
|
+
for (const c of captures) {
|
|
216
|
+
const key = c.pointer + ' ' + c.value;
|
|
217
|
+
if (seen.has(key)) continue;
|
|
218
|
+
seen.add(key);
|
|
219
|
+
out.push(c);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { ValidationConfigSchema } from '@vibe-agent-toolkit/agent-schema';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Official semver regex from https://semver.org/ (anchored).
|
|
6
|
+
*
|
|
7
|
+
* Used as the JSON-Schema-friendly source of truth for plugin version
|
|
8
|
+
* validation. A `.refine()` over `semver.valid()` would not survive
|
|
9
|
+
* `zod-to-json-schema` export — external consumers validating against the
|
|
10
|
+
* exported JSON Schema would silently accept invalid versions. A `.regex()`
|
|
11
|
+
* round-trips into JSON Schema as `pattern`, preserving the constraint.
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line security/detect-unsafe-regex, sonarjs/regex-complexity -- Official semver regex from https://semver.org/; not user-controlled input.
|
|
14
|
+
const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
15
|
+
|
|
4
16
|
// Re-export for downstream consumers (unicorn/prefer-export-from satisfied by the import above)
|
|
5
17
|
export { ValidationConfigSchema } from '@vibe-agent-toolkit/agent-schema';
|
|
6
18
|
|
|
@@ -49,6 +61,8 @@ export const CollectionValidationSchema = z.object({
|
|
|
49
61
|
.describe('Whether to validate external URL links (default: false)'),
|
|
50
62
|
checkGitIgnored: z.boolean().optional()
|
|
51
63
|
.describe('Whether to check if non-ignored files link to git-ignored files (default: true)'),
|
|
64
|
+
checkFrontmatterLinks: z.boolean().optional()
|
|
65
|
+
.describe('Whether to validate frontmatter values at JSON Schema positions with a URI-family format (default: true). Set to false to disable for this collection.'),
|
|
52
66
|
externalUrls: ExternalUrlValidationSchema.optional()
|
|
53
67
|
.describe('External URL validation configuration'),
|
|
54
68
|
}).describe('Validation configuration for a collection');
|
|
@@ -182,6 +196,12 @@ export const ClaudeMarketplacePluginEntrySchema = z.object({
|
|
|
182
196
|
.describe('Path to plugin directory (default: plugins/<name>)'),
|
|
183
197
|
files: z.array(SkillFileEntrySchema).optional()
|
|
184
198
|
.describe('Explicit source→dest file mappings for compiled artifacts outside the plugin directory'),
|
|
199
|
+
version: z.string().regex(SEMVER_REGEX, {
|
|
200
|
+
message: 'version must be a valid semver string (e.g., "1.2.3" or "1.0.0-rc.1")',
|
|
201
|
+
}).optional()
|
|
202
|
+
.describe('Per-plugin semver version (overrides root package.json:version for this plugin)'),
|
|
203
|
+
changelog: z.string().optional()
|
|
204
|
+
.describe('Path to per-plugin CHANGELOG (relative to plugin source dir; default: <source>/CHANGELOG.md if it exists)'),
|
|
185
205
|
}).strict().describe('Plugin entry within a marketplace configuration');
|
|
186
206
|
|
|
187
207
|
export type ClaudeMarketplacePluginEntry = z.infer<typeof ClaudeMarketplacePluginEntrySchema>;
|
package/src/types.ts
CHANGED
|
@@ -110,6 +110,7 @@ export {
|
|
|
110
110
|
// Link validation
|
|
111
111
|
export type { ValidateLinkOptions } from './link-validator.js';
|
|
112
112
|
export { validateLink } from './link-validator.js';
|
|
113
|
+
export { classifyLink } from './link-parser.js';
|
|
113
114
|
|
|
114
115
|
// Schema assignment
|
|
115
116
|
export {
|
|
@@ -124,3 +125,10 @@ export {
|
|
|
124
125
|
hasSchemaErrors,
|
|
125
126
|
validateFrontmatterMultiSchema,
|
|
126
127
|
} from './multi-schema-validator.js';
|
|
128
|
+
|
|
129
|
+
// Frontmatter link validation
|
|
130
|
+
export type {
|
|
131
|
+
FrontmatterExternalUrl,
|
|
132
|
+
FrontmatterLinkValidationResult,
|
|
133
|
+
} from './frontmatter-link-validator.js';
|
|
134
|
+
export { validateFrontmatterLinks } from './frontmatter-link-validator.js';
|
package/src/utils.ts
CHANGED
|
@@ -156,3 +156,57 @@ export function isWithinProject(filePath: string, projectRoot: string): boolean
|
|
|
156
156
|
// /project-other starting with /project
|
|
157
157
|
return normalizedFile.startsWith(normalizedRoot + '/') || normalizedFile === normalizedRoot;
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Escape a property name as a JSON Pointer segment per RFC 6901:
|
|
162
|
+
* `~` -> `~0`, `/` -> `~1`. Order matters (escape `~` first).
|
|
163
|
+
*/
|
|
164
|
+
export function encodeJsonPointerSegment(name: string): string {
|
|
165
|
+
return name.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reverse RFC 6901 escapes: `~1` -> `/`, `~0` -> `~`. Order matters
|
|
170
|
+
* (unescape `~1` first).
|
|
171
|
+
*/
|
|
172
|
+
export function decodeJsonPointerSegment(segment: string): string {
|
|
173
|
+
return segment.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Format a JSON Pointer (RFC 6901) as developer-friendly dotted notation.
|
|
178
|
+
*
|
|
179
|
+
* Numeric segments become bracketed array indices (`0` → `[0]`); non-numeric
|
|
180
|
+
* segments are dot-joined. Reverses RFC 6901 escapes inside segments.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* formatJsonPointerAsDotted('/adr-citations/0/adr') // 'adr-citations[0].adr'
|
|
184
|
+
* formatJsonPointerAsDotted('') // ''
|
|
185
|
+
*/
|
|
186
|
+
export function formatJsonPointerAsDotted(pointer: string): string {
|
|
187
|
+
if (pointer === '') return '';
|
|
188
|
+
// eslint-disable-next-line local/no-hardcoded-path-split -- JSON Pointer RFC 6901 delimiter, not a file path
|
|
189
|
+
const segments = pointer.slice(1).split('/').map(decodeJsonPointerSegment);
|
|
190
|
+
|
|
191
|
+
let out = '';
|
|
192
|
+
for (const seg of segments) {
|
|
193
|
+
if (isCanonicalArrayIndex(seg)) {
|
|
194
|
+
out += `[${seg}]`;
|
|
195
|
+
} else {
|
|
196
|
+
out += out === '' ? seg : `.${seg}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isCanonicalArrayIndex(s: string): boolean {
|
|
203
|
+
// Canonical integer per RFC 6901 §4 + JSON canonical form: no leading zeros
|
|
204
|
+
// except for "0" itself.
|
|
205
|
+
if (s === '') return false;
|
|
206
|
+
if (s === '0') return true;
|
|
207
|
+
if (s.startsWith('0')) return false;
|
|
208
|
+
for (const ch of s) {
|
|
209
|
+
if (ch < '0' || ch > '9') return false;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|