@vibe-agent-toolkit/resources 0.1.37 → 0.1.38
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/README.md +2 -2
- package/dist/ajv-factory.d.ts +33 -0
- package/dist/ajv-factory.d.ts.map +1 -0
- package/dist/ajv-factory.js +51 -0
- package/dist/ajv-factory.js.map +1 -0
- package/dist/config-parser.d.ts +0 -18
- package/dist/config-parser.d.ts.map +1 -1
- package/dist/config-parser.js +5 -46
- package/dist/config-parser.js.map +1 -1
- package/dist/frontmatter-editor.d.ts +45 -0
- package/dist/frontmatter-editor.d.ts.map +1 -0
- package/dist/frontmatter-editor.js +161 -0
- package/dist/frontmatter-editor.js.map +1 -0
- package/dist/frontmatter-validator.d.ts.map +1 -1
- package/dist/frontmatter-validator.js +11 -6
- package/dist/frontmatter-validator.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/json-pointer-path.d.ts +13 -0
- package/dist/json-pointer-path.d.ts.map +1 -0
- package/dist/json-pointer-path.js +30 -0
- package/dist/json-pointer-path.js.map +1 -0
- package/dist/link-parser.js +2 -11
- package/dist/link-parser.js.map +1 -1
- package/dist/link-validator.d.ts +22 -0
- package/dist/link-validator.d.ts.map +1 -1
- package/dist/link-validator.js +126 -75
- package/dist/link-validator.js.map +1 -1
- package/dist/rewriter-helpers.d.ts +49 -0
- package/dist/rewriter-helpers.d.ts.map +1 -0
- package/dist/rewriter-helpers.js +142 -0
- package/dist/rewriter-helpers.js.map +1 -0
- package/dist/types/resource-parser.d.ts.map +1 -1
- package/dist/types/resource-parser.js +2 -3
- package/dist/types/resource-parser.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +40 -11
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +39 -13
- package/dist/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/ajv-factory.ts +56 -0
- package/src/config-parser.ts +5 -51
- package/src/frontmatter-editor.ts +214 -0
- package/src/frontmatter-validator.ts +11 -7
- package/src/index.ts +21 -2
- package/src/json-pointer-path.ts +29 -0
- package/src/link-parser.ts +2 -11
- package/src/link-validator.ts +153 -88
- package/src/rewriter-helpers.ts +166 -0
- package/src/types/resource-parser.ts +2 -3
- package/src/types.ts +0 -1
- package/src/utils.ts +57 -14
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public rewriter helpers built on FrontmatterEditor and shared callback
|
|
3
|
+
* shape `(href: string) => string`.
|
|
4
|
+
*
|
|
5
|
+
* Three layers:
|
|
6
|
+
* 1. rewriteFrontmatterUriReferencesFromSchema — schema-driven; used by
|
|
7
|
+
* VAT packager and by schema-aware adopter scripts.
|
|
8
|
+
* 2. rewriteFrontmatterFieldsAtPaths — path-driven; used by ad-hoc
|
|
9
|
+
* adopter scripts (no JSON Schema available; field paths known by
|
|
10
|
+
* convention).
|
|
11
|
+
* 3. rewriteBodyLinks — standalone body-side counterpart. Parallel to
|
|
12
|
+
* `transformContent` but with a callback model instead of templates.
|
|
13
|
+
*
|
|
14
|
+
* See docs/superpowers/specs/2026-05-17-frontmatter-editor-and-yaml-consolidation-design.md
|
|
15
|
+
* §6 for the design contract.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { isNode } from 'yaml';
|
|
19
|
+
|
|
20
|
+
import type { FrontmatterEditor } from './frontmatter-editor.js';
|
|
21
|
+
import { jsonPointerToPath } from './json-pointer-path.js';
|
|
22
|
+
import { walkFrontmatterUriReferences } from './schema-uri-walker.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a value returned by `FrontmatterEditor.get(path)` into plain JS.
|
|
26
|
+
*
|
|
27
|
+
* The underlying yaml library's `getIn(path, false)` unwraps scalar leaves to
|
|
28
|
+
* JS primitives but leaves YAMLMap / YAMLSeq collections as Node instances.
|
|
29
|
+
* The rewriter helpers reason in terms of JS values (arrays, strings), so we
|
|
30
|
+
* unwrap collection Nodes here once at the boundary.
|
|
31
|
+
*/
|
|
32
|
+
function toPlainJs(value: unknown): unknown {
|
|
33
|
+
if (isNode(value)) return value.toJSON();
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Type for the rewrite policy callback — same shape for body and frontmatter. */
|
|
38
|
+
export type RewriteHref = (href: string) => string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply `rewriteHref` to every frontmatter scalar whose schema position has
|
|
42
|
+
* a URI-family `format`. Preserves comments via FrontmatterEditor.
|
|
43
|
+
*/
|
|
44
|
+
export function rewriteFrontmatterUriReferencesFromSchema(
|
|
45
|
+
editor: FrontmatterEditor,
|
|
46
|
+
schema: object,
|
|
47
|
+
rewriteHref: RewriteHref,
|
|
48
|
+
): void {
|
|
49
|
+
const frontmatter = editor.get([]);
|
|
50
|
+
if (frontmatter === undefined || frontmatter === null) return;
|
|
51
|
+
if (typeof frontmatter !== 'object') return;
|
|
52
|
+
|
|
53
|
+
const captures = walkFrontmatterUriReferences(frontmatter, schema);
|
|
54
|
+
for (const capture of captures) {
|
|
55
|
+
const path = jsonPointerToPath(capture.pointer);
|
|
56
|
+
if (path.length === 0) continue;
|
|
57
|
+
const newValue = rewriteHref(capture.value);
|
|
58
|
+
if (newValue === capture.value) continue;
|
|
59
|
+
const lastSegment = path.at(-1);
|
|
60
|
+
if (typeof lastSegment === 'number') {
|
|
61
|
+
const parentPath = path.slice(0, -1);
|
|
62
|
+
editor.setArrayItem(parentPath, lastSegment, newValue);
|
|
63
|
+
} else {
|
|
64
|
+
editor.set(path, newValue);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ParsedPath {
|
|
70
|
+
segments: readonly (string | number)[];
|
|
71
|
+
isArray: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parsePathPattern(pattern: string): ParsedPath {
|
|
75
|
+
const isArray = pattern.endsWith('[]');
|
|
76
|
+
const cleaned = isArray ? pattern.slice(0, -2) : pattern;
|
|
77
|
+
const segments = cleaned.split('.');
|
|
78
|
+
return { segments, isArray };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rewriteArrayPath(
|
|
82
|
+
editor: FrontmatterEditor,
|
|
83
|
+
segments: readonly (string | number)[],
|
|
84
|
+
value: unknown,
|
|
85
|
+
rewriteHref: RewriteHref,
|
|
86
|
+
): void {
|
|
87
|
+
if (!Array.isArray(value)) return;
|
|
88
|
+
for (const [i, item] of value.entries()) {
|
|
89
|
+
if (typeof item !== 'string') continue;
|
|
90
|
+
const newValue = rewriteHref(item);
|
|
91
|
+
if (newValue !== item) editor.setArrayItem(segments, i, newValue);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function rewriteScalarPath(
|
|
96
|
+
editor: FrontmatterEditor,
|
|
97
|
+
segments: readonly (string | number)[],
|
|
98
|
+
value: unknown,
|
|
99
|
+
rewriteHref: RewriteHref,
|
|
100
|
+
): void {
|
|
101
|
+
if (typeof value !== 'string') return;
|
|
102
|
+
const newValue = rewriteHref(value);
|
|
103
|
+
if (newValue !== value) editor.set(segments, newValue);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Apply `rewriteHref` to specific frontmatter fields named by explicit
|
|
108
|
+
* dotted paths. Used by adopter scripts that know which fields hold
|
|
109
|
+
* references without a JSON Schema.
|
|
110
|
+
*
|
|
111
|
+
* Path syntax:
|
|
112
|
+
* - 'name' — top-level scalar
|
|
113
|
+
* - 'name[]' — array of strings (rewrite each string item)
|
|
114
|
+
* - 'meta.parent' — dotted nested scalar
|
|
115
|
+
* - 'meta.refs[]' — dotted nested array of strings
|
|
116
|
+
*
|
|
117
|
+
* Missing paths are silent no-ops. Non-string values are skipped.
|
|
118
|
+
*/
|
|
119
|
+
export function rewriteFrontmatterFieldsAtPaths(
|
|
120
|
+
editor: FrontmatterEditor,
|
|
121
|
+
paths: readonly string[],
|
|
122
|
+
rewriteHref: RewriteHref,
|
|
123
|
+
): void {
|
|
124
|
+
for (const pattern of paths) {
|
|
125
|
+
const { segments, isArray } = parsePathPattern(pattern);
|
|
126
|
+
const value = toPlainJs(editor.get(segments));
|
|
127
|
+
if (value === undefined || value === null) continue;
|
|
128
|
+
if (isArray) {
|
|
129
|
+
rewriteArrayPath(editor, segments, value, rewriteHref);
|
|
130
|
+
} else {
|
|
131
|
+
rewriteScalarPath(editor, segments, value, rewriteHref);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Inline + reference-style link patterns. These match `transformContent`'s
|
|
138
|
+
* regex contract — see content-transform.ts for the rationale on negated
|
|
139
|
+
* character classes.
|
|
140
|
+
*/
|
|
141
|
+
// eslint-disable-next-line sonarjs/slow-regex -- negated character classes [^\]] and [^)] are non-backtracking
|
|
142
|
+
const INLINE_LINK_REGEX = /\[([^\]]*)\]\(([^)]*)\)/g;
|
|
143
|
+
// eslint-disable-next-line sonarjs/slow-regex -- Controlled markdown reference link definitions on line boundaries
|
|
144
|
+
const DEFINITION_LINK_REGEX = /^\[([^\]]*?)\]:\s*(.+)$/gm;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Apply `rewriteHref` to every inline-link and reference-definition href in
|
|
148
|
+
* a markdown body. Parallel to `transformContent` but with a callback model
|
|
149
|
+
* — appropriate for adopter scripts and for any caller that wants a
|
|
150
|
+
* per-href rewrite without rule/template ceremony.
|
|
151
|
+
*
|
|
152
|
+
* `rewriteHref` receives the raw href (including any fragment) and returns
|
|
153
|
+
* the new href. Returning the same string is a no-op for that link.
|
|
154
|
+
*/
|
|
155
|
+
export function rewriteBodyLinks(body: string, rewriteHref: RewriteHref): string {
|
|
156
|
+
let out = body.replaceAll(INLINE_LINK_REGEX, (full, text: string, href: string) => {
|
|
157
|
+
const next = rewriteHref(href);
|
|
158
|
+
return next === href ? full : `[${text}](${next})`;
|
|
159
|
+
});
|
|
160
|
+
out = out.replaceAll(DEFINITION_LINK_REGEX, (full, ref: string, href: string) => {
|
|
161
|
+
const trimmed = href.trim();
|
|
162
|
+
const next = rewriteHref(trimmed);
|
|
163
|
+
return next === trimmed ? full : `[${ref}]: ${next}`;
|
|
164
|
+
});
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { promises as fs } from 'node:fs';
|
|
9
9
|
|
|
10
|
-
import yaml from '
|
|
10
|
+
import * as yaml from 'yaml';
|
|
11
11
|
|
|
12
12
|
import { calculateChecksum } from '../checksum.js';
|
|
13
13
|
import { parseMarkdown } from '../link-parser.js';
|
|
@@ -213,8 +213,7 @@ export async function parseYamlResource(
|
|
|
213
213
|
// Read and parse YAML
|
|
214
214
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
215
215
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
216
|
-
|
|
217
|
-
const data = yaml.load(content, { schema: yaml.CORE_SCHEMA });
|
|
216
|
+
const data = yaml.parse(content);
|
|
218
217
|
|
|
219
218
|
// Calculate checksum
|
|
220
219
|
const checksum = await calculateChecksum(absolutePath);
|
package/src/types.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -73,35 +73,58 @@ export function splitHrefAnchor(href: string): [string, string | undefined] {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
76
|
+
* Discriminated union returned by {@link resolveLocalHref}.
|
|
77
|
+
*
|
|
78
|
+
* - `anchor_only` — the href was `#fragment` only (no file component).
|
|
79
|
+
* - `resolved` — the href resolved to an absolute filesystem path.
|
|
80
|
+
* - `absolute_no_root` — the href is an RFC 3986 §4.2 absolute-path
|
|
81
|
+
* reference (starts with `/`) but no `projectRoot` was supplied.
|
|
82
|
+
* - `absolute_escapes_root` — the absolute-path reference resolved to a
|
|
83
|
+
* location outside `projectRoot` (e.g., via `..` traversal or a symlink
|
|
84
|
+
* pointing outside the project).
|
|
85
|
+
*/
|
|
86
|
+
export type ResolveLocalHrefResult =
|
|
87
|
+
| { kind: 'anchor_only' }
|
|
88
|
+
| { kind: 'resolved'; resolvedPath: string; anchor: string | undefined }
|
|
89
|
+
| { kind: 'absolute_no_root'; href: string; anchor: string | undefined }
|
|
90
|
+
| { kind: 'absolute_escapes_root'; href: string; anchor: string | undefined };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a markdown link href to a filesystem path or a typed failure.
|
|
77
94
|
*
|
|
78
95
|
* Performs the standard href → path conversion used by both audit and validate:
|
|
79
96
|
* 1. Strips anchor fragment (`#section`)
|
|
80
97
|
* 2. Decodes URL-encoded characters (`%20` → space, `%26` → `&`)
|
|
81
|
-
* 3. Resolves the path
|
|
82
|
-
*
|
|
83
|
-
*
|
|
98
|
+
* 3. Resolves the path:
|
|
99
|
+
* - Leading `/` (RFC 3986 §4.2 absolute-path reference) → resolve against
|
|
100
|
+
* `projectRoot`. Requires a `projectRoot`; must not escape it.
|
|
101
|
+
* - Otherwise → resolve relative to the source file's directory.
|
|
84
102
|
*
|
|
85
103
|
* @param href - Raw href from a markdown link
|
|
86
104
|
* @param sourceFilePath - Absolute path of the file containing the link
|
|
87
|
-
* @
|
|
105
|
+
* @param projectRoot - Optional project root for absolute-path references.
|
|
106
|
+
* @returns A {@link ResolveLocalHrefResult} discriminating success vs failure modes.
|
|
88
107
|
*
|
|
89
108
|
* @example
|
|
90
109
|
* ```typescript
|
|
91
110
|
* resolveLocalHref('My%20Folder/doc.md#intro', '/project/README.md')
|
|
92
|
-
* // { resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
|
|
111
|
+
* // { kind: 'resolved', resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
|
|
93
112
|
*
|
|
94
113
|
* resolveLocalHref('#heading', '/project/README.md')
|
|
95
|
-
* //
|
|
114
|
+
* // { kind: 'anchor_only' }
|
|
115
|
+
*
|
|
116
|
+
* resolveLocalHref('/docs/foo.md', '/project/docs/sub/page.md', '/project')
|
|
117
|
+
* // { kind: 'resolved', resolvedPath: '/project/docs/foo.md', anchor: undefined }
|
|
96
118
|
* ```
|
|
97
119
|
*/
|
|
98
120
|
export function resolveLocalHref(
|
|
99
121
|
href: string,
|
|
100
122
|
sourceFilePath: string,
|
|
101
|
-
|
|
123
|
+
projectRoot?: string,
|
|
124
|
+
): ResolveLocalHrefResult {
|
|
102
125
|
const [fileHref, anchor] = splitHrefAnchor(href);
|
|
103
126
|
if (fileHref === '') {
|
|
104
|
-
return
|
|
127
|
+
return { kind: 'anchor_only' };
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
let decodedHref: string;
|
|
@@ -111,10 +134,22 @@ export function resolveLocalHref(
|
|
|
111
134
|
decodedHref = fileHref;
|
|
112
135
|
}
|
|
113
136
|
|
|
137
|
+
// RFC 3986 §4.2 absolute-path reference — resolve against projectRoot.
|
|
138
|
+
if (decodedHref.startsWith('/')) {
|
|
139
|
+
if (!projectRoot) {
|
|
140
|
+
return { kind: 'absolute_no_root', href: fileHref, anchor };
|
|
141
|
+
}
|
|
142
|
+
const candidate = safePath.resolve(projectRoot, decodedHref.slice(1));
|
|
143
|
+
if (!isWithinProject(candidate, projectRoot)) {
|
|
144
|
+
return { kind: 'absolute_escapes_root', href: fileHref, anchor };
|
|
145
|
+
}
|
|
146
|
+
return { kind: 'resolved', resolvedPath: candidate, anchor };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Relative reference — resolve against the source file's directory.
|
|
114
150
|
const sourceDir = path.dirname(sourceFilePath);
|
|
115
151
|
const resolvedPath = safePath.resolve(sourceDir, decodedHref);
|
|
116
|
-
|
|
117
|
-
return { resolvedPath, anchor };
|
|
152
|
+
return { kind: 'resolved', resolvedPath, anchor };
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
/**
|
|
@@ -135,7 +170,9 @@ export function resolveLocalHref(
|
|
|
135
170
|
* ```
|
|
136
171
|
*/
|
|
137
172
|
export function isWithinProject(filePath: string, projectRoot: string): boolean {
|
|
138
|
-
//
|
|
173
|
+
// Canonicalize both sides symmetrically. Asymmetric handling (realpath one
|
|
174
|
+
// side, resolve the other) false-flags legitimate matches when projectRoot
|
|
175
|
+
// traverses a symlink — e.g. macOS /tmp → /private/tmp, bind mounts.
|
|
139
176
|
let resolvedFilePath: string;
|
|
140
177
|
try {
|
|
141
178
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- filePath is validated path parameter
|
|
@@ -145,7 +182,13 @@ export function isWithinProject(filePath: string, projectRoot: string): boolean
|
|
|
145
182
|
resolvedFilePath = safePath.resolve(filePath);
|
|
146
183
|
}
|
|
147
184
|
|
|
148
|
-
|
|
185
|
+
let resolvedProjectRoot: string;
|
|
186
|
+
try {
|
|
187
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- projectRoot is validated path parameter
|
|
188
|
+
resolvedProjectRoot = fs.realpathSync(projectRoot);
|
|
189
|
+
} catch {
|
|
190
|
+
resolvedProjectRoot = safePath.resolve(projectRoot);
|
|
191
|
+
}
|
|
149
192
|
|
|
150
193
|
// Normalize to forward slashes for cross-platform comparison
|
|
151
194
|
const normalizedFile = toForwardSlash(resolvedFilePath);
|
|
@@ -199,7 +242,7 @@ export function formatJsonPointerAsDotted(pointer: string): string {
|
|
|
199
242
|
return out;
|
|
200
243
|
}
|
|
201
244
|
|
|
202
|
-
function isCanonicalArrayIndex(s: string): boolean {
|
|
245
|
+
export function isCanonicalArrayIndex(s: string): boolean {
|
|
203
246
|
// Canonical integer per RFC 6901 §4 + JSON canonical form: no leading zeros
|
|
204
247
|
// except for "0" itself.
|
|
205
248
|
if (s === '') return false;
|