@yemi33/minions 0.1.2365 → 0.1.2366
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/dashboard.js +46 -8
- package/engine/lifecycle.js +14 -5
- package/engine/queries.js +17 -8
- package/engine/shared.js +92 -50
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -14125,14 +14125,50 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14125
14125
|
|
|
14126
14126
|
{ method: 'POST', path: '/api/pull-requests/delete', desc: 'Remove a PR from tracking (sets userDeleted tombstone so pollers do not re-add it)', params: 'id, project?', handler: async (req, res) => {
|
|
14127
14127
|
const body = await readBody(req);
|
|
14128
|
-
const { id } = body;
|
|
14128
|
+
const { id, project: requestedProjectName } = body;
|
|
14129
14129
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
14130
14130
|
reloadConfig();
|
|
14131
14131
|
// Search all project PR files and central file
|
|
14132
|
-
const
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
];
|
|
14132
|
+
const projects = shared.getProjects(CONFIG);
|
|
14133
|
+
const projectByName = new Map(projects.map(project => [project.name, project]));
|
|
14134
|
+
const store = require('./engine/pull-requests-store');
|
|
14135
|
+
const storedPrs = store.readAllPullRequests() || [];
|
|
14136
|
+
const prScopes = new Map([
|
|
14137
|
+
...projects.map(project => [project.name, { prPath: shared.projectPrPath(project), project }]),
|
|
14138
|
+
['central', { prPath: path.join(MINIONS_DIR, 'pull-requests.json'), project: null }],
|
|
14139
|
+
]);
|
|
14140
|
+
const canonicalRequest = shared.parseCanonicalPrId(id);
|
|
14141
|
+
const requestedIdentities = new Set();
|
|
14142
|
+
if (canonicalRequest) {
|
|
14143
|
+
const compatibleProject = canonicalRequest.scope.startsWith('ado:')
|
|
14144
|
+
? projects.find(project => shared.isAdoPrScopeCompatible(canonicalRequest.scope, project))
|
|
14145
|
+
: null;
|
|
14146
|
+
requestedIdentities.add(shared.getPrIdentityKey(id, compatibleProject || null));
|
|
14147
|
+
} else {
|
|
14148
|
+
for (const pr of storedPrs) {
|
|
14149
|
+
if (pr?.id !== id) continue;
|
|
14150
|
+
if (requestedProjectName && pr._scope !== requestedProjectName) continue;
|
|
14151
|
+
const ownerProject = pr._scope && pr._scope !== 'central'
|
|
14152
|
+
? projectByName.get(pr._scope) || null
|
|
14153
|
+
: null;
|
|
14154
|
+
const identity = shared.getPrIdentityKey(pr, ownerProject);
|
|
14155
|
+
if (identity) requestedIdentities.add(identity);
|
|
14156
|
+
}
|
|
14157
|
+
}
|
|
14158
|
+
if (requestedIdentities.size > 1) {
|
|
14159
|
+
return jsonReply(res, 409, { error: 'Legacy PR id is ambiguous; specify project' });
|
|
14160
|
+
}
|
|
14161
|
+
if (requestedIdentities.size === 0) {
|
|
14162
|
+
return jsonReply(res, 404, { error: 'PR not found' });
|
|
14163
|
+
}
|
|
14164
|
+
for (const pr of storedPrs) {
|
|
14165
|
+
const scope = pr?._scope || 'central';
|
|
14166
|
+
const ownerProject = scope !== 'central' ? projectByName.get(scope) || null : null;
|
|
14167
|
+
if (!requestedIdentities.has(shared.getPrIdentityKey(pr, ownerProject))) continue;
|
|
14168
|
+
if (!prScopes.has(scope)) {
|
|
14169
|
+
prScopes.set(scope, { prPath: store._filePathForScope(scope), project: ownerProject });
|
|
14170
|
+
}
|
|
14171
|
+
}
|
|
14136
14172
|
let found = false;
|
|
14137
14173
|
// Issue #803: tombstone EVERY scope that has a copy of this PR id, not
|
|
14138
14174
|
// just the first one found. When enrollPrFromCanonicalId (issue #802)
|
|
@@ -14141,14 +14177,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14141
14177
|
// the other scope's copy fully untouched — queries.js#getPullRequests
|
|
14142
14178
|
// then de-dupes by picking that untouched duplicate as the "winner"
|
|
14143
14179
|
// and silently resurrects the "deleted" PR on the next read.
|
|
14144
|
-
for (const prPath of
|
|
14180
|
+
for (const { prPath, project } of prScopes.values()) {
|
|
14145
14181
|
// Issue #384: use a tombstone (userDeleted:true) instead of splice so
|
|
14146
14182
|
// the next poller cycle cannot re-add the PR. The entry stays in the
|
|
14147
14183
|
// file but is filtered from API responses and skipped by pollers.
|
|
14148
14184
|
shared.mutatePullRequests(prPath, (prs) => {
|
|
14149
14185
|
if (!Array.isArray(prs)) return prs;
|
|
14150
|
-
const
|
|
14151
|
-
|
|
14186
|
+
const matches = prs.filter(pr =>
|
|
14187
|
+
requestedIdentities.has(shared.getPrIdentityKey(pr, project))
|
|
14188
|
+
);
|
|
14189
|
+
for (const pr of matches) {
|
|
14152
14190
|
pr.userDeleted = true;
|
|
14153
14191
|
pr.userDeletedAt = shared.ts();
|
|
14154
14192
|
found = true;
|
package/engine/lifecycle.js
CHANGED
|
@@ -6695,6 +6695,7 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6695
6695
|
config = config || getConfig();
|
|
6696
6696
|
const projects = shared.getProjects(config) || [];
|
|
6697
6697
|
if (projects.length === 0) return { pruned: 0, relocated: 0, scanned: 0 };
|
|
6698
|
+
const projectByName = new Map(projects.filter(project => project?.name).map(project => [project.name, project]));
|
|
6698
6699
|
|
|
6699
6700
|
// Map project name -> canonical scope(s) so we can find which project IS
|
|
6700
6701
|
// the correctly-scoped owner for a given mismatch record. Includes
|
|
@@ -6717,14 +6718,19 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6717
6718
|
}
|
|
6718
6719
|
}
|
|
6719
6720
|
|
|
6720
|
-
// Index all PRs by
|
|
6721
|
+
// Index all PRs by repository-scoped identity across all projects.
|
|
6721
6722
|
const store = require('./pull-requests-store');
|
|
6722
6723
|
const allPrs = store.readAllPullRequests() || [];
|
|
6723
6724
|
const byId = new Map();
|
|
6724
6725
|
for (const pr of allPrs) {
|
|
6725
6726
|
if (!pr || !pr.id) continue;
|
|
6726
|
-
|
|
6727
|
-
|
|
6727
|
+
const ownerProject = pr._scope && pr._scope !== 'central'
|
|
6728
|
+
? projectByName.get(pr._scope) || null
|
|
6729
|
+
: null;
|
|
6730
|
+
const identity = shared.getPrIdentityKey(pr, ownerProject);
|
|
6731
|
+
if (!identity) continue;
|
|
6732
|
+
if (!byId.has(identity)) byId.set(identity, []);
|
|
6733
|
+
byId.get(identity).push(pr);
|
|
6728
6734
|
}
|
|
6729
6735
|
|
|
6730
6736
|
// Build per-project delete sets keyed by id, so we batch one mutation per
|
|
@@ -6741,6 +6747,9 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6741
6747
|
for (const records of byId.values()) {
|
|
6742
6748
|
for (const rec of records) {
|
|
6743
6749
|
scanned++;
|
|
6750
|
+
// A tombstone is the durable record of explicit user intent. Never prune
|
|
6751
|
+
// it in favor of an active sibling, or the sibling becomes visible again.
|
|
6752
|
+
if (rec.userDeleted === true) continue;
|
|
6744
6753
|
if (!rec._invalidProjectScope || rec._invalidProjectScope.reason !== 'pr_scope_mismatch') continue;
|
|
6745
6754
|
const correctScope = rec._invalidProjectScope.prScope;
|
|
6746
6755
|
if (!correctScope) continue;
|
|
@@ -6831,8 +6840,8 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6831
6840
|
return { pruned, relocated, scanned };
|
|
6832
6841
|
}
|
|
6833
6842
|
|
|
6834
|
-
// Repair helper: collapse
|
|
6835
|
-
//
|
|
6843
|
+
// Repair helper: collapse duplicate repository-scoped PR identities across all
|
|
6844
|
+
// project-scoped and central tracker data. Called once per reconcile cycle alongside
|
|
6836
6845
|
// pruneScopeMismatchDuplicatePrs so accumulated duplicates are cleaned up
|
|
6837
6846
|
// without requiring a one-off migration.
|
|
6838
6847
|
function collapseAllDuplicatePrRecords(config) {
|
package/engine/queries.js
CHANGED
|
@@ -1001,25 +1001,34 @@ function getPullRequests(config) {
|
|
|
1001
1001
|
// only tombstones one scope's copy would still have its untouched
|
|
1002
1002
|
// cross-scope duplicate survive grouping below and get picked as the
|
|
1003
1003
|
// dedupe "winner", silently resurrecting a PR the user just deleted.
|
|
1004
|
-
const
|
|
1004
|
+
const identityOf = (pr) => {
|
|
1005
|
+
const ownerProject = pr?._scope && pr._scope !== 'central'
|
|
1006
|
+
? projectByName.get(pr._scope) || null
|
|
1007
|
+
: null;
|
|
1008
|
+
return shared.getPrIdentityKey(pr, ownerProject);
|
|
1009
|
+
};
|
|
1010
|
+
const tombstonedIdentities = new Set();
|
|
1005
1011
|
for (const pr of sqlPrs) {
|
|
1006
|
-
|
|
1012
|
+
const identity = identityOf(pr);
|
|
1013
|
+
if (identity && pr.userDeleted === true) tombstonedIdentities.add(identity);
|
|
1007
1014
|
}
|
|
1008
1015
|
|
|
1009
1016
|
const groups = [];
|
|
1010
|
-
const
|
|
1017
|
+
const groupsByIdentity = new Map();
|
|
1011
1018
|
for (const pr of sqlPrs) {
|
|
1012
1019
|
if (!pr || !pr.id) continue;
|
|
1020
|
+
const identity = identityOf(pr);
|
|
1021
|
+
if (!identity) continue;
|
|
1013
1022
|
// Issue #384/#803: skip tombstoned records (user explicitly deleted), and
|
|
1014
|
-
// skip ANY
|
|
1023
|
+
// skip ANY alias of an identity that has a tombstoned copy elsewhere.
|
|
1015
1024
|
if (pr.userDeleted === true) continue;
|
|
1016
|
-
if (
|
|
1017
|
-
if (!
|
|
1025
|
+
if (tombstonedIdentities.has(identity)) continue;
|
|
1026
|
+
if (!groupsByIdentity.has(identity)) {
|
|
1018
1027
|
const g = [];
|
|
1019
|
-
|
|
1028
|
+
groupsByIdentity.set(identity, g);
|
|
1020
1029
|
groups.push(g);
|
|
1021
1030
|
}
|
|
1022
|
-
|
|
1031
|
+
groupsByIdentity.get(identity).push(pr);
|
|
1023
1032
|
}
|
|
1024
1033
|
for (const group of groups) {
|
|
1025
1034
|
const { winner, dropped } = _pickPrDedupeWinner(group, projects);
|
package/engine/shared.js
CHANGED
|
@@ -7519,6 +7519,19 @@ function getCanonicalPrId(project, prRef, url = '') {
|
|
|
7519
7519
|
return scope ? `${scope}#${prNumber}` : `PR-${prNumber}`;
|
|
7520
7520
|
}
|
|
7521
7521
|
|
|
7522
|
+
function getPrIdentityKey(prRef, project = null) {
|
|
7523
|
+
if (!prRef) return '';
|
|
7524
|
+
const url = typeof prRef === 'object' ? String(prRef.url || '') : '';
|
|
7525
|
+
const rawId = typeof prRef === 'object' ? prRef.id : prRef;
|
|
7526
|
+
const identity = getCanonicalPrId(project, prRef, url) || String(rawId || '');
|
|
7527
|
+
const parsed = parseCanonicalPrId(identity);
|
|
7528
|
+
if (parsed && project && isAdoPrScopeCompatible(parsed.scope, project)) {
|
|
7529
|
+
const projectScope = getProjectPrScope(project);
|
|
7530
|
+
if (projectScope?.startsWith('ado:')) return `${projectScope}#${parsed.prNumber}`;
|
|
7531
|
+
}
|
|
7532
|
+
return identity;
|
|
7533
|
+
}
|
|
7534
|
+
|
|
7522
7535
|
function findPrRecord(prs, prRef, project = null) {
|
|
7523
7536
|
if (!Array.isArray(prs) || !prRef) return null;
|
|
7524
7537
|
const isObjectRef = typeof prRef === 'object';
|
|
@@ -7814,14 +7827,19 @@ function _mergeDuplicatePrInto(winner, loser) {
|
|
|
7814
7827
|
}
|
|
7815
7828
|
}
|
|
7816
7829
|
winner.prdItems = normalizePrLinkItems([...(winner.prdItems || []), ...(loser.prdItems || [])]);
|
|
7830
|
+
if (winner.userDeleted === true || loser.userDeleted === true) {
|
|
7831
|
+
winner.userDeleted = true;
|
|
7832
|
+
if (winner.userDeletedAt == null && loser.userDeletedAt != null) {
|
|
7833
|
+
winner.userDeletedAt = loser.userDeletedAt;
|
|
7834
|
+
}
|
|
7835
|
+
}
|
|
7817
7836
|
}
|
|
7818
7837
|
|
|
7819
7838
|
/**
|
|
7820
|
-
* P-e9f0a2b4 —
|
|
7821
|
-
*
|
|
7839
|
+
* P-e9f0a2b4 — Repair helper: collapse duplicate records for the same
|
|
7840
|
+
* repository-scoped PR identity within a single tracker scope.
|
|
7822
7841
|
*
|
|
7823
|
-
*
|
|
7824
|
-
* Idempotent — no-op when no duplicates exist.
|
|
7842
|
+
* Called from the reconciliation sweep for every tracker scope. Idempotent.
|
|
7825
7843
|
*
|
|
7826
7844
|
* Selection rule (matching the task spec):
|
|
7827
7845
|
* 1. Prefer `merged` status over any other status.
|
|
@@ -7839,19 +7857,20 @@ function collapseDuplicatePrRecords(prPath, { project = null } = {}) {
|
|
|
7839
7857
|
let collapsed = 0;
|
|
7840
7858
|
mutatePullRequests(prPath, (prs) => {
|
|
7841
7859
|
normalizePrRecords(prs, project);
|
|
7842
|
-
//
|
|
7843
|
-
|
|
7860
|
+
// PR numbers are only unique within a repository. Grouping by number alone
|
|
7861
|
+
// merges unrelated GitHub/ADO repositories and can discard a tombstone.
|
|
7862
|
+
const byIdentity = new Map();
|
|
7844
7863
|
for (const pr of prs) {
|
|
7845
7864
|
if (!pr) continue;
|
|
7846
|
-
const
|
|
7847
|
-
if (
|
|
7848
|
-
if (!
|
|
7849
|
-
|
|
7865
|
+
const identity = getPrIdentityKey(pr, project);
|
|
7866
|
+
if (!identity) continue;
|
|
7867
|
+
if (!byIdentity.has(identity)) byIdentity.set(identity, []);
|
|
7868
|
+
byIdentity.get(identity).push(pr);
|
|
7850
7869
|
}
|
|
7851
7870
|
// Identify loser records to remove (by object reference, not id, because
|
|
7852
7871
|
// normalization may have changed multiple records to the same canonical id).
|
|
7853
7872
|
const toRemove = new Set();
|
|
7854
|
-
for (const [, group] of
|
|
7873
|
+
for (const [, group] of byIdentity) {
|
|
7855
7874
|
if (group.length <= 1) continue;
|
|
7856
7875
|
const best = _pickBestPrRecord(group);
|
|
7857
7876
|
for (const dup of group) {
|
|
@@ -7867,6 +7886,35 @@ function collapseDuplicatePrRecords(prPath, { project = null } = {}) {
|
|
|
7867
7886
|
return { collapsed };
|
|
7868
7887
|
}
|
|
7869
7888
|
|
|
7889
|
+
function _clearPrTombstonesForIdentity(identity) {
|
|
7890
|
+
if (!identity) return 0;
|
|
7891
|
+
const projectsByName = new Map(getProjects().map(project => [project.name, project]));
|
|
7892
|
+
const scopes = new Map();
|
|
7893
|
+
const store = require('./pull-requests-store');
|
|
7894
|
+
for (const pr of store.readAllPullRequests() || []) {
|
|
7895
|
+
if (pr?.userDeleted !== true) continue;
|
|
7896
|
+
const project = pr._scope && pr._scope !== 'central'
|
|
7897
|
+
? projectsByName.get(pr._scope) || null
|
|
7898
|
+
: null;
|
|
7899
|
+
if (getPrIdentityKey(pr, project) !== identity) continue;
|
|
7900
|
+
const prPath = store._filePathForScope(pr._scope || 'central');
|
|
7901
|
+
scopes.set(path.resolve(prPath), { prPath, project });
|
|
7902
|
+
}
|
|
7903
|
+
let cleared = 0;
|
|
7904
|
+
for (const { prPath, project } of scopes.values()) {
|
|
7905
|
+
mutatePullRequests(prPath, (prs) => {
|
|
7906
|
+
for (const pr of prs) {
|
|
7907
|
+
if (pr?.userDeleted !== true || getPrIdentityKey(pr, project) !== identity) continue;
|
|
7908
|
+
delete pr.userDeleted;
|
|
7909
|
+
delete pr.userDeletedAt;
|
|
7910
|
+
cleared++;
|
|
7911
|
+
}
|
|
7912
|
+
return prs;
|
|
7913
|
+
});
|
|
7914
|
+
}
|
|
7915
|
+
return cleared;
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7870
7918
|
/**
|
|
7871
7919
|
* Canonical PR-producing work contract helper.
|
|
7872
7920
|
*
|
|
@@ -7884,17 +7932,15 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7884
7932
|
...(Array.isArray(itemIds) ? itemIds : [itemId]),
|
|
7885
7933
|
]);
|
|
7886
7934
|
const prNumber = getPrNumber(entry.prNumber ?? entry.id ?? entry.url);
|
|
7887
|
-
//
|
|
7888
|
-
//
|
|
7889
|
-
//
|
|
7890
|
-
//
|
|
7891
|
-
// `
|
|
7892
|
-
//
|
|
7893
|
-
//
|
|
7894
|
-
|
|
7895
|
-
const canonicalId =
|
|
7896
|
-
? getCanonicalPrId(project, entry.id, entry.url || '')
|
|
7897
|
-
: getCanonicalPrId(project, entry.prNumber ?? entry.id ?? entry.url ?? prNumber, entry.url || '');
|
|
7935
|
+
// getPrIdentityKey(entry, project) passes the whole entry object through to
|
|
7936
|
+
// getCanonicalPrId, which (for object refs) reads `entry.id` directly rather
|
|
7937
|
+
// than the `entry.prNumber ?? entry.id ?? ...` coalesce used for bare refs.
|
|
7938
|
+
// That already preserves an already-resolved scope-qualified `entry.id`
|
|
7939
|
+
// (e.g. `ado:office/office/office#5370216`, as enrollPrFromCanonicalId
|
|
7940
|
+
// produces) instead of silently downgrading to a legacy `PR-<n>` stub when
|
|
7941
|
+
// `project` is null/unresolvable — see issue #802 and
|
|
7942
|
+
// test/unit/pr-number-dedup.test.js's "(#802)" case.
|
|
7943
|
+
const canonicalId = getPrIdentityKey(entry, project);
|
|
7898
7944
|
if (!canonicalId) throw new Error('PR id required');
|
|
7899
7945
|
const normalizedEntry = {
|
|
7900
7946
|
...entry,
|
|
@@ -7918,12 +7964,19 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7918
7964
|
let created = false;
|
|
7919
7965
|
let linked = false;
|
|
7920
7966
|
let skipped = false;
|
|
7921
|
-
let resurrected =
|
|
7967
|
+
let resurrected = allowResurrect && _clearPrTombstonesForIdentity(canonicalId) > 0;
|
|
7922
7968
|
let record = null;
|
|
7923
7969
|
|
|
7924
7970
|
mutatePullRequests(prPath, (prs) => {
|
|
7925
7971
|
normalizePrRecords(prs, project);
|
|
7926
|
-
|
|
7972
|
+
const identityMatches = prs.filter(pr => getPrIdentityKey(pr, project) === canonicalId);
|
|
7973
|
+
let target = identityMatches.length > 0 ? _pickBestPrRecord(identityMatches) : null;
|
|
7974
|
+
if (target && identityMatches.length > 1) {
|
|
7975
|
+
const duplicates = new Set(identityMatches.filter(pr => pr !== target));
|
|
7976
|
+
for (const duplicate of duplicates) _mergeDuplicatePrInto(target, duplicate);
|
|
7977
|
+
prs = prs.filter(pr => !duplicates.has(pr));
|
|
7978
|
+
}
|
|
7979
|
+
const tombstonedMatches = target?.userDeleted === true ? [target] : [];
|
|
7927
7980
|
// Issue #384: tombstoned records (userDeleted:true) must not be re-activated
|
|
7928
7981
|
// by any poller path. Return skipped so reconcilePrs / shared-branch-reconcile
|
|
7929
7982
|
// cannot resurrect a PR the user explicitly removed.
|
|
@@ -7933,13 +7986,15 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7933
7986
|
// via POST /api/pull-requests/link is explicit intent to bring the PR back.
|
|
7934
7987
|
// Callers that want that behavior opt in with `allowResurrect: true`, which
|
|
7935
7988
|
// clears the tombstone here and lets the normal field-merge logic below run.
|
|
7936
|
-
if (
|
|
7989
|
+
if (tombstonedMatches.length > 0) {
|
|
7937
7990
|
if (!allowResurrect) {
|
|
7938
7991
|
skipped = true;
|
|
7939
7992
|
return prs;
|
|
7940
7993
|
}
|
|
7941
|
-
|
|
7942
|
-
|
|
7994
|
+
for (const match of tombstonedMatches) {
|
|
7995
|
+
delete match.userDeleted;
|
|
7996
|
+
delete match.userDeletedAt;
|
|
7997
|
+
}
|
|
7943
7998
|
resurrected = true;
|
|
7944
7999
|
}
|
|
7945
8000
|
// W-mrduef3q000m2172 — defense-in-depth: `prs` here is scoped to a single
|
|
@@ -7948,37 +8003,23 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7948
8003
|
// record actually lives in a DIFFERENT scope's file), the above check
|
|
7949
8004
|
// never sees it and would otherwise fall through to insert-as-new,
|
|
7950
8005
|
// silently clearing the tombstone. Check globally before inserting.
|
|
7951
|
-
if (!target) {
|
|
8006
|
+
if (!target && !allowResurrect) {
|
|
7952
8007
|
try {
|
|
7953
8008
|
const store = require('./pull-requests-store');
|
|
7954
|
-
const
|
|
7955
|
-
|
|
8009
|
+
const projectsByName = new Map(getProjects().map(candidate => [candidate.name, candidate]));
|
|
8010
|
+
const globalMatch = (store.readAllPullRequests() || []).find(pr => {
|
|
8011
|
+
if (!pr || pr.userDeleted !== true) return false;
|
|
8012
|
+
const ownerProject = pr._scope && pr._scope !== 'central'
|
|
8013
|
+
? projectsByName.get(pr._scope) || null
|
|
8014
|
+
: null;
|
|
8015
|
+
return getPrIdentityKey(pr, ownerProject) === canonicalId;
|
|
8016
|
+
});
|
|
7956
8017
|
if (globalMatch) {
|
|
7957
8018
|
skipped = true;
|
|
7958
8019
|
return prs;
|
|
7959
8020
|
}
|
|
7960
8021
|
} catch { /* best-effort global tombstone check — fall through on failure */ }
|
|
7961
8022
|
}
|
|
7962
|
-
// P-e9f0a2b4 — When findPrRecord returns null but multiple records share the
|
|
7963
|
-
// same prNumber (e.g. cross-scope contamination or a project-config change
|
|
7964
|
-
// that shifted canonical IDs), collapse the duplicates before inserting.
|
|
7965
|
-
// Without this check, every subsequent upsert would append yet another record
|
|
7966
|
-
// because numberMatches.length > 1 forces findPrRecord to return null.
|
|
7967
|
-
if (!target && prNumber != null) {
|
|
7968
|
-
const numberMatches = prs.filter(pr => pr && !pr.userDeleted && getPrNumber(pr) === prNumber);
|
|
7969
|
-
if (numberMatches.length > 1) {
|
|
7970
|
-
const best = _pickBestPrRecord(numberMatches);
|
|
7971
|
-
const toRemoveIds = new Set(numberMatches.filter(p => p !== best).map(p => p.id));
|
|
7972
|
-
for (const dup of numberMatches) {
|
|
7973
|
-
if (dup === best) continue;
|
|
7974
|
-
_mergeDuplicatePrInto(best, dup);
|
|
7975
|
-
}
|
|
7976
|
-
for (let i = prs.length - 1; i >= 0; i--) {
|
|
7977
|
-
if (prs[i] && toRemoveIds.has(prs[i].id)) prs.splice(i, 1);
|
|
7978
|
-
}
|
|
7979
|
-
target = best; // update the surviving record instead of inserting new
|
|
7980
|
-
}
|
|
7981
|
-
}
|
|
7982
8023
|
if (!target && typeof beforeInsert === 'function' && beforeInsert(prs, normalizedEntry) === false) {
|
|
7983
8024
|
skipped = true;
|
|
7984
8025
|
return prs;
|
|
@@ -9363,6 +9404,7 @@ module.exports = {
|
|
|
9363
9404
|
isPrCompatibleWithProject,
|
|
9364
9405
|
isAdoPrScopeCompatible,
|
|
9365
9406
|
getCanonicalPrId,
|
|
9407
|
+
getPrIdentityKey,
|
|
9366
9408
|
findPrRecord,
|
|
9367
9409
|
isPlaceholderPrTitle,
|
|
9368
9410
|
snapshotPrRecord,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2366",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|