@opengeni/db 5.0.1-canary.2 → 5.0.1-canary.3

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.
@@ -36,4 +36,4 @@ export declare function assertCapabilityComponentVersionCanChange(db: Database,
36
36
  * have claimed a component while its installation is pending or repairing,
37
37
  * even though that owner is intentionally hidden from runtime reads.
38
38
  */
39
- export declare function cleanupOrphanedCapabilityComponents(db: Database, workspaceId: string, facetInstallationIds: string[], skillActor?: SkillActor): Promise<SkillSourceReleaseReceipt[]>;
39
+ export declare function cleanupOrphanedCapabilityComponents(db: Database, workspaceId: string, facetInstallationIds: string[], skillActor?: SkillActor, lockedSkillHeadIds?: readonly string[]): Promise<SkillSourceReleaseReceipt[]>;
package/dist/index.d.ts CHANGED
@@ -10532,7 +10532,7 @@ export { buildHostConnectionTokenResolver, buildHostGatewayConnectionTokenResolv
10532
10532
  export { CapabilityComponentVersionConflictError, cleanupOrphanedCapabilityComponents, effectiveCapabilityOwnerSql, type CapabilityComponentOwnerIdentity, } from "./capability-components.js";
10533
10533
  export { adoptPackComponentReferences, finalizePackComponentOwnership, listPackInstallationComponents, PackComponentResolutionError, previewPackComponentRelease, recordPackInlineSkillComponent, releasePackComponents, resolvePackComponentReferences, resolvePackInlineSkillReferences, type PackInlineSkillRequirement, type StoredPackInstallationComponent, } from "./pack-components.js";
10534
10534
  export { deferPackInstallationOperation, finalizePackInstallationOperation, finalizePackUninstallOperation, PackManifestChangedError, PackOperationClaimLostError, PackOperationInProgressError, PackInstallationVersionConflictError, PackInstallationVersionRequiredError, PackOperationIdempotencyError, preparePackInstallationOperation, preparePackUninstallOperation, touchPackInstallationOperation, type PreparedPackInstallation, } from "./pack-installations.js";
10535
- export { checkpointPluginPackageOperation, deferPluginPackageOperation, finalizePluginPackageInstall, getInstalledPluginPackage, getPluginPackageUninstallPreview, installPluginMcpReference, listInstalledPluginPackages, PluginInstallationVersionConflictError, PluginInstallationVersionRequiredError, PluginOperationIdempotencyError, preparePluginPackageInstall, uninstallPluginPackage, type InstalledPluginPackage, type InstalledPluginPackageSummary, type PluginBomComponent, type PreparedPluginPackage, } from "./plugin-packages.js";
10535
+ export { checkpointPluginPackageOperation, deferPluginPackageOperation, finalizePluginPackageInstall, getInstalledPluginPackage, getPluginPackageUninstallPreview, PluginUninstallPreviewChangedError, installPluginMcpReference, listInstalledPluginPackages, PluginInstallationVersionConflictError, PluginInstallationVersionRequiredError, PluginOperationIdempotencyError, preparePluginPackageInstall, uninstallPluginPackage, type InstalledPluginPackage, type InstalledPluginPackageSummary, type PluginBomComponent, type PreparedPluginPackage, } from "./plugin-packages.js";
10536
10536
  export * from "./workspace-artifacts.js";
10537
10537
  export * from "./mcp-oauth.js";
10538
10538
  export * from "./tool-gateway-approvals.js";
package/dist/index.js CHANGED
@@ -3798,7 +3798,40 @@ async function getPreferenceRegistryFullContent(db, claims, handle) {
3798
3798
  });
3799
3799
  }
3800
3800
 
3801
+ // src/skill-source-release-impact.ts
3802
+ function classifySkillSourceRelease(head, workspaceId) {
3803
+ if (head.status !== "active" || !head.active_revision_id)
3804
+ return { disposition: "inactive", retentionReasons: [] };
3805
+ const retentionReasons = [];
3806
+ if (head.provenance_source !== "portable_skill") retentionReasons.push("customized");
3807
+ if (head.scope !== "workspace" || head.scope_workspace_id !== workspaceId)
3808
+ retentionReasons.push("re_scoped");
3809
+ return {
3810
+ disposition: retentionReasons.length ? "retained" : "removed",
3811
+ retentionReasons
3812
+ };
3813
+ }
3814
+
3801
3815
  // src/skill-source-release.ts
