hive-lite 0.1.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.
Files changed (33) hide show
  1. package/README.md +443 -0
  2. package/bin/hive.js +6 -0
  3. package/docs/cli-semantics.md +386 -0
  4. package/docs/skills/hive-lite-finish/SKILL.md +282 -0
  5. package/docs/skills/hive-lite-finish/agents/openai.yaml +4 -0
  6. package/docs/skills/hive-lite-finish/references/safety.md +95 -0
  7. package/docs/skills/hive-lite-finish/references/verdicts.md +123 -0
  8. package/docs/skills/hive-lite-map-maintainer/SKILL.md +203 -0
  9. package/docs/skills/hive-lite-map-maintainer/agents/openai.yaml +7 -0
  10. package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +114 -0
  11. package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +201 -0
  12. package/docs/skills/hive-lite-start-prompt/SKILL.md +283 -0
  13. package/docs/skills/hive-lite-start-prompt/agents/openai.yaml +4 -0
  14. package/docs/skills/hive-lite-start-prompt/references/input-calibration.md +82 -0
  15. package/docs/skills/hive-lite-start-prompt/references/preflight.md +116 -0
  16. package/package.json +40 -0
  17. package/src/cli.js +910 -0
  18. package/src/lib/change.js +642 -0
  19. package/src/lib/context.js +1104 -0
  20. package/src/lib/evidence.js +230 -0
  21. package/src/lib/fsx.js +54 -0
  22. package/src/lib/git.js +128 -0
  23. package/src/lib/glob.js +47 -0
  24. package/src/lib/health.js +1012 -0
  25. package/src/lib/id.js +13 -0
  26. package/src/lib/map.js +713 -0
  27. package/src/lib/next.js +341 -0
  28. package/src/lib/risk.js +122 -0
  29. package/src/lib/roles.js +109 -0
  30. package/src/lib/scope.js +168 -0
  31. package/src/lib/skills.js +349 -0
  32. package/src/lib/status.js +344 -0
  33. package/src/lib/yaml.js +223 -0
