ghcr-manager 0.0.4 → 0.9.0

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +166 -57
  3. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +6 -0
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +113 -0
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +40 -0
  6. package/dist/cleanup-summary/_cleanup-summary.js +40 -0
  7. package/dist/cleanup-summary/index.d.ts +2 -0
  8. package/dist/cleanup-summary/index.js +2 -0
  9. package/dist/cli/_args.d.ts +1 -0
  10. package/dist/cli/_args.js +3 -0
  11. package/dist/cli/_cleanup-command.d.ts +1 -0
  12. package/dist/cli/_cleanup-command.js +63 -0
  13. package/dist/cli/_db-merge-command.d.ts +1 -0
  14. package/dist/cli/_db-merge-command.js +41 -0
  15. package/dist/cli/_github-output.d.ts +10 -0
  16. package/dist/cli/_github-output.js +13 -0
  17. package/dist/cli/_logger.d.ts +2 -1
  18. package/dist/cli/_logger.js +2 -0
  19. package/dist/cli/_older-than.d.ts +5 -0
  20. package/dist/cli/_older-than.js +42 -0
  21. package/dist/cli/_planner-options.d.ts +20 -0
  22. package/dist/cli/_planner-options.js +101 -0
  23. package/dist/cli/_scan-command.js +11 -4
  24. package/dist/cli/_tag-selector-resolver.d.ts +3 -0
  25. package/dist/cli/_tag-selector-resolver.js +109 -0
  26. package/dist/cli/_untag-command.d.ts +1 -0
  27. package/dist/cli/_untag-command.js +57 -0
  28. package/dist/cli/index.js +19 -1
  29. package/dist/config/_service-constants.d.ts +3 -0
  30. package/dist/config/_service-constants.js +3 -0
  31. package/dist/{tuning → config}/index.d.ts +3 -0
  32. package/dist/{tuning → config}/index.js +3 -0
  33. package/dist/core/_github-package-owner.d.ts +11 -0
  34. package/dist/core/_github-package-owner.js +45 -0
  35. package/dist/core/_http-error.d.ts +6 -0
  36. package/dist/core/_http-error.js +33 -0
  37. package/dist/core/_types.d.ts +3 -2
  38. package/dist/core/index.d.ts +4 -1
  39. package/dist/core/index.js +2 -1
  40. package/dist/db/_cleanup-run-writer.d.ts +10 -0
  41. package/dist/db/_cleanup-run-writer.js +73 -0
  42. package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
  43. package/dist/db/_db-merge-cleanup-copy.js +122 -0
  44. package/dist/db/_db-merge-history.d.ts +2 -0
  45. package/dist/db/_db-merge-history.js +15 -0
  46. package/dist/db/_db-merge-repository.d.ts +8 -0
  47. package/dist/db/_db-merge-repository.js +95 -0
  48. package/dist/db/_db-merge-scan-copy.d.ts +10 -0
  49. package/dist/db/_db-merge-scan-copy.js +69 -0
  50. package/dist/db/_db-merge-types.d.ts +44 -0
  51. package/dist/db/_db-merge-types.js +1 -0
  52. package/dist/db/_github-actions-run-url.d.ts +1 -0
  53. package/dist/db/_github-actions-run-url.js +9 -0
  54. package/dist/db/_scan-writer.d.ts +3 -1
  55. package/dist/db/_scan-writer.js +28 -13
  56. package/dist/db/_snapshot-repository.d.ts +9 -9
  57. package/dist/db/_snapshot-repository.js +37 -49
  58. package/dist/db/_sql-placeholders.d.ts +2 -0
  59. package/dist/db/_sql-placeholders.js +16 -0
  60. package/dist/db/index.d.ts +5 -0
  61. package/dist/db/index.js +3 -0
  62. package/dist/db/planner/_planner-delete-tag-root-targets.d.ts +7 -0
  63. package/dist/db/planner/_planner-delete-tag-root-targets.js +130 -0
  64. package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
  65. package/dist/db/planner/_planner-direct-target-tags.js +47 -0
  66. package/dist/db/planner/_planner-keep-tagged-root-targets.d.ts +7 -0
  67. package/dist/db/planner/_planner-keep-tagged-root-targets.js +74 -0
  68. package/dist/db/planner/_planner-output.d.ts +5 -0
  69. package/dist/db/planner/_planner-output.js +101 -0
  70. package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
  71. package/dist/db/planner/_planner-plan-artifacts.js +211 -0
  72. package/dist/db/planner/_planner-repository.d.ts +34 -0
  73. package/dist/db/planner/_planner-repository.js +126 -0
  74. package/dist/db/planner/_planner-sql.d.ts +12 -0
  75. package/dist/db/planner/_planner-sql.js +35 -0
  76. package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
  77. package/dist/db/planner/_planner-tag-selectors.js +57 -0
  78. package/dist/db/planner/_planner-tagged-root-targets.d.ts +15 -0
  79. package/dist/db/planner/_planner-tagged-root-targets.js +19 -0
  80. package/dist/db/planner/_planner-tagged-targets.d.ts +8 -0
  81. package/dist/db/planner/_planner-tagged-targets.js +16 -0
  82. package/dist/db/planner/_planner-types.d.ts +135 -0
  83. package/dist/db/planner/_planner-types.js +38 -0
  84. package/dist/db/planner/_planner-untagged-targets.d.ts +9 -0
  85. package/dist/db/planner/_planner-untagged-targets.js +91 -0
  86. package/dist/db/planner/index.d.ts +2 -0
  87. package/dist/db/planner/index.js +1 -0
  88. package/dist/execute/_http.d.ts +7 -0
  89. package/dist/execute/_http.js +48 -0
  90. package/dist/execute/_manifest-detach.d.ts +4 -0
  91. package/dist/execute/_manifest-detach.js +31 -0
  92. package/dist/execute/_package-version-delete-client.d.ts +4 -0
  93. package/dist/execute/_package-version-delete-client.js +34 -0
  94. package/dist/execute/_package-version-page-client.d.ts +14 -0
  95. package/dist/execute/_package-version-page-client.js +64 -0
  96. package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
  97. package/dist/execute/_package-version-tag-source-client.js +65 -0
  98. package/dist/execute/_plan-executor.d.ts +3 -0
  99. package/dist/execute/_plan-executor.js +47 -0
  100. package/dist/execute/_registry-manifest-client.d.ts +12 -0
  101. package/dist/execute/_registry-manifest-client.js +79 -0
  102. package/dist/execute/_registry-token-client.d.ts +4 -0
  103. package/dist/execute/_registry-token-client.js +37 -0
  104. package/dist/execute/_types.d.ts +51 -0
  105. package/dist/execute/_types.js +1 -0
  106. package/dist/execute/_untag-client.d.ts +2 -0
  107. package/dist/execute/_untag-client.js +71 -0
  108. package/dist/execute/index.d.ts +5 -0
  109. package/dist/execute/index.js +3 -0
  110. package/dist/ingest/github/_manifest-client.d.ts +7 -1
  111. package/dist/ingest/github/_manifest-client.js +8 -0
  112. package/dist/ingest/github/_manifest-ingest.js +39 -53
  113. package/dist/ingest/github/_manifest-kind.d.ts +20 -0
  114. package/dist/ingest/github/_manifest-kind.js +50 -0
  115. package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
  116. package/dist/ingest/github/_package-metadata-load.js +45 -0
  117. package/dist/ingest/github/_package-version-page-load.d.ts +1 -1
  118. package/dist/ingest/github/_package-version-page-load.js +8 -5
  119. package/dist/ingest/github/_packages-client.d.ts +1 -1
  120. package/dist/ingest/github/_packages-client.js +21 -4
  121. package/dist/ingest/github/_parallel-paginated-ingest.d.ts +1 -0
  122. package/dist/ingest/github/_parallel-paginated-ingest.js +2 -2
  123. package/dist/ingest/github/_shared.d.ts +1 -1
  124. package/dist/ingest/github/_shared.js +2 -34
  125. package/dist/ingest/github/index.d.ts +4 -0
  126. package/dist/ingest/github/index.js +8 -5
  127. package/package.json +7 -5
  128. package/resources/sql/schema/001_schema.sql +82 -8
  129. package/resources/sql/views/001_v_latest_scan_per_package.sql +2 -2
  130. package/resources/sql/views/003_v_scan_root_manifests.sql +43 -0
  131. package/resources/sql/views/004_v_digest_derived_tag_relations.sql +51 -0
  132. package/resources/sql/views/005_v_cleanup_root_closure_members.sql +100 -0
  133. package/resources/sql/views/006_v_cleanup_blocking_overlaps.sql +42 -0
  134. package/dist/ingest/github/_paginated-ingest.d.ts +0 -11
  135. package/dist/ingest/github/_paginated-ingest.js +0 -28
  136. package/resources/sql/views/003_v_missing_digests_related_manifests.sql +0 -78
  137. package/resources/sql/views/004_v_manifests_related_manifests.sql +0 -142