3816
+ async function readSkillSourceReleaseHeads(db, workspaceId, facetInstallationIds) {
3817
+ if (!facetInstallationIds.length) return [];
3818
+ return rawRows(
3819
+ db,
3820
+ sql12`
3821
+ SELECT DISTINCT h.id,h.account_id,h.scope,h.scope_workspace_id,h.scope_version,
3822
+ h.status,h.active_revision_id,r.provenance_source,r.title
3823
+ FROM skill_source_bindings b
3824
+ JOIN capability_facet_installations fi ON fi.facet_id=b.skill_facet_id
3825
+ AND fi.account_id=b.account_id AND fi.workspace_id=b.workspace_id
3826
+ JOIN preference_registry_preferences h ON h.id=b.preference_id AND h.account_id=b.account_id
3827
+ LEFT JOIN preference_registry_revisions r ON r.id=h.active_revision_id
3828
+ AND r.preference_id=h.id AND r.account_id=h.account_id
3829
+ WHERE b.workspace_id=${workspaceId}::uuid
3830
+ AND fi.id=ANY(string_to_array(${facetInstallationIds.join(",")},',')::uuid[])
3831
+ ORDER BY h.id
3832
+ `
3833
+ );
3834
+ }
3802
3835
  var SkillSourceRemovalAuthorityError = class extends Error {
3803
3836
  code = "skill_source_removal_requires_human";
3804
3837
  constructor() {
@@ -3808,6 +3841,12 @@ var SkillSourceRemovalAuthorityError = class extends Error {
3808
3841
  this.name = "SkillSourceRemovalAuthorityError";
3809
3842
  }
3810
3843
  };
3844
+ var SkillSourceReleaseSnapshotChangedError = class extends Error {
3845
+ constructor() {
3846
+ super("Skill source visibility changed during removal; review a refreshed preview.");
3847
+ this.name = "SkillSourceReleaseSnapshotChangedError";
3848
+ }
3849
+ };
3811
3850
  async function releaseOrphanedSkillHeads(db, input) {
3812
3851
  if (!input.facetInstallationIds.length) return [];
3813
3852
  const bindings = await rawRows(
@@ -3827,24 +3866,30 @@ async function releaseOrphanedSkillHeads(db, input) {
3827
3866
  throw new SkillSourceRemovalAuthorityError();
3828
3867
  }
3829
3868
  return withWorkspaceSubjectRls(db, input.workspaceId, actor.subjectId, async (tx) => {
3830
- for (const binding of bindings) {
3869
+ const visibleHeads = await readSkillSourceReleaseHeads(
3870
+ tx,
3871
+ input.workspaceId,
3872
+ input.facetInstallationIds
3873
+ );
3874
+ const lockedHeadIds = new Set(input.lockedHeadIds ?? visibleHeads.map((head) => head.id));
3875
+ if (visibleHeads.some((head) => !lockedHeadIds.has(head.id)))
3876
+ throw new SkillSourceReleaseSnapshotChangedError();
3877
+ for (const binding of input.lockedHeadIds === void 0 ? visibleHeads : []) {
3831
3878
  await tx.execute(
3832
3879
  sql12`SELECT preference_id FROM preference_registry_lock_heads(ARRAY[${binding.id}::uuid])`
3833
3880
  );
3834
3881
  }
3835
- const rows = await rawRows(
3882
+ const rows = await readSkillSourceReleaseHeads(
3836
3883
  tx,
3837
- sql12`
3838
- SELECT h.id,h.account_id,h.scope,h.scope_workspace_id,h.scope_version,h.status,h.active_revision_id,r.provenance_source
3839
- FROM preference_registry_preferences h LEFT JOIN preference_registry_revisions r
3840
- ON r.id=h.active_revision_id AND r.preference_id=h.id AND r.account_id=h.account_id
3841
- WHERE h.id=ANY(string_to_array(${bindings.map((binding) => binding.id).join(",")},',')::uuid[])
3842
- ORDER BY h.id
3843
- `
3884
+ input.workspaceId,
3885
+ input.facetInstallationIds
3844
3886
  );
3887
+ if (rows.some((head) => !lockedHeadIds.has(head.id)))
3888
+ throw new SkillSourceReleaseSnapshotChangedError();
3845
3889
  const receipts = [];
3846
3890
  for (const head of rows) {
3847
- if (head.status !== "active" || !head.active_revision_id) {
3891
+ const impact = classifySkillSourceRelease(head, input.workspaceId);
3892
+ if (impact.disposition === "inactive") {
3848
3893
  receipts.push({
3849
3894
  skillId: head.id,
3850
3895
  revisionId: head.active_revision_id,
@@ -3852,7 +3897,7 @@ async function releaseOrphanedSkillHeads(db, input) {
3852
3897
  eventId: null,
3853
3898
  warning: null
3854
3899
  });
3855
- } else if (head.provenance_source !== "portable_skill" || head.scope !== "workspace" || head.scope_workspace_id !== input.workspaceId) {
3900
+ } else if (impact.disposition === "retained") {
3856
3901
  receipts.push({
3857
3902
  skillId: head.id,
3858
3903
  revisionId: head.active_revision_id,
@@ -10179,7 +10224,7 @@ async function assertCapabilityComponentVersionCanChange(db, input) {
10179
10224
  throw new CapabilityComponentVersionConflictError(input.pluginKey);
10180
10225
  }
10181
10226
  }
10182
- async function cleanupOrphanedCapabilityComponents(db, workspaceId, facetInstallationIds, skillActor) {
10227
+ async function cleanupOrphanedCapabilityComponents(db, workspaceId, facetInstallationIds, skillActor, lockedSkillHeadIds) {
10183
10228
  const uniqueIds = [...new Set(facetInstallationIds)];
10184
10229
  if (uniqueIds.length === 0) return [];
10185
10230
  await db.execute(sql26`SELECT p.id FROM capability_plugin_installations p
@@ -10204,7 +10249,8 @@ async function cleanupOrphanedCapabilityComponents(db, workspaceId, facetInstall
10204
10249
  const skillReleases = await releaseOrphanedSkillHeads(db, {
10205
10250
  workspaceId,
10206
10251
  facetInstallationIds: orphanRows.map((row) => row.facetInstallationId),
10207
- ...skillActor ? { skillActor } : {}
10252
+ ...skillActor ? { skillActor } : {},
10253
+ ...lockedSkillHeadIds !== void 0 ? { lockedHeadIds: lockedSkillHeadIds } : {}
10208
10254
  });
10209
10255
  await db.delete(capabilityFacetInstallations).where(
10210
10256
  inArray5(
@@ -33932,7 +33978,9 @@ function mapPackInstallation(row) {
33932
33978
 
33933
33979
  // src/plugin-packages.ts
33934
33980
  import { createHash as createHash27 } from "crypto";
33935
- import { stableJson as stableJson14 } from "@opengeni/contracts";
33981
+ import {
33982
+ stableJson as stableJson14
33983
+ } from "@opengeni/contracts";
33936
33984
  import { and as and40, asc as asc18, eq as eq41, inArray as inArray23, ne as ne6, or as or9, sql as sql66 } from "drizzle-orm";
33937
33985
  var PluginOperationIdempotencyError = class extends Error {
33938
33986
  name = "PluginOperationIdempotencyError";
@@ -33940,6 +33988,14 @@ var PluginOperationIdempotencyError = class extends Error {
33940
33988
  var PluginInstallationVersionConflictError = class extends Error {
33941
33989
  name = "PluginInstallationVersionConflictError";
33942
33990
  };
33991
+ var PluginUninstallPreviewChangedError = class extends Error {
33992
+ constructor(preview) {
33993
+ super("Plugin removal impact changed. Review the refreshed preview before confirming again.");
33994
+ this.preview = preview;
33995
+ this.name = "PluginUninstallPreviewChangedError";
33996
+ }
33997
+ code = "plugin_uninstall_preview_changed";
33998
+ };
33943
33999
  var PluginInstallationVersionRequiredError = class extends Error {
33944
34000
  name = "PluginInstallationVersionRequiredError";
33945
34001
  };
@@ -34533,22 +34589,20 @@ async function installPluginMcpReferenceInScope(db, input) {
34533
34589
  }
34534
34590
  );
34535
34591
  }
34536
- async function getPluginPackageUninstallPreview(db, workspaceId, pluginKey) {
34592
+ async function getPluginPackageUninstallPreview(db, workspaceId, pluginKey, subjectId) {
34537
34593
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
34594
+ if (subjectId) await setSubjectRlsContext(scopedDb, subjectId);
34538
34595
  const plugin = await pluginPackageInScope(scopedDb, workspaceId, pluginKey);
34539
34596
  if (!plugin || plugin.status === "disabled") {
34540
34597
  return { installed: false, version: null, installationVersion: null, components: [] };
34541
34598
  }
34542
- const components = await pluginOwnedComponents(
34543
- scopedDb,
34544
- workspaceId,
34545
- plugin.pluginInstallationId
34546
- );
34599
+ const impact = await pluginOwnedComponents(scopedDb, workspaceId, plugin.pluginInstallationId);
34547
34600
  return {
34548
34601
  installed: true,
34549
34602
  version: plugin.version,
34550
34603
  installationVersion: plugin.installationVersion,
34551
- components
34604
+ components: impact.components,
34605
+ previewToken: pluginUninstallPreviewToken(plugin, impact.stateToken)
34552
34606
  };
34553
34607
  });
34554
34608
  }
@@ -34556,7 +34610,8 @@ async function uninstallPluginPackage(db, input) {
34556
34610
  const requestDigest4 = sha2567(
34557
34611
  stableJson14({
34558
34612
  pluginKey: input.pluginKey,
34559
- expectedInstallationVersion: input.expectedInstallationVersion
34613
+ expectedInstallationVersion: input.expectedInstallationVersion,
34614
+ ...input.expectedPreviewToken ? { expectedPreviewToken: input.expectedPreviewToken } : {}
34560
34615
  })
34561
34616
  );
34562
34617
  return await withRlsContext(
@@ -34566,6 +34621,7 @@ async function uninstallPluginPackage(db, input) {
34566
34621
  await setSubjectRlsContext(scopedDb, input.subjectId);
34567
34622
  return await scopedDb.transaction(async (txRaw) => {
34568
34623
  const tx = txRaw;
34624
+ await lockSkillPublication(tx, input.workspaceId);
34569
34625
  await tx.execute(
34570
34626
  sql66`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`
34571
34627
  );
@@ -34603,11 +34659,69 @@ async function uninstallPluginPackage(db, input) {
34603
34659
  if (plugin.installationVersion !== input.expectedInstallationVersion) {
34604
34660
  throw new PluginInstallationVersionConflictError("Plugin installation changed");
34605
34661
  }
34606
- const components = await pluginOwnedComponents(
34662
+ await tx.execute(sql66`SELECT p.id FROM capability_plugin_installations p
34663
+ WHERE p.workspace_id=${input.workspaceId}::uuid AND p.id IN (
34664
+ SELECT fi.plugin_installation_id FROM capability_facet_installations fi
34665
+ JOIN capability_component_owners o ON o.facet_installation_id=fi.id
34666
+ WHERE o.workspace_id=${input.workspaceId}::uuid AND o.owner_kind='plugin'
34667
+ AND o.owner_id=${plugin.pluginInstallationId}
34668
+ ) ORDER BY p.id FOR UPDATE`);
34669
+ try {
34670
+ await tx.execute(sql66`SELECT fi.id FROM capability_facet_installations fi
34671
+ WHERE fi.workspace_id=${input.workspaceId}::uuid AND EXISTS (
34672
+ SELECT 1 FROM capability_component_owners own
34673
+ WHERE own.facet_installation_id=fi.id AND own.owner_kind='plugin'
34674
+ AND own.owner_id=${plugin.pluginInstallationId}
34675
+ ) ORDER BY fi.id FOR UPDATE NOWAIT`);
34676
+ await tx.execute(sql66`SELECT o.id FROM capability_component_owners o
34677
+ WHERE o.workspace_id=${input.workspaceId}::uuid AND EXISTS (
34678
+ SELECT 1 FROM capability_component_owners own
34679
+ WHERE own.facet_installation_id=o.facet_installation_id
34680
+ AND own.owner_kind='plugin' AND own.owner_id=${plugin.pluginInstallationId}
34681
+ ) ORDER BY o.id FOR UPDATE NOWAIT`);
34682
+ } catch (error) {
34683
+ if (nestedPostgresSqlState(error) === "55P03")
34684
+ throw new PluginUninstallPreviewChangedError();
34685
+ throw error;
34686
+ }
34687
+ const skillBindings = await rawRows(
34688
+ tx,
34689
+ sql66`
34690
+ SELECT DISTINCT h.id FROM skill_source_bindings b
34691
+ JOIN capability_facet_installations fi ON fi.facet_id=b.skill_facet_id
34692
+ AND fi.account_id=b.account_id AND fi.workspace_id=b.workspace_id
34693
+ JOIN capability_component_owners o ON o.facet_installation_id=fi.id
34694
+ JOIN preference_registry_preferences h ON h.id=b.preference_id AND h.account_id=b.account_id
34695
+ WHERE b.workspace_id=${input.workspaceId}::uuid AND o.owner_kind='plugin'
34696
+ AND o.owner_id=${plugin.pluginInstallationId} ORDER BY h.id`
34697
+ );
34698
+ for (const head of skillBindings) {
34699
+ try {
34700
+ await tx.execute(
34701
+ sql66`SELECT preference_id FROM preference_registry_lock_heads(ARRAY[${head.id}::uuid])`
34702
+ );
34703
+ } catch (error) {
34704
+ if (nestedPostgresSqlState(error) === "42501")
34705
+ throw new PluginUninstallPreviewChangedError();
34706
+ throw error;
34707
+ }
34708
+ }
34709
+ const impact = await pluginOwnedComponents(
34607
34710
  tx,
34608
34711
  input.workspaceId,
34609
34712
  plugin.pluginInstallationId
34610
34713
  );
34714
+ const previewToken = pluginUninstallPreviewToken(plugin, impact.stateToken);
34715
+ if (input.expectedPreviewToken && input.expectedPreviewToken !== previewToken) {
34716
+ throw new PluginUninstallPreviewChangedError({
34717
+ pluginKey: input.pluginKey,
34718
+ installed: true,
34719
+ version: plugin.version,
34720
+ installationVersion: plugin.installationVersion,
34721
+ previewToken,
34722
+ components: impact.components
34723
+ });
34724
+ }
34611
34725
  await removeIntegrationFacetBindingOwnersForOwner(tx, {
34612
34726
  workspaceId: input.workspaceId,
34613
34727
  owner: { kind: "plugin", id: plugin.pluginInstallationId }
@@ -34619,18 +34733,26 @@ async function uninstallPluginPackage(db, input) {
34619
34733
  eq41(capabilityComponentOwners.ownerId, plugin.pluginInstallationId)
34620
34734
  )
34621
34735
  ).returning({ facetInstallationId: capabilityComponentOwners.facetInstallationId });
34622
- const skillReleases = await cleanupOrphanedCapabilityComponents(
34623
- tx,
34624
- input.workspaceId,
34625
- owned.map((row) => row.facetInstallationId),
34626
- input.skillActor
34627
- );
34736
+ let skillReleases;
34737
+ try {
34738
+ skillReleases = await cleanupOrphanedCapabilityComponents(
34739
+ tx,
34740
+ input.workspaceId,
34741
+ owned.map((row) => row.facetInstallationId),
34742
+ input.skillActor,
34743
+ skillBindings.map((head) => head.id)
34744
+ );
34745
+ } catch (error) {
34746
+ if (error instanceof SkillSourceReleaseSnapshotChangedError)
34747
+ throw new PluginUninstallPreviewChangedError();
34748
+ throw error;
34749
+ }
34628
34750
  await tx.update(capabilityPluginInstallations).set({
34629
34751
  status: "disabled",
34630
34752
  version: plugin.installationVersion + 1,
34631
34753
  updatedAt: /* @__PURE__ */ new Date()
34632
34754
  }).where(eq41(capabilityPluginInstallations.id, plugin.pluginInstallationId));
34633
- const retainedComponents = components.filter((component) => component.retainedByOtherOwners).map((component) => component.capabilityId);
34755
+ const retainedComponents = impact.components.filter((component) => component.disposition === "retained").map((component) => component.capabilityId);
34634
34756
  await completeInlineOperation(tx, input, requestDigest4, {
34635
34757
  status: "uninstalled",
34636
34758
  retainedComponents,
@@ -34676,6 +34798,7 @@ async function pluginOwnedComponents(db, workspaceId, ownerPluginInstallationId)
34676
34798
  facetInstallationId: capabilityFacetInstallations.id,
34677
34799
  facetKind: capabilityFacets.kind,
34678
34800
  skillCapabilityId: capabilitySkillFacets.capabilityId,
34801
+ name: capabilityPlugins.name,
34679
34802
  manifest: capabilityPluginVersions.manifest
34680
34803
  }).from(capabilityComponentOwners).innerJoin(
34681
34804
  capabilityFacetInstallations,
@@ -34695,6 +34818,9 @@ async function pluginOwnedComponents(db, workspaceId, ownerPluginInstallationId)
34695
34818
  ).innerJoin(
34696
34819
  capabilityPluginVersions,
34697
34820
  eq41(capabilityPluginVersions.id, capabilityPluginInstallations.pluginVersionId)
34821
+ ).innerJoin(
34822
+ capabilityPlugins,
34823
+ eq41(capabilityPlugins.id, capabilityPluginInstallations.pluginId)
34698
34824
  ).leftJoin(
34699
34825
  capabilitySkillFacets,
34700
34826
  eq41(capabilitySkillFacets.facetId, capabilityFacets.id)
@@ -34713,12 +34839,23 @@ async function pluginOwnedComponents(db, workspaceId, ownerPluginInstallationId)
34713
34839
  if (!capabilityId || !kind) continue;
34714
34840
  const existing = components.get(capabilityId);
34715
34841
  if (existing) existing.facetIds.push(row.facetInstallationId);
34716
- else components.set(capabilityId, { capabilityId, kind, facetIds: [row.facetInstallationId] });
34842
+ else
34843
+ components.set(capabilityId, {
34844
+ capabilityId,
34845
+ kind,
34846
+ name: row.name,
34847
+ facetIds: [row.facetInstallationId]
34848
+ });
34717
34849
  }
34718
34850
  const result = [];
34851
+ const state = [];
34719
34852
  for (const component of components.values()) {
34720
- const [ownerCount] = await db.select({ count: sql66`count(*)::int` }).from(capabilityComponentOwners).where(
34853
+ const owners = await db.select({
34854
+ kind: capabilityComponentOwners.ownerKind,
34855
+ id: capabilityComponentOwners.ownerId
34856
+ }).from(capabilityComponentOwners).where(
34721
34857
  and40(
34858
+ eq41(capabilityComponentOwners.workspaceId, workspaceId),
34722
34859
  inArray23(capabilityComponentOwners.facetInstallationId, component.facetIds),
34723
34860
  // Retention is deliberately physical, not runtime-effective. A Plugin
34724
34861
  // in needs_attention may be resumable and must keep shared component
@@ -34731,13 +34868,76 @@ async function pluginOwnedComponents(db, workspaceId, ownerPluginInstallationId)
34731
34868
  )
34732
34869
  )
34733
34870
  );
34871
+ const remainingOwners = [];
34872
+ for (const owner of owners.sort(
34873
+ (a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`)
34874
+ )) {
34875
+ let name = owner.kind === "direct" ? "Direct installation" : owner.kind === "migration" ? "Migrated installation" : "Another installed owner";
34876
+ if (owner.kind === "plugin") {
34877
+ const [named] = await db.select({ manifest: capabilityPluginVersions.manifest }).from(capabilityPluginInstallations).innerJoin(
34878
+ capabilityPluginVersions,
34879
+ eq41(
34880
+ capabilityPluginVersions.id,
34881
+ capabilityPluginInstallations.pluginVersionId
34882
+ )
34883
+ ).where(
34884
+ and40(
34885
+ eq41(capabilityPluginInstallations.workspaceId, workspaceId),
34886
+ sql66`${capabilityPluginInstallations.id}::text = ${owner.id}`
34887
+ )
34888
+ );
34889
+ name = stringValue5(objectValue3(named?.manifest).name) ?? name;
34890
+ } else if (owner.kind === "pack") {
34891
+ const [named] = await db.select({
34892
+ manifest: packInstallations.manifestSnapshot,
34893
+ packId: packInstallations.packId
34894
+ }).from(packInstallations).where(
34895
+ and40(
34896
+ eq41(packInstallations.workspaceId, workspaceId),
34897
+ or9(
34898
+ sql66`${packInstallations.id}::text = ${owner.id}`,
34899
+ eq41(packInstallations.packId, owner.id)
34900
+ )
34901
+ )
34902
+ );
34903
+ name = stringValue5(objectValue3(named?.manifest).name) ?? named?.packId ?? name;
34904
+ }
34905
+ if (!remainingOwners.some((item) => item.kind === owner.kind && item.name === name))
34906
+ remainingOwners.push({
34907
+ kind: owner.kind,
34908
+ name
34909
+ });
34910
+ }
34911
+ const heads = component.kind === "skill" ? await readSkillSourceReleaseHeads(db, workspaceId, component.facetIds) : [];
34912
+ const head = heads[0];
34913
+ const skillImpact = head ? classifySkillSourceRelease(head, workspaceId) : null;
34914
+ const retainedByOtherOwners = owners.length > 0;
34915
+ const retentionReasons = [];
34916
+ if (retainedByOtherOwners) retentionReasons.push("other_owners");
34917
+ if (skillImpact) retentionReasons.push(...skillImpact.retentionReasons);
34918
+ else if (component.kind === "skill") retentionReasons.push("registry_unavailable");
34919
+ state.push({ facetIds: component.facetIds, owners, heads });
34734
34920
  result.push({
34735
34921
  capabilityId: component.capabilityId,
34736
34922
  kind: component.kind,
34737
- retainedByOtherOwners: (ownerCount?.count ?? 0) > 0
34923
+ name: head?.title ?? component.name,
34924
+ retainedByOtherOwners,
34925
+ disposition: retainedByOtherOwners ? "retained" : skillImpact?.disposition ?? (component.kind === "skill" ? "retained" : "removed"),
34926
+ retentionReasons,
34927
+ remainingOwners,
34928
+ ...head ? { skillId: head.id } : {}
34738
34929
  });
34739
34930
  }
34740
- return result;
34931
+ return { components: result, stateToken: sha2567(stableJson14({ state, components: result })) };
34932
+ }
34933
+ function pluginUninstallPreviewToken(plugin, stateToken) {
34934
+ return sha2567(
34935
+ stableJson14({
34936
+ installationId: plugin.pluginInstallationId,
34937
+ version: plugin.installationVersion,
34938
+ stateToken
34939
+ })
34940
+ );
34741
34941
  }
34742
34942
  async function completeInlineOperation(db, input, requestDigest4, result) {
34743
34943
  await db.insert(capabilityOperations).values({
@@ -94043,6 +94243,7 @@ export {
94043
94243
  PluginInstallationVersionConflictError,
94044
94244
  PluginInstallationVersionRequiredError,
94045
94245
  PluginOperationIdempotencyError,
94246
+ PluginUninstallPreviewChangedError,
94046
94247
  PortableSkillInstallationVersionConflictError,
94047
94248
  PortableSkillInstallationVersionRequiredError,
94048
94249
  PortableSkillSourcePathConflictError,