hmgr 0.0.44 → 0.0.46

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.
@@ -659,7 +659,7 @@ export class Anwendungsfaelle {
659
659
  await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
660
660
  entfernteLinks.push(name);
661
661
  }
662
- if (Anwendungsfaelle.bundlesZerbrechen(nachZustand, ziel, entfernteLinks).length > 0)
662
+ if (Anwendungsfaelle.bundleVerweiseFallenLassen(nachZustand, ziel))
663
663
  bundlesGeaendert = true;
664
664
  const konfigurationZurueckgebaut = kannZurueckbauen(harness) ? await harness.zurueckbauen(ziel.ort) : false;
665
665
  zielErgebnisse.push({ ziel, entfernteLinks, konfigurationZurueckgebaut });
@@ -801,12 +801,12 @@ export class Anwendungsfaelle {
801
801
  throw new QuelleAufraeumenLokaleAenderungen(inventar.checkoutsMitLokalenAenderungen);
802
802
  for (const link of inventar.links)
803
803
  await this.p.ziele.linkEntfernen(link.ziel, link.verzeichnis, link.name);
804
- // Wo dabei ein Bundle-Mitglied fiel, zerbricht das Bundle in diesem Ziel (E69), wie beim
805
- // einzelnen `linkEntfernen`: die übrigen Links bleiben als Einzel-Installationen liegen.
804
+ // Wo dabei ein Bundle-Mitglied fiel, gilt es als herausgenommen (E114), wie beim einzelnen
805
+ // `linkEntfernen`: das Bundle bleibt, der Abgleich legt das Mitglied nicht wieder an.
806
806
  const zustand = await this.p.zustand.lesen();
807
807
  let bundlesGeaendert = false;
808
808
  for (const link of inventar.links)
809
- if (Anwendungsfaelle.bundlesZerbrechen(zustand, link.ziel, [link.name]).length > 0)
809
+ if (Anwendungsfaelle.bundleMitgliederHerausnehmen(zustand, link.ziel, [link.name]).length > 0)
810
810
  bundlesGeaendert = true;
811
811
  if (bundlesGeaendert)
812
812
  await this.p.zustand.schreiben(zustand);
@@ -1122,8 +1122,11 @@ export class Anwendungsfaelle {
1122
1122
  }
1123
1123
  return angelegt;
1124
1124
  }
1125
- /** Die Zugehörigkeit der Links eines Bundles im Ziel merken (E69), einen alten Eintrag ersetzend. */
1126
- async bundleInstallationMerken(ziel, bundle, skills) {
1125
+ /**
1126
+ * Die Zugehörigkeit der Links eines Bundles im Ziel merken (E69), einen alten Eintrag ersetzend.
1127
+ * `herausgenommen` (E114) wird nur geschrieben, wenn es etwas enthält.
1128
+ */
1129
+ async bundleInstallationMerken(ziel, bundle, skills, herausgenommen = []) {
1127
1130
  const zustand = await this.p.zustand.lesen();
1128
1131
  const schluessel = zielSchluessel(ziel);
1129
1132
  const eigener = bundleSchluessel(bundle);
@@ -1135,27 +1138,43 @@ export class Anwendungsfaelle {
1135
1138
  bundle: bundle.name,
1136
1139
  quelle: bundle.quelle,
1137
1140
  skills,
1141
+ ...(herausgenommen.length === 0 ? {} : { herausgenommen }),
1138
1142
  });
1139
1143
  await this.p.zustand.schreiben(zustand);
1140
1144
  }
