hive-lite 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,7 +54,7 @@ function isCreateCapablePattern(pattern) {
54
54
  return !looksLikeFilePattern(pattern);
55
55
  }
56
56
 
57
- const IDENTITY_ALIASES = {
57
+ const DEFAULT_IDENTITY_ALIASES = {
58
58
  openai: ['openai', 'chatgpt', 'gpt'],
59
59
  google: ['google', 'gemini'],
60
60
  qwen: ['qwen', 'tongyi'],
@@ -64,37 +64,101 @@ const IDENTITY_ALIASES = {
64
64
  azure: ['azure'],
65
65
  };
66
66
 
67
- function canonicalIdentity(value) {
67
+ function identityAliasList(config) {
68
+ if (Array.isArray(config)) return arrayFrom(config);
69
+ if (config && typeof config === 'object') {
70
+ return [
71
+ ...arrayFrom(config.aliases || config.alias),
72
+ ...arrayFrom(config.tokens),
73
+ ];
74
+ }
75
+ return arrayFrom(config);
76
+ }
77
+
78
+ function identityDisplayName(identity, area) {
79
+ const raw = area && (area.identity_aliases || area.identityAliases) ? area.identity_aliases || area.identityAliases : null;
80
+ if (raw) {
81
+ const entries = Array.isArray(raw)
82
+ ? raw.map((item) => [item && (item.id || item.name || item.canonical), item])
83
+ : Object.entries(raw);
84
+ for (const [id, config] of entries) {
85
+ const candidate = (config && typeof config === 'object' ? config.canonical || config.id || config.name : null) || id;
86
+ if (normalize(candidate) === normalize(identity)) return String(candidate);
87
+ }
88
+ }
89
+ return normalize(identity).split(/[^a-z0-9]+/).filter(Boolean)
90
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
91
+ .join('') || String(identity || '');
92
+ }
93
+
94
+ function aliasRegistryForArea(area) {
95
+ const registry = Object.fromEntries(Object.entries(DEFAULT_IDENTITY_ALIASES)
96
+ .map(([canonical, aliases]) => [canonical, unique([canonical, ...aliases.map(normalize)])]));
97
+ const raw = area && (area.identity_aliases || area.identityAliases) ? area.identity_aliases || area.identityAliases : null;
98
+ if (!raw) return registry;
99
+
100
+ const entries = Array.isArray(raw)
101
+ ? raw.map((item) => [item && (item.id || item.name || item.canonical), item])
102
+ : Object.entries(raw);
103
+ for (const [id, config] of entries) {
104
+ if (!config) continue;
105
+ const canonical = normalize(
106
+ (typeof config === 'object' ? config.canonical || config.id || config.name : null)
107
+ || id
108
+ );
109
+ if (!canonical) continue;
110
+ const aliases = identityAliasList(config);
111
+ registry[canonical] = unique([
112
+ ...(registry[canonical] || []),
113
+ canonical,
114
+ normalize(id),
115
+ ...aliases.map(normalize),
116
+ ].filter(Boolean));
117
+ }
118
+ return registry;
119
+ }
120
+
121
+ function isKnownIdentity(value, aliases = DEFAULT_IDENTITY_ALIASES) {
122
+ return Object.prototype.hasOwnProperty.call(aliases, value);
123
+ }
124
+
125
+ function canonicalIdentity(value, aliases = DEFAULT_IDENTITY_ALIASES) {
68
126
  const text = normalize(value);
69
127
  if (!text) return null;
70
- for (const [canonical, aliases] of Object.entries(IDENTITY_ALIASES)) {
71
- if (aliases.includes(text)) return canonical;
128
+ for (const [canonical, values] of Object.entries(aliases)) {
129
+ if (canonical === text || values.includes(text)) return canonical;
72
130
  }
73
131
  return text;
74
132
  }
75
133
 
76
- function targetIdentityForIntent(intent) {
134
+ function targetIdentityForIntent(intent, aliases) {
77
135
  const tokens = tokenize(intent);
78
136
  for (const token of tokens) {
79
- const identity = canonicalIdentity(token);
80
- if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
137
+ const identity = canonicalIdentity(token, aliases);
138
+ if (identity && isKnownIdentity(identity, aliases)) return identity;
81
139
  }
82
140
  return null;
83
141
  }
84
142
 
85
- function peerIdentityFromPath(pattern) {
143
+ function targetIdentitiesForIntent(intent, aliases) {
144
+ return unique(tokenize(intent)
145
+ .map((token) => canonicalIdentity(token, aliases))
146
+ .filter((identity) => identity && isKnownIdentity(identity, aliases)));
147
+ }
148
+
149
+ function peerIdentityFromPath(pattern, aliases) {
86
150
  if (!looksLikeFilePattern(pattern)) return null;
87
151
  const tokens = basenameTokens(pattern);
88
152
  for (const token of tokens) {
89
- const identity = canonicalIdentity(token);
90
- if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
153
+ const identity = canonicalIdentity(token, aliases);
154
+ if (identity && isKnownIdentity(identity, aliases)) return identity;
91
155
  }
92
156
  return null;
93
157
  }
94
158
 
95
- function identityMatches(left, right) {
159
+ function identityMatches(left, right, aliases) {
96
160
  if (!left || !right) return false;
97
- return canonicalIdentity(left) === canonicalIdentity(right);
161
+ return canonicalIdentity(left, aliases) === canonicalIdentity(right, aliases);
98
162
  }
99
163
 
100
164
  function intentTargetsAllPeers(intent) {
@@ -141,6 +205,50 @@ function configuredArtifactFamilies(area) {
141
205
  }));
142
206
  }
143
207
 
208
+ function configuredPeerFileTemplates(area) {
209
+ const raw = area && (area.peer_file_templates || area.peerFileTemplates) ? area.peer_file_templates || area.peerFileTemplates : null;
210
+ if (!raw) return [];
211
+ const groups = Array.isArray(raw)
212
+ ? raw.map((item) => [item && (item.id || item.name || item.artifact_family || item.artifactFamily), item])
213
+ : Object.entries(raw);
214
+ const templates = [];
215
+ for (const [id, value] of groups) {
216
+ if (!value) continue;
217
+ const group = typeof value === 'object' && !Array.isArray(value) ? value : {};
218
+ const artifacts = group.artifacts || group.files || group.templates || (typeof value === 'object' && !Array.isArray(value) ? value : {});
219
+ for (const [artifactId, artifactValue] of Object.entries(artifacts || {})) {
220
+ const template = typeof artifactValue === 'string'
221
+ ? artifactValue
222
+ : artifactValue && (artifactValue.path || artifactValue.template || artifactValue.pattern);
223
+ if (!template) continue;
224
+ templates.push({
225
+ id: id || group.id || group.name || '',
226
+ artifact: typeof artifactValue === 'object' && artifactValue.artifact ? artifactValue.artifact : artifactId,
227
+ artifactFamily: group.artifact_family || group.artifactFamily || id || '',
228
+ targetSlot: group.target_slot || group.targetSlot || null,
229
+ template: String(template),
230
+ intentKinds: arrayFrom(group.intent_kinds || group.intentKinds),
231
+ reason: (typeof artifactValue === 'object' && artifactValue.reason) || group.reason || '',
232
+ });
233
+ }
234
+ }
235
+ return templates;
236
+ }
237
+
238
+ function peerTemplateFamilyMatches(template, hypothesis) {
239
+ const templateFamily = normalize(template.artifactFamily).replace(/-/g, '_');
240
+ const artifactFamily = normalize(hypothesis.artifactFamily).replace(/-/g, '_');
241
+ if (!templateFamily || !artifactFamily || artifactFamily === 'unknown') return true;
242
+ return templateFamily === artifactFamily || templateFamily.endsWith(artifactFamily);
243
+ }
244
+
245
+ function peerTemplateApplies(template, hypothesis) {
246
+ if (template.intentKinds.length > 0 && !template.intentKinds.includes(hypothesis.intentKind)) return false;
247
+ if (!peerTemplateFamilyMatches(template, hypothesis)) return false;
248
+ if (hypothesis.createRequires.length > 0 && !hypothesis.createRequires.includes(template.artifact)) return false;
249
+ return true;
250
+ }
251
+
144
252
  function configuredArtifactFamilyForIntent(area, intent) {
145
253
  const text = normalize(intent);
146
254
  for (const family of configuredArtifactFamilies(area)) {
@@ -189,10 +297,15 @@ function classifyArtifactFamily(intent, area) {
189
297
  }
190
298
 
191
299
  function classifyIntentWrite(intent, area) {
300
+ const identityAliases = aliasRegistryForArea(area);
192
301
  const text = normalize(intent);
193
- const createTerm = [
194
- /\b(add|create|new|introduce|support|implement)\b/,
195
- /新增|新建|创建|添加|接入|支持/,
302
+ const strongCreateTerm = [
303
+ /\b(create|new|introduce|implement)\b/,
304
+ /新增|新建|创建|接入/,
305
+ ].some((pattern) => pattern.test(text));
306
+ const createTerm = strongCreateTerm || [
307
+ /\b(add|support)\b/,
308
+ /添加|支持/,
196
309
  ].some((pattern) => pattern.test(text));
197
310
  const deleteTerm = [
198
311
  /\b(delete|remove)\b/,
@@ -205,7 +318,8 @@ function classifyIntentWrite(intent, area) {
205
318
  const targetAll = intentTargetsAllPeers(intent);
206
319
  const artifactFamily = classifyArtifactFamily(intent, area);
207
320
  const familyConfig = configuredArtifactFamilies(area).find((item) => item.id === artifactFamily) || null;
208
- const targetIdentity = targetIdentityForIntent(intent);
321
+ const targetIdentity = targetIdentityForIntent(intent, identityAliases);
322
+ const targetIdentities = targetIdentitiesForIntent(intent, identityAliases);
209
323
  let action = 'modify';
210
324
  if (deleteTerm) action = 'delete';
211
325
  else if (renameTerm) action = 'rename';
@@ -214,6 +328,7 @@ function classifyIntentWrite(intent, area) {
214
328
  action,
215
329
  artifactFamily,
216
330
  targetIdentity,
331
+ targetIdentities,
217
332
  operation: action === 'create' && artifactFamily === 'provider_proxy' ? 'create_file' : 'modify_existing_file',
218
333
  required: true,
219
334
  targetAll,
@@ -221,10 +336,11 @@ function classifyIntentWrite(intent, area) {
221
336
  createRequires: familyConfig ? familyConfig.createRequires : [],
222
337
  hooks: familyConfig ? familyConfig.hooks : [],
223
338
  confidence: artifactFamily === 'unknown' ? 'low' : 'medium',
339
+ strongCreateIntent: strongCreateTerm,
224
340
  };
225
341
  }
226
342
 
227
- function directCapabilities(scope) {
343
+ function directCapabilities(scope, identityAliases) {
228
344
  const explicitItems = [
229
345
  ...(scope.writableDirectItems || []),
230
346
  ];
@@ -248,12 +364,14 @@ function directCapabilities(scope) {
248
364
  breadth: createCapable ? 'narrow_pattern' : 'exact',
249
365
  reviewGated: false,
250
366
  source: item.source || 'writable_direct',
367
+ operationSpecified: configuredOperations.length > 0,
251
368
  artifact: item.artifact || null,
252
369
  artifactFamily: item.artifactFamily || item.artifact_family || null,
253
370
  intentKinds: arrayFrom(item.intentKinds || item.intent_kinds),
254
371
  requiredWhen: arrayFrom(item.requiredWhen || item.required_when),
255
372
  targetSlot: item.targetSlot || item.target_slot || null,
256
- peerIdentity: canonicalIdentity(item.targetIdentity || item.target_identity || item.peerIdentity || item.peer_identity) || peerIdentityFromPath(pattern),
373
+ peerIdentity: canonicalIdentity(item.targetIdentity || item.target_identity || item.peerIdentity || item.peer_identity, identityAliases)
374
+ || peerIdentityFromPath(pattern, identityAliases),
257
375
  };
258
376
  }).filter(Boolean);
259
377
  }
@@ -288,32 +406,155 @@ function capabilityMatchesIntent(capability, hypothesis) {
288
406
  return capability.intentKinds.includes(hypothesis.intentKind);
289
407
  }
290
408
 
409
+ function expandPeerTemplate(template, hypothesis, area) {
410
+ if (!hypothesis.targetIdentity) return template;
411
+ const label = identityDisplayName(hypothesis.targetIdentity, area);
412
+ const lower = normalize(hypothesis.targetIdentity);
413
+ return template
414
+ .replace(/\{Provider\}|\{Identity\}|\{Target\}/g, label)
415
+ .replace(/\{provider\}|\{identity\}|\{target\}/g, lower)
416
+ .replace(/\{PROVIDER\}|\{IDENTITY\}|\{TARGET\}/g, lower.toUpperCase());
417
+ }
418
+
419
+ function peerFileCandidatesFor(area, hypothesis) {
420
+ return configuredPeerFileTemplates(area)
421
+ .filter((item) => peerTemplateApplies(item, hypothesis))
422
+ .map((item) => ({
423
+ artifact: item.artifact,
424
+ artifactFamily: item.artifactFamily,
425
+ targetSlot: item.targetSlot,
426
+ targetIdentity: hypothesis.targetIdentity || null,
427
+ template: item.template,
428
+ candidatePath: expandPeerTemplate(item.template, hypothesis, area),
429
+ writable: false,
430
+ reason: item.reason || 'peer_file_templates are read-only hints; use writable_create_patterns to grant create permission',
431
+ }));
432
+ }
433
+
434
+ function selectedCapabilitiesFor(capabilities, selected) {
435
+ const selectedSet = new Set(selected);
436
+ return capabilities.filter((item) => selectedSet.has(item.scope));
437
+ }
438
+
439
+ function directReferenceConflicts(references, capabilities) {
440
+ const conflicts = [];
441
+ for (const reference of references) {
442
+ const referencePath = reference.path || reference.pattern;
443
+ if (!referencePath) continue;
444
+ const capability = capabilities.find((item) => (
445
+ item.source !== 'writable_create_patterns'
446
+ && item.operations.includes('update_existing')
447
+ && matchesPattern(referencePath, item.scope)
448
+ ));
449
+ if (capability) {
450
+ conflicts.push({
451
+ path: referencePath,
452
+ directScope: capability.scope,
453
+ });
454
+ }
455
+ }
456
+ return conflicts;
457
+ }
458
+
459
+ function unspecifiedOperationScopes(capabilities, hypothesis) {
460
+ return capabilities
461
+ .filter((item) => (
462
+ item.source === 'writable_direct'
463
+ && item.operationSpecified === false
464
+ && (hypothesis.operation === 'create_file' || item.operations.includes('create_file'))
465
+ ))
466
+ .map((item) => item.scope);
467
+ }
468
+
469
+ function certificateOperation(capability, hypothesis) {
470
+ if (hypothesis.operation === 'create_file' && capability.operations.includes('create_file')) return 'create_file';
471
+ if (capability.operations.includes('update_existing')) return 'update_existing';
472
+ return capability.operations[0] || hypothesis.operation || 'update_existing';
473
+ }
474
+
475
+ function certificateMapEntry(area, capability) {
476
+ const areaId = area && area.id ? area.id : 'unknown_area';
477
+ const source = capability.source || 'writable_direct';
478
+ const label = capability.artifact || capability.artifactFamily || null;
479
+ return label ? `${areaId}.scope.${source}.${label}` : `${areaId}.scope.${source}`;
480
+ }
481
+
482
+ function buildCapabilityCertificate(area, capability, hypothesis) {
483
+ const operation = certificateOperation(capability, hypothesis);
484
+ return {
485
+ path: capability.scope,
486
+ scope: capability.scope,
487
+ operation,
488
+ artifact: capability.artifact || null,
489
+ artifactFamily: capability.artifactFamily || hypothesis.artifactFamily || null,
490
+ targetIdentity: capability.peerIdentity || hypothesis.targetIdentity || null,
491
+ status: 'certified',
492
+ certifiedBy: {
493
+ mapEntry: certificateMapEntry(area, capability),
494
+ source: capability.source || 'writable_direct',
495
+ pattern: capability.scope,
496
+ intentKind: hypothesis.intentKind,
497
+ },
498
+ };
499
+ }
500
+
501
+ function certificatesForSelected(area, capabilities, selected, hypothesis) {
502
+ return selectedCapabilitiesFor(capabilities, selected).map((capability) => (
503
+ buildCapabilityCertificate(area, capability, hypothesis)
504
+ ));
505
+ }
506
+
507
+ /*
508
+ * Write permission is certified here, not by relevance alone:
509
+ * relevant files, entrypoints, peer examples, and broad fallback scopes may help
510
+ * an agent understand the repo, but they do not become Direct Writable unless
511
+ * they cover the inferred required write operation narrowly for this intent.
512
+ */
291
513
  function buildWritePlan(scope, intent, area) {
514
+ const identityAliases = aliasRegistryForArea(area);
292
515
  const hypothesis = classifyIntentWrite(intent, area);
293
- const capabilities = directCapabilities(scope);
516
+ const capabilities = directCapabilities(scope, identityAliases);
294
517
  const warnings = [];
295
518
  const references = (scope.readableReference || []).map((item) => buildReference(
296
519
  {
297
520
  path: item.path,
298
521
  role: item.role || 'reference',
299
522
  artifact: item.artifact || null,
300
- peerIdentity: canonicalIdentity(item.peerIdentity) || peerIdentityFromPath(item.path),
523
+ peerIdentity: canonicalIdentity(item.peerIdentity, identityAliases) || peerIdentityFromPath(item.path, identityAliases),
301
524
  },
302
525
  item.reason || 'readable reference from Project Map'
303
526
  ));
527
+ const peerFileCandidates = peerFileCandidatesFor(area, hypothesis);
304
528
  let selected = capabilities.map((item) => item.scope);
305
529
 
530
+ if (hypothesis.targetIdentities && hypothesis.targetIdentities.length > 1 && !hypothesis.targetAll) {
531
+ warnings.push(warning(
532
+ 'AMBIGUOUS_TARGET_IDENTITY',
533
+ `Intent mentions multiple target identities (${hypothesis.targetIdentities.join(', ')}); Direct Writable must be selected for one unambiguous target.`,
534
+ { targetIdentities: hypothesis.targetIdentities }
535
+ ));
536
+ }
537
+ if (hypothesis.confidence === 'low' && hypothesis.action !== 'modify' && (hypothesis.strongCreateIntent || hypothesis.action !== 'create')) {
538
+ warnings.push(warning(
539
+ 'LOW_CONFIDENCE_INTENT_WRITE_PLAN',
540
+ 'Intent appears to require writes, but Hive Lite could not infer a known artifact family for the required write plan.',
541
+ { action: hypothesis.action, artifactFamily: hypothesis.artifactFamily }
542
+ ));
543
+ }
544
+
306
545
  if (hypothesis.operation === 'create_file' && hypothesis.artifactFamily === 'provider_proxy') {
307
546
  const createCapabilities = capabilities.filter((item) => (
308
547
  item.operations.includes('create_file') && capabilityMatchesIntent(item, hypothesis)
309
548
  ));
310
549
  const genericExistingCapabilities = capabilities.filter((item) => (
311
- !item.peerIdentity && !item.operations.includes('create_file')
550
+ item.source === 'writable_existing'
551
+ && !item.operations.includes('create_file')
552
+ && (!item.peerIdentity || identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases))
312
553
  ));
313
554
  const referencePeers = capabilities.filter((item) => (
314
555
  item.peerIdentity
315
556
  && hypothesis.targetIdentity
316
- && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
557
+ && !identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases)
317
558
  ));
318
559
  const selectedCreateCapabilities = hypothesis.createRequires.length
319
560
  ? createCapabilities.filter((item) => hypothesis.createRequires.some((artifact) => artifactMatches(item, artifact)))
@@ -374,13 +615,13 @@ function buildWritePlan(scope, intent, area) {
374
615
  && !hypothesis.targetAll
375
616
  ) {
376
617
  const targetMatches = capabilities.filter((item) => (
377
- item.peerIdentity && identityMatches(item.peerIdentity, hypothesis.targetIdentity)
618
+ item.peerIdentity && identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases)
378
619
  ));
379
620
  const genericCapabilities = capabilities.filter((item) => (
380
621
  !item.peerIdentity && !item.operations.includes('create_file')
381
622
  ));
382
623
  const referencePeers = capabilities.filter((item) => (
383
- item.peerIdentity && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
624
+ item.peerIdentity && !identityMatches(item.peerIdentity, hypothesis.targetIdentity, identityAliases)
384
625
  ));
385
626
  selected = unique([...targetMatches, ...genericCapabilities].map((item) => item.scope));
386
627
  for (const capability of referencePeers) {
@@ -398,11 +639,31 @@ function buildWritePlan(scope, intent, area) {
398
639
  }
399
640
 
400
641
  selected = unique(selected);
642
+ const selectedCapabilities = selectedCapabilitiesFor(capabilities, selected);
643
+ const selectedWritableCertificates = certificatesForSelected(area, capabilities, selected, hypothesis);
644
+ const referenceConflicts = directReferenceConflicts(references, selectedCapabilities);
645
+ if (referenceConflicts.length > 0) {
646
+ warnings.push(warning(
647
+ 'REFERENCE_DIRECT_ROLE_CONFLICT',
648
+ 'One or more reference files also match selected Direct Writable update scope; keep peer examples reference-only or move new-file permission to writable_create_patterns.',
649
+ { blocking: false, conflicts: referenceConflicts }
650
+ ));
651
+ }
652
+ const unspecifiedScopes = unspecifiedOperationScopes(selectedCapabilities, hypothesis);
653
+ if (unspecifiedScopes.length > 0) {
654
+ warnings.push(warning(
655
+ 'DIRECT_SCOPE_OPERATION_UNSPECIFIED',
656
+ 'Selected Direct Writable scope relies on inferred operations; declare operations under writable_existing or writable_create_patterns for safer write certification.',
657
+ { blocking: false, scopes: unspecifiedScopes }
658
+ ));
659
+ }
401
660
  return {
402
661
  hypotheses: [hypothesis],
403
662
  capabilities,
404
663
  selectedWritableDirect: selected,
664
+ selectedWritableCertificates,
405
665
  referenceFiles: references,
666
+ peerFileCandidates,
406
667
  blockingWarnings: warnings.filter((item) => item.blocking !== false),
407
668
  warnings,
408
669
  };
@@ -677,9 +938,10 @@ function competingHighScoringAreas(scored) {
677
938
  ));
678
939
  }
679
940
 
680
- function decompositionSignals({ map, area, confidence, intent, relevant, scope, scored, validationPlan, constrainedAreaId }) {
941
+ function decompositionSignals({ map, area, confidence, intent, relevant, scope, selectedWritableDirect, scored, validationPlan, constrainedAreaId }) {
681
942
  if (!area) return [];
682
943
  const signals = [];
944
+ const writableDirectForIntent = selectedWritableDirect || scope.writableDirect || [];
683
945
  const highAreas = highScoringAreas(scored)
684
946
  .filter((item) => !constrainedAreaId || item.area.id === constrainedAreaId);
685
947
  const competingAreas = competingHighScoringAreas(scored)
@@ -711,16 +973,16 @@ function decompositionSignals({ map, area, confidence, intent, relevant, scope,
711
973
  }));
712
974
  }
713
975
 
714
- if (scope.writableDirect.length > DECOMPOSITION_LIMITS.writableFilesBlocking) {
976
+ if (writableDirectForIntent.length > DECOMPOSITION_LIMITS.writableFilesBlocking) {
715
977
  signals.push(signal('TOO_MANY_WRITABLE_FILES', 'Direct writable files exceed the safe change boundary.', {
716
- count: scope.writableDirect.length,
978
+ count: writableDirectForIntent.length,
717
979
  threshold: DECOMPOSITION_LIMITS.writableFilesBlocking,
718
980
  }));
719
- } else if (scope.writableDirect.length > DECOMPOSITION_LIMITS.writableFilesWarning) {
981
+ } else if (writableDirectForIntent.length > DECOMPOSITION_LIMITS.writableFilesWarning) {
720
982
  signals.push(signal('TOO_MANY_WRITABLE_FILES', 'Direct writable files are high for one Change Record.', {
721
983
  severity: 'warning',
722
984
  blocking: false,
723
- count: scope.writableDirect.length,
985
+ count: writableDirectForIntent.length,
724
986
  threshold: DECOMPOSITION_LIMITS.writableFilesWarning,
725
987
  }));
726
988
  }
@@ -777,8 +1039,8 @@ function candidatePhaseSeeds(scored, intent) {
777
1039
  phaseHint: area.name || area.id,
778
1040
  findIntent: `${intent} [focus: ${area.id}]`,
779
1041
  roles,
780
- evidenceClasses: evidenceClassesForRoles(roles),
781
- validationProfiles: ((area.validation || {}).profiles || []).filter(Boolean),
1042
+ evidenceClasses: unique(evidenceClassesForRoles(roles)),
1043
+ validationProfiles: unique(((area.validation || {}).profiles || []).filter(Boolean)),
782
1044
  risk: riskLevelFor(area),
783
1045
  reason: item.signals.slice(0, 4).join(', '),
784
1046
  };
@@ -823,14 +1085,14 @@ function buildSplitMarkdown(note) {
823
1085
  '',
824
1086
  `Area: ${phase.primaryAreaId}`,
825
1087
  '',
826
- 'Run with this Hive Lite CLI invocation:',
1088
+ 'Copy one command block below to run this phase. Do not append the notes after it.',
827
1089
  '',
828
1090
  '```bash',
829
1091
  findCommandForPhase(currentArgv, note, phase),
830
1092
  '```',
831
1093
  '',
832
1094
  ...(!sameArgv(currentArgv, packageAliasArgv) ? [
833
- 'If the package alias is installed:',
1095
+ 'Or, if the package alias is installed:',
834
1096
  '',
835
1097
  '```bash',
836
1098
  findCommandForPhase(packageAliasArgv, note, phase),
@@ -892,10 +1154,10 @@ function createSplitNote(root, packet, phaseSeeds, decomposition, options = {})
892
1154
  title: phase.phaseHint,
893
1155
  findIntent: phase.findIntent,
894
1156
  primaryAreaId: phase.areaId,
895
- expectedEvidence: [
1157
+ expectedEvidence: unique([
896
1158
  ...(phase.validationProfiles || []).map((profile) => `validation:${profile}`),
897
1159
  ...(phase.evidenceClasses || []).map((item) => `evidence:${item}`),
898
- ],
1160
+ ]),
899
1161
  nonGoals: phaseSeeds
900
1162
  .filter((other) => other.areaId !== phase.areaId)
901
1163
  .map((other) => `Do not change ${other.areaId} in this phase.`),
@@ -1110,6 +1372,8 @@ function hasBlockingMapGapWarning(warnings = []) {
1110
1372
  'TARGET_ENTITY_MISMATCH',
1111
1373
  'MISSING_ARTIFACT_FAMILY_SCOPE',
1112
1374
  'MISSING_REQUIRED_HOOK_SCOPE',
1375
+ 'AMBIGUOUS_TARGET_IDENTITY',
1376
+ 'LOW_CONFIDENCE_INTENT_WRITE_PLAN',
1113
1377
  ].includes(warning.code));
1114
1378
  }
1115
1379
 
@@ -1238,11 +1502,21 @@ function buildContextMarkdown(packet) {
1238
1502
  : ['- Coverage: direct writable scope covers the inferred write operation.']),
1239
1503
  '',
1240
1504
  ] : []),
1505
+ ...(packet.writePlan && packet.writePlan.selectedWritableCertificates && packet.writePlan.selectedWritableCertificates.length ? [
1506
+ '## Certified Direct Writable',
1507
+ ...packet.writePlan.selectedWritableCertificates.map((item) => `- ${item.path}: ${item.operation}${item.artifact ? ` ${item.artifact}` : ''}${item.targetIdentity ? ` for ${item.targetIdentity}` : ''} (certified by ${item.certifiedBy && item.certifiedBy.mapEntry ? item.certifiedBy.mapEntry : 'unknown map entry'})`),
1508
+ '',
1509
+ ] : []),
1241
1510
  ...(packet.writePlan && packet.writePlan.referenceFiles.length ? [
1242
1511
  '## Reference Files',
1243
1512
  ...packet.writePlan.referenceFiles.map((file) => `- ${file.path}: ${file.reason}`),
1244
1513
  '',
1245
1514
  ] : []),
1515
+ ...(packet.writePlan && packet.writePlan.peerFileCandidates && packet.writePlan.peerFileCandidates.length ? [
1516
+ '## Peer File Template Candidates',
1517
+ ...packet.writePlan.peerFileCandidates.map((file) => `- ${file.artifact}: ${file.candidatePath || file.template} (read-only hint; not Direct Writable)`),
1518
+ '',
1519
+ ] : []),
1246
1520
  '## Relevant Files',
1247
1521
  ...packet.relevantFiles.map((file) => `- ${file.path} (${file.role || file.source}): ${file.reason}`),
1248
1522
  '',
@@ -1334,7 +1608,9 @@ function createContextPacket(root, intent, options = {}) {
1334
1608
  hypotheses: [],
1335
1609
  capabilities: [],
1336
1610
  selectedWritableDirect: [],
1611
+ selectedWritableCertificates: [],
1337
1612
  referenceFiles: [],
1613
+ peerFileCandidates: [],
1338
1614
  blockingWarnings: [],
1339
1615
  warnings: [],
1340
1616
  };
@@ -1403,6 +1679,7 @@ function createContextPacket(root, intent, options = {}) {
1403
1679
  intent,
1404
1680
  relevant,
1405
1681
  scope,
1682
+ selectedWritableDirect: writePlan.selectedWritableDirect,
1406
1683
  scored,
1407
1684
  validationPlan,
1408
1685
  constrainedAreaId,
package/src/lib/git.js CHANGED
@@ -123,6 +123,7 @@ module.exports = {
123
123
  repoRoot,
124
124
  requireGitRepo,
125
125
  runGit,
126
+ runGitRaw,
126
127
  statusLines,
127
128
  tryGit,
128
129
  };