hive-lite 0.1.3 → 0.1.7

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/src/lib/change.js CHANGED
@@ -174,9 +174,15 @@ function scopeFromContext(root, context) {
174
174
  });
175
175
  }
176
176
  const scope = context.scope;
177
+ const selectedDirect = Array.isArray(context.writableScope) && context.writableScope.length > 0
178
+ ? context.writableScope
179
+ : (scope.writableDirect || []);
177
180
  return {
178
181
  readable: (scope.readable || []).slice(),
179
- writableDirect: (scope.writableDirect || []).map(itemPattern).filter(Boolean),
182
+ readableReference: (scope.readableReference || []).slice(),
183
+ writableDirect: selectedDirect.map(itemPattern).filter(Boolean),
184
+ writableExisting: normalizePatternList(scope.writableExisting || [], { source: 'writable_existing' }),
185
+ writableCreatePatterns: normalizePatternList(scope.writableCreatePatterns || [], { source: 'writable_create_patterns' }),
180
186
  writableConditional: normalizePatternList(scope.writableConditional || [], { requiresReview: true }),
181
187
  writableBroadFallback: normalizePatternList(scope.writableBroadFallback || [], { requiresReview: true }),
182
188
  forbidden: (scope.forbidden || []).slice(),
@@ -190,6 +196,9 @@ function firstItemMatch(file, items) {
190
196
 
191
197
  function scopeCheck(root, files, context, map) {
192
198
  const filePaths = files.map((item) => item.path || item);
199
+ const referenceOnly = context && context.writePlan && Array.isArray(context.writePlan.referenceFiles)
200
+ ? context.writePlan.referenceFiles.map((item) => item.path || item.pattern).filter(Boolean)
201
+ : [];
193
202
  const matchedAreas = [];
194
203
  let scope = {
195
204
  readable: [],
@@ -236,6 +245,13 @@ function scopeCheck(root, files, context, map) {
236
245
  violations.push(`${file} matched doNotTouch ${forbidden}`);
237
246
  continue;
238
247
  }
248
+ const reference = firstPatternMatch(file, referenceOnly);
249
+ if (reference) {
250
+ matchedTiers.unmatched.push(file);
251
+ review.push(file);
252
+ reviewDetails.push(`${file} matched reference-only file ${reference}`);
253
+ continue;
254
+ }
239
255
  const direct = firstItemMatch(file, scope.writableDirect);
240
256
  if (direct) {
241
257
  matchedTiers.direct.push(file);
@@ -298,6 +314,63 @@ function validationPlanFromContextOrMap(context, map) {
298
314
  }] : [];
299
315
  }
300
316
 
317
+ function evaluateWritePlanStatus(files, context) {
318
+ if (!context) {
319
+ return {
320
+ status: 'not_available',
321
+ contextMode: null,
322
+ blockingReasons: [],
323
+ selectedWritableDirect: [],
324
+ referenceOnlyTouched: [],
325
+ requiredWrites: [],
326
+ blockingWarnings: [],
327
+ };
328
+ }
329
+
330
+ const writePlan = context.writePlan || null;
331
+ const blockingWarnings = writePlan && Array.isArray(writePlan.blockingWarnings)
332
+ ? writePlan.blockingWarnings
333
+ : [];
334
+ const selectedWritableDirect = Array.isArray(context.writableScope) ? context.writableScope : [];
335
+ const changedPaths = files.map((item) => item.path || item);
336
+ const referenceFiles = writePlan && Array.isArray(writePlan.referenceFiles) ? writePlan.referenceFiles : [];
337
+ const referenceOnlyTouched = [];
338
+ for (const file of changedPaths) {
339
+ const reference = referenceFiles.find((item) => matchesPattern(file, item.path || item.pattern));
340
+ if (reference) {
341
+ referenceOnlyTouched.push({
342
+ path: file,
343
+ reference: reference.path || reference.pattern,
344
+ reason: reference.reason || '',
345
+ });
346
+ }
347
+ }
348
+
349
+ const blockingReasons = [];
350
+ if (context.mode && context.mode !== 'edit_context') {
351
+ blockingReasons.push(`context mode is ${context.mode}, not edit_context`);
352
+ }
353
+ for (const warning of blockingWarnings) {
354
+ blockingReasons.push(`${warning.code}: ${warning.message}`);
355
+ }
356
+ for (const item of referenceOnlyTouched) {
357
+ blockingReasons.push(`${item.path} touched reference-only file ${item.reference}`);
358
+ }
359
+ if (selectedWritableDirect.length === 0 && changedPaths.length > 0) {
360
+ blockingReasons.push('context has no selected Direct Writable scope');
361
+ }
362
+
363
+ return {
364
+ status: blockingReasons.length > 0 ? 'blocked' : 'clean',
365
+ contextMode: context.mode || null,
366
+ blockingReasons: [...new Set(blockingReasons)],
367
+ selectedWritableDirect,
368
+ referenceOnlyTouched,
369
+ requiredWrites: writePlan && Array.isArray(writePlan.hypotheses) ? writePlan.hypotheses : [],
370
+ blockingWarnings,
371
+ };
372
+ }
373
+
301
374
  function createOrUpdateChange(cwd, options = {}) {
302
375
  const root = repoRoot(cwd);
303
376
  const map = loadProjectMap(root);
@@ -339,6 +412,7 @@ function createOrUpdateChange(cwd, options = {}) {
339
412
  text: diffText,
340
413
  },
341
414
  scope: scopeCheck(root, files, context, map),
415
+ writePlanStatus: evaluateWritePlanStatus(files, context),
342
416
  validation: {
343
417
  status: existing && existing.validation ? existing.validation.status : 'not_run',
344
418
  plan: validationPlanFromContextOrMap(context, map),
@@ -40,6 +40,374 @@ function tokenize(value) {
40
40
  return [...new Set([...latin, ...cjk])];
41
41
  }
42
42
 
43
+ function unique(values) {
44
+ return [...new Set((values || []).filter(Boolean))];
45
+ }
46
+
47
+ function looksLikeFilePattern(pattern) {
48
+ if (String(pattern || '').includes('*')) return false;
49
+ const base = path.basename(String(pattern || ''));
50
+ return /\.[A-Za-z0-9]+$/.test(base);
51
+ }
52
+
53
+ function isCreateCapablePattern(pattern) {
54
+ return !looksLikeFilePattern(pattern);
55
+ }
56
+
57
+ const IDENTITY_ALIASES = {
58
+ openai: ['openai', 'chatgpt', 'gpt'],
59
+ google: ['google', 'gemini'],
60
+ qwen: ['qwen', 'tongyi'],
61
+ wan: ['wan'],
62
+ aliyun: ['aliyun', 'ali'],
63
+ anthropic: ['anthropic', 'claude'],
64
+ azure: ['azure'],
65
+ };
66
+
67
+ function canonicalIdentity(value) {
68
+ const text = normalize(value);
69
+ if (!text) return null;
70
+ for (const [canonical, aliases] of Object.entries(IDENTITY_ALIASES)) {
71
+ if (aliases.includes(text)) return canonical;
72
+ }
73
+ return text;
74
+ }
75
+
76
+ function targetIdentityForIntent(intent) {
77
+ const tokens = tokenize(intent);
78
+ for (const token of tokens) {
79
+ const identity = canonicalIdentity(token);
80
+ if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function peerIdentityFromPath(pattern) {
86
+ if (!looksLikeFilePattern(pattern)) return null;
87
+ const tokens = basenameTokens(pattern);
88
+ for (const token of tokens) {
89
+ const identity = canonicalIdentity(token);
90
+ if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function identityMatches(left, right) {
96
+ if (!left || !right) return false;
97
+ return canonicalIdentity(left) === canonicalIdentity(right);
98
+ }
99
+
100
+ function intentTargetsAllPeers(intent) {
101
+ const text = normalize(intent);
102
+ return [
103
+ /\b(all|every|each)\b/,
104
+ /所有|全部|每个|统一|全量|全部/,
105
+ ].some((pattern) => pattern.test(text));
106
+ }
107
+
108
+ function arrayFrom(value) {
109
+ if (Array.isArray(value)) return value.map((item) => String(item || '')).filter(Boolean);
110
+ if (value == null || value === '') return [];
111
+ if (typeof value === 'object') return [];
112
+ return [String(value)];
113
+ }
114
+
115
+ function configuredArtifactFamilies(area) {
116
+ const raw = area && area.artifact_families ? area.artifact_families : null;
117
+ if (!raw) return [];
118
+ if (Array.isArray(raw)) {
119
+ return raw.map((item) => ({
120
+ id: item.id || item.name || '',
121
+ triggerTerms: [
122
+ ...arrayFrom(item.trigger_terms || item.triggerTerms),
123
+ ...arrayFrom(item.trigger_terms && item.trigger_terms.en),
124
+ ...arrayFrom(item.trigger_terms && item.trigger_terms.zh),
125
+ ],
126
+ createRequires: arrayFrom(item.create_requires || item.createRequires),
127
+ hooks: arrayFrom(item.hooks),
128
+ examples: arrayFrom(item.examples),
129
+ })).filter((item) => item.id);
130
+ }
131
+ return Object.entries(raw).map(([id, value]) => ({
132
+ id,
133
+ triggerTerms: [
134
+ ...arrayFrom(value && (value.trigger_terms || value.triggerTerms)),
135
+ ...arrayFrom(value && value.trigger_terms && value.trigger_terms.en),
136
+ ...arrayFrom(value && value.trigger_terms && value.trigger_terms.zh),
137
+ ],
138
+ createRequires: arrayFrom(value && (value.create_requires || value.createRequires)),
139
+ hooks: arrayFrom(value && value.hooks),
140
+ examples: arrayFrom(value && value.examples),
141
+ }));
142
+ }
143
+
144
+ function configuredArtifactFamilyForIntent(area, intent) {
145
+ const text = normalize(intent);
146
+ for (const family of configuredArtifactFamilies(area)) {
147
+ const terms = unique([
148
+ family.id,
149
+ ...family.triggerTerms,
150
+ ]).map(normalize).filter(Boolean);
151
+ if (terms.some((term) => text.includes(term))) return family;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function classifyArtifactFamily(intent, area) {
157
+ const configured = configuredArtifactFamilyForIntent(area, intent);
158
+ if (configured) return configured.id;
159
+ const text = normalize(intent);
160
+ if ([
161
+ /\b(provider|proxy|adapter|integration|connector|plugin)\b/,
162
+ /代理|提供商|适配器|集成|连接器|插件/,
163
+ ].some((pattern) => pattern.test(text))) return 'provider_proxy';
164
+ if ([
165
+ /\b(endpoint|controller|route|api)\b/,
166
+ /接口|端点|路由|控制器/,
167
+ ].some((pattern) => pattern.test(text))) return 'endpoint';
168
+ if ([
169
+ /\b(dto|request|response|payload)\b/,
170
+ /请求|响应|入参|出参/,
171
+ ].some((pattern) => pattern.test(text))) return 'dto';
172
+ if ([
173
+ /\b(migration|database|db|sql)\b/,
174
+ /数据库|迁移|表结构/,
175
+ ].some((pattern) => pattern.test(text))) return 'migration';
176
+ if ([
177
+ /\b(test|spec)\b/,
178
+ /测试|用例/,
179
+ ].some((pattern) => pattern.test(text))) return 'test';
180
+ if ([
181
+ /\b(component|page|view|ui)\b/,
182
+ /组件|页面|视图|界面/,
183
+ ].some((pattern) => pattern.test(text))) return 'component';
184
+ if ([
185
+ /\b(service)\b/,
186
+ /服务/,
187
+ ].some((pattern) => pattern.test(text))) return 'service';
188
+ return 'unknown';
189
+ }
190
+
191
+ function classifyIntentWrite(intent, area) {
192
+ const text = normalize(intent);
193
+ const createTerm = [
194
+ /\b(add|create|new|introduce|support|implement)\b/,
195
+ /新增|新建|创建|添加|接入|支持/,
196
+ ].some((pattern) => pattern.test(text));
197
+ const deleteTerm = [
198
+ /\b(delete|remove)\b/,
199
+ /删除|移除/,
200
+ ].some((pattern) => pattern.test(text));
201
+ const renameTerm = [
202
+ /\b(rename|move)\b/,
203
+ /重命名|改名|迁移/,
204
+ ].some((pattern) => pattern.test(text));
205
+ const targetAll = intentTargetsAllPeers(intent);
206
+ const artifactFamily = classifyArtifactFamily(intent, area);
207
+ const familyConfig = configuredArtifactFamilies(area).find((item) => item.id === artifactFamily) || null;
208
+ const targetIdentity = targetIdentityForIntent(intent);
209
+ let action = 'modify';
210
+ if (deleteTerm) action = 'delete';
211
+ else if (renameTerm) action = 'rename';
212
+ else if (createTerm && !targetAll) action = 'create';
213
+ return {
214
+ action,
215
+ artifactFamily,
216
+ targetIdentity,
217
+ operation: action === 'create' && artifactFamily === 'provider_proxy' ? 'create_file' : 'modify_existing_file',
218
+ required: true,
219
+ targetAll,
220
+ intentKind: action === 'create' ? `add_${artifactFamily}` : `${action}_${artifactFamily}`,
221
+ createRequires: familyConfig ? familyConfig.createRequires : [],
222
+ hooks: familyConfig ? familyConfig.hooks : [],
223
+ confidence: artifactFamily === 'unknown' ? 'low' : 'medium',
224
+ };
225
+ }
226
+
227
+ function directCapabilities(scope) {
228
+ const explicitItems = [
229
+ ...(scope.writableDirectItems || []),
230
+ ];
231
+ const items = explicitItems.length
232
+ ? explicitItems
233
+ : (scope.writableDirect || []).map((pattern) => ({ pattern, source: 'writable_direct' }));
234
+ const seen = new Set();
235
+ return items.map((item) => {
236
+ const pattern = item.pattern || item.path || item;
237
+ const createCapable = isCreateCapablePattern(pattern);
238
+ const configuredOperations = arrayFrom(item.operations);
239
+ const operations = configuredOperations.length
240
+ ? configuredOperations
241
+ : (createCapable ? ['update_existing', 'create_file'] : ['update_existing']);
242
+ const key = `${pattern}:${operations.join(',')}:${item.source || ''}:${item.artifact || ''}`;
243
+ if (seen.has(key)) return null;
244
+ seen.add(key);
245
+ return {
246
+ scope: pattern,
247
+ operations,
248
+ breadth: createCapable ? 'narrow_pattern' : 'exact',
249
+ reviewGated: false,
250
+ source: item.source || 'writable_direct',
251
+ artifact: item.artifact || null,
252
+ artifactFamily: item.artifactFamily || item.artifact_family || null,
253
+ intentKinds: arrayFrom(item.intentKinds || item.intent_kinds),
254
+ requiredWhen: arrayFrom(item.requiredWhen || item.required_when),
255
+ targetSlot: item.targetSlot || item.target_slot || null,
256
+ peerIdentity: canonicalIdentity(item.targetIdentity || item.target_identity || item.peerIdentity || item.peer_identity) || peerIdentityFromPath(pattern),
257
+ };
258
+ }).filter(Boolean);
259
+ }
260
+
261
+ function warning(code, message, data = {}) {
262
+ return {
263
+ code,
264
+ message,
265
+ blocking: data.blocking !== false,
266
+ ...data,
267
+ };
268
+ }
269
+
270
+ function buildReference(capability, reason) {
271
+ return {
272
+ path: capability.scope || capability.path,
273
+ role: capability.role || 'peer_example',
274
+ writable: false,
275
+ reason,
276
+ artifact: capability.artifact || null,
277
+ peerIdentity: capability.peerIdentity || null,
278
+ };
279
+ }
280
+
281
+ function artifactMatches(capability, artifact) {
282
+ if (!artifact) return false;
283
+ return capability.artifact === artifact || capability.artifactFamily === artifact;
284
+ }
285
+
286
+ function capabilityMatchesIntent(capability, hypothesis) {
287
+ if (!capability.intentKinds || capability.intentKinds.length === 0) return true;
288
+ return capability.intentKinds.includes(hypothesis.intentKind);
289
+ }
290
+
291
+ function buildWritePlan(scope, intent, area) {
292
+ const hypothesis = classifyIntentWrite(intent, area);
293
+ const capabilities = directCapabilities(scope);
294
+ const warnings = [];
295
+ const references = (scope.readableReference || []).map((item) => buildReference(
296
+ {
297
+ path: item.path,
298
+ role: item.role || 'reference',
299
+ artifact: item.artifact || null,
300
+ peerIdentity: canonicalIdentity(item.peerIdentity) || peerIdentityFromPath(item.path),
301
+ },
302
+ item.reason || 'readable reference from Project Map'
303
+ ));
304
+ let selected = capabilities.map((item) => item.scope);
305
+
306
+ if (hypothesis.operation === 'create_file' && hypothesis.artifactFamily === 'provider_proxy') {
307
+ const createCapabilities = capabilities.filter((item) => (
308
+ item.operations.includes('create_file') && capabilityMatchesIntent(item, hypothesis)
309
+ ));
310
+ const genericExistingCapabilities = capabilities.filter((item) => (
311
+ !item.peerIdentity && !item.operations.includes('create_file')
312
+ ));
313
+ const referencePeers = capabilities.filter((item) => (
314
+ item.peerIdentity
315
+ && hypothesis.targetIdentity
316
+ && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
317
+ ));
318
+ const selectedCreateCapabilities = hypothesis.createRequires.length
319
+ ? createCapabilities.filter((item) => hypothesis.createRequires.some((artifact) => artifactMatches(item, artifact)))
320
+ : createCapabilities;
321
+ const hookCapabilities = hypothesis.hooks.length
322
+ ? genericExistingCapabilities.filter((item) => hypothesis.hooks.some((artifact) => artifactMatches(item, artifact)))
323
+ : genericExistingCapabilities;
324
+
325
+ selected = selectedCreateCapabilities.length > 0
326
+ ? [...selectedCreateCapabilities, ...hookCapabilities].map((item) => item.scope)
327
+ : [];
328
+ for (const capability of referencePeers) {
329
+ references.push(buildReference(
330
+ capability,
331
+ `peer identity ${capability.peerIdentity} differs from target ${hypothesis.targetIdentity}; use as a reference example only`
332
+ ));
333
+ }
334
+
335
+ if (createCapabilities.length === 0) {
336
+ warnings.push(warning(
337
+ 'MISSING_DIRECT_NEW_FILE_SCOPE',
338
+ 'Intent appears to add a new peer provider/proxy artifact, but direct writable scope does not include a narrow create pattern.'
339
+ ));
340
+ }
341
+ for (const artifact of hypothesis.createRequires) {
342
+ if (!createCapabilities.some((item) => artifactMatches(item, artifact))) {
343
+ warnings.push(warning(
344
+ 'MISSING_ARTIFACT_FAMILY_SCOPE',
345
+ `Intent requires provider/proxy artifact ${artifact}, but no direct create capability covers it.`,
346
+ { artifact }
347
+ ));
348
+ }
349
+ }
350
+ for (const artifact of hypothesis.hooks) {
351
+ if (!genericExistingCapabilities.some((item) => artifactMatches(item, artifact))) {
352
+ warnings.push(warning(
353
+ 'MISSING_REQUIRED_HOOK_SCOPE',
354
+ `Intent requires provider/proxy hook ${artifact}, but no direct existing-file capability covers it.`,
355
+ { artifact }
356
+ ));
357
+ }
358
+ }
359
+ if (referencePeers.length > 0 && createCapabilities.length === 0) {
360
+ warnings.push(warning(
361
+ 'DIRECT_ONLY_REFERENCE_PEERS',
362
+ 'Direct writable scope only contains existing peer provider/proxy files for other identities; those are reference examples, not the target write files.'
363
+ ));
364
+ }
365
+ if (createCapabilities.length === 0 && (scope.writableBroadFallback || []).length > 0) {
366
+ warnings.push(warning(
367
+ 'BROAD_FALLBACK_ONLY',
368
+ 'Only broad fallback scope appears able to cover new provider/proxy files; broad fallback is review-gated and is not direct write permission.'
369
+ ));
370
+ }
371
+ } else if (
372
+ hypothesis.artifactFamily === 'provider_proxy'
373
+ && hypothesis.targetIdentity
374
+ && !hypothesis.targetAll
375
+ ) {
376
+ const targetMatches = capabilities.filter((item) => (
377
+ item.peerIdentity && identityMatches(item.peerIdentity, hypothesis.targetIdentity)
378
+ ));
379
+ const genericCapabilities = capabilities.filter((item) => (
380
+ !item.peerIdentity && !item.operations.includes('create_file')
381
+ ));
382
+ const referencePeers = capabilities.filter((item) => (
383
+ item.peerIdentity && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
384
+ ));
385
+ selected = unique([...targetMatches, ...genericCapabilities].map((item) => item.scope));
386
+ for (const capability of referencePeers) {
387
+ references.push(buildReference(
388
+ capability,
389
+ `peer identity ${capability.peerIdentity} differs from target ${hypothesis.targetIdentity}`
390
+ ));
391
+ }
392
+ if (targetMatches.length === 0 && referencePeers.length > 0) {
393
+ warnings.push(warning(
394
+ 'TARGET_ENTITY_MISMATCH',
395
+ `Intent targets ${hypothesis.targetIdentity}, but direct writable peer files are for other identities.`
396
+ ));
397
+ }
398
+ }
399
+
400
+ selected = unique(selected);
401
+ return {
402
+ hypotheses: [hypothesis],
403
+ capabilities,
404
+ selectedWritableDirect: selected,
405
+ referenceFiles: references,
406
+ blockingWarnings: warnings.filter((item) => item.blocking !== false),
407
+ warnings,
408
+ };
409
+ }
410
+
43
411
  function basenameTokens(file) {
44
412
  return tokenize(path.basename(file || '').replace(/([a-z])([A-Z])/g, '$1 $2'));
45
413
  }
@@ -734,10 +1102,22 @@ function reviewGatedNotices(scope) {
734
1102
  return notices;
735
1103
  }
736
1104
 
737
- function contextMode(area, scope, validationPlan, relevant, decomposition = []) {
1105
+ function hasBlockingMapGapWarning(warnings = []) {
1106
+ return warnings.some((warning) => warning.blocking === true || [
1107
+ 'MISSING_DIRECT_NEW_FILE_SCOPE',
1108
+ 'DIRECT_ONLY_REFERENCE_PEERS',
1109
+ 'BROAD_FALLBACK_ONLY',
1110
+ 'TARGET_ENTITY_MISMATCH',
1111
+ 'MISSING_ARTIFACT_FAMILY_SCOPE',
1112
+ 'MISSING_REQUIRED_HOOK_SCOPE',
1113
+ ].includes(warning.code));
1114
+ }
1115
+
1116
+ function contextMode(area, selectedWritableDirect, validationPlan, relevant, decomposition = [], warnings = []) {
738
1117
  if (!area) return 'needs_map';
739
1118
  if (decomposition.some((item) => item.blocking)) return 'needs_decomposition';
740
- if (scope.writableDirect.length === 0) return 'discovery_context';
1119
+ if (selectedWritableDirect.length === 0) return 'discovery_context';
1120
+ if (hasBlockingMapGapWarning(warnings)) return 'discovery_context';
741
1121
  if (relevant.length === 0 || !relevant.some((file) => file.source === 'project_map')) return 'discovery_context';
742
1122
  if (validationPlan.length === 0) return 'discovery_context';
743
1123
  return 'edit_context';
@@ -850,12 +1230,31 @@ function buildContextMarkdown(packet) {
850
1230
  ...packet.phaseDependencyStatus.missingRequiredAcceptedPhases.map((item) => `- Missing required phase: ${item}`),
851
1231
  '',
852
1232
  ] : []),
1233
+ ...(packet.writePlan ? [
1234
+ '## Write Plan',
1235
+ ...packet.writePlan.hypotheses.map((item) => `- ${item.action} ${item.artifactFamily}: ${item.operation}${item.targetIdentity ? ` for ${item.targetIdentity}` : ''}`),
1236
+ ...(packet.writePlan.blockingWarnings.length
1237
+ ? packet.writePlan.blockingWarnings.map((item) => `- Blocking: ${item.code}: ${item.message}`)
1238
+ : ['- Coverage: direct writable scope covers the inferred write operation.']),
1239
+ '',
1240
+ ] : []),
1241
+ ...(packet.writePlan && packet.writePlan.referenceFiles.length ? [
1242
+ '## Reference Files',
1243
+ ...packet.writePlan.referenceFiles.map((file) => `- ${file.path}: ${file.reason}`),
1244
+ '',
1245
+ ] : []),
853
1246
  '## Relevant Files',
854
1247
  ...packet.relevantFiles.map((file) => `- ${file.path} (${file.role || file.source}): ${file.reason}`),
855
1248
  '',
856
1249
  '## Writable Scope',
857
1250
  ...(packet.writableScope.length ? packet.writableScope.map((item) => `- ${item}`) : ['- (none; this is not an edit permit)']),
858
1251
  '',
1252
+ '## Writable Existing Scope',
1253
+ ...(packet.scope.writableExisting && packet.scope.writableExisting.length ? packet.scope.writableExisting.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
1254
+ '',
1255
+ '## Writable Create Patterns',
1256
+ ...(packet.scope.writableCreatePatterns && packet.scope.writableCreatePatterns.length ? packet.scope.writableCreatePatterns.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
1257
+ '',
859
1258
  '## Conditional Writable Scope',
860
1259
  ...(packet.scope.writableConditional.length ? packet.scope.writableConditional.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
861
1260
  '',
@@ -921,12 +1320,24 @@ function createContextPacket(root, intent, options = {}) {
921
1320
  const candidateAreas = candidateAreasFromScored(scored);
922
1321
  const scope = area ? normalizeAreaScope(root, area) : {
923
1322
  readable: [],
1323
+ readableReference: [],
924
1324
  writableDirect: [],
1325
+ writableDirectItems: [],
1326
+ writableExisting: [],
1327
+ writableCreatePatterns: [],
925
1328
  writableConditional: [],
926
1329
  writableBroadFallback: [],
927
1330
  forbidden: [],
928
1331
  quality: 'unknown',
929
1332
  };
1333
+ const writePlan = area ? buildWritePlan(scope, intent, area) : {
1334
+ hypotheses: [],
1335
+ capabilities: [],
1336
+ selectedWritableDirect: [],
1337
+ referenceFiles: [],
1338
+ blockingWarnings: [],
1339
+ warnings: [],
1340
+ };
930
1341
  const restrictGrepToReadable = area && (confidence === 'high' || findOptions.area) && scope.readable.length > 0;
931
1342
  const grepFiles = grepHints(root, tokens, maxFiles, {
932
1343
  allowedPatterns: restrictGrepToReadable ? scope.readable : [],
@@ -969,6 +1380,7 @@ function createContextPacket(root, intent, options = {}) {
969
1380
  });
970
1381
  } else {
971
1382
  warnings.push(...scopeWarnings(scope));
1383
+ warnings.push(...writePlan.warnings);
972
1384
  if (relevant.length === 0 || !relevant.some((file) => file.source === 'project_map')) {
973
1385
  warnings.push({
974
1386
  code: 'MISSING_ENTRYPOINT',
@@ -996,7 +1408,7 @@ function createContextPacket(root, intent, options = {}) {
996
1408
  constrainedAreaId,
997
1409
  });
998
1410
  const phaseSeeds = decomposition.length > 0 ? candidatePhaseSeeds(scored, intent) : [];
999
- let mode = contextMode(area, scope, validationPlan, relevant, decomposition);
1411
+ let mode = contextMode(area, writePlan.selectedWritableDirect, validationPlan, relevant, decomposition, warnings);
1000
1412
  if (missingSplitReference(phaseStatus) && mode === 'edit_context') mode = 'discovery_context';
1001
1413
  const actions = recommendedActions(mode, intent, id, phaseStatus);
1002
1414
  const packet = {
@@ -1034,7 +1446,8 @@ function createContextPacket(root, intent, options = {}) {
1034
1446
  phaseDependencyStatus: phaseStatus,
1035
1447
  relevantFiles: relevant,
1036
1448
  scope,
1037
- writableScope: scope.writableDirect.slice(),
1449
+ writePlan,
1450
+ writableScope: writePlan.selectedWritableDirect.slice(),
1038
1451
  readableScope: scope.readable.slice(),
1039
1452
  doNotTouch: scope.forbidden.slice(),
1040
1453
  validationPlan,
@@ -1076,6 +1489,7 @@ function createContextPacket(root, intent, options = {}) {
1076
1489
  })),
1077
1490
  validationQuality: validationPlan.length > 0 ? 'configured' : 'missing',
1078
1491
  warnings,
1492
+ writePlan,
1079
1493
  reviewGated,
1080
1494
  decompositionSignals: decomposition,
1081
1495
  candidatePhaseSeeds: phaseSeeds,
@@ -106,6 +106,13 @@ function evaluateEvidencePolicy(change, map) {
106
106
  let changeClass = 'unknown';
107
107
  let verdict = 'acceptable';
108
108
 
109
+ if (change.writePlanStatus && change.writePlanStatus.status === 'blocked') {
110
+ verdict = 'blocked';
111
+ missing.push('write_plan_clean');
112
+ required.push('write_plan_clean');
113
+ reasons.push(...(change.writePlanStatus.blockingReasons || []).map((item) => `Write plan blocked: ${item}`));
114
+ }
115
+
109
116
  if (change.scope.status === 'violation') {
110
117
  verdict = 'blocked';
111
118
  missing.push('scope_clean');
package/src/lib/health.js CHANGED
@@ -69,7 +69,7 @@ function readYamlHealth(root, name, fallback, findings) {
69
69
  field: '',
70
70
  message: `.hive/map/${name} is missing.`,
71
71
  impact: 'Hive Lite cannot evaluate Project Map health.',
72
- fix: 'Run hive-lite init or restore the missing map file.',
72
+ fix: 'Use $hive-lite-bootstrap for first-time or partial setup. If this repo already had a committed Project Map, restore the missing map file from git instead of rebuilding it silently.',
73
73
  });
74
74
  return { doc: fallback, valid: false };
75
75
  }