ghcr-manager 0.9.4 → 0.9.6

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 (32) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +9 -9
  3. package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -6
  4. package/dist/cleanup-summary/_cleanup-summary.d.ts +8 -4
  5. package/dist/cleanup-summary/_cleanup-summary.js +8 -4
  6. package/dist/cleanup-summary/index.d.ts +1 -1
  7. package/dist/cli/_cleanup-command.js +22 -2
  8. package/dist/cli/_tag-selector-resolver.js +2 -2
  9. package/dist/core/_digest-tag.d.ts +2 -0
  10. package/dist/core/_digest-tag.js +12 -0
  11. package/dist/core/_types.d.ts +2 -1
  12. package/dist/core/index.d.ts +2 -1
  13. package/dist/core/index.js +1 -0
  14. package/dist/db/_cleanup-run-writer.js +69 -50
  15. package/dist/db/_db-merge-cleanup-copy.js +128 -80
  16. package/dist/db/_db-merge-scan-copy.js +1 -1
  17. package/dist/db/_manifest-reachability.js +25 -0
  18. package/dist/db/_scan-writer.js +118 -116
  19. package/dist/db/index.d.ts +1 -1
  20. package/dist/db/planner/_planner-direct-target-roots.js +2 -0
  21. package/dist/db/planner/_planner-direct-target-tags.js +5 -0
  22. package/dist/db/planner/_planner-output.d.ts +1 -1
  23. package/dist/db/planner/_planner-output.js +0 -11
  24. package/dist/db/planner/_planner-repository.d.ts +1 -1
  25. package/dist/db/planner/_planner-types.d.ts +12 -18
  26. package/dist/db/planner/index.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/resources/sql/schema/001_schema.sql +20 -0
  29. package/resources/sql/views/003_v_scan_root_manifests.sql +1 -0
  30. package/resources/sql/views/{004_v_digest_derived_tag_relations.sql → 004_v_digest_tag_relations.sql} +7 -8
  31. package/resources/sql/views/005_v_cleanup_root_closure_members.sql +2 -1
  32. package/resources/sql/views/007_v_cleanup_root_decision_readable.sql +67 -0
