@lesliechan721/aw 0.1.0-beta.10 → 0.1.0-beta.11

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 (218) hide show
  1. package/dist/authoring/assets/aw-config.d.mts +1 -50
  2. package/dist/authoring/assets/aw-config.mjs +58 -928
  3. package/dist/authoring/assets/aw-config.mjs.map +1 -1
  4. package/dist/authoring/scripts/init-plugin.mjs +28 -33
  5. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  6. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +1 -0
  7. package/dist/authoring/scripts/lib/plugin-version-check.mjs +229 -8
  8. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  9. package/dist/authoring/scripts/validate-plugin.mjs +165 -45
  10. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  11. package/dist/scripts/bundle-plugins.js +18 -4
  12. package/dist/scripts/bundle-plugins.js.map +1 -1
  13. package/dist/scripts/test-plugin-scripts.d.ts +2 -0
  14. package/dist/scripts/test-plugin-scripts.js +21 -5
  15. package/dist/scripts/test-plugin-scripts.js.map +1 -1
  16. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  17. package/dist/src/commands/aw-capabilities.js +1 -1
  18. package/dist/src/commands/aw-capabilities.js.map +1 -1
  19. package/dist/src/commands/catalog-dev-source.d.ts +6 -3
  20. package/dist/src/commands/catalog-dev-source.js +96 -35
  21. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  22. package/dist/src/commands/catalog-publication-inspection.d.ts +1 -1
  23. package/dist/src/commands/catalog-publication-inspection.js +54 -8
  24. package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
  25. package/dist/src/commands/catalog-publication-journal.js +1 -1
  26. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  27. package/dist/src/commands/catalog-source.d.ts +19 -4
  28. package/dist/src/commands/catalog-source.js +121 -12
  29. package/dist/src/commands/catalog-source.js.map +1 -1
  30. package/dist/src/commands/catalog-store.d.ts +26 -7
  31. package/dist/src/commands/catalog-store.js +361 -42
  32. package/dist/src/commands/catalog-store.js.map +1 -1
  33. package/dist/src/commands/console.js +28 -17
  34. package/dist/src/commands/console.js.map +1 -1
  35. package/dist/src/commands/operation-plan.d.ts +2 -1
  36. package/dist/src/commands/operation-plan.js +24 -0
  37. package/dist/src/commands/operation-plan.js.map +1 -1
  38. package/dist/src/commands/operation-planner.d.ts +2 -2
  39. package/dist/src/commands/operation-planner.js +11 -19
  40. package/dist/src/commands/operation-planner.js.map +1 -1
  41. package/dist/src/commands/options.d.ts +2 -2
  42. package/dist/src/commands/options.js +18 -4
  43. package/dist/src/commands/options.js.map +1 -1
  44. package/dist/src/commands/plugin-capabilities.js +2 -2
  45. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  46. package/dist/src/commands/rebuild-command.d.ts +7 -0
  47. package/dist/src/commands/rebuild-command.js +769 -279
  48. package/dist/src/commands/rebuild-command.js.map +1 -1
  49. package/dist/src/commands/rebuild-integration.d.ts +24 -47
  50. package/dist/src/commands/rebuild-integration.js +1074 -1011
  51. package/dist/src/commands/rebuild-integration.js.map +1 -1
  52. package/dist/src/commands/rebuild-operations.d.ts +50 -12
  53. package/dist/src/commands/rebuild-operations.js +207 -101
  54. package/dist/src/commands/rebuild-operations.js.map +1 -1
  55. package/dist/src/commands/status-v6.d.ts +21 -2
  56. package/dist/src/commands/status-v6.js +163 -31
  57. package/dist/src/commands/status-v6.js.map +1 -1
  58. package/dist/src/commands/sync-command.d.ts +17 -0
  59. package/dist/src/commands/sync-command.js +45 -37
  60. package/dist/src/commands/sync-command.js.map +1 -1
  61. package/dist/src/commands/write-commands.d.ts +5 -0
  62. package/dist/src/commands/write-commands.js +50 -3
  63. package/dist/src/commands/write-commands.js.map +1 -1
  64. package/dist/src/generators/local-plugin.js +14 -4
  65. package/dist/src/generators/local-plugin.js.map +1 -1
  66. package/dist/src/index.js +216 -85
  67. package/dist/src/index.js.map +1 -1
  68. package/dist/src/output/command-receipt.js +1 -3
  69. package/dist/src/output/command-receipt.js.map +1 -1
  70. package/dist/src/output/rebuild-progress.js +1 -3
  71. package/dist/src/output/rebuild-progress.js.map +1 -1
  72. package/dist/src/portable/materializer.js +6 -6
  73. package/dist/src/portable/materializer.js.map +1 -1
  74. package/dist/src/tui/catalog-generation.js +41 -1
  75. package/dist/src/tui/catalog-generation.js.map +1 -1
  76. package/dist/src/tui/catalog-sources/session.d.ts +1 -0
  77. package/dist/src/tui/catalog-sources/session.js +16 -3
  78. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  79. package/dist/src/tui/catalog-sources/types.d.ts +3 -2
  80. package/dist/src/tui/components.d.ts +4 -0
  81. package/dist/src/tui/components.js +36 -24
  82. package/dist/src/tui/components.js.map +1 -1
  83. package/dist/src/tui/console-app-v4.d.ts +13 -4
  84. package/dist/src/tui/console-app-v4.js +2067 -839
  85. package/dist/src/tui/console-app-v4.js.map +1 -1
  86. package/dist/src/tui/controller-runtime.js +585 -33
  87. package/dist/src/tui/controller-runtime.js.map +1 -1
  88. package/dist/src/tui/controller.d.ts +152 -6
  89. package/dist/src/tui/controller.js +1168 -156
  90. package/dist/src/tui/controller.js.map +1 -1
  91. package/dist/src/tui/diagnostic-projection.d.ts +22 -0
  92. package/dist/src/tui/diagnostic-projection.js +437 -0
  93. package/dist/src/tui/diagnostic-projection.js.map +1 -0
  94. package/dist/src/tui/diagnostics.d.ts +1 -1
  95. package/dist/src/tui/diagnostics.js +6 -1
  96. package/dist/src/tui/diagnostics.js.map +1 -1
  97. package/dist/src/tui/glossary.d.ts +23 -0
  98. package/dist/src/tui/glossary.js +42 -0
  99. package/dist/src/tui/glossary.js.map +1 -0
  100. package/dist/src/tui/index.d.ts +4 -1
  101. package/dist/src/tui/index.js +4 -1
  102. package/dist/src/tui/index.js.map +1 -1
  103. package/dist/src/tui/keys.d.ts +44 -0
  104. package/dist/src/tui/keys.js +43 -15
  105. package/dist/src/tui/keys.js.map +1 -1
  106. package/dist/src/tui/lifecycle-action-view.d.ts +6 -0
  107. package/dist/src/tui/lifecycle-action-view.js +34 -0
  108. package/dist/src/tui/lifecycle-action-view.js.map +1 -0
  109. package/dist/src/tui/model.d.ts +12 -2
  110. package/dist/src/tui/model.js +128 -52
  111. package/dist/src/tui/model.js.map +1 -1
  112. package/dist/src/tui/operation-diagnostic-screens.d.ts +21 -0
  113. package/dist/src/tui/operation-diagnostic-screens.js +101 -0
  114. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -0
  115. package/dist/src/tui/operation-history.d.ts +80 -0
  116. package/dist/src/tui/operation-history.js +130 -0
  117. package/dist/src/tui/operation-history.js.map +1 -0
  118. package/dist/src/tui/operation-review-planner.js +306 -86
  119. package/dist/src/tui/operation-review-planner.js.map +1 -1
  120. package/dist/src/tui/router.d.ts +82 -17
  121. package/dist/src/tui/router.js +186 -38
  122. package/dist/src/tui/router.js.map +1 -1
  123. package/dist/src/tui/screen-registry.d.ts +546 -0
  124. package/dist/src/tui/screen-registry.js +179 -0
  125. package/dist/src/tui/screen-registry.js.map +1 -0
  126. package/dist/src/tui/session-console-app.d.ts +6 -0
  127. package/dist/src/tui/session-console-app.js +13 -0
  128. package/dist/src/tui/session-console-app.js.map +1 -0
  129. package/dist/src/tui/session-host.d.ts +44 -0
  130. package/dist/src/tui/session-host.js +197 -0
  131. package/dist/src/tui/session-host.js.map +1 -0
  132. package/dist/src/types.d.ts +64 -3
  133. package/dist/src/types.js +1 -1
  134. package/dist/src/types.js.map +1 -1
  135. package/dist/src/utils/agent-version.js +1 -1
  136. package/dist/src/utils/agent-version.js.map +1 -1
  137. package/dist/src/utils/artifact-renderer.d.ts +2 -1
  138. package/dist/src/utils/artifact-renderer.js +52 -52
  139. package/dist/src/utils/artifact-renderer.js.map +1 -1
  140. package/dist/src/utils/bundle-layout.js +4 -4
  141. package/dist/src/utils/bundle-layout.js.map +1 -1
  142. package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
  143. package/dist/src/utils/catalog-capability-projection.js +14 -0
  144. package/dist/src/utils/catalog-capability-projection.js.map +1 -0
  145. package/dist/src/utils/catalog-generation-resolver.d.ts +14 -2
  146. package/dist/src/utils/catalog-generation-resolver.js +49 -27
  147. package/dist/src/utils/catalog-generation-resolver.js.map +1 -1
  148. package/dist/src/utils/catalog-generation-service.d.ts +8 -1
  149. package/dist/src/utils/catalog-generation-service.js +81 -47
  150. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  151. package/dist/src/utils/catalog-generation-store.d.ts +1 -0
  152. package/dist/src/utils/catalog-generation-store.js +60 -5
  153. package/dist/src/utils/catalog-generation-store.js.map +1 -1
  154. package/dist/src/utils/catalog-operation-context.js +1 -0
  155. package/dist/src/utils/catalog-operation-context.js.map +1 -1
  156. package/dist/src/utils/catalog-public-error.js +4 -0
  157. package/dist/src/utils/catalog-public-error.js.map +1 -1
  158. package/dist/src/utils/catalog-source-impact.d.ts +3 -2
  159. package/dist/src/utils/catalog-source-impact.js +142 -20
  160. package/dist/src/utils/catalog-source-impact.js.map +1 -1
  161. package/dist/src/utils/catalog-source-references.js +95 -9
  162. package/dist/src/utils/catalog-source-references.js.map +1 -1
  163. package/dist/src/utils/catalog-sources.d.ts +1 -1
  164. package/dist/src/utils/catalog-sources.js +19 -3
  165. package/dist/src/utils/catalog-sources.js.map +1 -1
  166. package/dist/src/utils/codex-manager.js +8 -0
  167. package/dist/src/utils/codex-manager.js.map +1 -1
  168. package/dist/src/utils/codex-native-observation.d.ts +34 -1
  169. package/dist/src/utils/codex-native-observation.js +132 -0
  170. package/dist/src/utils/codex-native-observation.js.map +1 -1
  171. package/dist/src/utils/codex-reconciler.d.ts +0 -2
  172. package/dist/src/utils/codex-reconciler.js.map +1 -1
  173. package/dist/src/utils/codex-shared-native.d.ts +2 -26
  174. package/dist/src/utils/codex-shared-native.js +8 -82
  175. package/dist/src/utils/codex-shared-native.js.map +1 -1
  176. package/dist/src/utils/git-catalog-provider.d.ts +3 -0
  177. package/dist/src/utils/git-catalog-provider.js +40 -1
  178. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  179. package/dist/src/utils/local-marketplace.d.ts +1 -0
  180. package/dist/src/utils/local-marketplace.js +69 -73
  181. package/dist/src/utils/local-marketplace.js.map +1 -1
  182. package/dist/src/utils/multi-source-catalog.js +1 -0
  183. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  184. package/dist/src/utils/native-cli.d.ts +9 -4
  185. package/dist/src/utils/native-cli.js +280 -34
  186. package/dist/src/utils/native-cli.js.map +1 -1
  187. package/dist/src/utils/path-catalog-provider.js +2 -0
  188. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  189. package/dist/src/utils/plugin-config-write.d.ts +15 -13
  190. package/dist/src/utils/plugin-config-write.js +418 -198
  191. package/dist/src/utils/plugin-config-write.js.map +1 -1
  192. package/dist/src/utils/plugin-config.d.ts +48 -4
  193. package/dist/src/utils/plugin-config.js +289 -38
  194. package/dist/src/utils/plugin-config.js.map +1 -1
  195. package/dist/src/utils/plugin-scripts.d.ts +20 -12
  196. package/dist/src/utils/plugin-scripts.js +376 -109
  197. package/dist/src/utils/plugin-scripts.js.map +1 -1
  198. package/dist/src/utils/process-tree.d.ts +2 -2
  199. package/dist/src/utils/process-tree.js +1 -1
  200. package/dist/src/utils/process-tree.js.map +1 -1
  201. package/dist/src/utils/project-boundary.d.ts +11 -1
  202. package/dist/src/utils/project-boundary.js +17 -8
  203. package/dist/src/utils/project-boundary.js.map +1 -1
  204. package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
  205. package/dist/src/utils/safe-operation-diagnostic.js +33 -0
  206. package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
  207. package/dist/src/utils/sensitive-content.d.ts +1 -0
  208. package/dist/src/utils/sensitive-content.js +15 -0
  209. package/dist/src/utils/sensitive-content.js.map +1 -0
  210. package/dist/src/utils/windows-claude-shim.d.ts +9 -0
  211. package/dist/src/utils/windows-claude-shim.js +141 -0
  212. package/dist/src/utils/windows-claude-shim.js.map +1 -0
  213. package/dist/vitest.config.js +91 -10
  214. package/dist/vitest.config.js.map +1 -1
  215. package/package.json +9 -9
  216. package/dist/authoring/templates/aw-config-runtime.d.mts +0 -2
  217. package/dist/authoring/templates/aw-config-runtime.mjs +0 -407
  218. package/dist/authoring/templates/aw-config-runtime.mjs.map +0 -1
