@shahimat/harness 0.1.0 → 0.3.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.
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shahimat/harness",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Harness для проектов на контрактах ai-prompts: генераторы и проверки",
5
5
  "license": "MIT",
6
6
  "author": "Gevorg Nakhapetyan",
@@ -33,6 +33,10 @@
33
33
  ".": {
34
34
  "types": "./dist/index.d.ts",
35
35
  "import": "./dist/index.js"
36
+ },
37
+ "./expect": {
38
+ "types": "./dist/expect/index.d.ts",
39
+ "import": "./dist/expect/index.js"
36
40
  }
37
41
  },
38
42
  "files": [
@@ -46,6 +50,7 @@
46
50
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
47
51
  },
48
52
  "dependencies": {
53
+ "ajv": "^8.20.0",
49
54
  "yaml": "^2.9.1"
50
55
  },
51
56
  "devDependencies": {
@@ -55,5 +60,13 @@
55
60
  },
56
61
  "publishConfig": {
57
62
  "access": "public"
63
+ },
64
+ "peerDependencies": {
65
+ "vitest": ">=4"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "vitest": {
69
+ "optional": true
70
+ }
58
71
  }
59
72
  }