@shahimat/harness 0.1.0 → 0.2.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/README.md +1 -1
- package/dist/checks/context-index.js +34 -18
- package/dist/prompt-index.d.ts +2 -0
- package/dist/prompt-index.js +57 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,36 +229,55 @@ class Collector {
|
|
|
229
229
|
this.report(manifest, `index_layers: dir «${String(dir)}» — нужен путь внутри проекта`);
|
|
230
230
|
continue;
|
|
231
231
|
}
|
|
232
|
+
const format = (isRecord(item) ? item['format'] : undefined) ?? 'md';
|
|
233
|
+
if (format !== 'md' && format !== 'yml') {
|
|
234
|
+
this.report(manifest, `index_layers: format «${String(format)}» — md или yml`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
232
237
|
const taken = layers.find((l) => l.section === section);
|
|
233
238
|
if (taken !== undefined) {
|
|
234
239
|
this.report(manifest, `index_layers: section «${section}» уже объявлена в ${taken.manifest}`);
|
|
235
240
|
continue;
|
|
236
241
|
}
|
|
237
|
-
layers.push({ section, dir: dir.replace(/\/+$/, ''), manifest });
|
|
242
|
+
layers.push({ section, dir: dir.replace(/\/+$/, ''), format, manifest });
|
|
238
243
|
}
|
|
239
244
|
}
|
|
240
245
|
return layers;
|
|
241
246
|
}
|
|
242
247
|
/**
|
|
243
|
-
* Файл слоя
|
|
244
|
-
*
|
|
248
|
+
* Файл слоя модуля: `md` — frontmatter `id`, `namespace`, строка индекса —
|
|
249
|
+
* первый `# …`; `yml` — поля `id`, `namespace`, `summary`. `id` = путь
|
|
250
|
+
* внутри слоя без расширения, `/` → `-`.
|
|
245
251
|
*/
|
|
246
252
|
async collectLayer(layer) {
|
|
247
|
-
|
|
253
|
+
const ext = layer.format === 'md' ? '.md' : '.yml';
|
|
254
|
+
for (const file of await listFiles(this.root, layer.dir, ext)) {
|
|
248
255
|
let data;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
data =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
this.
|
|
255
|
-
continue;
|
|
256
|
+
let title;
|
|
257
|
+
if (layer.format === 'yml') {
|
|
258
|
+
data = await this.readYaml(file);
|
|
259
|
+
if (data === null)
|
|
260
|
+
continue;
|
|
261
|
+
title = this.oneLine(file, data, 'summary');
|
|
256
262
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
else {
|
|
264
|
+
const text = await readFile(path.join(this.root, file), 'utf8');
|
|
265
|
+
try {
|
|
266
|
+
data = parseFrontmatter(text);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
this.report(file, `невалидный frontmatter: ${error.message.split('\n')[0]}`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (data === null) {
|
|
273
|
+
this.report(file, 'нет frontmatter (id, namespace)');
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
title = firstHeading(text);
|
|
277
|
+
if (title === null)
|
|
278
|
+
this.report(file, 'нет заголовка «# …» (одна строка для индекса)');
|
|
260
279
|
}
|
|
261
|
-
const expected = file.slice(layer.dir.length + 1, -
|
|
280
|
+
const expected = file.slice(layer.dir.length + 1, -ext.length).split('/').join('-');
|
|
262
281
|
const id = data['id'];
|
|
263
282
|
if (typeof id !== 'string' || id === '') {
|
|
264
283
|
this.report(file, 'нет поля id');
|
|
@@ -267,9 +286,6 @@ class Collector {
|
|
|
267
286
|
if (id !== expected)
|
|
268
287
|
this.report(file, `id «${id}» не совпадает с путём «${expected}»`);
|
|
269
288
|
const ns = this.checkNamespace(file, data);
|
|
270
|
-
const title = firstHeading(text);
|
|
271
|
-
if (title === null)
|
|
272
|
-
this.report(file, 'нет заголовка «# …» (одна строка для индекса)');
|
|
273
289
|
if (ns === null)
|
|
274
290
|
continue;
|
|
275
291
|
const sections = this.namespace(ns).layers;
|
package/dist/prompt-index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface IndexEntry {
|
|
|
8
8
|
applicableTo: string[];
|
|
9
9
|
patterns: string[];
|
|
10
10
|
title?: string;
|
|
11
|
+
/** Чем заменён устаревший промпт: ключ или путь замены. */
|
|
12
|
+
deprecated?: string;
|
|
11
13
|
/** Блок `feature` модуля-возможности — то, что нужно для чеклиста. */
|
|
12
14
|
feature?: FeatureInfo;
|
|
13
15
|
/** Файлы skill: `path` от корня репозитория и SHA-256 каждого. */
|
package/dist/prompt-index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { stringify } from 'yaml';
|
|
4
|
+
import { parse, stringify } from 'yaml';
|
|
5
5
|
import { normalizeEol, parseFrontmatter } from './frontmatter.js';
|
|
6
6
|
export const INDEX_PATH = 'prompts/index.yml';
|
|
7
7
|
const SOURCE_DIRS = ['prompts', 'patterns'];
|
|
8
8
|
const HEADER = '# generated by harness index build — do not edit manually';
|
|
9
9
|
const SKILLS_DIR = 'skills';
|
|
10
10
|
const AGENTS_DIR = 'agents';
|
|
11
|
+
const PRESETS_DIR = 'presets';
|
|
11
12
|
const GROUPS = [
|
|
12
13
|
{ kind: 'contract', label: 'contracts' },
|
|
13
14
|
{ kind: 'pattern', label: 'patterns' },
|
|
14
15
|
{ kind: 'module', label: 'modules' },
|
|
15
16
|
{ kind: 'skill', label: 'skills' },
|
|
16
17
|
{ kind: 'agent', label: 'agents' },
|
|
18
|
+
{ kind: 'preset', label: 'presets' },
|
|
17
19
|
{ kind: 'prompt', label: 'prompts' },
|
|
18
20
|
];
|
|
19
21
|
/** Поля `feature` во frontmatter; `asks` читается из самого модуля, в индекс не идёт. */
|
|
@@ -128,6 +130,9 @@ export async function collectEntries(root) {
|
|
|
128
130
|
const title = oneLine(fm?.['title'], 'title', file);
|
|
129
131
|
if (title !== undefined)
|
|
130
132
|
entry.title = title;
|
|
133
|
+
const deprecated = oneLine(fm?.['deprecated'], 'deprecated', file);
|
|
134
|
+
if (deprecated !== undefined)
|
|
135
|
+
entry.deprecated = deprecated;
|
|
131
136
|
const feature = parseFeature(fm?.['feature'], kind, file);
|
|
132
137
|
if (feature !== undefined)
|
|
133
138
|
entry.feature = feature;
|
|
@@ -135,6 +140,7 @@ export async function collectEntries(root) {
|
|
|
135
140
|
}
|
|
136
141
|
entries.push(...(await collectSkills(root)), ...(await collectAgents(root)));
|
|
137
142
|
checkFeatureRefs(entries);
|
|
143
|
+
entries.push(...(await collectPresets(root, entries)));
|
|
138
144
|
return { entries, skipped };
|
|
139
145
|
}
|
|
140
146
|
async function listNames(abs) {
|
|
@@ -191,6 +197,53 @@ async function collectSkills(root) {
|
|
|
191
197
|
}
|
|
192
198
|
return entries;
|
|
193
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Пресет — `presets/<key>.yml`: готовый набор возможностей для типа
|
|
202
|
+
* проекта. Модули пресета должны быть в индексе и применимы к его контракту.
|
|
203
|
+
*/
|
|
204
|
+
async function collectPresets(root, known) {
|
|
205
|
+
const entries = [];
|
|
206
|
+
for (const entry of await listNames(path.join(root, PRESETS_DIR))) {
|
|
207
|
+
if (!entry.endsWith('.yml'))
|
|
208
|
+
continue;
|
|
209
|
+
const file = `${PRESETS_DIR}/${entry}`;
|
|
210
|
+
const key = entry.slice(0, -4);
|
|
211
|
+
const content = await readFile(path.join(root, file), 'utf8');
|
|
212
|
+
let data;
|
|
213
|
+
try {
|
|
214
|
+
data = parse(content);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
throw new Error(`${file}: невалидный YAML: ${error.message.split('\n')[0]}`, { cause: error });
|
|
218
|
+
}
|
|
219
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data))
|
|
220
|
+
throw new Error(`${file}: корень должен быть объектом`);
|
|
221
|
+
const preset = data;
|
|
222
|
+
if (preset['key'] !== key)
|
|
223
|
+
throw new Error(`${file}: key должно совпадать с именем файла «${key}»`);
|
|
224
|
+
const title = oneLine(preset['title'], 'title', file);
|
|
225
|
+
if (title === undefined)
|
|
226
|
+
throw new Error(`${file}: нет title`);
|
|
227
|
+
const contract = preset['contract'];
|
|
228
|
+
if (!known.some((e) => e.kind === 'contract' && e.key === contract)) {
|
|
229
|
+
throw new Error(`${file}: контракта «${String(contract)}» нет в индексе`);
|
|
230
|
+
}
|
|
231
|
+
const modules = preset['modules'];
|
|
232
|
+
if (!Array.isArray(modules) || modules.length === 0)
|
|
233
|
+
throw new Error(`${file}: modules — непустой список`);
|
|
234
|
+
for (const item of modules) {
|
|
235
|
+
const moduleKey = typeof item === 'object' && item !== null ? item['key'] : undefined;
|
|
236
|
+
const module = known.find((e) => e.kind === 'module' && e.key === moduleKey);
|
|
237
|
+
if (module === undefined)
|
|
238
|
+
throw new Error(`${file}: модуля «${String(moduleKey)}» нет в индексе`);
|
|
239
|
+
if (!module.applicableTo.includes(String(contract))) {
|
|
240
|
+
throw new Error(`${file}: модуль «${module.key}» не применим к «${String(contract)}»`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
entries.push({ kind: 'preset', key, path: file, sha256: sha256(content), applicableTo: [String(contract)], patterns: [], title });
|
|
244
|
+
}
|
|
245
|
+
return entries;
|
|
246
|
+
}
|
|
194
247
|
/** Субагент — `agents/<name>.md`: frontmatter name, description, skills, tools + инструкция. */
|
|
195
248
|
async function collectAgents(root) {
|
|
196
249
|
const entries = [];
|
|
@@ -222,6 +275,8 @@ export function renderIndex(entries, options) {
|
|
|
222
275
|
lines.push(` - key: ${e.key}`, ...(e.title === undefined ? [] : [` title: ${scalar(e.title)}`]), ` path: ${e.path}`,
|
|
223
276
|
// У skill raw нет: это каталог, raw — у каждого файла.
|
|
224
277
|
...(e.files === undefined ? [` raw: ${options.rawBase}/${e.path}`] : []), ` sha256: ${e.sha256}`);
|
|
278
|
+
if (e.deprecated !== undefined)
|
|
279
|
+
lines.push(` deprecated: ${scalar(e.deprecated)}`);
|
|
225
280
|
if (e.files !== undefined) {
|
|
226
281
|
lines.push(' files:');
|
|
227
282
|
for (const f of e.files) {
|
|
@@ -230,7 +285,7 @@ export function renderIndex(entries, options) {
|
|
|
230
285
|
}
|
|
231
286
|
if (e.patterns.length > 0)
|
|
232
287
|
lines.push(` patterns: ${flowList(e.patterns)}`);
|
|
233
|
-
if (e.kind === 'module' && e.applicableTo.length > 0) {
|
|
288
|
+
if ((e.kind === 'module' || e.kind === 'preset') && e.applicableTo.length > 0) {
|
|
234
289
|
lines.push(` applicable_to: ${flowList(e.applicableTo)}`);
|
|
235
290
|
}
|
|
236
291
|
if (e.feature !== undefined) {
|