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/README.md +14 -14
- package/docs/cli-semantics.md +7 -7
- package/docs/skills/hive-lite-bootstrap/SKILL.md +28 -10
- package/docs/skills/hive-lite-finish/SKILL.md +18 -6
- package/docs/skills/hive-lite-map-maintainer/SKILL.md +21 -15
- package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +1 -1
- package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +4 -0
- package/docs/skills/hive-lite-start-prompt/SKILL.md +33 -12
- package/docs/skills/hive-lite-start-prompt/references/preflight.md +3 -3
- package/package.json +2 -2
- package/src/cli.js +43 -1
- package/src/lib/change.js +75 -1
- package/src/lib/context.js +418 -4
- package/src/lib/evidence.js +7 -0
- package/src/lib/health.js +1 -1
- package/src/lib/map.js +63 -0
- package/src/lib/next.js +10 -10
- package/src/lib/risk.js +5 -0
- package/src/lib/scope.js +117 -2
- package/src/lib/skills.js +6 -6
- package/src/lib/status.js +3 -3
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
|
-
|
|
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),
|
package/src/lib/context.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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,
|
package/src/lib/evidence.js
CHANGED
|
@@ -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: '
|
|
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
|
}
|