mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-06 13:55:39 -04:00
fix(catalog): project aliases from match commands
This commit is contained in:
5 files changed
+156
-89
No files matched your search
@@ -748,6 +748,14 @@ public sealed class PlaylistOrchestrationIntegrationTests(ITestOutputHelper outp
|
||||
Assert.Equal(source.CanonicalRecordingId, selected.CanonicalRecordingId);
|
||||
Assert.Equal(ProviderIdentityVerification.Verified, source.Verification);
|
||||
Assert.Equal(ProviderIdentityVerification.Pinned, selected.Verification);
|
||||
var aliases = await verify.CanonicalCatalogAliases
|
||||
.Where(item => item.CanonicalEntityId == source.CanonicalRecordingId)
|
||||
.ToListAsync();
|
||||
Assert.Equal(2, aliases.Count);
|
||||
Assert.All(aliases, item =>
|
||||
Assert.StartsWith("provider:", item.Namespace, StringComparison.Ordinal));
|
||||
Assert.Contains(aliases, item => item.ExternalId == source.ExternalId);
|
||||
Assert.Contains(aliases, item => item.ExternalId == selected.ExternalId);
|
||||
var review = await _trackMatches.GetReviewDataAsync(
|
||||
new TrackMatchActor(_tenant, _user, false),
|
||||
externalSnapshotId: externalSnapshotId);
|
||||
|
||||
@@ -78,6 +78,78 @@ public static class CanonicalCatalogKeys
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CanonicalCatalogIdentityProjection
|
||||
{
|
||||
public static async Task ProjectRecordingSignalsAsync(
|
||||
AllstarrDbContext db,
|
||||
ProviderActorContext actor,
|
||||
CanonicalRecordingRecord recording,
|
||||
DateTimeOffset observedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CanonicalCatalogAliasInput[] aliases =
|
||||
[
|
||||
.. recording.Isrc == null
|
||||
? []
|
||||
: new[] { new CanonicalCatalogAliasInput("isrc", recording.Isrc) },
|
||||
.. recording.MusicBrainzRecordingId == null
|
||||
? []
|
||||
: new[] { new CanonicalCatalogAliasInput("musicbrainz", recording.MusicBrainzRecordingId) }
|
||||
];
|
||||
if (aliases.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await CanonicalCatalogEvidenceStore.RecordInContextAsync(
|
||||
db,
|
||||
actor,
|
||||
new CanonicalCatalogEntityReference(CanonicalCatalogEntityKind.Recording, recording.Id),
|
||||
new CanonicalCatalogSourceStamp(
|
||||
"canonical-signal",
|
||||
null,
|
||||
CanonicalCatalogKeys.Hash(string.Join(
|
||||
'\n', aliases.Select(alias => $"{alias.Namespace}:{alias.ExternalId}"))),
|
||||
1,
|
||||
observedAt,
|
||||
null),
|
||||
aliases,
|
||||
[],
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public static Task ProjectProviderIdentityAsync(
|
||||
AllstarrDbContext db,
|
||||
ProviderActorContext actor,
|
||||
ProviderTrackIdentityRecord identity,
|
||||
DateTimeOffset observedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var aliasNamespace = CanonicalCatalogKeys.ProviderTrackNamespace(
|
||||
identity.ProviderId,
|
||||
identity.ResourceKind,
|
||||
identity.CatalogNamespace,
|
||||
identity.Scope,
|
||||
identity.ProviderAccountId);
|
||||
return CanonicalCatalogEvidenceStore.RecordInContextAsync(
|
||||
db,
|
||||
actor,
|
||||
new CanonicalCatalogEntityReference(
|
||||
CanonicalCatalogEntityKind.Recording,
|
||||
identity.CanonicalRecordingId),
|
||||
new CanonicalCatalogSourceStamp(
|
||||
"provider-identity",
|
||||
$"{identity.Verification}:{identity.DecisionVersion}",
|
||||
CanonicalCatalogKeys.Hash($"{aliasNamespace}\n{identity.ExternalId}"),
|
||||
1,
|
||||
observedAt,
|
||||
null),
|
||||
[new CanonicalCatalogAliasInput(aliasNamespace, identity.ExternalId)],
|
||||
[],
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICanonicalCatalogEvidenceStore
|
||||
{
|
||||
Task<CanonicalCatalogEvidenceResult> RecordAsync(
|
||||
|
||||
@@ -171,7 +171,7 @@ public sealed class TrackIdentityService : ITrackIdentityService
|
||||
if (existing != null)
|
||||
{
|
||||
EnsureSignalsCompatible(existing, normalizedIsrc, normalizedMusicBrainzId);
|
||||
await ProjectCanonicalSignalAliasesAsync(
|
||||
await CanonicalCatalogIdentityProjection.ProjectRecordingSignalsAsync(
|
||||
context,
|
||||
actor,
|
||||
existing,
|
||||
@@ -207,7 +207,7 @@ public sealed class TrackIdentityService : ITrackIdentityService
|
||||
UpdatedAt = now
|
||||
};
|
||||
context.CanonicalRecordings.Add(record);
|
||||
await ProjectCanonicalSignalAliasesAsync(
|
||||
await CanonicalCatalogIdentityProjection.ProjectRecordingSignalsAsync(
|
||||
context,
|
||||
actor,
|
||||
record,
|
||||
@@ -247,7 +247,7 @@ public sealed class TrackIdentityService : ITrackIdentityService
|
||||
}
|
||||
|
||||
EnsureSignalsCompatible(existing, normalizedIsrc, normalizedMusicBrainzId);
|
||||
await ProjectCanonicalSignalAliasesAsync(
|
||||
await CanonicalCatalogIdentityProjection.ProjectRecordingSignalsAsync(
|
||||
context,
|
||||
actor,
|
||||
existing,
|
||||
@@ -350,7 +350,7 @@ public sealed class TrackIdentityService : ITrackIdentityService
|
||||
UpdatedAt = now
|
||||
};
|
||||
context.ProviderTrackIdentities.Add(link);
|
||||
await ProjectProviderAliasAsync(
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
context,
|
||||
executionContext.Actor,
|
||||
link,
|
||||
@@ -517,7 +517,7 @@ public sealed class TrackIdentityService : ITrackIdentityService
|
||||
bool concurrent = false)
|
||||
{
|
||||
EnsureExactExternalId(existing, request.ExternalId.Value);
|
||||
await ProjectProviderAliasAsync(
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
context,
|
||||
executionContext.Actor,
|
||||
existing,
|
||||
@@ -824,76 +824,6 @@ public sealed class TrackIdentityService : ITrackIdentityService
|
||||
externalId.Catalog ?? DefaultCatalog,
|
||||
CanonicalCatalogKeys.Hash(externalId.Value));
|
||||
|
||||
private static async Task ProjectCanonicalSignalAliasesAsync(
|
||||
AllstarrDbContext context,
|
||||
ProviderActorContext actor,
|
||||
CanonicalRecordingRecord recording,
|
||||
DateTimeOffset observedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CanonicalCatalogAliasInput[] aliases =
|
||||
[
|
||||
.. recording.Isrc == null
|
||||
? []
|
||||
: new[] { new CanonicalCatalogAliasInput("isrc", recording.Isrc) },
|
||||
.. recording.MusicBrainzRecordingId == null
|
||||
? []
|
||||
: new[] { new CanonicalCatalogAliasInput("musicbrainz", recording.MusicBrainzRecordingId) }
|
||||
];
|
||||
if (aliases.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fingerprint = CanonicalCatalogKeys.Hash(string.Join(
|
||||
'\n', aliases.Select(alias => $"{alias.Namespace}:{alias.ExternalId}")));
|
||||
await CanonicalCatalogEvidenceStore.RecordInContextAsync(
|
||||
context,
|
||||
actor,
|
||||
new CanonicalCatalogEntityReference(CanonicalCatalogEntityKind.Recording, recording.Id),
|
||||
new CanonicalCatalogSourceStamp(
|
||||
"canonical-signal",
|
||||
null,
|
||||
fingerprint,
|
||||
1,
|
||||
observedAt,
|
||||
null),
|
||||
aliases,
|
||||
[],
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task ProjectProviderAliasAsync(
|
||||
AllstarrDbContext context,
|
||||
ProviderActorContext actor,
|
||||
ProviderTrackIdentityRecord identity,
|
||||
DateTimeOffset observedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var aliasNamespace = CanonicalCatalogKeys.ProviderTrackNamespace(
|
||||
identity.ProviderId,
|
||||
identity.ResourceKind,
|
||||
identity.CatalogNamespace,
|
||||
identity.Scope,
|
||||
identity.ProviderAccountId);
|
||||
await CanonicalCatalogEvidenceStore.RecordInContextAsync(
|
||||
context,
|
||||
actor,
|
||||
new CanonicalCatalogEntityReference(
|
||||
CanonicalCatalogEntityKind.Recording,
|
||||
identity.CanonicalRecordingId),
|
||||
new CanonicalCatalogSourceStamp(
|
||||
"provider-identity",
|
||||
$"{identity.Verification}:{identity.DecisionVersion}",
|
||||
CanonicalCatalogKeys.Hash($"{aliasNamespace}\n{identity.ExternalId}"),
|
||||
1,
|
||||
observedAt,
|
||||
null),
|
||||
[new CanonicalCatalogAliasInput(aliasNamespace, identity.ExternalId)],
|
||||
[],
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static void EnsureExactExternalId(
|
||||
ProviderTrackIdentityRecord record,
|
||||
string externalId)
|
||||
|
||||
@@ -1169,6 +1169,14 @@ public sealed class TrackMatchCommandService(
|
||||
identity.UserId == owner.Id)
|
||||
.OrderByDescending(identity => identity.LastSeenAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
var catalogActor = new ProviderActorContext(
|
||||
owner.TenantId,
|
||||
ProviderActorKind.User,
|
||||
owner.Id,
|
||||
new ProviderBackendPrincipal(
|
||||
backend?.BackendType ?? "jellyfin",
|
||||
backend?.BackendInstanceId ?? "source-import",
|
||||
backend?.PrincipalId ?? owner.Id.ToString("N")));
|
||||
|
||||
foreach (var providerGroup in tracks.GroupBy(
|
||||
item => item.ProviderId.Trim().ToLowerInvariant(),
|
||||
@@ -1206,6 +1214,7 @@ public sealed class TrackMatchCommandService(
|
||||
Id = Guid.CreateVersion7(),
|
||||
TenantId = owner.TenantId,
|
||||
CreatedByUserId = owner.Id,
|
||||
IsProvisional = true,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
@@ -1229,9 +1238,14 @@ public sealed class TrackMatchCommandService(
|
||||
};
|
||||
db.CanonicalRecordings.Add(canonical);
|
||||
db.ProviderTrackIdentities.Add(identity);
|
||||
await CanonicalCatalogIdentityProjection.ProjectRecordingSignalsAsync(
|
||||
db, catalogActor, canonical, now, cancellationToken);
|
||||
created++;
|
||||
}
|
||||
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
db, catalogActor, identity, now, cancellationToken);
|
||||
|
||||
var payloadJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
providerId,
|
||||
@@ -1666,6 +1680,7 @@ public sealed class TrackMatchCommandService(
|
||||
return new(false, TrackMatchCommandFailure.NotFound, "Track snapshot was not found");
|
||||
if (!actor.IsAdministrator && snapshot.OwnerUserId != actor.UserId)
|
||||
return new(false, TrackMatchCommandFailure.Forbidden, "Track snapshot is outside your account");
|
||||
var catalogActor = CatalogActor(actor, snapshot);
|
||||
|
||||
var source = snapshot.ProviderTrackIdentityId.HasValue
|
||||
? await db.ProviderTrackIdentities.SingleOrDefaultAsync(
|
||||
@@ -1802,15 +1817,20 @@ public sealed class TrackMatchCommandService(
|
||||
TenantId = actor.TenantId,
|
||||
CreatedByUserId = actor.UserId,
|
||||
Isrc = payload.Isrc,
|
||||
IsProvisional = true,
|
||||
CreatedAt = clock.UtcNow,
|
||||
UpdatedAt = clock.UtcNow
|
||||
};
|
||||
db.CanonicalRecordings.Add(canonical);
|
||||
source = AddSourceSnapshotIdentity(
|
||||
db, snapshot, canonical.Id, latestVersion + 1, clock.UtcNow);
|
||||
await CanonicalCatalogIdentityProjection.ProjectRecordingSignalsAsync(
|
||||
db, catalogActor, canonical, clock.UtcNow, cancellationToken);
|
||||
source = await AddSourceSnapshotIdentityAsync(
|
||||
db, catalogActor, snapshot, canonical.Id, latestVersion + 1,
|
||||
clock.UtcNow, cancellationToken);
|
||||
}
|
||||
canonicalRecordingId = await LinkExternalIdentitiesAsync(
|
||||
db,
|
||||
catalogActor,
|
||||
source,
|
||||
selectedExternal,
|
||||
playable!.RoutableExternalCandidates,
|
||||
@@ -1926,6 +1946,7 @@ public sealed class TrackMatchCommandService(
|
||||
|
||||
private static async Task<Guid> LinkExternalIdentitiesAsync(
|
||||
AllstarrDbContext db,
|
||||
ProviderActorContext actor,
|
||||
ProviderTrackIdentityRecord source,
|
||||
Song selected,
|
||||
IReadOnlyList<Song> routable,
|
||||
@@ -1938,14 +1959,14 @@ public sealed class TrackMatchCommandService(
|
||||
? "automatic-suggestion"
|
||||
: "automatic-match";
|
||||
var canonicalRecordingId = await LinkExternalIdentityAsync(
|
||||
db, source, selected, source.CanonicalRecordingId, true, verificationMethod,
|
||||
db, actor, source, selected, source.CanonicalRecordingId, true, verificationMethod,
|
||||
decisionVersion, now, cancellationToken);
|
||||
foreach (var alternate in routable.Where(song =>
|
||||
!string.Equals(song.ExternalProvider, selected.ExternalProvider, StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(song.ExternalId, selected.ExternalId, StringComparison.Ordinal)))
|
||||
{
|
||||
await LinkExternalIdentityAsync(
|
||||
db, source, alternate, canonicalRecordingId, false, verificationMethod,
|
||||
db, actor, source, alternate, canonicalRecordingId, false, verificationMethod,
|
||||
decisionVersion, now, cancellationToken);
|
||||
}
|
||||
return canonicalRecordingId;
|
||||
@@ -1953,6 +1974,7 @@ public sealed class TrackMatchCommandService(
|
||||
|
||||
private static async Task<Guid> LinkExternalIdentityAsync(
|
||||
AllstarrDbContext db,
|
||||
ProviderActorContext actor,
|
||||
ProviderTrackIdentityRecord source,
|
||||
Song song,
|
||||
Guid canonicalRecordingId,
|
||||
@@ -1997,10 +2019,12 @@ public sealed class TrackMatchCommandService(
|
||||
identity.UpdatedAt = now;
|
||||
identity.Revision++;
|
||||
}
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
db, actor, identity, now, cancellationToken);
|
||||
return canonicalRecordingId;
|
||||
}
|
||||
|
||||
db.ProviderTrackIdentities.Add(new ProviderTrackIdentityRecord
|
||||
identity = new ProviderTrackIdentityRecord
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
TenantId = source.TenantId,
|
||||
@@ -2017,7 +2041,10 @@ public sealed class TrackMatchCommandService(
|
||||
VerifiedAt = now,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
});
|
||||
};
|
||||
db.ProviderTrackIdentities.Add(identity);
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
db, actor, identity, now, cancellationToken);
|
||||
return canonicalRecordingId;
|
||||
}
|
||||
|
||||
@@ -2146,6 +2173,7 @@ public sealed class TrackMatchCommandService(
|
||||
return TrackMatchCommandResult.Fail(TrackMatchCommandFailure.NotFound, "Track snapshot was not found");
|
||||
if (!actor.IsAdministrator && snapshot.OwnerUserId != actor.UserId)
|
||||
return TrackMatchCommandResult.Fail(TrackMatchCommandFailure.Forbidden, "Track snapshot is outside your account");
|
||||
var catalogActor = CatalogActor(actor, snapshot);
|
||||
|
||||
var sourceIdentity = snapshot.ProviderTrackIdentityId.HasValue
|
||||
? await db.ProviderTrackIdentities.SingleOrDefaultAsync(
|
||||
@@ -2265,17 +2293,21 @@ public sealed class TrackMatchCommandService(
|
||||
TenantId = actor.TenantId,
|
||||
CreatedByUserId = actor.UserId,
|
||||
Isrc = metadata.Isrc,
|
||||
IsProvisional = true,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
db.CanonicalRecordings.Add(canonical);
|
||||
await CanonicalCatalogIdentityProjection.ProjectRecordingSignalsAsync(
|
||||
db, catalogActor, canonical, now, cancellationToken);
|
||||
canonicalId = canonical.Id;
|
||||
}
|
||||
|
||||
if (sourceIdentity == null)
|
||||
{
|
||||
sourceIdentity = AddSourceSnapshotIdentity(
|
||||
db, snapshot, canonicalId.Value, decisionVersion, now);
|
||||
sourceIdentity = await AddSourceSnapshotIdentityAsync(
|
||||
db, catalogActor, snapshot, canonicalId.Value, decisionVersion,
|
||||
now, cancellationToken);
|
||||
}
|
||||
|
||||
var externalHash = Hash(externalId);
|
||||
@@ -2302,7 +2334,7 @@ public sealed class TrackMatchCommandService(
|
||||
cancellationToken);
|
||||
if (identity == null)
|
||||
{
|
||||
db.ProviderTrackIdentities.Add(new ProviderTrackIdentityRecord
|
||||
identity = new ProviderTrackIdentityRecord
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
TenantId = actor.TenantId,
|
||||
@@ -2319,7 +2351,8 @@ public sealed class TrackMatchCommandService(
|
||||
VerifiedAt = now,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
});
|
||||
};
|
||||
db.ProviderTrackIdentities.Add(identity);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2331,6 +2364,8 @@ public sealed class TrackMatchCommandService(
|
||||
identity.UpdatedAt = now;
|
||||
identity.Revision++;
|
||||
}
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
db, catalogActor, identity, now, cancellationToken);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -2364,12 +2399,14 @@ public sealed class TrackMatchCommandService(
|
||||
}
|
||||
}
|
||||
|
||||
private static ProviderTrackIdentityRecord AddSourceSnapshotIdentity(
|
||||
private static async Task<ProviderTrackIdentityRecord> AddSourceSnapshotIdentityAsync(
|
||||
AllstarrDbContext db,
|
||||
ProviderActorContext actor,
|
||||
ExternalMetadataSnapshotRecord snapshot,
|
||||
Guid canonicalRecordingId,
|
||||
int decisionVersion,
|
||||
DateTimeOffset now)
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var identity = new ProviderTrackIdentityRecord
|
||||
{
|
||||
@@ -2391,9 +2428,25 @@ public sealed class TrackMatchCommandService(
|
||||
UpdatedAt = now
|
||||
};
|
||||
db.ProviderTrackIdentities.Add(identity);
|
||||
await CanonicalCatalogIdentityProjection.ProjectProviderIdentityAsync(
|
||||
db, actor, identity, now, cancellationToken);
|
||||
return identity;
|
||||
}
|
||||
|
||||
private static ProviderActorContext CatalogActor(
|
||||
TrackMatchActor actor,
|
||||
ExternalMetadataSnapshotRecord snapshot) => new(
|
||||
actor.TenantId,
|
||||
actor.IsAdministrator ? ProviderActorKind.Administrator : ProviderActorKind.User,
|
||||
actor.UserId,
|
||||
new ProviderBackendPrincipal(
|
||||
snapshot.Protocol,
|
||||
snapshot.BackendInstanceId,
|
||||
snapshot.BackendPrincipalId),
|
||||
actingForUserId: actor.IsAdministrator && snapshot.OwnerUserId != actor.UserId
|
||||
? snapshot.OwnerUserId
|
||||
: null);
|
||||
|
||||
private static string CleanReason(string? value, string fallback)
|
||||
{
|
||||
var reason = string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
||||
@@ -2467,6 +2520,10 @@ public sealed class TrackMatchCommandService(
|
||||
"IX_provider_track_identity_catalog_exact" or
|
||||
"IX_track_match_scoped_decision")
|
||||
return true;
|
||||
if (postgres.ConstraintName?.StartsWith(
|
||||
"IX_canonical_catalog_aliases_",
|
||||
StringComparison.Ordinal) == true)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -338,12 +338,12 @@ Checkpoint as of 2026-09-22; the alias projection changes are not yet deployed:
|
||||
- **Catalog client:** The bounded `/ws/2` client supports source-scoped caches and artist, release-group, release, recording, ISRC, media, and release-track reads. Fixtures cover response hierarchy, cache isolation, size limits, rate limiting, cancellation, negative caching, and 401/403 handling. On 2026-09-15, eight operator-authorized BrainzMash requests passed with responses below 78 KB and latency from 656 ms to 11.129 seconds. A literal `Sunroof` search ranked acoustic/remix editions ahead of the base recording, confirming that Allstarr must rank normalized title, artist credit, duration, and version evidence itself.
|
||||
- **Atomic ingestion:** `MusicBrainzCatalogIngestService` validates the full payload before a serializable transaction, preserves separate editions, replaces provisional fields with canonical facts, and writes provenance through the shared evidence owner. Repeated input preserves IDs and creates no duplicate facts. A disposable PostgreSQL run passed 54 catalog, client, environment, migration-snapshot, and storage tests, including rollback of malformed media.
|
||||
- **Discovery and refresh:** `MusicBrainzCatalogRefreshQueue` creates seven-day idempotency generations scoped by tenant, user, source, and revision. Recording discovery accepts at most 50 distinct editions. Release refresh accepts one release hierarchy and at most 64 credited artists before atomic ingestion. Both jobs preserve upstream retry delays, separate permanent hierarchy failures from transient failures, and stay outside search and playback requests. The focused lane passes 62/62 tests; a separate PostgreSQL run passes all 21 selected identity, discovery, and ingestion tests.
|
||||
- **Identity projection:** Creating a recording now projects exact ISRC and MusicBrainz aliases through the shared evidence owner. Accepted provider identities project account- or catalog-scoped aliases transactionally, so the same provider ID cannot leak or collide across account boundaries. A forward migration backfills those aliases and marks recordings without an MBID provisional. The isolated PostgreSQL lane passes all 15 selected identity, migration, and model-snapshot tests.
|
||||
- **Identity projection:** Creating a recording now projects exact ISRC and MusicBrainz aliases through the shared evidence owner. Both the identity service and matching commands project accepted provider identities transactionally with account- or catalog-scoped namespaces, so the same provider ID cannot leak or collide across account boundaries. Concurrent match writers treat an alias insert race as a retryable identity write. A forward migration backfills those aliases and marks recordings without an MBID provisional. The isolated PostgreSQL lane passes all 18 selected identity, migration, manual-selection, automatic-fallback, concurrency, and model-snapshot tests.
|
||||
- **Remaining:** Project legacy library, source-snapshot, and protocol identities into the catalog; add the relationship and image request shapes needed by Stage 3; and reconcile provisional records that begin without an MBID.
|
||||
|
||||
| Stage 2 checkpoint measure | Stage start | Current | Interpretation |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Production source in the established C#/WebUI/Python scope | 135,882 | 136,875 | +993 lines implement the catalog graph, evidence owner, bounded client, ingestion/refresh jobs, and exact identity projection; migrations and tests are excluded, and no consolidation saving is claimed yet |
|
||||
| Production source in the established C#/WebUI/Python scope | 135,882 | 136,934 | +1,052 lines implement the catalog graph, evidence owner, bounded client, ingestion/refresh jobs, and exact identity projection; migrations and tests are excluded, and no consolidation saving is claimed yet |
|
||||
| Accepted provider-identity alias writers | none | 1 shared evidence path | New links, repeat links, conflict checks, and the forward migration use the same namespace and hashing rules |
|
||||
|
||||
### Stage 3: Replace provider-shaped search with catalog search
|
||||
|
||||
Reference in new issue
Block a user