@@ -4,12 +4,29 @@ import fs from 'fs-extra';
4
4
  import { CATALOG_STORE_DIRECTORY } from '../constants.js';
5
5
  import { withAwHomeMutationLock } from '../utils/aw-home-mutation-lock.js';
6
6
  import { createCatalogStoreKey, readCatalogStoreEntry } from '../utils/catalog-generation-store.js';
7
+ import { catalogGenerationErrorCode } from '../utils/catalog-generation-error.js';
7
8
  import { readCatalogGenerationState } from '../utils/catalog-generation-state.js';
9
+ import { readCatalogSourcesConfig } from '../utils/catalog-sources.js';
8
10
  import { resolveAwHome } from '../utils/state-paths.js';
9
11
  const FORMAL_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
10
12
  const TEMP_RETENTION_MS = 24 * 60 * 60 * 1000;
11
13
  const HEX_SEGMENT = /^[0-9a-f]{64}$/u;
12
14
  const TEMP_SEGMENT = /^\.(staging|replacement|quarantine)-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/u;
15
+ const DELETE_CLAIM_SEGMENT = /^\.delete-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/u;
16
+ const CATALOG_STORE_APPLICATION_ERROR_CODES = new Set([
17
+ 'invalidArguments',
18
+ 'catalogStoreInspectionFailed',
19
+ 'catalogStoreDeleteFailed',
20
+ ]);
21
+ export function catalogStoreErrorCode(error) {
22
+ const generationCode = catalogGenerationErrorCode(error);
23
+ if (generationCode !== 'catalogGenerationFailed')
24
+ return generationCode;
25
+ const explicitCode = error?.code;
26
+ return typeof explicitCode === 'string' && CATALOG_STORE_APPLICATION_ERROR_CODES.has(explicitCode)
27
+ ? explicitCode
28
+ : 'catalogStoreInspectionFailed';
29
+ }
13
30
  function opaqueForeignKey(parent, name) {
14
31
  const digest = crypto.createHash('sha256').update(name).digest('hex').slice(0, 12);
15
32
  return `${parent === '' ? '' : `${parent}/`}<foreign-${digest}>`;
@@ -17,21 +34,29 @@ function opaqueForeignKey(parent, name) {
17
34
  async function inspectTree(root, nowMs) {
18
35
  let newestMtimeMs = Number.NEGATIVE_INFINITY;
19
36
  let bytes = 0;
37
+ let rootIdentity = null;
20
38
  const pending = [root];
21
39
  try {
22
40
  while (pending.length > 0) {
23
41
  const current = pending.pop();
24
42
  const stat = await fs.lstat(current);
43
+ if (current === root)
44
+ rootIdentity = {
45
+ device: stat.dev,
46
+ inode: stat.ino,
47
+ mode: stat.mode,
48
+ birthtimeMs: stat.birthtimeMs,
49
+ };
25
50
  newestMtimeMs = Math.max(newestMtimeMs, stat.mtimeMs);
26
51
  if (stat.isSymbolicLink()) {
27
- return { ageMs: null, bytes: null, safe: false, reason: 'unsafeSymlink' };
52
+ return { ageMs: null, bytes: null, safe: false, reason: 'unsafeSymlink', rootIdentity };
28
53
  }
29
54
  if (stat.isFile()) {
30
55
  bytes += stat.size;
31
56
  continue;
32
57
  }
33
58
  if (!stat.isDirectory()) {
34
- return { ageMs: null, bytes: null, safe: false, reason: 'unsafeNode' };
59
+ return { ageMs: null, bytes: null, safe: false, reason: 'unsafeNode', rootIdentity };
35
60
  }
36
61
  const children = await fs.readdir(current);
37
62
  for (const child of children)
@@ -41,10 +66,11 @@ async function inspectTree(root, nowMs) {
41
66
  ageMs: Math.max(0, nowMs - newestMtimeMs),
42
67
  bytes,
43
68
  safe: true,
69
+ rootIdentity,
44
70
  };
45
71
  }
46
72
  catch {
47
- return { ageMs: null, bytes: null, safe: false, reason: 'unreadable' };
73
+ return { ageMs: null, bytes: null, safe: false, reason: 'unreadable', rootIdentity };
48
74
  }
49
75
  }
50
76
  async function readEntryIdentity(entryDir) {
@@ -65,24 +91,29 @@ function retainedEntry(options) {
65
91
  storeKey: options.storeKey,
66
92
  category: options.category,
67
93
  status: options.evidence.safe ? 'retained' : 'error',
94
+ generationReference: options.generationReference ?? 'none',
68
95
  ageMs: options.evidence.ageMs,
69
96
  reclaimableBytes: null,
70
97
  reason: options.evidence.reason ?? options.reason,
71
98
  absolutePath: options.absolutePath,
99
+ rootIdentity: options.evidence.rootIdentity,
72
100
  };
73
101
  }
74
102
  async function inspectFormalEntry(options) {
75
103
  const storeKey = `${options.identitySegment}/${options.digestSegment}`;
104
+ const generationReference = options.activeKeys.has(storeKey)
105
+ ? 'active'
106
+ : options.orphanKeys.has(storeKey) ? 'orphan' : 'none';
76
107
  if (!options.evidence.safe)
77
108
  return retainedEntry({
78
109
  storeKey, category: 'entry', absolutePath: options.entryDir,
79
- evidence: options.evidence, reason: 'unsafeEntry',
110
+ evidence: options.evidence, reason: 'unsafeEntry', generationReference,
80
111
  });
81
112
  const identity = await readEntryIdentity(options.entryDir);
82
113
  if (!identity)
83
114
  return retainedEntry({
84
115
  storeKey, category: 'entry', absolutePath: options.entryDir,
85
- evidence: options.evidence, reason: 'validationFailed',
116
+ evidence: options.evidence, reason: 'validationFailed', generationReference,
86
117
  });
87
118
  try {
88
119
  if (createCatalogStoreKey(identity.logicalIdentity, identity.snapshotDigest) !== storeKey) {
@@ -93,7 +124,7 @@ async function inspectFormalEntry(options) {
93
124
  catch {
94
125
  return retainedEntry({
95
126
  storeKey, category: 'entry', absolutePath: options.entryDir,
96
- evidence: options.evidence, reason: 'validationFailed',
127
+ evidence: options.evidence, reason: 'validationFailed', generationReference,
97
128
  });
98
129
  }
99
130
  if (options.activeKeys.has(storeKey))
@@ -101,20 +132,24 @@ async function inspectFormalEntry(options) {
101
132
  storeKey,
102
133
  category: 'entry',
103
134
  status: 'active',
135
+ generationReference,
104
136
  ageMs: options.evidence.ageMs,
105
137
  reclaimableBytes: null,
106
138
  reason: 'active',
107
139
  absolutePath: options.entryDir,
140
+ rootIdentity: options.evidence.rootIdentity,
108
141
  };
109
142
  const reclaimable = options.evidence.ageMs !== null && options.evidence.ageMs > FORMAL_RETENTION_MS;
110
143
  return {
111
144
  storeKey,
112
145
  category: 'entry',
113
146
  status: reclaimable ? 'reclaimable' : 'retained',
147
+ generationReference,
114
148
  ageMs: options.evidence.ageMs,
115
149
  reclaimableBytes: reclaimable ? options.evidence.bytes : null,
116
150
  reason: reclaimable ? 'inactiveOlderThan30Days' : 'inactiveWithin30Days',
117
151
  absolutePath: options.entryDir,
152
+ rootIdentity: options.evidence.rootIdentity,
118
153
  };
119
154
  }
120
155
  async function inspectTempEntry(options) {
@@ -163,21 +198,104 @@ async function inspectTempEntry(options) {
163
198
  storeKey,
164
199
  category,
165
200
  status: 'reclaimable',
201
+ generationReference: 'none',
166
202
  ageMs: options.evidence.ageMs,
167
203
  reclaimableBytes: options.evidence.bytes,
168
204
  reason: 'temporaryOlderThan24Hours',
169
205
  absolutePath: options.entryDir,
206
+ rootIdentity: options.evidence.rootIdentity,
207
+ };
208
+ }
209
+ async function inspectDeleteClaim(options) {
210
+ const storeKey = `${options.identitySegment}/${options.name}`;
211
+ const rootStat = await fs.lstat(options.entryDir).catch(() => null);
212
+ if (!rootStat?.isDirectory() || rootStat.isSymbolicLink())
213
+ return retainedEntry({
214
+ storeKey, category: 'deleteClaim', absolutePath: options.entryDir,
215
+ evidence: { ...options.evidence, safe: false, reason: 'unsafeDeleteClaimRoot' },
216
+ reason: 'unsafeDeleteClaimRoot',
217
+ });
218
+ if (!options.evidence.safe)
219
+ return retainedEntry({
220
+ storeKey, category: 'deleteClaim', absolutePath: options.entryDir,
221
+ evidence: options.evidence, reason: 'unsafeDeleteClaim',
222
+ });
223
+ const reclaimable = options.evidence.ageMs !== null && options.evidence.ageMs > TEMP_RETENTION_MS;
224
+ return {
225
+ storeKey,
226
+ category: 'deleteClaim',
227
+ status: reclaimable ? 'reclaimable' : 'retained',
228
+ generationReference: 'none',
229
+ ageMs: options.evidence.ageMs,
230
+ reclaimableBytes: reclaimable ? options.evidence.bytes : null,
231
+ reason: reclaimable ? 'deleteClaimOlderThan24Hours' : 'deleteClaimWithin24Hours',
232
+ absolutePath: options.entryDir,
233
+ rootIdentity: options.evidence.rootIdentity,
170
234
  };
171
235
  }
172
- async function activeStoreKeys(env) {
173
- const state = await readCatalogGenerationState(env);
174
- return new Set(Object.values(state.sources)
175
- .flatMap((source) => source.active
176
- ? [createCatalogStoreKey(source.active.logicalIdentity, source.active.snapshotDigest)]
177
- : []));
236
+ async function generationReferenceKeys(env) {
237
+ const [config, state] = await Promise.all([
238
+ readCatalogSourcesConfig(env),
239
+ readCatalogGenerationState(env),
240
+ ]);
241
+ const configuredSourceIds = new Set(config.sources.map((source) => source.id));
242
+ const activeKeys = new Set();
243
+ const orphanKeys = new Set();
244
+ for (const [sourceId, source] of Object.entries(state.sources)) {
245
+ if (!source.active)
246
+ continue;
247
+ const storeKey = createCatalogStoreKey(source.active.logicalIdentity, source.active.snapshotDigest);
248
+ if (configuredSourceIds.has(sourceId))
249
+ activeKeys.add(storeKey);
250
+ else
251
+ orphanKeys.add(storeKey);
252
+ }
253
+ for (const storeKey of activeKeys)
254
+ orphanKeys.delete(storeKey);
255
+ return { activeKeys, orphanKeys };
256
+ }
257
+ async function reinspectCandidate(options) {
258
+ const identityDir = path.dirname(options.initial.absolutePath);
259
+ const storeRoot = path.dirname(identityDir);
260
+ const identitySegment = path.basename(identityDir);
261
+ const name = path.basename(options.initial.absolutePath);
262
+ const [storeRootStat, identityStat] = await Promise.all([
263
+ fs.lstat(storeRoot).catch(() => null),
264
+ fs.lstat(identityDir).catch(() => null),
265
+ ]);
266
+ if (!storeRootStat?.isDirectory() || storeRootStat.isSymbolicLink()
267
+ || !identityStat?.isDirectory() || identityStat.isSymbolicLink()
268
+ || !HEX_SEGMENT.test(identitySegment)) {
269
+ return failedReinspectionEntry(options.initial);
270
+ }
271
+ const evidence = await inspectTree(options.initial.absolutePath, options.nowMs);
272
+ if (options.initial.category === 'entry')
273
+ return inspectFormalEntry({
274
+ identitySegment,
275
+ digestSegment: name,
276
+ entryDir: options.initial.absolutePath,
277
+ activeKeys: options.references.activeKeys,
278
+ orphanKeys: options.references.orphanKeys,
279
+ evidence,
280
+ env: options.env,
281
+ });
282
+ if (options.initial.category === 'deleteClaim')
283
+ return inspectDeleteClaim({
284
+ identitySegment,
285
+ name,
286
+ entryDir: options.initial.absolutePath,
287
+ evidence,
288
+ });
289
+ return inspectTempEntry({
290
+ identitySegment,
291
+ name,
292
+ entryDir: options.initial.absolutePath,
293
+ evidence,
294
+ env: options.env,
295
+ });
178
296
  }
179
297
  async function inspectCatalogStore(options) {
180
- const activeKeys = await activeStoreKeys(options.env);
298
+ const { activeKeys, orphanKeys } = await generationReferenceKeys(options.env);
181
299
  const storeRoot = path.join(resolveAwHome(options.env), CATALOG_STORE_DIRECTORY);
182
300
  const rootStat = await fs.lstat(storeRoot).catch((error) => {
183
301
  if (error.code === 'ENOENT')
@@ -189,18 +307,14 @@ async function inspectCatalogStore(options) {
189
307
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
190
308
  return [retainedEntry({
191
309
  storeKey: '.', category: 'foreign', absolutePath: storeRoot,
192
- evidence: { ageMs: null, bytes: null, safe: false, reason: 'unsafeStoreRoot' },
310
+ evidence: {
311
+ ageMs: null, bytes: null, safe: false, reason: 'unsafeStoreRoot', rootIdentity: null,
312
+ },
193
313
  reason: 'unsafeStoreRoot',
194
314
  })];
195
315
  }
196
316
  const entries = [];
197
- const identityNames = await fs.readdir(storeRoot).then((names) => names.sort()).catch(() => null);
198
- if (!identityNames)
199
- return [retainedEntry({
200
- storeKey: '.', category: 'foreign', absolutePath: storeRoot,
201
- evidence: { ageMs: null, bytes: null, safe: false, reason: 'unreadableStoreRoot' },
202
- reason: 'unreadableStoreRoot',
203
- })];
317
+ const identityNames = await fs.readdir(storeRoot).then((names) => names.sort());
204
318
  for (const identityName of identityNames) {
205
319
  const identityDir = path.join(storeRoot, identityName);
206
320
  const identityStat = await fs.lstat(identityDir).catch(() => null);
@@ -216,7 +330,18 @@ async function inspectCatalogStore(options) {
216
330
  if (!childNames) {
217
331
  entries.push(retainedEntry({
218
332
  storeKey: `${identityName}/<unreadable>`, category: 'foreign', absolutePath: identityDir,
219
- evidence: { ageMs: null, bytes: null, safe: false, reason: 'unreadableIdentityDirectory' },
333
+ evidence: {
334
+ ageMs: null,
335
+ bytes: null,
336
+ safe: false,
337
+ reason: 'unreadableIdentityDirectory',
338
+ rootIdentity: identityStat ? {
339
+ device: identityStat.dev,
340
+ inode: identityStat.ino,
341
+ mode: identityStat.mode,
342
+ birthtimeMs: identityStat.birthtimeMs,
343
+ } : null,
344
+ },
220
345
  reason: 'unreadableIdentityDirectory',
221
346
  }));
222
347
  continue;
@@ -230,6 +355,7 @@ async function inspectCatalogStore(options) {
230
355
  digestSegment: childName,
231
356
  entryDir,
232
357
  activeKeys,
358
+ orphanKeys,
233
359
  evidence,
234
360
  env: options.env,
235
361
  }));
@@ -243,6 +369,14 @@ async function inspectCatalogStore(options) {
243
369
  env: options.env,
244
370
  }));
245
371
  }
372
+ else if (DELETE_CLAIM_SEGMENT.test(childName)) {
373
+ entries.push(await inspectDeleteClaim({
374
+ identitySegment: identityName,
375
+ name: childName,
376
+ entryDir,
377
+ evidence,
378
+ }));
379
+ }
246
380
  else {
247
381
  entries.push(retainedEntry({
248
382
  storeKey: opaqueForeignKey(identityName, childName), category: 'foreign', absolutePath: entryDir,
@@ -254,7 +388,7 @@ async function inspectCatalogStore(options) {
254
388
  return entries;
255
389
  }
256
390
  function publicEntries(entries) {
257
- return entries.map(({ absolutePath: _absolutePath, ...entry }) => entry);
391
+ return entries.map(({ absolutePath: _absolutePath, rootIdentity: _rootIdentity, ...entry }) => entry);
258
392
  }
259
393
  function reclaimableTotal(entries) {
260
394
  if (entries.some((entry) => entry.status === 'error'))
@@ -264,61 +398,245 @@ function reclaimableTotal(entries) {
264
398
  return null;
265
399
  return reclaimable.reduce((sum, entry) => sum + entry.reclaimableBytes, 0);
266
400
  }
401
+ function inspectionErrors(entries) {
402
+ return entries
403
+ .filter((entry) => entry.status === 'error')
404
+ .map((entry) => ({
405
+ storeKey: entry.storeKey,
406
+ code: 'catalogStoreInspectionFailed',
407
+ message: `Could not fully inspect Catalog store item ${entry.storeKey}: ${entry.reason}.`,
408
+ }));
409
+ }
410
+ function sameNodeIdentity(left, right) {
411
+ return left !== null
412
+ && right !== null
413
+ && left.device === right.device
414
+ && left.inode === right.inode
415
+ && left.mode === right.mode
416
+ && Object.is(left.birthtimeMs, right.birthtimeMs);
417
+ }
418
+ function sameDeletionEvidence(initial, current) {
419
+ return current.status === 'reclaimable'
420
+ && current.storeKey === initial.storeKey
421
+ && current.category === initial.category
422
+ && current.generationReference === initial.generationReference
423
+ && current.absolutePath === initial.absolutePath
424
+ && current.ageMs === initial.ageMs
425
+ && current.reclaimableBytes === initial.reclaimableBytes
426
+ && current.reason === initial.reason
427
+ && sameNodeIdentity(current.rootIdentity, initial.rootIdentity);
428
+ }
429
+ function staleInspectionEntry(initial, current) {
430
+ if (current?.status === 'active' || current?.status === 'error')
431
+ return current;
432
+ return {
433
+ ...(current ?? initial),
434
+ status: 'retained',
435
+ ageMs: current?.ageMs ?? null,
436
+ reclaimableBytes: null,
437
+ reason: 'staleBeforeDelete',
438
+ rootIdentity: current?.rootIdentity ?? null,
439
+ };
440
+ }
441
+ function failedReinspectionEntry(initial) {
442
+ return {
443
+ ...initial,
444
+ status: 'error',
445
+ ageMs: null,
446
+ reclaimableBytes: null,
447
+ reason: 'reinspectionFailed',
448
+ };
449
+ }
450
+ async function readNodeIdentity(target) {
451
+ const stat = await fs.lstat(target).catch(() => null);
452
+ if (!stat)
453
+ return null;
454
+ return {
455
+ device: stat.dev,
456
+ inode: stat.ino,
457
+ mode: stat.mode,
458
+ birthtimeMs: stat.birthtimeMs,
459
+ };
460
+ }
461
+ async function readDirectoryIdentity(target) {
462
+ const stat = await fs.lstat(target).catch(() => null);
463
+ if (!stat?.isDirectory() || stat.isSymbolicLink())
464
+ return null;
465
+ return {
466
+ device: stat.dev,
467
+ inode: stat.ino,
468
+ mode: stat.mode,
469
+ birthtimeMs: stat.birthtimeMs,
470
+ };
471
+ }
472
+ async function restoreStaleClaim(claimPath, originalPath) {
473
+ const originalExists = await fs.lstat(originalPath)
474
+ .then(() => true)
475
+ .catch(() => false);
476
+ if (originalExists)
477
+ return false;
478
+ return fs.rename(claimPath, originalPath)
479
+ .then(() => true)
480
+ .catch(() => false);
481
+ }
482
+ async function inspectDeleteClaimAfterFailure(options) {
483
+ const exists = await fs.lstat(options.claimPath)
484
+ .then(() => true)
485
+ .catch((error) => error.code === 'ENOENT' ? false : true);
486
+ if (!exists)
487
+ return null;
488
+ return inspectDeleteClaim({
489
+ identitySegment: path.basename(path.dirname(options.claimPath)),
490
+ name: path.basename(options.claimPath),
491
+ entryDir: options.claimPath,
492
+ evidence: await inspectTree(options.claimPath, options.nowMs),
493
+ });
494
+ }
267
495
  export async function catalogStoreStatus(options = {}) {
268
496
  const entries = publicEntries(await inspectCatalogStore({
269
497
  env: options.env ?? process.env,
270
498
  nowMs: (options.now ?? new Date()).getTime(),
271
499
  }));
500
+ const errors = inspectionErrors(entries);
272
501
  return {
273
- catalogStoreSchemaVersion: 1,
502
+ catalogStoreSchemaVersion: 2,
274
503
  command: 'catalog:store:status',
275
- ok: true,
504
+ ok: errors.length === 0,
276
505
  entries,
277
506
  totals: {
278
507
  entries: entries.length,
279
508
  reclaimableEntries: entries.filter((entry) => entry.status === 'reclaimable').length,
280
509
  reclaimableBytes: reclaimableTotal(entries),
281
510
  },
511
+ errors,
282
512
  };
283
513
  }
284
514
  async function executePrune(options) {
285
515
  const inspected = await inspectCatalogStore({ env: options.env, nowMs: options.nowMs });
286
516
  const deleted = [];
287
- const errors = [];
517
+ const deleteErrors = [];
288
518
  const retainedAfterError = new Set();
519
+ const retainedReasons = new Map();
289
520
  if (!options.dryRun) {
290
- for (const entry of inspected.filter((candidate) => candidate.status === 'reclaimable')) {
521
+ const candidates = inspected
522
+ .map((entry, index) => ({ entry, index }))
523
+ .filter(({ entry }) => entry.status === 'reclaimable');
524
+ await options.runtime?.afterInitialInspection?.(publicEntries(inspected));
525
+ for (const { entry: initial, index } of candidates) {
526
+ let current;
527
+ try {
528
+ const references = await generationReferenceKeys(options.env);
529
+ current = await reinspectCandidate({
530
+ initial,
531
+ references,
532
+ env: options.env,
533
+ nowMs: options.nowMs,
534
+ });
535
+ }
536
+ catch {
537
+ inspected[index] = failedReinspectionEntry(initial);
538
+ retainedReasons.set(initial.storeKey, 'reinspectionFailed');
539
+ continue;
540
+ }
541
+ if (!current || !sameDeletionEvidence(initial, current)) {
542
+ inspected[index] = staleInspectionEntry(initial, current);
543
+ retainedReasons.set(initial.storeKey, current?.status === 'active'
544
+ ? 'active'
545
+ : current?.status === 'error' ? current.reason : 'staleBeforeDelete');
546
+ continue;
547
+ }
548
+ inspected[index] = current;
549
+ const parentPath = path.dirname(current.absolutePath);
550
+ const parentIdentity = await readDirectoryIdentity(parentPath);
551
+ if (!parentIdentity) {
552
+ inspected[index] = staleInspectionEntry(initial, current);
553
+ retainedReasons.set(initial.storeKey, 'staleBeforeDelete');
554
+ continue;
555
+ }
556
+ const claimPath = path.join(parentPath, `.delete-${crypto.randomUUID()}.tmp`);
291
557
  try {
292
- await options.remove(entry.absolutePath);
293
- deleted.push({ storeKey: entry.storeKey, bytes: entry.reclaimableBytes });
558
+ await fs.rename(current.absolutePath, claimPath);
294
559
  }
295
560
  catch {
296
- const stillExists = await fs.lstat(entry.absolutePath)
297
- .then(() => true)
298
- .catch(() => false);
299
- if (stillExists)
300
- retainedAfterError.add(entry.storeKey);
301
- errors.push({
302
- storeKey: entry.storeKey,
561
+ inspected[index] = staleInspectionEntry(initial, current);
562
+ retainedReasons.set(initial.storeKey, 'staleBeforeDelete');
563
+ continue;
564
+ }
565
+ try {
566
+ const claimTime = new Date(options.nowMs);
567
+ if (options.runtime?.updateDeleteClaimTime) {
568
+ await options.runtime.updateDeleteClaimTime(claimPath, claimTime);
569
+ }
570
+ else {
571
+ await fs.utimes(claimPath, claimTime, claimTime);
572
+ }
573
+ }
574
+ catch {
575
+ const restored = await restoreStaleClaim(claimPath, current.absolutePath);
576
+ if (restored) {
577
+ inspected[index] = staleInspectionEntry(initial, current);
578
+ retainedReasons.set(initial.storeKey, 'staleBeforeDelete');
579
+ }
580
+ else {
581
+ const failedClaim = await inspectDeleteClaimAfterFailure({
582
+ claimPath,
583
+ nowMs: options.nowMs,
584
+ });
585
+ inspected[index] = staleInspectionEntry(initial, failedClaim ?? undefined);
586
+ retainedReasons.set(inspected[index].storeKey, 'staleBeforeDelete');
587
+ }
588
+ continue;
589
+ }
590
+ await options.runtime?.afterDeleteClaim?.({ storeKey: initial.storeKey, claimPath });
591
+ const [claimedIdentity, currentParentIdentity] = await Promise.all([
592
+ readNodeIdentity(claimPath),
593
+ readDirectoryIdentity(parentPath),
594
+ ]);
595
+ if (!sameNodeIdentity(current.rootIdentity, claimedIdentity)
596
+ || !sameNodeIdentity(parentIdentity, currentParentIdentity)) {
597
+ await restoreStaleClaim(claimPath, current.absolutePath);
598
+ inspected[index] = staleInspectionEntry(initial, current);
599
+ retainedReasons.set(initial.storeKey, 'staleBeforeDelete');
600
+ continue;
601
+ }
602
+ try {
603
+ await options.remove(claimPath);
604
+ deleted.push({ storeKey: initial.storeKey, bytes: current.reclaimableBytes });
605
+ }
606
+ catch {
607
+ const failedClaim = await inspectDeleteClaimAfterFailure({
608
+ claimPath,
609
+ nowMs: options.nowMs,
610
+ });
611
+ if (!failedClaim) {
612
+ deleted.push({ storeKey: initial.storeKey, bytes: current.reclaimableBytes });
613
+ continue;
614
+ }
615
+ inspected[index] = failedClaim;
616
+ retainedAfterError.add(failedClaim.storeKey);
617
+ deleteErrors.push({
618
+ storeKey: failedClaim.storeKey,
303
619
  code: 'catalogStoreDeleteFailed',
304
- message: `Could not safely complete deletion of Catalog store item ${entry.storeKey}. Retry prune.`,
620
+ message: `Could not safely complete deletion of Catalog store item ${failedClaim.storeKey}. Retry prune.`,
305
621
  });
306
622
  }
307
623
  }
308
624
  }
309
625
  const entries = publicEntries(inspected);
626
+ const errors = [...inspectionErrors(entries), ...deleteErrors];
310
627
  const deletedKeys = new Set(deleted.map((entry) => entry.storeKey));
311
628
  const retained = entries
312
629
  .filter((entry) => !deletedKeys.has(entry.storeKey)
313
- && (options.dryRun || entry.status !== 'reclaimable' || retainedAfterError.has(entry.storeKey)))
630
+ && (options.dryRun
631
+ || entry.status !== 'reclaimable'
632
+ || retainedAfterError.has(entry.storeKey)
633
+ || retainedReasons.has(entry.storeKey)))
314
634
  .map((entry) => ({
315
635
  storeKey: entry.storeKey,
316
- reason: errors.some((error) => error.storeKey === entry.storeKey)
317
- ? 'deleteFailed'
318
- : entry.reason,
636
+ reason: retainedReasons.get(entry.storeKey) ?? entry.reason,
319
637
  }));
320
638
  return {
321
- catalogStoreSchemaVersion: 1,
639
+ catalogStoreSchemaVersion: 2,
322
640
  command: 'catalog:store:prune',
323
641
  ok: errors.length === 0,
324
642
  overallState: errors.length === 0 ? 'succeeded' : 'partial',
@@ -340,6 +658,7 @@ export async function catalogStorePrune(options = {}) {
340
658
  nowMs,
341
659
  dryRun,
342
660
  remove: options.remove ?? ((target) => fs.remove(target)),
661
+ runtime: options.runtime,
343
662
  });
344
663
  if (dryRun)
345
664
  return action();