package/src/lib/map.js ADDED
@@ -0,0 +1,713 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { appendIfMissing, ensureDir, exists, readText, writeText } = require('./fsx');
4
+ const { currentBranch, repoRoot, tryGit } = require('./git');
5
+ const { roleListText } = require('./roles');
6
+ const { parseYaml, stringifyYaml } = require('./yaml');
7
+
8
+ function hiveDir(root) {
9
+ return path.join(root, '.hive');
10
+ }
11
+
12
+ function mapDir(root) {
13
+ return path.join(hiveDir(root), 'map');
14
+ }
15
+
16
+ function mapFile(root, name) {
17
+ return path.join(mapDir(root), name);
18
+ }
19
+
20
+ function readYamlFile(file, fallback) {
21
+ if (!exists(file)) return fallback;
22
+ return parseYaml(readText(file));
23
+ }
24
+
25
+ function writeYamlFile(file, value) {
26
+ writeText(file, stringifyYaml(value));
27
+ }
28
+
29
+ function ignoredDurableHiveFiles(root) {
30
+ const durable = [
31
+ '.hive/config.yaml',
32
+ '.hive/map/project.yaml',
33
+ '.hive/map/areas.yaml',
34
+ '.hive/map/rules.yaml',
35
+ '.hive/map/validation.yaml',
36
+ ];
37
+ const output = tryGit(['check-ignore', '-v', '--no-index', ...durable], { cwd: root });
38
+ return output ? output.split(/\r?\n/).filter(Boolean) : [];
39
+ }
40
+
41
+ function defaultProject(root) {
42
+ const pkgPath = path.join(root, 'package.json');
43
+ let packageManager = 'unknown';
44
+ let validation = '';
45
+ if (exists(path.join(root, 'pnpm-lock.yaml'))) packageManager = 'pnpm';
46
+ else if (exists(path.join(root, 'bun.lock')) || exists(path.join(root, 'bun.lockb'))) packageManager = 'bun';
47
+ else if (exists(path.join(root, 'package-lock.json'))) packageManager = 'npm';
48
+
49
+ if (exists(pkgPath)) {
50
+ try {
51
+ const pkg = JSON.parse(readText(pkgPath));
52
+ const scripts = pkg.scripts || {};
53
+ if (scripts.typecheck) validation = `${packageManager === 'unknown' ? 'npm' : packageManager} run typecheck`;
54
+ else if (scripts.test) validation = `${packageManager === 'unknown' ? 'npm' : packageManager} test`;
55
+ else if (scripts.build) validation = `${packageManager === 'unknown' ? 'npm' : packageManager} run build`;
56
+ } catch {
57
+ validation = '';
58
+ }
59
+ }
60
+
61
+ return {
62
+ version: 1,
63
+ project: {
64
+ name: path.basename(root),
65
+ summary: 'Local Project Map + Change Control project.',
66
+ tech: {
67
+ languages: [],
68
+ package_manager: packageManager,
69
+ },
70
+ },
71
+ defaults: {
72
+ validation_cmd: validation,
73
+ baseline_branch: currentBranch(root) || 'main',
74
+ },
75
+ workspace_readiness: {
76
+ required: [],
77
+ checks: [],
78
+ },
79
+ };
80
+ }
81
+
82
+ function defaultAreas() {
83
+ return {
84
+ version: 1,
85
+ areas: [],
86
+ };
87
+ }
88
+
89
+ function defaultRules() {
90
+ return {
91
+ version: 1,
92
+ constraints: [
93
+ {
94
+ id: 'no_ci_weakening',
95
+ description: 'Do not weaken CI, validation, or deploy checks.',
96
+ severity: 'block',
97
+ },
98
+ {
99
+ id: 'code_wins_over_docs',
100
+ description: 'If map entries and code conflict, current code wins.',
101
+ severity: 'info',
102
+ },
103
+ ],
104
+ sensitive_paths: [
105
+ { pattern: '**/auth/**', reason: 'auth' },
106
+ { pattern: '**/permissions/**', reason: 'permissions' },
107
+ { pattern: '**/payment/**', reason: 'payment' },
108
+ { pattern: '**/billing/**', reason: 'billing' },
109
+ { pattern: '**/security/**', reason: 'security' },
110
+ { pattern: '**/secrets/**', reason: 'secrets' },
111
+ { pattern: '**/migrations/**', reason: 'db_migration' },
112
+ { pattern: '**/drizzle/**', reason: 'db_migration' },
113
+ { pattern: '.github/workflows/**', reason: 'ci_cd' },
114
+ { pattern: '**/package.json', reason: 'dependency_surface' },
115
+ { pattern: '**/pnpm-lock.yaml', reason: 'lockfile' },
116
+ { pattern: '**/bun.lock', reason: 'lockfile' },
117
+ { pattern: '**/package-lock.json', reason: 'lockfile' },
118
+ ],
119
+ code_owner_paths: [],
120
+ security_review_paths: [],
121
+ };
122
+ }
123
+
124
+ function defaultValidation(project) {
125
+ const profiles = [];
126
+ const command = project.defaults && project.defaults.validation_cmd;
127
+ if (command) {
128
+ profiles.push({
129
+ id: 'baseline',
130
+ description: 'Baseline project validation.',
131
+ command,
132
+ evidence_type: command.includes('test') ? 'existing_tests' : command.includes('typecheck') ? 'typecheck' : 'build',
133
+ covers_roles: [
134
+ 'page',
135
+ 'ui_component',
136
+ 'styles',
137
+ 'data_transform',
138
+ 'schema_logic',
139
+ 'cli_behavior',
140
+ ],
141
+ safety: 'safe_auto',
142
+ });
143
+ }
144
+ return {
145
+ version: 1,
146
+ profiles,
147
+ };
148
+ }
149
+
150
+ function initProject(cwd) {
151
+ const root = repoRoot(cwd);
152
+ ensureDir(hiveDir(root));
153
+ ensureDir(mapDir(root));
154
+ ensureDir(path.join(hiveDir(root), 'context'));
155
+ ensureDir(path.join(hiveDir(root), 'changes'));
156
+ ensureDir(path.join(hiveDir(root), 'deltas'));
157
+ ensureDir(path.join(hiveDir(root), 'patches'));
158
+
159
+ const project = defaultProject(root);
160
+ const files = [
161
+ ['config.yaml', { version: 1, mapDir: '.hive/map' }],
162
+ ['map/project.yaml', project],
163
+ ['map/areas.yaml', defaultAreas()],
164
+ ['map/rules.yaml', defaultRules()],
165
+ ['map/validation.yaml', defaultValidation(project)],
166
+ ];
167
+
168
+ const created = [];
169
+ for (const [relative, value] of files) {
170
+ const file = path.join(hiveDir(root), relative);
171
+ if (!exists(file)) {
172
+ writeYamlFile(file, value);
173
+ created.push(path.relative(root, file));
174
+ }
175
+ }
176
+
177
+ appendIfMissing(path.join(root, '.gitignore'), [
178
+ '.hive/state/',
179
+ '.hive/changes/',
180
+ '.hive/context/',
181
+ '.hive/patches/',
182
+ ]);
183
+
184
+ const warnings = ignoredDurableHiveFiles(root).map((line) => (
185
+ `durable Hive Lite map file is ignored: ${line}`
186
+ ));
187
+
188
+ return { root, created, warnings };
189
+ }
190
+
191
+ function loadProjectMap(root) {
192
+ const project = readYamlFile(mapFile(root, 'project.yaml'), defaultProject(root));
193
+ const areas = readYamlFile(mapFile(root, 'areas.yaml'), defaultAreas());
194
+ const rules = readYamlFile(mapFile(root, 'rules.yaml'), defaultRules());
195
+ const validation = readYamlFile(mapFile(root, 'validation.yaml'), defaultValidation(project));
196
+ return {
197
+ project,
198
+ areas: Array.isArray(areas.areas) ? areas.areas : [],
199
+ areasDoc: areas,
200
+ rules,
201
+ validation,
202
+ validationProfiles: Array.isArray(validation.profiles) ? validation.profiles : [],
203
+ };
204
+ }
205
+
206
+ function verifyProjectMap(root) {
207
+ const problems = [];
208
+ const warnings = [];
209
+ const required = ['project.yaml', 'areas.yaml', 'rules.yaml', 'validation.yaml'];
210
+ for (const name of required) {
211
+ const file = mapFile(root, name);
212
+ if (!exists(file)) problems.push(`missing .hive/map/${name}`);
213
+ else {
214
+ try {
215
+ parseYaml(readText(file));
216
+ } catch (error) {
217
+ problems.push(`invalid .hive/map/${name}: ${error.message}`);
218
+ }
219
+ }
220
+ }
221
+
222
+ if (problems.length > 0) return { problems, warnings };
223
+
224
+ const map = loadProjectMap(root);
225
+ const areaIds = new Set();
226
+ for (const area of map.areas) {
227
+ if (!area.id) problems.push('area missing id');
228
+ if (area.id && areaIds.has(area.id)) problems.push(`duplicate area id: ${area.id}`);
229
+ if (area.id) areaIds.add(area.id);
230
+ for (const entry of area.entrypoints || []) {
231
+ if (!entry.path) problems.push(`area ${area.id} has entrypoint without path`);
232
+ else if (!fs.existsSync(path.join(root, entry.path))) warnings.push(`entrypoint missing on disk: ${entry.path}`);
233
+ }
234
+ }
235
+
236
+ const profileIds = new Set();
237
+ for (const profile of map.validationProfiles) {
238
+ if (!profile.id) problems.push('validation profile missing id');
239
+ if (profile.id && profileIds.has(profile.id)) problems.push(`duplicate validation profile id: ${profile.id}`);
240
+ if (profile.id) profileIds.add(profile.id);
241
+ if (!profile.command && profile.type !== 'manual') warnings.push(`validation profile has no command: ${profile.id}`);
242
+ }
243
+
244
+ return { problems, warnings };
245
+ }
246
+
247
+ function saveAreas(root, areasDoc) {
248
+ writeYamlFile(mapFile(root, 'areas.yaml'), areasDoc);
249
+ }
250
+
251
+ function packageScripts(root) {
252
+ const pkgPath = path.join(root, 'package.json');
253
+ if (!exists(pkgPath)) return {};
254
+ try {
255
+ return JSON.parse(readText(pkgPath)).scripts || {};
256
+ } catch {
257
+ return {};
258
+ }
259
+ }
260
+
261
+ function suggestMap(root) {
262
+ const dirs = fs.readdirSync(root, { withFileTypes: true })
263
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules')
264
+ .map((entry) => entry.name)
265
+ .slice(0, 20);
266
+ return {
267
+ root,
268
+ branch: currentBranch(root),
269
+ scripts: packageScripts(root),
270
+ topLevelDirs: dirs,
271
+ trackedFilesSample: tryGit(['ls-files'], { cwd: root }).split(/\r?\n/).filter(Boolean).slice(0, 60),
272
+ };
273
+ }
274
+
275
+ function findContextPath(root, value) {
276
+ const text = String(value || '').trim();
277
+ if (!text) return null;
278
+ const candidates = [];
279
+ if (path.isAbsolute(text)) candidates.push(text);
280
+ else {
281
+ candidates.push(path.join(root, text));
282
+ candidates.push(path.join(hiveDir(root), 'context', `${text}.json`));
283
+ }
284
+ if (text.endsWith('.md')) candidates.push(path.resolve(root, text).replace(/\.md$/, '.json'));
285
+ if (text.endsWith('.json')) candidates.push(path.resolve(root, text));
286
+
287
+ const found = candidates.find((file) => exists(file));
288
+ if (found && found.endsWith('.md')) return found.replace(/\.md$/, '.json');
289
+ return found || null;
290
+ }
291
+
292
+ function loadFindContext(root, value) {
293
+ const file = findContextPath(root, value);
294
+ if (!file || !exists(file)) {
295
+ throw new Error(`find context not found: ${value}; pass a ctx_xxx id or .hive/context/ctx_xxx.json`);
296
+ }
297
+ return parseYaml(readText(file));
298
+ }
299
+
300
+ function readMapText(root, name) {
301
+ const file = mapFile(root, name);
302
+ return exists(file) ? readText(file) : '';
303
+ }
304
+
305
+ function currentMapFilesSection(result) {
306
+ if (!result.findContext) return [];
307
+ const files = [
308
+ ['project.yaml', result.currentProjectYaml],
309
+ ['areas.yaml', result.currentAreasYaml],
310
+ ['rules.yaml', result.currentRulesYaml],
311
+ ['validation.yaml', result.currentValidationYaml],
312
+ ];
313
+ const lines = [];
314
+ for (const [name, text] of files) {
315
+ lines.push(`Current \`.hive/map/${name}\`:`, '```yaml', text || `# missing .hive/map/${name}`, '```', '');
316
+ }
317
+ return lines;
318
+ }
319
+
320
+ function summarizeFindContext(context) {
321
+ if (!context) return null;
322
+ const scope = context.scope || {};
323
+ return {
324
+ id: context.id,
325
+ intent: context.intent || {},
326
+ mode: context.mode || 'unknown',
327
+ selectedArea: context.area && context.area.id ? context.area.id : null,
328
+ confidence: context.confidence || {
329
+ level: context.area && context.area.confidence ? context.area.confidence : 'unknown',
330
+ reasons: context.area && context.area.matchedSignals ? context.area.matchedSignals : [],
331
+ },
332
+ warnings: context.warnings || [],
333
+ reviewGated: context.reviewGated || (context.explain && context.explain.reviewGated) || [],
334
+ candidateAreas: context.candidateAreas || [],
335
+ areaScores: context.explain && context.explain.areaScores ? context.explain.areaScores : [],
336
+ scopeQuality: scope.quality || (context.explain ? context.explain.scopeQuality : 'unknown'),
337
+ writableDirect: scope.writableDirect || context.writableScope || [],
338
+ writableConditional: scope.writableConditional || [],
339
+ writableBroadFallback: scope.writableBroadFallback || [],
340
+ forbidden: scope.forbidden || context.doNotTouch || [],
341
+ relevantFiles: context.relevantFiles || [],
342
+ validationPlan: context.validationPlan || [],
343
+ };
344
+ }
345
+
346
+ function clampNumber(value, fallback, min, max) {
347
+ const number = Number(value);
348
+ if (!Number.isFinite(number)) return fallback;
349
+ return Math.max(min, Math.min(max, Math.floor(number)));
350
+ }
351
+
352
+ function listLines(items, render) {
353
+ if (!items || items.length === 0) return ['- (none)'];
354
+ return items.map(render);
355
+ }
356
+
357
+ function patternLine(item) {
358
+ if (typeof item === 'string') return `- ${item}`;
359
+ const reason = item.reason ? ` (${item.reason})` : '';
360
+ return `- ${item.pattern || item.path}${reason}`;
361
+ }
362
+
363
+ const GENERIC_AREA_IDS = new Set([
364
+ 'dashboard',
365
+ 'frontend',
366
+ 'backend',
367
+ 'server',
368
+ 'client',
369
+ 'components',
370
+ 'src',
371
+ 'app',
372
+ 'web',
373
+ 'ui',
374
+ 'api',
375
+ 'database',
376
+ 'db',
377
+ 'common',
378
+ 'shared',
379
+ 'utils',
380
+ 'lib',
381
+ ]);
382
+
383
+ function slugPart(value) {
384
+ return String(value || '')
385
+ .toLowerCase()
386
+ .replace(/[^a-z0-9]+/g, '_')
387
+ .replace(/^_+|_+$/g, '')
388
+ .replace(/_+/g, '_');
389
+ }
390
+
391
+ function isGenericAreaId(id) {
392
+ const text = String(id || '').toLowerCase();
393
+ if (!text) return true;
394
+ if (!text.includes('.')) return true;
395
+ const last = text.split('.').pop();
396
+ return GENERIC_AREA_IDS.has(text) || GENERIC_AREA_IDS.has(last);
397
+ }
398
+
399
+ function suggestedAreaIdFromFind(findContext) {
400
+ if (!findContext) return '';
401
+ const selected = slugPart(findContext.selectedArea || '');
402
+ const parent = selected ? selected.split('_')[0] : 'area';
403
+ const intent = String(findContext.intent.raw || findContext.intent.summary || '').toLowerCase();
404
+ const text = [
405
+ intent,
406
+ ...(findContext.relevantFiles || []).map((file) => file.path || ''),
407
+ ].join(' ').toLowerCase();
408
+
409
+ let suffix = '';
410
+ if (text.includes('action') && text.includes('inbox')) suffix = 'action_inbox';
411
+ else if (text.includes('inbox')) suffix = 'inbox';
412
+ else if (text.includes('evidence')) suffix = 'evidence_viewer';
413
+ else if (text.includes('validation')) suffix = 'validation_flow';
414
+ else if (text.includes('delta')) suffix = 'delta_review';
415
+ else {
416
+ const tokens = [...new Set(text.match(/[a-z][a-z0-9]{2,}/g) || [])]
417
+ .filter((token) => !['should', 'with', 'that', 'this', 'from', 'into', 'feature', 'task'].includes(token))
418
+ .slice(0, 2);
419
+ suffix = tokens.map(slugPart).filter(Boolean).join('_') || 'focused_area';
420
+ }
421
+
422
+ return `${parent || 'area'}.${suffix}`;
423
+ }
424
+
425
+ function repairFocusFromFind(findContext, userFocus) {
426
+ if (!findContext) return userFocus || '';
427
+ const area = findContext.selectedArea || 'matched area';
428
+ const suggestedArea = suggestedAreaIdFromFind(findContext);
429
+ const warnings = (findContext.warnings || []).map((warning) => warning.code);
430
+ const goals = [];
431
+ if (isGenericAreaId(area)) goals.push(`replace generic area id with a focused id such as ${suggestedArea}`);
432
+ if (warnings.includes('BROAD_WRITABLE_SCOPE')) goals.push('move broad writable scope to reviewed fallback');
433
+ if (warnings.includes('MISSING_DIRECT_WRITABLE_SCOPE')) goals.push('add narrow writable_direct files');
434
+ if (warnings.includes('MISSING_ENTRYPOINT')) goals.push('add durable entrypoints');
435
+ if (warnings.includes('MISSING_VALIDATION')) goals.push('add validation profile references');
436
+ if (warnings.includes('NO_CONFIDENT_AREA')) goals.push('create or refine a focused area');
437
+ const suffix = goals.length ? `: ${goals.join('; ')}` : '';
438
+ const original = userFocus ? ` Original user intent/focus: ${userFocus}` : '';
439
+ return `Repair Project Map for ${area}${suffix}.${original}`;
440
+ }
441
+
442
+ function findContextPromptSection(findContext) {
443
+ if (!findContext) return [];
444
+ const suggestedArea = suggestedAreaIdFromFind(findContext);
445
+ const genericNote = isGenericAreaId(findContext.selectedArea)
446
+ ? [
447
+ `- selected area id is generic; do not keep it as the repaired edit area`,
448
+ `- suggested repaired area id: ${suggestedArea}`,
449
+ ]
450
+ : [`- suggested repaired area id: ${findContext.selectedArea || suggestedArea}`];
451
+ return [
452
+ 'Find Context:',
453
+ `- id: ${findContext.id}`,
454
+ `- mode: ${findContext.mode}`,
455
+ `- intent: ${findContext.intent.raw || findContext.intent.summary || ''}`,
456
+ `- selected area: ${findContext.selectedArea || '(none)'}`,
457
+ `- confidence: ${findContext.confidence.level || 'unknown'}`,
458
+ `- scope quality: ${findContext.scopeQuality}`,
459
+ ...genericNote,
460
+ '',
461
+ 'Find warnings:',
462
+ ...listLines(findContext.warnings, (warning) => `- ${warning.code}: ${warning.message}`),
463
+ '',
464
+ 'Review-gated scope from find:',
465
+ ...listLines(findContext.reviewGated, (notice) => `- ${notice.code}: ${notice.message}`),
466
+ '',
467
+ 'Candidate/area scores:',
468
+ ...listLines(findContext.areaScores.length ? findContext.areaScores : findContext.candidateAreas, (area) => {
469
+ const signals = area.signals && area.signals.length ? ` (${area.signals.slice(0, 3).join(', ')})` : '';
470
+ return `- ${area.id}: ${area.score == null ? 'candidate' : area.score}${signals}`;
471
+ }),
472
+ '',
473
+ 'Relevant files from find:',
474
+ ...listLines(findContext.relevantFiles, (file) => `- ${file.path} [${file.source || file.role || 'unknown'}]: ${file.reason || ''}`),
475
+ '',
476
+ 'Current direct writable scope:',
477
+ ...listLines(findContext.writableDirect, patternLine),
478
+ '',
479
+ 'Current conditional writable scope:',
480
+ ...listLines(findContext.writableConditional, patternLine),
481
+ '',
482
+ 'Current broad fallback scope:',
483
+ ...listLines(findContext.writableBroadFallback, patternLine),
484
+ '',
485
+ 'Current forbidden scope:',
486
+ ...listLines(findContext.forbidden, patternLine),
487
+ '',
488
+ 'Validation plan from find:',
489
+ ...listLines(findContext.validationPlan, (item) => `- ${item.profile || item.type || 'validation'}${item.command ? `: ${item.command}` : ''}`),
490
+ '',
491
+ ];
492
+ }
493
+
494
+ function buildMapPrompt(result) {
495
+ const focus = result.focus ? result.focus : '(none)';
496
+ const currentAreas = result.currentAreas.length
497
+ ? result.currentAreas.map((area) => `- ${area.id}${area.name ? `: ${area.name}` : ''}`).join('\n')
498
+ : '- (none yet)';
499
+ const scripts = Object.keys(result.repo.scripts || {}).length
500
+ ? JSON.stringify(result.repo.scripts, null, 2)
501
+ : '{}';
502
+ const topLevelDirs = result.repo.topLevelDirs.length
503
+ ? result.repo.topLevelDirs.map((dir) => `- ${dir}`).join('\n')
504
+ : '- (none found)';
505
+ const trackedFiles = result.repo.trackedFilesSample.length
506
+ ? result.repo.trackedFilesSample.map((file) => `- ${file}`).join('\n')
507
+ : '- (none found)';
508
+ const fromFindTask = result.findContext
509
+ ? [
510
+ 'Your task: improve `.hive/map/areas.yaml` for the find gap below.',
511
+ 'This is a Project Map repair task, not an application code task.',
512
+ 'Do not analyze whether the business feature is already implemented.',
513
+ 'If you are an editing-capable agent CLI, edit the relevant `.hive/map/*.yaml` files directly.',
514
+ 'If you cannot edit files, output complete replacement YAML for every map file that needs changes.',
515
+ ]
516
+ : [
517
+ 'Your task: draft `.hive/map/areas.yaml` with up to ' + result.maxAreas + ' high-value areas.',
518
+ result.maxAreas >= 3
519
+ ? 'Prefer 3-' + result.maxAreas + ' areas when the repo has enough distinct work areas.'
520
+ : 'Prefer up to ' + result.maxAreas + ' focused areas when the repo has enough distinct work areas.',
521
+ ];
522
+
523
+ return [
524
+ '# Hive Lite Map Draft Prompt',
525
+ '',
526
+ 'You are helping draft a Project Map for Hive Lite.',
527
+ 'Hive Lite is a deterministic local CLI. It does not call an LLM, run agents, or write code.',
528
+ '',
529
+ ...fromFindTask,
530
+ '',
531
+ 'Boundaries:',
532
+ '- Group areas by user intent or work area, not by a pure directory tree.',
533
+ '- Do not invent nonexistent paths.',
534
+ '- Do not write a full architecture document.',
535
+ '- Do not put every file in the map.',
536
+ '- Only include validation or risk details that are confirmed by repo scripts or explicit files.',
537
+ '- If uncertain, write TODO notes instead of guessing.',
538
+ '- Output a draft for human review; do not claim it has been applied.',
539
+ '- Area ids must be dot-separated product/work ids such as `dashboard.action_inbox`; do not use one-part ids or hyphenated ids like `dashboard-inbox`.',
540
+ '- If the selected area id is generic or one-part, do not keep it as the repaired edit area. Replace it with a dot-separated focused area id.',
541
+ '- Every `writable_conditional` and `writable_broad_fallback` item must set `requires_review: true`; never output `requires_review: false` for those tiers.',
542
+ '- Keep broad patterns like `apps/*/src/**` only under `writable_broad_fallback`, never under `writable_direct`.',
543
+ '- `writable_direct` should use exact existing files unless there is a very small, justified pattern.',
544
+ '- Entrypoint roles must use this fixed taxonomy only: ' + roleListText() + '.',
545
+ '- Do not invent role names. If unsure, use `role: unknown` plus a TODO note.',
546
+ '- Add `source` and `confidence` to entrypoints when possible. Use `source: agent_draft` for your own draft and `confidence: low|medium|high`.',
547
+ '- Use only validation profile ids that exist in the current validation.yaml, or output a complete validation.yaml replacement that defines them from confirmed repo scripts.',
548
+ '- If a validation or manual profile is plausible but unconfirmed, put it under TODO notes or a candidate section; do not reference it as an active profile.',
549
+ '- Validation profiles should include `evidence_type`, `covers_roles`, and `safety` when you edit validation.yaml.',
550
+ '- Allowed files to edit: `.hive/map/project.yaml`, `.hive/map/areas.yaml`, `.hive/map/rules.yaml`, `.hive/map/validation.yaml`.',
551
+ '- Do not edit application source, tests, `.gitignore`, `.hive/context`, `.hive/changes`, `.hive/deltas`, or generated artifacts.',
552
+ '- For `--from-find`, preserve unrelated areas exactly unless a change is required to fix the reported map gap.',
553
+ '- For `--from-find`, if editing files directly, output only a concise change summary and TODO notes.',
554
+ '- For `--from-find`, if you cannot edit files, do not output a partial patch fragment; output full replacement files.',
555
+ '',
556
+ result.findContext ? 'Map Repair Focus:' : 'Focus:',
557
+ focus,
558
+ '',
559
+ ...findContextPromptSection(result.findContext),
560
+ 'Current Areas:',
561
+ currentAreas,
562
+ '',
563
+ 'Repo Evidence:',
564
+ '',
565
+ 'Package scripts:',
566
+ '```json',
567
+ scripts,
568
+ '```',
569
+ '',
570
+ 'Top-level directories:',
571
+ topLevelDirs,
572
+ '',
573
+ 'Tracked files sample:',
574
+ trackedFiles,
575
+ '',
576
+ ...currentMapFilesSection(result),
577
+ 'Area schema example:',
578
+ '```yaml',
579
+ 'version: 1',
580
+ 'areas:',
581
+ ' - id: area.id',
582
+ ' name: Human Friendly Area Name',
583
+ ' description: One sentence describing the work area.',
584
+ ' aliases:',
585
+ ' - phrase users may say',
586
+ ' concepts:',
587
+ ' - concept keyword',
588
+ ' entrypoints:',
589
+ ' - path: path/to/real/file.ts',
590
+ ' role: ui_component',
591
+ ' label: Short durable label for this entrypoint',
592
+ ' source: agent_draft',
593
+ ' confidence: medium',
594
+ ' scope:',
595
+ ' readable:',
596
+ ' - path/to/read/**',
597
+ ' writable_direct:',
598
+ ' - path/to/real/file.ts',
599
+ ' writable_conditional:',
600
+ ' - pattern: path/to/shared/**',
601
+ ' reason: only if shared model changes are required',
602
+ ' requires_review: true',
603
+ ' writable_broad_fallback:',
604
+ ' - pattern: path/to/read/**',
605
+ ' reason: fallback only when direct files are insufficient',
606
+ ' requires_review: true',
607
+ ' forbidden:',
608
+ ' - path/to/sensitive/**',
609
+ ' validation:',
610
+ ' profiles:',
611
+ ' - baseline',
612
+ ' verification:',
613
+ ' direct_manual_allowed: true',
614
+ ' manual_profiles:',
615
+ ' - manual-verification',
616
+ ' focused_test_recommended_for_roles:',
617
+ ' - grouping_logic',
618
+ ' - data_transform',
619
+ ' - error_handling',
620
+ ' review_with_reason_allowed: true',
621
+ ' risk_acceptance_allowed: false',
622
+ ' risk:',
623
+ ' default_level: low',
624
+ ' tags:',
625
+ ' - ui',
626
+ '```',
627
+ '',
628
+ 'Validation profile schema example:',
629
+ '```yaml',
630
+ 'version: 1',
631
+ 'profiles:',
632
+ ' - id: dashboard-build',
633
+ ' type: command',
634
+ ' command: npm run build',
635
+ ' evidence_type: build',
636
+ ' covers_roles:',
637
+ ' - page',
638
+ ' - ui_component',
639
+ ' - styles',
640
+ ' safety: safe_auto',
641
+ '',
642
+ ' - id: generic-ui-visual',
643
+ ' type: manual',
644
+ ' evidence_type: manual_visual',
645
+ ' instructions:',
646
+ ' - Open the affected UI.',
647
+ ' - Confirm the requested visible behavior is correct.',
648
+ ' - Confirm no obvious visual regression.',
649
+ ' covers_roles:',
650
+ ' - page',
651
+ ' - ui_component',
652
+ ' - styles',
653
+ ' safety: manual_only',
654
+ '```',
655
+ '',
656
+ 'Return:',
657
+ result.findContext
658
+ ? '1. If you edited files directly: a concise summary of changed map files and area/profile ids.'
659
+ : '1. A concise `areas.yaml` draft with no more than ' + result.maxAreas + ' areas.',
660
+ result.findContext
661
+ ? '2. If you could not edit files: complete replacement YAML for every `.hive/map/*.yaml` file that needs changes.'
662
+ : '2. Short TODO notes for uncertain paths, validation profiles, or risk tags.',
663
+ result.findContext
664
+ ? '3. TODO notes for uncertain paths, validation profiles, or risk tags.'
665
+ : '3. A brief explanation of why the draft fixes the find warning or map gap.',
666
+ result.findContext
667
+ ? '4. Suggested verification commands: `hive-lite map verify`, `hive-lite map health --area <id>`, and rerun the original `hive-lite find ... --explain`.'
668
+ : '',
669
+ ].join('\n') + '\n';
670
+ }
671
+
672
+ function createMapPrompt(root, options = {}) {
673
+ const maxAreas = clampNumber(options.maxAreas, 8, 1, 20);
674
+ const findContext = options.fromFind ? summarizeFindContext(loadFindContext(root, options.fromFind)) : null;
675
+ const userFocus = options.focus ? String(options.focus) : '';
676
+ const focus = findContext ? repairFocusFromFind(findContext, userFocus) : userFocus;
677
+ const repo = suggestMap(root);
678
+ const map = loadProjectMap(root);
679
+ const currentAreas = map.areas.map((area) => ({
680
+ id: area.id || '',
681
+ name: area.name || '',
682
+ entrypoints: (area.entrypoints || []).map((entry) => entry.path).filter(Boolean),
683
+ }));
684
+ const result = {
685
+ root,
686
+ focus,
687
+ userFocus,
688
+ maxAreas,
689
+ fromFind: options.fromFind ? String(options.fromFind) : '',
690
+ findContext,
691
+ currentProjectYaml: findContext ? readMapText(root, 'project.yaml') : '',
692
+ currentAreasYaml: findContext ? readMapText(root, 'areas.yaml') : '',
693
+ currentRulesYaml: findContext ? readMapText(root, 'rules.yaml') : '',
694
+ currentValidationYaml: findContext ? readMapText(root, 'validation.yaml') : '',
695
+ repo,
696
+ currentAreas,
697
+ };
698
+ result.prompt = buildMapPrompt(result);
699
+ return result;
700
+ }
701
+
702
+ module.exports = {
703
+ createMapPrompt,
704
+ hiveDir,
705
+ initProject,
706
+ loadProjectMap,
707
+ mapDir,
708
+ mapFile,
709
+ saveAreas,
710
+ suggestMap,
711
+ verifyProjectMap,
712
+ writeYamlFile,
713
+ };