@@ -1,7 +1,30 @@
1
1
  export class DbMergeCleanupCopy {
2
2
  #database;
3
+ #insertCleanupRunStatement;
4
+ #copyRootDecisionsStatementByAttachName = new Map();
5
+ #copySelectedTagsStatementByAttachName = new Map();
6
+ #copyProtectedRootBlocksStatementByAttachName = new Map();
7
+ #listCleanupUuidsStatementByTableName = new Map();
3
8
  constructor(database) {
4
9
  this.#database = database;
10
+ this.#insertCleanupRunStatement = this.#database.prepare(`
11
+ INSERT INTO cleanup_runs(
12
+ scan_id,
13
+ cleanup_uuid,
14
+ cleanup_started_at,
15
+ github_actions_run_url,
16
+ dry_run,
17
+ planner_inputs_json,
18
+ direct_target_tag_count,
19
+ direct_target_root_count,
20
+ delete_root_candidate_count,
21
+ untag_only_root_count,
22
+ fully_deletable_root_count,
23
+ blocked_delete_root_count,
24
+ protected_root_count
25
+ )
26
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
+ `);
5
28
  }
6
29
  copyCleanupRuns(attachName, sourceScanId, targetScanId, existingCleanupUuids) {
7
30
  const rows = this.#database
@@ -31,92 +54,117 @@ export class DbMergeCleanupCopy {
31
54
  if (knownCleanupUuids.has(row.cleanup_uuid)) {
32
55
  continue;
33
56
  }
34
- const cleanupRunId = Number(this.#database
35
- .prepare(`
36
- INSERT INTO cleanup_runs(
37
- scan_id,
38
- cleanup_uuid,
39
- cleanup_started_at,
40
- github_actions_run_url,
41
- dry_run,
42
- planner_inputs_json,
43
- direct_target_tag_count,
44
- direct_target_root_count,
45
- delete_root_candidate_count,
46
- untag_only_root_count,
47
- fully_deletable_root_count,
48
- blocked_delete_root_count,
49
- protected_root_count
50
- )
51
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
52
- `)
53
- .run(targetScanId, row.cleanup_uuid, row.cleanup_started_at, row.github_actions_run_url, row.dry_run, row.planner_inputs_json, row.direct_target_tag_count, row.direct_target_root_count, row.delete_root_candidate_count, row.untag_only_root_count, row.fully_deletable_root_count, row.blocked_delete_root_count, row.protected_root_count).lastInsertRowid);
54
- this.#database
55
- .prepare(`
56
- INSERT INTO cleanup_root_decisions(
57
- cleanup_run_id,
58
- scan_id,
59
- digest,
60
- selection_mode,
61
- selection_reason,
62
- validation_status,
63
- validation_reason_code,
64
- validation_reason,
65
- blocking_digest,
66
- overlap_digest
67
- )
68
- SELECT
69
- ?,
70
- ?,
71
- digest,
72
- selection_mode,
73
- selection_reason,
74
- validation_status,
75
- validation_reason_code,
76
- validation_reason,
77
- blocking_digest,
78
- overlap_digest
79
- FROM ${attachName}.cleanup_root_decisions
80
- WHERE cleanup_run_id = ?
81
- AND scan_id = ?
82
- `)
83
- .run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
84
- this.#database
85
- .prepare(`
86
- INSERT INTO cleanup_protected_root_blocks(
87
- cleanup_run_id,
88
- scan_id,
89
- protected_digest,
90
- blocked_digest,
91
- block_reason_code,
92
- overlap_digest
93
- )
94
- SELECT
95
- ?,
96
- ?,
97
- protected_digest,
98
- blocked_digest,
99
- block_reason_code,
100
- overlap_digest
101
- FROM ${attachName}.cleanup_protected_root_blocks
102
- WHERE cleanup_run_id = ?
103
- AND scan_id = ?
104
- `)
105
- .run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
57
+ const cleanupRunId = Number(this.#insertCleanupRunStatement.run(targetScanId, row.cleanup_uuid, row.cleanup_started_at, row.github_actions_run_url, row.dry_run, row.planner_inputs_json, row.direct_target_tag_count, row.direct_target_root_count, row.delete_root_candidate_count, row.untag_only_root_count, row.fully_deletable_root_count, row.blocked_delete_root_count, row.protected_root_count).lastInsertRowid);
58
+ this.#copyRootDecisionsStatement(attachName).run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
59
+ this.#copySelectedTagsStatement(attachName).run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
60
+ this.#copyProtectedRootBlocksStatement(attachName).run(cleanupRunId, targetScanId, row.cleanup_run_id, sourceScanId);
106
61
  knownCleanupUuids.add(row.cleanup_uuid);
107
62
  importedCleanupRunCount += 1;
108
63
  }
109
64
  return importedCleanupRunCount;
110
65
  }
111
66
  listCleanupUuids(tableName, scanId) {
112
- const rows = this.#database
113
- .prepare(`
114
- SELECT cleanup_uuid
115
- FROM ${tableName}
116
- WHERE scan_id = ?
117
- ORDER BY cleanup_run_id
118
- `)
119
- .all(scanId);
67
+ const rows = this.#listCleanupUuidsStatement(tableName).all(scanId);
120
68
  return rows.map((row) => row.cleanup_uuid);
121
69
  }
70
+ #copyRootDecisionsStatement(attachName) {
71
+ const cached = this.#copyRootDecisionsStatementByAttachName.get(attachName);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ const statement = this.#database.prepare(`
76
+ INSERT INTO cleanup_root_decisions(
77
+ cleanup_run_id,
78
+ scan_id,
79
+ digest,
80
+ selection_mode,
81
+ selection_reason,
82
+ validation_status,
83
+ validation_reason_code,
84
+ validation_reason,
85
+ blocking_digest,
86
+ overlap_digest
87
+ )
88
+ SELECT
89
+ ?,
90
+ ?,
91
+ digest,
92
+ selection_mode,
93
+ selection_reason,
94
+ validation_status,
95
+ validation_reason_code,
96
+ validation_reason,
97
+ blocking_digest,
98
+ overlap_digest
99
+ FROM ${attachName}.cleanup_root_decisions
100
+ WHERE cleanup_run_id = ?
101
+ AND scan_id = ?
102
+ `);
103
+ this.#copyRootDecisionsStatementByAttachName.set(attachName, statement);
104
+ return statement;
105
+ }
106
+ #copySelectedTagsStatement(attachName) {
107
+ const cached = this.#copySelectedTagsStatementByAttachName.get(attachName);
108
+ if (cached) {
109
+ return cached;
110
+ }
111
+ const statement = this.#database.prepare(`
112
+ INSERT INTO cleanup_selected_tags(
113
+ cleanup_run_id,
114
+ scan_id,
115
+ tag
116
+ )
117
+ SELECT
118
+ ?,
119
+ ?,
120
+ tag
121
+ FROM ${attachName}.cleanup_selected_tags
122
+ WHERE cleanup_run_id = ?
123
+ AND scan_id = ?
124
+ `);
125
+ this.#copySelectedTagsStatementByAttachName.set(attachName, statement);
126
+ return statement;
127
+ }
128
+ #copyProtectedRootBlocksStatement(attachName) {
129
+ const cached = this.#copyProtectedRootBlocksStatementByAttachName.get(attachName);
130
+ if (cached) {
131
+ return cached;
132
+ }
133
+ const statement = this.#database.prepare(`
134
+ INSERT INTO cleanup_protected_root_blocks(
135
+ cleanup_run_id,
136
+ scan_id,
137
+ protected_digest,
138
+ blocked_digest,
139
+ block_reason_code,
140
+ overlap_digest
141
+ )
142
+ SELECT
143
+ ?,
144
+ ?,
145
+ protected_digest,
146
+ blocked_digest,
147
+ block_reason_code,
148
+ overlap_digest
149
+ FROM ${attachName}.cleanup_protected_root_blocks
150
+ WHERE cleanup_run_id = ?
151
+ AND scan_id = ?
152
+ `);
153
+ this.#copyProtectedRootBlocksStatementByAttachName.set(attachName, statement);
154
+ return statement;
155
+ }
156
+ #listCleanupUuidsStatement(tableName) {
157
+ const cached = this.#listCleanupUuidsStatementByTableName.get(tableName);
158
+ if (cached) {
159
+ return cached;
160
+ }
161
+ const statement = this.#database.prepare(`
162
+ SELECT cleanup_uuid
163
+ FROM ${tableName}
164
+ WHERE scan_id = ?
165
+ ORDER BY cleanup_run_id
166
+ `);
167
+ this.#listCleanupUuidsStatementByTableName.set(tableName, statement);
168
+ return statement;
169
+ }
122
170
  }
@@ -25,7 +25,7 @@ export class DbMergeScanCopy {
25
25
  const copySpecs = [
26
26
  "package_versions(scan_id, version_id, created_at, updated_at)",
27
27
  "package_version_payloads(scan_id, version_id, raw_json)",
28
- "tags(scan_id, tag, version_id)",
28
+ "tags(scan_id, tag, version_id, is_digest_tag)",
29
29
  "manifests(scan_id, version_id, digest, media_type, artifact_type, config_media_type, subject_digest, annotations_json, platform_os, platform_architecture, platform_variant, manifest_kind)",
30
30
  "manifest_descriptors(scan_id, parent_digest, child_digest, media_type, artifact_type, platform_os, platform_architecture, platform_variant)",
31
31
  "manifest_payloads(scan_id, digest, raw_json)",
@@ -1,4 +1,5 @@
1
1
  export function rebuildManifestReachability(database, scanId) {
2
+ _refreshDigestTagEdges(database, scanId);
2
3
  const manifestDigests = _loadManifestDigests(database, scanId);
3
4
  const childDigestsByParent = new Map();
4
5
  const parentDigestsByChild = new Map();
@@ -70,6 +71,30 @@ export function rebuildManifestReachability(database, scanId) {
70
71
  });
71
72
  rebuild();
72
73
  }
74
+ function _refreshDigestTagEdges(database, scanId) {
75
+ database.prepare("DELETE FROM manifest_edges WHERE scan_id = ? AND edge_kind = 'digest-tag-referrer'").run(scanId);
76
+ database
77
+ .prepare(`
78
+ INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
79
+ SELECT
80
+ t.scan_id,
81
+ 'sha256:' || SUBSTR(t.tag, 8, 64) AS parent_digest,
82
+ m.digest AS child_digest,
83
+ 'digest-tag-referrer' AS edge_kind
84
+ FROM tags t
85
+ JOIN manifests m
86
+ ON m.scan_id = t.scan_id
87
+ AND m.version_id = t.version_id
88
+ JOIN manifests parent_manifest
89
+ ON parent_manifest.scan_id = t.scan_id
90
+ AND parent_manifest.digest = 'sha256:' || SUBSTR(t.tag, 8, 64)
91
+ WHERE t.scan_id = ?
92
+ AND t.tag LIKE 'sha256-%'
93
+ AND LENGTH(t.tag) >= 71
94
+ AND SUBSTR(t.tag, 8, 64) NOT GLOB '*[^0-9A-Fa-f]*'
95
+ `)
96
+ .run(scanId);
97
+ }
73
98
  function _loadManifestDigests(database, scanId) {
74
99
  const rows = database
75
100
  .prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
@@ -1,55 +1,130 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { isDigestTag } from "../core/index.js";
2
3
  import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
3
4
  import { rebuildManifestReachability } from "./_manifest-reachability.js";
4
5
  export class ScanWriter {
5
6
  #database;
7
+ #startScanStatement;
8
+ #markScanCompletedStatement;
9
+ #markScanFailedStatement;
10
+ #insertPackageVersionStatement;
11
+ #insertPackageVersionPayloadStatement;
12
+ #insertTagStatement;
13
+ #insertManifestStatement;
14
+ #insertManifestPayloadStatement;
15
+ #insertManifestDescriptorStatement;
16
+ #insertManifestEdgeStatement;
6
17
  #activeScanId = null;
7
18
  constructor(database) {
8
19
  this.#database = database;
20
+ this.#startScanStatement = this.#database.prepare(`
21
+ INSERT INTO package_scans(
22
+ scan_uuid,
23
+ owner,
24
+ package_name,
25
+ package_metadata_json,
26
+ github_actions_run_url,
27
+ scan_started_at,
28
+ scan_completed_at,
29
+ status
30
+ )
31
+ VALUES(?, ?, ?, ?, ?, ?, NULL, 'running')
32
+ `);
33
+ this.#markScanCompletedStatement = this.#database.prepare(`
34
+ UPDATE package_scans
35
+ SET scan_completed_at = ?, status = 'completed'
36
+ WHERE scan_id = ?
37
+ `);
38
+ this.#markScanFailedStatement = this.#database.prepare(`
39
+ UPDATE package_scans
40
+ SET scan_completed_at = ?, status = 'failed'
41
+ WHERE scan_id = ?
42
+ `);
43
+ this.#insertPackageVersionStatement = this.#database.prepare(`
44
+ INSERT OR REPLACE INTO package_versions(scan_id, version_id, created_at, updated_at)
45
+ VALUES(@scanId, @versionId, @createdAt, @updatedAt)
46
+ `);
47
+ this.#insertPackageVersionPayloadStatement = this.#database.prepare(`
48
+ INSERT OR REPLACE INTO package_version_payloads(scan_id, version_id, raw_json)
49
+ VALUES(?, ?, ?)
50
+ `);
51
+ this.#insertTagStatement = this.#database.prepare(`
52
+ INSERT OR REPLACE INTO tags(scan_id, tag, version_id, is_digest_tag)
53
+ VALUES(@scanId, @tag, @versionId, @isDigestTag)
54
+ `);
55
+ this.#insertManifestStatement = this.#database.prepare(`
56
+ INSERT OR REPLACE INTO manifests(
57
+ scan_id,
58
+ version_id,
59
+ digest,
60
+ media_type,
61
+ artifact_type,
62
+ config_media_type,
63
+ subject_digest,
64
+ annotations_json,
65
+ platform_os,
66
+ platform_architecture,
67
+ platform_variant,
68
+ manifest_kind
69
+ )
70
+ VALUES(
71
+ @scanId,
72
+ @versionId,
73
+ @digest,
74
+ @mediaType,
75
+ @artifactType,
76
+ @configMediaType,
77
+ @subjectDigest,
78
+ @annotationsJson,
79
+ @platformOs,
80
+ @platformArchitecture,
81
+ @platformVariant,
82
+ @manifestKind
83
+ )
84
+ `);
85
+ this.#insertManifestPayloadStatement = this.#database.prepare(`
86
+ INSERT OR REPLACE INTO manifest_payloads(scan_id, digest, raw_json)
87
+ VALUES(?, ?, ?)
88
+ `);
89
+ this.#insertManifestDescriptorStatement = this.#database.prepare(`
90
+ INSERT OR REPLACE INTO manifest_descriptors(
91
+ scan_id,
92
+ parent_digest,
93
+ child_digest,
94
+ media_type,
95
+ artifact_type,
96
+ platform_os,
97
+ platform_architecture,
98
+ platform_variant
99
+ )
100
+ VALUES(
101
+ @scanId,
102
+ @parentDigest,
103
+ @childDigest,
104
+ @mediaType,
105
+ @artifactType,
106
+ @platformOs,
107
+ @platformArchitecture,
108
+ @platformVariant
109
+ )
110
+ `);
111
+ this.#insertManifestEdgeStatement = this.#database.prepare(`
112
+ INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
113
+ VALUES(@scanId, @parentDigest, @childDigest, @edgeKind)
114
+ `);
9
115
  }
10
116
  startScan(owner, packageName, scanStartedAt, packageMetadata) {
11
- const result = this.#database
12
- .prepare(`
13
- INSERT INTO package_scans(
14
- scan_uuid,
15
- owner,
16
- package_name,
17
- package_metadata_json,
18
- github_actions_run_url,
19
- scan_started_at,
20
- scan_completed_at,
21
- status
22
- )
23
- VALUES(?, ?, ?, ?, ?, ?, NULL, 'running')
24
- `)
25
- .run(randomUUID(), owner, packageName, packageMetadata.rawJson, resolveGitHubActionsRunUrl(), scanStartedAt);
117
+ const result = this.#startScanStatement.run(randomUUID(), owner, packageName, packageMetadata.rawJson, resolveGitHubActionsRunUrl(), scanStartedAt);
26
118
  this.#activeScanId = Number(result.lastInsertRowid);
27
119
  }
28
120
  markScanCompleted(scanCompletedAt) {
29
- this.#database
30
- .prepare(`
31
- UPDATE package_scans
32
- SET scan_completed_at = ?, status = 'completed'
33
- WHERE scan_id = ?
34
- `)
35
- .run(scanCompletedAt, this.#requireScanId());
121
+ this.#markScanCompletedStatement.run(scanCompletedAt, this.#requireScanId());
36
122
  }
37
123
  markScanFailed(scanCompletedAt) {
38
- this.#database
39
- .prepare(`
40
- UPDATE package_scans
41
- SET scan_completed_at = ?, status = 'failed'
42
- WHERE scan_id = ?
43
- `)
44
- .run(scanCompletedAt, this.#requireScanId());
124
+ this.#markScanFailedStatement.run(scanCompletedAt, this.#requireScanId());
45
125
  }
46
126
  insertPackageVersion(version) {
47
- this.#database
48
- .prepare(`
49
- INSERT OR REPLACE INTO package_versions(scan_id, version_id, created_at, updated_at)
50
- VALUES(@scanId, @versionId, @createdAt, @updatedAt)
51
- `)
52
- .run({
127
+ this.#insertPackageVersionStatement.run({
53
128
  scanId: this.#requireScanId(),
54
129
  versionId: version.versionId,
55
130
  createdAt: version.createdAt,
@@ -57,57 +132,17 @@ export class ScanWriter {
57
132
  });
58
133
  }
59
134
  insertPackageVersionPayload(versionId, rawJson) {
60
- this.#database
61
- .prepare(`
62
- INSERT OR REPLACE INTO package_version_payloads(scan_id, version_id, raw_json)
63
- VALUES(?, ?, ?)
64
- `)
65
- .run(this.#requireScanId(), versionId, rawJson);
135
+ this.#insertPackageVersionPayloadStatement.run(this.#requireScanId(), versionId, rawJson);
66
136
  }
67
137
  insertTag(tag) {
68
- this.#database
69
- .prepare(`
70
- INSERT OR REPLACE INTO tags(scan_id, tag, version_id)
71
- VALUES(@scanId, @tag, @versionId)
72
- `)
73
- .run({
138
+ this.#insertTagStatement.run({
74
139
  scanId: this.#requireScanId(),
75
- ...tag
140
+ ...tag,
141
+ isDigestTag: isDigestTag(tag.tag) ? 1 : 0
76
142
  });
77
143
  }
78
144
  insertManifest(manifest) {
79
- this.#database
80
- .prepare(`
81
- INSERT OR REPLACE INTO manifests(
82
- scan_id,
83
- version_id,
84
- digest,
85
- media_type,
86
- artifact_type,
87
- config_media_type,
88
- subject_digest,
89
- annotations_json,
90
- platform_os,
91
- platform_architecture,
92
- platform_variant,
93
- manifest_kind
94
- )
95
- VALUES(
96
- @scanId,
97
- @versionId,
98
- @digest,
99
- @mediaType,
100
- @artifactType,
101
- @configMediaType,
102
- @subjectDigest,
103
- @annotationsJson,
104
- @platformOs,
105
- @platformArchitecture,
106
- @platformVariant,
107
- @manifestKind
108
- )
109
- `)
110
- .run({
145
+ this.#insertManifestStatement.run({
111
146
  scanId: this.#requireScanId(),
112
147
  versionId: manifest.versionId,
113
148
  digest: manifest.digest,
@@ -123,38 +158,10 @@ export class ScanWriter {
123
158
  });
124
159
  }
125
160
  insertManifestPayload(digest, rawJson) {
126
- this.#database
127
- .prepare(`
128
- INSERT OR REPLACE INTO manifest_payloads(scan_id, digest, raw_json)
129
- VALUES(?, ?, ?)
130
- `)
131
- .run(this.#requireScanId(), digest, rawJson);
161
+ this.#insertManifestPayloadStatement.run(this.#requireScanId(), digest, rawJson);
132
162
  }
133
163
  insertManifestDescriptor(descriptor) {
134
- this.#database
135
- .prepare(`
136
- INSERT OR REPLACE INTO manifest_descriptors(
137
- scan_id,
138
- parent_digest,
139
- child_digest,
140
- media_type,
141
- artifact_type,
142
- platform_os,
143
- platform_architecture,
144
- platform_variant
145
- )
146
- VALUES(
147
- @scanId,
148
- @parentDigest,
149
- @childDigest,
150
- @mediaType,
151
- @artifactType,
152
- @platformOs,
153
- @platformArchitecture,
154
- @platformVariant
155
- )
156
- `)
157
- .run({
164
+ this.#insertManifestDescriptorStatement.run({
158
165
  scanId: this.#requireScanId(),
159
166
  parentDigest: descriptor.parentDigest,
160
167
  childDigest: descriptor.childDigest,
@@ -166,12 +173,7 @@ export class ScanWriter {
166
173
  });
167
174
  }
168
175
  insertManifestEdge(edge) {
169
- this.#database
170
- .prepare(`
171
- INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
172
- VALUES(@scanId, @parentDigest, @childDigest, @edgeKind)
173
- `)
174
- .run({
176
+ this.#insertManifestEdgeStatement.run({
175
177
  scanId: this.#requireScanId(),
176
178
  ...edge
177
179
  });
@@ -4,6 +4,6 @@ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
4
4
  export { DbMergeRepository } from "./_db-merge-repository.js";
5
5
  export { PlannerRepository } from "./planner/index.js";
6
6
  export { SnapshotRepository } from "./_snapshot-repository.js";
7
- export type { DeletePlan } from "./planner/index.js";
7
+ export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanSelectionMode, DeletePlanSelectionReason } from "./planner/index.js";
8
8
  export type { DbMergeSourceSummary } from "./_db-merge-repository.js";
9
9
  export declare function openDatabase(databasePath: string): Database.Database;
@@ -22,6 +22,7 @@ export class PlannerDirectTargetRoots {
22
22
  SELECT DISTINCT t.version_id, t.tag
23
23
  FROM tags t
24
24
  WHERE t.scan_id = ?
25
+ AND t.is_digest_tag = 0
25
26
  AND (${selectedTagPredicate.sql})
26
27
  `
27
28
  : `
@@ -36,6 +37,7 @@ export class PlannerDirectTargetRoots {
36
37
  SELECT DISTINCT xt.version_id
37
38
  FROM tags xt
38
39
  WHERE xt.scan_id = ?
40
+ AND xt.is_digest_tag = 0
39
41
  AND (${excludedTagPredicate.sql})
40
42
  `
41
43
  : `
@@ -36,7 +36,12 @@ export class PlannerDirectTargetTags {
36
36
  JOIN package_versions pv
37
37
  ON pv.scan_id = t.scan_id
38
38
  AND pv.version_id = t.version_id
39
+ JOIN v_scan_root_manifests roots
40
+ ON roots.scan_id = t.scan_id
41
+ AND roots.root_version_id = t.version_id
39
42
  WHERE t.scan_id = ?
43
+ AND t.is_digest_tag = 0
44
+ AND roots.has_ancestor = 0
40
45
  AND (${selectedTagPredicate.sql})
41
46
  ${excludedRootSql}
42
47
  ${olderThanSql}
@@ -1,5 +1,5 @@
1
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">;
2
+ export declare function buildPlanOutputs(directTargetTags: string[], directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): Pick<DeletePlan, "directTargetTags" | "directTargetRoots" | "rootDecisions" | "protectedRoots" | "closureManifests" | "blockedRoots" | "fullyDeletableRoots" | "collateralTags">;
3
3
  export declare function buildRootDecisions(directTargetRoots: DeletePlanRoot[], planArtifacts: PlanArtifacts): DeletePlanRootDecision[];
4
4
  export declare function buildProtectedRoots(blockedRoots: DeletePlanBlockedRoot[]): DeletePlanProtectedRoot[];
5
5
  export declare function buildBlockedValidationReason(blockedRoot?: DeletePlanBlockedRoot): string;
@@ -1,18 +1,7 @@
1
1
  export function buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts) {
2
2
  const rootDecisions = buildRootDecisions(directTargetRoots, planArtifacts);
3
3
  const protectedRoots = buildProtectedRoots(planArtifacts.blockedRoots);
4
- const deleteRootCandidateCount = directTargetRoots.filter((root) => root.selectionMode === "delete-root").length;
5
- const untagOnlyRootCount = directTargetRoots.length - deleteRootCandidateCount;
6
4
  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
5
  directTargetTags,
17
6
  directTargetRoots,
18
7
  rootDecisions,
@@ -1,6 +1,6 @@
1
1
  import type Database from "better-sqlite3";
2
2
  import type { DeletePlan, PlannerLogger } from "./_planner-types.js";
3
- export type { DeletePlan, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision } from "./_planner-types.js";
3
+ export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanBlockedRoot, DeletePlanClosureManifest, DeletePlanProtectedRoot, DeletePlanRoot, DeletePlanRootDecision, DeletePlanSelectionMode, DeletePlanSelectionReason } from "./_planner-types.js";
4
4
  export declare class PlannerRepository {
5
5
  #private;
6
6
  constructor(database: Database.Database, logger?: PlannerLogger);