@@ -7,18 +7,18 @@ export declare class SnapshotRepository {
7
7
  packageName: string;
8
8
  scanCompletedAt: string;
9
9
  };
10
- getTaggedDigests(scanId: number): Set<string>;
11
- getDigestsForTags(scanId: number, tags: string[]): Set<string>;
12
- getChildDigests(scanId: number, parentDigests: Iterable<string>): string[];
13
- getVersionsCreatedBefore(scanId: number, cutoffTimestamp: string): Array<{
14
- versionId: number;
15
- digest: string;
16
- }>;
17
- getTaggedVersionIds(scanId: number): number[];
18
10
  countPackageVersions(scanId: number): number;
19
11
  countTaggedVersions(scanId: number): number;
20
12
  countTags(scanId: number): number;
21
13
  countManifests(scanId: number): number;
22
14
  countManifestEdges(scanId: number): number;
23
- listPackageVersionDigests(scanId: number): string[];
15
+ listManifestDigests(scanId: number): string[];
16
+ listManifestPayloads(scanId: number): Array<{
17
+ digest: string;
18
+ rawJson: string;
19
+ }>;
20
+ listPackageVersionManifestRefs(scanId: number): Array<{
21
+ versionId: number;
22
+ digest: string;
23
+ }>;
24
24
  }
@@ -23,50 +23,6 @@ export class SnapshotRepository {
23
23
  scanCompletedAt: row.scan_completed_at
24
24
  };
25
25
  }
26
- getTaggedDigests(scanId) {
27
- return _getDigestSet(this.#database.prepare("SELECT DISTINCT digest FROM tags WHERE scan_id = ?").all(scanId), "digest");
28
- }
29
- getDigestsForTags(scanId, tags) {
30
- if (tags.length === 0) {
31
- return new Set();
32
- }
33
- const placeholders = tags.map(() => "?").join(", ");
34
- const rows = this.#database
35
- .prepare(`SELECT DISTINCT digest FROM tags WHERE scan_id = ? AND tag IN (${placeholders})`)
36
- .all(scanId, ...tags);
37
- return _getDigestSet(rows, "digest");
38
- }
39
- getChildDigests(scanId, parentDigests) {
40
- const digestList = [...parentDigests];
41
- if (digestList.length === 0) {
42
- return [];
43
- }
44
- const placeholders = digestList.map(() => "?").join(", ");
45
- const rows = this.#database
46
- .prepare(`SELECT child_digest FROM manifest_edges WHERE scan_id = ? AND parent_digest IN (${placeholders})`)
47
- .all(scanId, ...digestList);
48
- return rows.map((row) => row.child_digest);
49
- }
50
- getVersionsCreatedBefore(scanId, cutoffTimestamp) {
51
- const rows = this.#database
52
- .prepare(`
53
- SELECT version_id, digest
54
- FROM package_versions
55
- WHERE scan_id = ? AND created_at < ?
56
- ORDER BY version_id
57
- `)
58
- .all(scanId, cutoffTimestamp);
59
- return rows.map((row) => ({
60
- versionId: row.version_id,
61
- digest: row.digest
62
- }));
63
- }
64
- getTaggedVersionIds(scanId) {
65
- const rows = this.#database
66
- .prepare("SELECT DISTINCT version_id FROM tags WHERE scan_id = ? ORDER BY version_id")
67
- .all(scanId);
68
- return rows.map((row) => row.version_id);
69
- }
70
26
  countPackageVersions(scanId) {
71
27
  return _count(this.#database, "SELECT COUNT(*) AS total FROM package_versions WHERE scan_id = ?", "total", scanId);
72
28
  }
@@ -82,17 +38,49 @@ export class SnapshotRepository {
82
38
  countManifestEdges(scanId) {
83
39
  return _count(this.#database, "SELECT COUNT(*) AS total FROM manifest_edges WHERE scan_id = ?", "total", scanId);
84
40
  }
85
- listPackageVersionDigests(scanId) {
41
+ listManifestDigests(scanId) {
86
42
  const rows = this.#database
87
- .prepare("SELECT digest FROM package_versions WHERE scan_id = ? ORDER BY version_id")
43
+ .prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
88
44
  .all(scanId);
89
45
  return rows.map((row) => row.digest);
90
46
  }
91
- }
92
- function _getDigestSet(rows, key) {
93
- return new Set(rows.map((row) => row[key]));
47
+ listManifestPayloads(scanId) {
48
+ const rows = this.#database
49
+ .prepare(`
50
+ SELECT digest, raw_json
51
+ FROM manifest_payloads
52
+ WHERE scan_id = ?
53
+ ORDER BY digest
54
+ `)
55
+ .all(scanId);
56
+ return rows.map((row) => ({
57
+ digest: row.digest,
58
+ rawJson: row.raw_json
59
+ }));
60
+ }
61
+ listPackageVersionManifestRefs(scanId) {
62
+ const rows = this.#database
63
+ .prepare(`
64
+ SELECT version_id, raw_json
65
+ FROM package_version_payloads
66
+ WHERE scan_id = ?
67
+ ORDER BY version_id
68
+ `)
69
+ .all(scanId);
70
+ return rows.map((row) => ({
71
+ versionId: row.version_id,
72
+ digest: _parsePackageVersionDigest(row.version_id, row.raw_json)
73
+ }));
74
+ }
94
75
  }
95
76
  function _count(database, sql, field, ...params) {
96
77
  const row = database.prepare(sql).get(...params);
97
78
  return row[field];
98
79
  }
80
+ function _parsePackageVersionDigest(versionId, rawJson) {
81
+ const payload = JSON.parse(rawJson);
82
+ if (typeof payload.name !== "string" || payload.name.length === 0) {
83
+ throw new Error(`package version payload for version_id=${versionId} did not include digest name`);
84
+ }
85
+ return payload.name;
86
+ }
@@ -0,0 +1,2 @@
1
+ export declare function buildInClausePlaceholders(valueCount: number): string;
2
+ export declare function buildTuplePlaceholders(rowCount: number, columnCount: number): string;
@@ -0,0 +1,16 @@
1
+ export function buildInClausePlaceholders(valueCount) {
2
+ if (valueCount <= 0) {
3
+ throw new Error("valueCount must be greater than 0");
4
+ }
5
+ return Array.from({ length: valueCount }, () => "?").join(", ");
6
+ }
7
+ export function buildTuplePlaceholders(rowCount, columnCount) {
8
+ if (rowCount <= 0) {
9
+ throw new Error("rowCount must be greater than 0");
10
+ }
11
+ if (columnCount <= 0) {
12
+ throw new Error("columnCount must be greater than 0");
13
+ }
14
+ const tuple = `(${Array.from({ length: columnCount }, () => "?").join(", ")})`;
15
+ return Array.from({ length: rowCount }, () => tuple).join(", ");
16
+ }
@@ -1,4 +1,9 @@
1
1
  import Database from "better-sqlite3";
2
2
  export { ScanWriter } from "./_scan-writer.js";
3
+ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
4
+ export { DbMergeRepository } from "./_db-merge-repository.js";
5
+ export { PlannerRepository } from "./planner/index.js";
3
6
  export { SnapshotRepository } from "./_snapshot-repository.js";
7
+ export type { DeletePlan } from "./planner/index.js";
8
+ export type { DbMergeSourceSummary } from "./_db-merge-repository.js";
4
9
  export declare function openDatabase(databasePath: string): Database.Database;
package/dist/db/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import Database from "better-sqlite3";
2
2
  import { initializeSchema } from "./_schema.js";
3
3
  export { ScanWriter } from "./_scan-writer.js";
4
+ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
5
+ export { DbMergeRepository } from "./_db-merge-repository.js";
6
+ export { PlannerRepository } from "./planner/index.js";
4
7
  export { SnapshotRepository } from "./_snapshot-repository.js";
5
8
  export function openDatabase(databasePath) {
6
9
  const database = new Database(databasePath);
@@ -0,0 +1,7 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ import { type DeletePlanRoot } from "./_planner-types.js";
3
+ export declare class PlannerDeleteTagRootTargets {
4
+ #private;
5
+ constructor(sql: PlannerSql);
6
+ list(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, keepCount?: number, cutoffTimestamp?: string): DeletePlanRoot[];
7
+ }
@@ -0,0 +1,130 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ import { mapPlanRootRow } from "./_planner-types.js";
3
+ export class PlannerDeleteTagRootTargets {
4
+ #sql;
5
+ constructor(sql) {
6
+ this.#sql = sql;
7
+ }
8
+ list(scanId, deleteTags, excludeTags, useRegex, keepCount, cutoffTimestamp) {
9
+ const selectedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "st.tag", deleteTags, useRegex);
10
+ const excludedTagPredicate = excludeTags.length > 0
11
+ ? buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex)
12
+ : undefined;
13
+ const excludedVersionsCte = excludedTagPredicate
14
+ ? `
15
+ excluded_versions AS (
16
+ SELECT DISTINCT xt.version_id
17
+ FROM tags xt
18
+ WHERE xt.scan_id = ?
19
+ AND (${excludedTagPredicate.sql})
20
+ ),
21
+ `
22
+ : "";
23
+ const excludedJoinSql = excludedTagPredicate
24
+ ? `
25
+ LEFT JOIN excluded_versions ev
26
+ ON ev.version_id = st.version_id
27
+ `
28
+ : "";
29
+ const excludedWhereSql = excludedTagPredicate ? "AND ev.version_id IS NULL" : "";
30
+ const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
31
+ const keepSql = keepCount !== undefined ? "WHERE recency_rank > ?" : "";
32
+ const params = [scanId, ...selectedTagPredicate.params];
33
+ if (excludedTagPredicate) {
34
+ params.push(scanId, ...excludedTagPredicate.params);
35
+ }
36
+ if (cutoffTimestamp) {
37
+ params.push(cutoffTimestamp);
38
+ }
39
+ params.push(scanId);
40
+ const tailParams = [keepCount !== undefined ? 1 : 0];
41
+ if (keepCount !== undefined) {
42
+ tailParams.push(keepCount);
43
+ }
44
+ const sql = `
45
+ WITH selected_tags AS (
46
+ SELECT st.scan_id, st.version_id, st.tag
47
+ FROM tags st
48
+ WHERE st.scan_id = ?
49
+ AND (${selectedTagPredicate.sql})
50
+ ),
51
+ ${excludedVersionsCte}
52
+ matched_candidate_roots AS (
53
+ SELECT DISTINCT
54
+ st.version_id AS version_id,
55
+ m.digest AS root_digest,
56
+ m.manifest_kind AS root_manifest_kind,
57
+ pv.created_at
58
+ FROM selected_tags st
59
+ JOIN manifests m
60
+ ON m.scan_id = st.scan_id
61
+ AND m.version_id = st.version_id
62
+ JOIN package_versions pv
63
+ ON pv.scan_id = st.scan_id
64
+ AND pv.version_id = st.version_id
65
+ ${excludedJoinSql}
66
+ WHERE 1 = 1
67
+ ${excludedWhereSql}
68
+ ${cutoffSql}
69
+ AND NOT EXISTS (
70
+ SELECT 1
71
+ FROM manifest_reachability mr
72
+ WHERE mr.scan_id = st.scan_id
73
+ AND mr.descendant_digest = m.digest
74
+ AND mr.min_distance > 0
75
+ )
76
+ ),
77
+ matched_roots AS (
78
+ SELECT
79
+ mcr.version_id,
80
+ mcr.root_digest,
81
+ mcr.root_manifest_kind,
82
+ mcr.created_at,
83
+ COUNT(t.tag) AS total_tag_count,
84
+ COUNT(st.tag) AS matched_tag_count
85
+ FROM matched_candidate_roots mcr
86
+ JOIN tags t
87
+ ON t.scan_id = ?
88
+ AND t.version_id = mcr.version_id
89
+ LEFT JOIN selected_tags st
90
+ ON st.scan_id = t.scan_id
91
+ AND st.version_id = t.version_id
92
+ AND st.tag = t.tag
93
+ GROUP BY mcr.version_id, mcr.root_digest, mcr.root_manifest_kind, mcr.created_at
94
+ HAVING matched_tag_count > 0
95
+ ),
96
+ ranked_roots AS (
97
+ SELECT
98
+ version_id,
99
+ root_digest,
100
+ root_manifest_kind,
101
+ total_tag_count,
102
+ matched_tag_count,
103
+ ROW_NUMBER() OVER (
104
+ ORDER BY created_at DESC, version_id DESC, root_digest DESC
105
+ ) AS recency_rank
106
+ FROM matched_roots
107
+ )
108
+ SELECT
109
+ version_id,
110
+ root_digest,
111
+ root_manifest_kind,
112
+ CASE
113
+ WHEN total_tag_count = matched_tag_count AND ? = 1
114
+ THEN 'keep-n-tagged-overflow'
115
+ WHEN total_tag_count = matched_tag_count
116
+ THEN 'delete-tags-all-tags-selected'
117
+ ELSE 'delete-tags-partial-tag-match'
118
+ END AS direct_target_reason,
119
+ CASE
120
+ WHEN total_tag_count = matched_tag_count
121
+ THEN 'delete-root'
122
+ ELSE 'untag-only'
123
+ END AS selection_mode
124
+ FROM ranked_roots
125
+ ${keepSql}
126
+ ORDER BY root_digest
127
+ `;
128
+ return this.#sql.all(sql, [...params, ...tailParams]).map(mapPlanRootRow);
129
+ }
130
+ }
@@ -0,0 +1,6 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ export declare class PlannerDirectTargetTags {
3
+ #private;
4
+ constructor(sql: PlannerSql);
5
+ listDeleteTagDirectTargetTags(scanId: number, deleteTags: string[], excludeTags: string[], useRegex: boolean, cutoffTimestamp?: string): string[];
6
+ }
@@ -0,0 +1,47 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ import { mapPlanTagRows } from "./_planner-types.js";
3
+ export class PlannerDirectTargetTags {
4
+ #sql;
5
+ constructor(sql) {
6
+ this.#sql = sql;
7
+ }
8
+ listDeleteTagDirectTargetTags(scanId, deleteTags, excludeTags, useRegex, cutoffTimestamp) {
9
+ if (deleteTags.length === 0) {
10
+ return [];
11
+ }
12
+ const selectedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "t.tag", deleteTags, useRegex);
13
+ const params = [scanId, ...selectedTagPredicate.params];
14
+ let excludedRootSql = "";
15
+ let olderThanSql = "";
16
+ if (excludeTags.length > 0) {
17
+ const excludedTagPredicate = buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex);
18
+ excludedRootSql = `
19
+ AND NOT EXISTS (
20
+ SELECT 1
21
+ FROM tags xt
22
+ WHERE xt.scan_id = t.scan_id
23
+ AND xt.version_id = t.version_id
24
+ AND (${excludedTagPredicate.sql})
25
+ )
26
+ `;
27
+ params.push(...excludedTagPredicate.params);
28
+ }
29
+ if (cutoffTimestamp) {
30
+ olderThanSql = "AND pv.created_at < ?";
31
+ params.push(cutoffTimestamp);
32
+ }
33
+ const sql = `
34
+ SELECT DISTINCT tag AS target_tag
35
+ FROM tags t
36
+ JOIN package_versions pv
37
+ ON pv.scan_id = t.scan_id
38
+ AND pv.version_id = t.version_id
39
+ WHERE t.scan_id = ?
40
+ AND (${selectedTagPredicate.sql})
41
+ ${excludedRootSql}
42
+ ${olderThanSql}
43
+ ORDER BY tag
44
+ `;
45
+ return mapPlanTagRows(this.#sql.all(sql, params));
46
+ }
47
+ }
@@ -0,0 +1,7 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ import { type DeletePlanRoot } from "./_planner-types.js";
3
+ export declare class PlannerKeepTaggedRootTargets {
4
+ #private;
5
+ constructor(sql: PlannerSql);
6
+ list(scanId: number, excludeTags: string[], useRegex: boolean, keepCount?: number, cutoffTimestamp?: string): DeletePlanRoot[];
7
+ }
@@ -0,0 +1,74 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ import { mapPlanRootRow } from "./_planner-types.js";
3
+ export class PlannerKeepTaggedRootTargets {
4
+ #sql;
5
+ constructor(sql) {
6
+ this.#sql = sql;
7
+ }
8
+ list(scanId, excludeTags, useRegex, keepCount, cutoffTimestamp) {
9
+ const excludedTagPredicate = excludeTags.length > 0
10
+ ? buildTagSelectorPredicate(this.#sql.database, "xt.tag", excludeTags, useRegex)
11
+ : undefined;
12
+ const excludedRootSql = excludedTagPredicate
13
+ ? `
14
+ AND NOT EXISTS (
15
+ SELECT 1
16
+ FROM tags xt
17
+ WHERE xt.scan_id = pv.scan_id
18
+ AND xt.version_id = pv.version_id
19
+ AND (${excludedTagPredicate.sql})
20
+ )
21
+ `
22
+ : "";
23
+ const cutoffSql = cutoffTimestamp ? "AND pv.created_at < ?" : "";
24
+ const keepSql = keepCount !== undefined ? "WHERE recency_rank > ?" : "";
25
+ const params = [scanId, ...(excludedTagPredicate?.params ?? [])];
26
+ if (cutoffTimestamp) {
27
+ params.push(cutoffTimestamp);
28
+ }
29
+ if (keepCount !== undefined) {
30
+ params.push(keepCount);
31
+ }
32
+ const sql = `
33
+ WITH eligible_tagged_roots AS (
34
+ SELECT
35
+ pv.version_id AS version_id,
36
+ m.digest AS root_digest,
37
+ m.manifest_kind AS root_manifest_kind,
38
+ ROW_NUMBER() OVER (
39
+ ORDER BY pv.created_at DESC, pv.version_id DESC, m.digest DESC
40
+ ) AS recency_rank
41
+ FROM package_versions pv
42
+ JOIN manifests m
43
+ ON m.scan_id = pv.scan_id
44
+ AND m.version_id = pv.version_id
45
+ WHERE pv.scan_id = ?
46
+ AND EXISTS (
47
+ SELECT 1
48
+ FROM tags t
49
+ WHERE t.scan_id = pv.scan_id
50
+ AND t.version_id = pv.version_id
51
+ )
52
+ ${excludedRootSql}
53
+ AND NOT EXISTS (
54
+ SELECT 1
55
+ FROM manifest_reachability mr
56
+ WHERE mr.scan_id = pv.scan_id
57
+ AND mr.descendant_digest = m.digest
58
+ AND mr.min_distance > 0
59
+ )
60
+ ${cutoffSql}
61
+ )
62
+ SELECT
63
+ version_id,
64
+ root_digest,
65
+ root_manifest_kind,
66
+ 'keep-n-tagged-overflow' AS direct_target_reason,
67
+ 'delete-root' AS selection_mode
68
+ FROM eligible_tagged_roots
69
+ ${keepSql}
70
+ ORDER BY root_digest
71
+ `;
72
+ return this.#sql.all(sql, params).map(mapPlanRootRow);
73
+ }
74
+ }
@@ -0,0 +1,5 @@
1
+ import type { DeletePlan, DeletePlanBlockedRoot, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, PlanArtifacts } from "./_planner-types.js";
2
+ export declare function buildPlanOutputs(directTargetTags: string[], directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): Pick<DeletePlan, "validationSummary" | "directTargetTags" | "directTargetRoots" | "rootDecisions" | "protectedRoots" | "closureManifests" | "blockedRoots" | "fullyDeletableRoots" | "collateralTags">;
3
+ export declare function buildRootDecisions(directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): DeletePlanRootDecision[];
4
+ export declare function buildProtectedRoots(blockedRoots: DeletePlanBlockedRoot[]): DeletePlanProtectedRoot[];
5
+ export declare function buildBlockedValidationReason(blockedRoot?: DeletePlanBlockedRoot): string;
@@ -0,0 +1,101 @@
1
+ export function buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts) {
2
+ const rootDecisions = buildRootDecisions(directTargetRoots, planArtifacts);
3
+ const protectedRoots = buildProtectedRoots(planArtifacts.blockedRoots);
4
+ const deleteRootCandidateCount = directTargetRoots.filter((root) => root.selectionMode === "delete-root").length;
5
+ const untagOnlyRootCount = directTargetRoots.length - deleteRootCandidateCount;
6
+ return {
7
+ validationSummary: {
8
+ directTargetTagCount: directTargetTags.length,
9
+ directTargetRootCount: directTargetRoots.length,
10
+ deleteRootCandidateCount,
11
+ untagOnlyRootCount,
12
+ fullyDeletableRootCount: planArtifacts.fullyDeletableRoots.length,
13
+ blockedDeleteRootCount: rootDecisions.filter((decision) => decision.validationStatus === "blocked").length,
14
+ protectedRootCount: protectedRoots.length
15
+ },
16
+ directTargetTags,
17
+ directTargetRoots,
18
+ rootDecisions,
19
+ protectedRoots,
20
+ closureManifests: planArtifacts.closureManifests,
21
+ blockedRoots: planArtifacts.blockedRoots,
22
+ fullyDeletableRoots: planArtifacts.fullyDeletableRoots,
23
+ collateralTags: []
24
+ };
25
+ }
26
+ export function buildRootDecisions(directTargetRoots, planArtifacts) {
27
+ const fullyDeletableDigests = new Set(planArtifacts.fullyDeletableRoots.map((root) => root.digest));
28
+ const blockedRootByDigest = new Map();
29
+ for (const blockedRoot of planArtifacts.blockedRoots) {
30
+ if (!blockedRootByDigest.has(blockedRoot.blockedDigest)) {
31
+ blockedRootByDigest.set(blockedRoot.blockedDigest, blockedRoot);
32
+ }
33
+ }
34
+ return directTargetRoots.map((root) => {
35
+ if (root.selectionMode === "untag-only") {
36
+ return {
37
+ versionId: root.versionId,
38
+ digest: root.digest,
39
+ manifestKind: root.manifestKind,
40
+ selectionMode: root.selectionMode,
41
+ selectionReason: root.reason,
42
+ validationStatus: "untag-only",
43
+ validationReasonCode: "untag-only-partial-tag-match",
44
+ validationReason: "matched tags cover only part of this root's tag set, so the version is retained and only those tags can be detached"
45
+ };
46
+ }
47
+ if (fullyDeletableDigests.has(root.digest)) {
48
+ return {
49
+ versionId: root.versionId,
50
+ digest: root.digest,
51
+ manifestKind: root.manifestKind,
52
+ selectionMode: root.selectionMode,
53
+ selectionReason: root.reason,
54
+ validationStatus: "fully-deletable",
55
+ validationReasonCode: "fully-deletable-no-retained-overlap",
56
+ validationReason: "selected tags cover the whole root and its manifest closure does not overlap any retained root"
57
+ };
58
+ }
59
+ const blockedRoot = blockedRootByDigest.get(root.digest);
60
+ return {
61
+ versionId: root.versionId,
62
+ digest: root.digest,
63
+ manifestKind: root.manifestKind,
64
+ selectionMode: root.selectionMode,
65
+ selectionReason: root.reason,
66
+ validationStatus: "blocked",
67
+ validationReasonCode: "blocked-overlap-with-retained-root",
68
+ validationReason: buildBlockedValidationReason(blockedRoot),
69
+ blockingVersionId: blockedRoot?.blockingVersionId,
70
+ blockingDigest: blockedRoot?.blockingDigest,
71
+ overlapDigest: blockedRoot?.overlapDigest,
72
+ overlapManifestKind: blockedRoot?.overlapManifestKind
73
+ };
74
+ });
75
+ }
76
+ export function buildProtectedRoots(blockedRoots) {
77
+ const protectedRoots = new Map();
78
+ for (const blockedRoot of blockedRoots) {
79
+ const key = `${blockedRoot.blockingVersionId}:${blockedRoot.blockingDigest}`;
80
+ const current = protectedRoots.get(key) ?? {
81
+ versionId: blockedRoot.blockingVersionId,
82
+ digest: blockedRoot.blockingDigest,
83
+ blocks: []
84
+ };
85
+ current.blocks.push({
86
+ blockedVersionId: blockedRoot.blockedVersionId,
87
+ blockedDigest: blockedRoot.blockedDigest,
88
+ blockReasonCode: blockedRoot.reason,
89
+ overlapDigest: blockedRoot.overlapDigest,
90
+ overlapManifestKind: blockedRoot.overlapManifestKind
91
+ });
92
+ protectedRoots.set(key, current);
93
+ }
94
+ return [...protectedRoots.values()].sort((left, right) => left.digest.localeCompare(right.digest));
95
+ }
96
+ export function buildBlockedValidationReason(blockedRoot) {
97
+ if (!blockedRoot) {
98
+ return "root closure overlaps manifest members still required by a retained root";
99
+ }
100
+ return `blocked because retained root ${blockedRoot.blockingDigest} still requires shared manifest ${blockedRoot.overlapDigest}`;
101
+ }
@@ -0,0 +1,7 @@
1
+ import { PlannerSql } from "./_planner-sql.js";
2
+ import { type DeletePlanRoot, type PlanArtifacts } from "./_planner-types.js";
3
+ export declare class PlannerPlanArtifacts {
4
+ #private;
5
+ constructor(sql: PlannerSql);
6
+ build(scanId: number, directTargetRoots: DeletePlanRoot[]): PlanArtifacts;
7
+ }