1141
1145
  /**
1142
- * Zerbrechen (E69): Wird ein einzelner Link entfernt, der zu einem Bundle gehört, zerbricht das
1143
- * Bundle in diesem Ziel — die übrigen Links werden Einzel-Installationen, der Bundle-Verweis
1144
- * fällt weg. Ändert nur den übergebenen Zustand, geschrieben wird er vom Aufrufer, der ohnehin
1145
- * schreibt. Gibt die Namen der zerbrochenen Bundles zurück, damit die Oberfläche es sagen kann.
1146
+ * Herausnehmen (E114 L2, ersetzt das Zerbrechen aus E69): Wird ein einzelner Link entfernt, der
1147
+ * zu einem Bundle gehört, bleibt das Bundle im Ziel und merkt sich das Mitglied als herausgenommen
1148
+ * — auch wenn danach keines mehr verlinkt ist („0 von N", weiter entfernbar). Ändert nur den
1149
+ * übergebenen Zustand, geschrieben wird er vom Aufrufer. Gibt die Namen der betroffenen Bundles
1150
+ * zurück, damit die Oberfläche es sagen kann.
1146
1151
  */
1147
- static bundlesZerbrechen(zustand, ziel, namen) {
1152
+ static bundleMitgliederHerausnehmen(zustand, ziel, namen) {
1148
1153
  const schluessel = zielSchluessel(ziel);
1149
- const zerbrochen = [];
1150
- zustand.bundleInstallationen = zustand.bundleInstallationen.filter((installation) => {
1154
+ const betroffen = [];
1155
+ for (const installation of zustand.bundleInstallationen) {
1151
1156
  if (zielSchluessel(Anwendungsfaelle.installationZiel(installation)) !== schluessel)
1152
- return true;
1153
- if (!installation.skills.some((name) => namen.includes(name)))
1154
- return true;
1155
- zerbrochen.push(installation.bundle);
1156
- return false;
1157
- });
1158
- return zerbrochen;
1157
+ continue;
1158
+ const raus = installation.skills.filter((name) => namen.includes(name));
1159
+ if (raus.length === 0)
1160
+ continue;
1161
+ installation.skills = installation.skills.filter((name) => !raus.includes(name));
1162
+ installation.herausgenommen = [...(installation.herausgenommen ?? []), ...raus];
1163
+ betroffen.push(installation.bundle);
1164
+ }
1165
+ return betroffen;
1166
+ }
1167
+ /**
1168
+ * Bundle-Verweise eines Ziels fallen lassen, wenn „Säubern" dort alle Links abbaut (E69). Kein
1169
+ * Herausnehmen: Säubern räumt den Harness ab, ein Bundle mit „0 von N" bliebe ein Rest, den der
1170
+ * Nutzer gerade loswerden wollte. Darum fallen alle Verweise des Ziels, auch die von Bundles, von
1171
+ * denen gerade kein Link mehr lag.
1172
+ */
1173
+ static bundleVerweiseFallenLassen(zustand, ziel) {
1174
+ const schluessel = zielSchluessel(ziel);
1175
+ const vorher = zustand.bundleInstallationen.length;
1176
+ zustand.bundleInstallationen = zustand.bundleInstallationen.filter((installation) => zielSchluessel(Anwendungsfaelle.installationZiel(installation)) !== schluessel);
1177
+ return zustand.bundleInstallationen.length !== vorher;
1159
1178
  }
1160
1179
  /**
1161
1180
  * Bundle installieren (E69): das Bundle als Ganzes in ein Ziel. Erst wird alles geprüft — jedes
@@ -1205,9 +1224,11 @@ export class Anwendungsfaelle {
1205
1224
  return treffer[0];
1206
1225
  }
1207
1226
  /**
1208
- * Bundle entfernen (E69): alle seine Links im Ziel und den Bundle-Verweis. Entfernt wird direkt
1209
- * über den Ziele-Port, nicht über den Anwendungsfall `linkEntfernen` — der macht Zeiger-Arbeit
1210
- * (E57) und ließe das Bundle bei jedem einzelnen Link zerbrechen, das es hier gerade verlässt.
1227
+ * Bundle entfernen (E69): alle seine noch verlinkten Mitglieder im Ziel und den Bundle-Verweis,
1228
+ * auch nach Teil-Löschung (E114 L2): herausgenommene stehen nicht in `skills` und bleiben
1229
+ * unberührt, ein Link gleichen Namens an ihrer Stelle gehört nicht dem Bundle. Entfernt wird
1230
+ * direkt über den Ziele-Port, nicht über den Anwendungsfall `linkEntfernen` — der macht
1231
+ * Zeiger-Arbeit (E57) und nähme jedes Mitglied einzeln heraus.
1211
1232
  */
1212
1233
  async bundleEntfernen(args) {
1213
1234
  const harness = await this.harnessGeprueft(args.ziel.harness);
@@ -1237,6 +1258,11 @@ export class Anwendungsfaelle {
1237
1258
  * Wirkt auf das Bundle als Ganzes: Lässt sich eine Definitionsänderung nicht vollständig nachziehen
1238
1259
  * (Quelle weg, Pfad kein Skill mehr, Namenskonflikt), bleibt das Bundle im betroffenen Ziel auf dem
1239
1260
  * alten Stand und der Lauf meldet es, statt es halb nachzuziehen (F66b).
1261
+ *
1262
+ * Herausgenommene Mitglieder (E114 L5) lässt der Abgleich aus: kein Link, keine Konfliktprüfung
1263
+ * (ein anderer Link gleichen Namens an ihrer Stelle ist dann kein Konflikt). Fällt eines aus der
1264
+ * Definition, fällt es auch aus der Liste. `aufnehmen` holt herausgenommene zurück: 'alle' ist
1265
+ * „Vervollständigen" (L4), eine Namensliste „Wieder verlinken" einzelner Mitglieder (L3).
1240
1266
  */
1241
1267
  async bundleAbgleich(args) {
1242
1268
  const zustand = await this.p.zustand.lesen();
@@ -1256,7 +1282,10 @@ export class Anwendungsfaelle {
1256
1282
  // ein Fehlschlag (kein Netz) arbeitet mit dem vorhandenen Stand weiter.
1257
1283
  await this.refsNachziehen(installation.quelle);
1258
1284
  const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
1259
- const { funde, neue } = await this.bundleAufloesen(bundle);
1285
+ const { funde: alle, neue } = await this.bundleAufloesen(bundle);
1286
+ const aufnehmen = args?.aufnehmen;
1287
+ const herausgenommen = (installation.herausgenommen ?? []).filter((name) => aufnehmen !== 'alle' && !(aufnehmen ?? []).includes(name) && alle.some((f) => f.skill.name === name));
1288
+ const funde = alle.filter((f) => !herausgenommen.includes(f.skill.name));
1260
1289
  const verzeichnis = harness.zielVerzeichnis(ziel.ort);
1261
1290
  const { konflikte } = await this.bundleKonflikte(bundle, ziel, verzeichnis, funde);
1262
1291
  if (konflikte.length > 0)
@@ -1272,7 +1301,7 @@ export class Anwendungsfaelle {
1272
1301
  await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
1273
1302
  entfernt.push(name);
1274
1303
  }
1275
- await this.bundleInstallationMerken(ziel, bundle, namen);
1304
+ await this.bundleInstallationMerken(ziel, bundle, namen, herausgenommen);
1276
1305
  const gelegt = namen.filter((n) => !installation.skills.includes(n));
1277
1306
  ergebnis.push({
1278
1307
  ...kopf,
@@ -1486,7 +1515,8 @@ export class Anwendungsfaelle {
1486
1515
  const version = args.version;
1487
1516
  if (version === undefined) {
1488
1517
  const fund = await this.skillFinden(args.skill, args.quelle);
1489
- return await this.linkAusFund(harness, args.ziel, fund, args.ersetzen, args.lokal);
1518
+ const ergebnis = await this.linkAusFund(harness, args.ziel, fund, args.ersetzen, args.lokal);
1519
+ return Anwendungsfaelle.hinweisAnhaengen(ergebnis, await this.bundleMitgliedZurueckholen(args.ziel, fund));
1490
1520
  }
1491
1521
  if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
1492
1522
  throw new EinschraenkungOhneZeiger(args.ziel, version);
@@ -1500,7 +1530,57 @@ export class Anwendungsfaelle {
1500
1530
  const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
1501
1531
  await this.zeigerErgaenzen(zeigerPfad, fund, version);
1502
1532
  const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
1503
- return hinweis === undefined ? link : { ...link, hinweis };
1533
+ return Anwendungsfaelle.hinweisAnhaengen(hinweis === undefined ? link : { ...link, hinweis }, await this.bundleMitgliedZurueckholen(args.ziel, fund));
1534
+ }
1535
+ /** Weitere Hinweiszeilen an ein Link-Ergebnis hängen, ohne einen vorhandenen Hinweis zu verdrängen. */
1536
+ static hinweisAnhaengen(ergebnis, zeilen) {
1537
+ if (zeilen.length === 0)
1538
+ return ergebnis;
1539
+ const alle = ergebnis.hinweis === undefined ? zeilen : [ergebnis.hinweis, ...zeilen];
1540
+ return { ...ergebnis, hinweis: alle.join('\n') };
1541
+ }
1542
+ /**
1543
+ * `link` eines herausgenommenen Bundle-Mitglieds (E114 L6): Ist es derselbe Skill aus derselben
1544
+ * Quelle, wie ihn die Definition nennt (Quelle und Pfad, dieselbe Probe wie die Übernahme in
1545
+ * F67), gehört er wieder zum Bundle. Die Definition wird nur gelesen, wenn der Name in diesem
1546
+ * Ziel herausgenommen ist; lässt sie sich nicht lesen, bleibt es beim Einzel-Link. Gibt je
1547
+ * zurückgeholtem Bundle eine Hinweiszeile zurück.
1548
+ */
1549
+ async bundleMitgliedZurueckholen(ziel, fund) {
1550
+ const name = fund.skill.name;
1551
+ const kandidaten = Anwendungsfaelle.installationenAn(await this.p.zustand.lesen(), ziel).filter((i) => (i.herausgenommen ?? []).includes(name));
1552
+ if (kandidaten.length === 0)
1553
+ return [];
1554
+ const passend = [];
1555
+ for (const installation of kandidaten) {
1556
+ try {
1557
+ const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
1558
+ const gleich = bundle.mitglieder.some((m) => quelleSchluessel(m.quelle) === quelleSchluessel(fund.quelle) && m.pfad === fund.skill.pfad);
1559
+ if (gleich)
1560
+ passend.push(bundleSchluessel(bundle));
1561
+ }
1562
+ catch {
1563
+ // Definition nicht lesbar: der Link bleibt ein Einzel-Link, das Mitglied herausgenommen.
1564
+ }
1565
+ }
1566
+ if (passend.length === 0)
1567
+ return [];
1568
+ const zustand = await this.p.zustand.lesen();
1569
+ const zeilen = [];
1570
+ for (const installation of Anwendungsfaelle.installationenAn(zustand, ziel)) {
1571
+ if (!passend.includes(bundleSchluessel({ name: installation.bundle, quelle: installation.quelle })))
1572
+ continue;
1573
+ const rest = (installation.herausgenommen ?? []).filter((n) => n !== name);
1574
+ if (rest.length === 0)
1575
+ delete installation.herausgenommen;
1576
+ else
1577
+ installation.herausgenommen = rest;
1578
+ if (!installation.skills.includes(name))
1579
+ installation.skills.push(name);
1580
+ zeilen.push(`„${name}" gehört wieder zum Bundle „${installation.bundle}".`);
1581
+ }
1582
+ await this.p.zustand.schreiben(zustand);
1583
+ return zeilen;
1504
1584
  }
1505
1585
  /** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
1506
1586
  async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
@@ -1553,9 +1633,9 @@ export class Anwendungsfaelle {
1553
1633
  * Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
1554
1634
  * einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
1555
1635
  *
1556
- * Gehörte der Link zu einem Bundle, zerbricht es in diesem Ziel (E69): die übrigen Links bleiben
1557
- * als Einzel-Installationen liegen, der Bundle-Verweis fällt weg. `zerbrochen` nennt die Bundles,
1558
- * damit die Oberfläche es sagen kann.
1636
+ * Gehörte der Link zu einem Bundle, bleibt das Bundle im Ziel und merkt sich den Skill als
1637
+ * herausgenommen (E114 L2). `herausgenommenAus` nennt die Bundles, damit die Oberfläche es sagen
1638
+ * kann.
1559
1639
  */
1560
1640
  async linkEntfernen(args) {
1561
1641
  const harness = await this.harnessGeprueft(args.ziel.harness);
@@ -1563,25 +1643,25 @@ export class Anwendungsfaelle {
1563
1643
  await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
1564
1644
  await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
1565
1645
  const zustand = await this.p.zustand.lesen();
1566
- const zerbrochen = Anwendungsfaelle.bundlesZerbrechen(zustand, args.ziel, [args.skill]);
1567
- if (zerbrochen.length > 0)
1646
+ const herausgenommenAus = Anwendungsfaelle.bundleMitgliederHerausnehmen(zustand, args.ziel, [args.skill]);
1647
+ if (herausgenommenAus.length > 0)
1568
1648
  await this.p.zustand.schreiben(zustand);
1569
1649
  if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
1570
1650
  const zeigerPfad = await this.bindungFinden(args.ziel);
1571
1651
  if (zeigerPfad !== undefined)
1572
- return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).', zerbrochen };
1573
- return { zerbrochen };
1652
+ return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).', herausgenommenAus };
1653
+ return { herausgenommenAus };
1574
1654
  }
1575
1655
  if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
1576
- return { zerbrochen };
1656
+ return { herausgenommenAus };
1577
1657
  const zeigerPfad = await this.bindungFinden(args.ziel);
1578
1658
  if (zeigerPfad === undefined)
1579
- return { zerbrochen };
1659
+ return { herausgenommenAus };
1580
1660
  const zeiger = await this.p.zeiger.lesen(zeigerPfad);
1581
1661
  if (!zeiger.skills.delete(args.skill))
1582
- return { zerbrochen };
1662
+ return { herausgenommenAus };
1583
1663
  await this.p.zeiger.schreiben(zeigerPfad, zeiger);
1584
- return { zerbrochen };
1664
+ return { herausgenommenAus };
1585
1665
  }
1586
1666
  /**
1587
1667
  * Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
@@ -2580,14 +2660,20 @@ export class Anwendungsfaelle {
2580
2660
  for (const ei of eintraege)
2581
2661
  if (ei.art === 'fremd')
2582
2662
  uebernahme.set(ei.name, await this.erkennenFremd(harness, verzeichnis, ei, bekannt, true));
2583
- // Bundles dieses Ziels (E69), rein aus dem Zustand: nur die Mitglieder, die dort auch
2584
- // wirklich noch liegen — ein von Hand gelöschter Link soll die Gruppe nicht länger führen.
2663
+ // Bundles dieses Ziels (E69), rein aus dem Zustand: verlinkt sind die Mitglieder, die dort
2664
+ // auch wirklich noch liegen; herausgenommene und von Hand gelöschte stehen als nicht
2665
+ // verlinkt daneben (E114 L3), die Gruppe zeigt alle.
2585
2666
  const eigeneNamen = new Set(eintraege.filter((ei) => ei.art !== 'fremd').map((ei) => (ei.art === 'link' ? ei.link.name : ei.name)));
2586
- const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) => ({
2587
- name: i.bundle,
2588
- quelle: i.quelle,
2589
- skills: i.skills.filter((name) => eigeneNamen.has(name)),
2590
- }));
2667
+ const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) => {
2668
+ const herausgenommen = i.herausgenommen ?? [];
2669
+ return {
2670
+ name: i.bundle,
2671
+ quelle: i.quelle,
2672
+ skills: i.skills.filter((name) => eigeneNamen.has(name)),
2673
+ nichtVerlinkt: [...i.skills.filter((name) => !eigeneNamen.has(name)), ...herausgenommen],
2674
+ herausgenommen,
2675
+ };
2676
+ });
2591
2677
  ergebnis.push({
2592
2678
  ziel,
2593
2679
  verzeichnis,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",