@indigoai-us/hq-cli 5.194.0 → 5.194.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.194.1] — 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - `hq core ontology apply` no longer creates a new entity that is probably an
10
+ existing one. A candidate whose alias is already another entity's name or
11
+ alias, or a person whose name contains every word of an existing person's
12
+ full name (for example "Chaudhry Hassaan Saleem" and "Hassaan Saleem"), is
13
+ held back and reported as a suspect duplicate.
14
+
5
15
  ## [5.194.0] — 2026-09-25
6
16
 
7
17
  ### Added
@@ -108,6 +108,13 @@ type Resolution = {
108
108
  };
109
109
  /** Design section 5.3, tiers 1 to 4. */
110
110
  export declare function resolveCandidate(index: EntityIndex, type: string, canonical: string): Resolution;
111
+ /**
112
+ * Existing entities of `type` that a new candidate probably is: one of the
113
+ * candidate's aliases is already their name or alias, or, for people, every
114
+ * word of their name (two words or more) is in the candidate's name
115
+ * (`Chaudhry Hassaan Saleem` and `Hassaan Saleem`).
116
+ */
117
+ export declare function overlappingEntities(index: EntityIndex, type: string, canonical: string, aliases: string[]): string[];
111
118
  export declare function applyOntologyRun(options: ApplyOptions): ApplyReport;
112
119
  export {};
113
120
  //# sourceMappingURL=apply.d.ts.map
@@ -81,6 +81,38 @@ export function resolveCandidate(index, type, canonical) {
81
81
  return { tier: "type_conflict", types: otherTypes };
82
82
  return { tier: "new" };
83
83
  }
84
+ /**
85
+ * Existing entities of `type` that a new candidate probably is: one of the
86
+ * candidate's aliases is already their name or alias, or, for people, every
87
+ * word of their name (two words or more) is in the candidate's name
88
+ * (`Chaudhry Hassaan Saleem` and `Hassaan Saleem`).
89
+ */
90
+ export function overlappingEntities(index, type, canonical, aliases) {
91
+ const paths = new Set();
92
+ for (const alias of aliases) {
93
+ const normalized = normalizeEntityName(alias, type);
94
+ // A generic alias still counts when an existing entity already holds it.
95
+ if (!normalized)
96
+ continue;
97
+ for (const rel of index.canonical.get(key(type, normalized)) ?? [])
98
+ paths.add(rel);
99
+ for (const rel of index.alias.get(key(type, normalized)) ?? [])
100
+ paths.add(rel);
101
+ }
102
+ if (type === "person") {
103
+ const words = new Set(normalizeEntityName(canonical, type).split(" "));
104
+ const prefix = key(type, "");
105
+ for (const [name, rels] of index.canonical) {
106
+ if (!name.startsWith(prefix))
107
+ continue;
108
+ const other = name.slice(prefix.length).split(" ");
109
+ if (other.length >= 2 && other.every((word) => words.has(word)))
110
+ for (const rel of rels)
111
+ paths.add(rel);
112
+ }
113
+ }
114
+ return [...paths].sort();
115
+ }
84
116
  /**
85
117
  * Aliases worth adding to the entity at `self`: new to it, and held by no
86
118
  * other entity of any type, so an alias can never make a later match
@@ -301,6 +333,12 @@ export function applyOntologyRun(options) {
301
333
  note(report.rejected, "type_conflict");
302
334
  continue;
303
335
  }
336
+ const overlap = overlappingEntities(index, candidate.type, canonical, candidate.aliases);
337
+ if (overlap.length > 0) {
338
+ note(report.deferred, "suspect_duplicate");
339
+ report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: overlap });
340
+ continue;
341
+ }
304
342
  // Tier 5: create.
305
343
  if (candidate.confidence < MIN_CREATE_CONFIDENCE) {
306
344
  note(report.rejected, "low_confidence");
@@ -569,7 +569,7 @@ async function withReconcileLock(hqRoot, fn) {
569
569
  }
570
570
  }
571
571
  }
572
- function classifyDeadLocalLane(hqRoot, laneId) {
572
+ function classifyDeadLocalLane(hqRoot, laneId, now) {
573
573
  const runDir = laneRunDir(hqRoot, laneId);
574
574
  const manifest = readSpawnManifest(runDir);
575
575
  const minEnvelopeName = manifest.envelope_path ? path.basename(manifest.envelope_path) : undefined;
@@ -586,7 +586,6 @@ function classifyDeadLocalLane(hqRoot, laneId) {
586
586
  // the quota signal, any other becomes provider_rejected, and only a stream
587
587
  // with no failure object falls back to the quota reader.
588
588
  const workerOutOffset = readSpawnManifest(runDir).worker_out_offset ?? 0;
589
- const now = new Date();
590
589
  const routed = envelope
591
590
  ? { quota: null, rejection: null }
592
591
  : routeProviderFailure(readProviderRejection(runDir, lane.provider, workerOutOffset), () => readQuotaSignal(runDir, lane.model, now, workerOutOffset), lane.model, now);
@@ -894,7 +893,7 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
894
893
  continue;
895
894
  }
896
895
  try {
897
- const { classification, latest, quota, providerRejection, lane: classified } = classifyDeadLocalLane(hqRoot, lane.lane_id);
896
+ const { classification, latest, quota, providerRejection, lane: classified } = classifyDeadLocalLane(hqRoot, lane.lane_id, now);
898
897
  // Match the watcher's one-repair decision before classifying an
899
898
  // unparseable envelope after both processes have died. As in the
900
899
  // watcher, a provider rejection or a quota signal skips the repair.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.194.0",
3
+ "version": "5.194.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {