pinokiod 8.0.73 → 8.0.75

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.
@@ -52,7 +52,6 @@ class AutomaticScans {
52
52
  this.manualDepth = 0
53
53
  this.observedApps = new Set()
54
54
  this.changedPaths = new Map()
55
- this.changedPathIndex = new Map()
56
55
  this.pendingStops = new Map()
57
56
  this.lifecycleWork = new Set()
58
57
  this.stopSettleMs = STOP_SETTLE_MS
@@ -138,7 +137,6 @@ class AutomaticScans {
138
137
  }
139
138
  this.observedApps.clear()
140
139
  this.changedPaths.clear()
141
- this.changedPathIndex.clear()
142
140
  if (watcherError) throw watcherError
143
141
  }
144
142
 
@@ -151,117 +149,123 @@ class AutomaticScans {
151
149
  })
152
150
  return
153
151
  }
154
- for (const event of events || []) {
152
+ const watcherEvents = Array.isArray(events) ? events : []
153
+ if (!watcherEvents.some((event) => event && event.type === "delete")) {
154
+ for (const event of watcherEvents) {
155
+ if (event && ["create", "update"].includes(event.type)) {
156
+ this.recordChangedPath(event.path)
157
+ }
158
+ }
159
+ return
160
+ }
161
+ const batches = new Map()
162
+ for (let index = 0; index < watcherEvents.length; index++) {
163
+ const event = watcherEvents[index]
155
164
  if (!event || !["create", "update", "delete"].includes(event.type)) {
156
165
  continue
157
166
  }
158
- if (event.type === "delete") this.removeChangedPath(event.path)
159
- else this.recordChangedPath(event.path)
167
+ const target = this.changedPathTarget(
168
+ event.path, event.type === "delete")
169
+ if (!target) continue
170
+ let batch = batches.get(target.app)
171
+ if (!batch) {
172
+ batch = {
173
+ root: target.root,
174
+ updates: new Map(),
175
+ deletes: new Map()
176
+ }
177
+ batches.set(target.app, batch)
178
+ }
179
+ const key = pathKey(target.path)
180
+ if (event.type === "delete") batch.deletes.set(key, index)
181
+ else batch.updates.set(key, { index, path: target.path })
182
+ }
183
+ for (const [app, batch] of batches) {
184
+ this.applyChangedPathBatch(app, batch)
160
185
  }
161
186
  }
162
187
 
163
- recordChangedPath(filePath) {
188
+ changedPathTarget(filePath, allowRoot = false) {
164
189
  if (this.disposed || !this.supported ||
165
190
  typeof filePath !== "string" || !filePath) {
166
- return false
191
+ return null
167
192
  }
168
193
  const app = this.appForLaunchPath(filePath)
169
- if (!app || !this.observedApps.has(app)) return false
170
- const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
194
+ if (!app || !this.observedApps.has(app)) return null
195
+ const root = path.resolve(this.vault.kernel.homedir, "api", app)
171
196
  const resolved = path.resolve(filePath)
172
- if (resolved === appRoot || !inside(appRoot, resolved)) return false
197
+ if ((!allowRoot && resolved === root) || !inside(root, resolved)) return null
198
+ return { app, root, path: resolved }
199
+ }
200
+
201
+ recordChangedPath(filePath) {
202
+ const target = this.changedPathTarget(filePath)
203
+ if (!target) return false
204
+ const { app, path: resolved } = target
173
205
  let paths = this.changedPaths.get(app)
174
206
  if (!paths) {
175
207
  paths = new Set()
176
208
  this.changedPaths.set(app, paths)
177
209
  }
178
- let index = this.changedPathIndex.get(app)
179
- if (!index) {
180
- index = { exact: new Map(), descendants: new Map() }
181
- this.changedPathIndex.set(app, index)
182
- }
183
- const key = pathKey(resolved)
184
- if (index.exact.has(key)) return true
185
210
  paths.add(resolved)
186
- index.exact.set(key, resolved)
187
- this.addChangedPathToIndex(index.descendants, appRoot, resolved)
188
211
  return true
189
212
  }
190
213
 
191
- changedPathParentKeys(appRoot, filePath) {
192
- const rootKey = pathKey(appRoot)
193
- const keys = []
194
- let current = path.dirname(path.resolve(filePath))
214
+ lastDeleteIndex(root, filePath, deletes) {
215
+ const rootKey = pathKey(root)
216
+ let current = path.resolve(filePath)
217
+ let latest = -1
195
218
  while (true) {
196
219
  const key = pathKey(current)
197
- keys.push(key)
198
- if (key === rootKey) return keys
220
+ const index = deletes.get(key)
221
+ if (index !== undefined && index > latest) latest = index
222
+ if (key === rootKey) return latest
199
223
  current = path.dirname(current)
200
224
  }
201
225
  }
202
226
 
203
- addChangedPathToIndex(index, appRoot, filePath) {
204
- for (const key of this.changedPathParentKeys(appRoot, filePath)) {
205
- let descendants = index.get(key)
206
- if (!descendants) {
207
- descendants = new Set()
208
- index.set(key, descendants)
209
- }
210
- descendants.add(filePath)
227
+ applyChangedPathBatch(app, { root, updates, deletes }) {
228
+ const paths = this.changedPaths.get(app)
229
+ if (!deletes.size) {
230
+ const collected = paths || new Set()
231
+ for (const update of updates.values()) collected.add(update.path)
232
+ if (collected.size) this.changedPaths.set(app, collected)
233
+ return
211
234
  }
212
- }
213
-
214
- removeChangedPathFromIndex(index, appRoot, filePath) {
215
- index.exact.delete(pathKey(filePath))
216
- for (const key of this.changedPathParentKeys(appRoot, filePath)) {
217
- const descendants = index.descendants.get(key)
218
- if (!descendants) continue
219
- descendants.delete(filePath)
220
- if (!descendants.size) index.descendants.delete(key)
235
+ const retained = new Set()
236
+ if (paths) {
237
+ for (const candidate of paths) {
238
+ const key = pathKey(candidate)
239
+ const update = updates.get(key)
240
+ if (this.lastDeleteIndex(root, candidate, deletes) >
241
+ (update ? update.index : -1) || retained.has(key)) {
242
+ paths.delete(candidate)
243
+ } else {
244
+ retained.add(key)
245
+ }
246
+ }
221
247
  }
222
- }
223
-
224
- removeChangedPath(filePath) {
225
- if (this.disposed || !this.supported ||
226
- typeof filePath !== "string" || !filePath) {
227
- return false
248
+ let collected = paths
249
+ for (const [key, update] of updates) {
250
+ if (update.index <= this.lastDeleteIndex(root, update.path, deletes) ||
251
+ retained.has(key)) continue
252
+ if (!collected) collected = new Set()
253
+ collected.add(update.path)
254
+ retained.add(key)
228
255
  }
229
- const app = this.appForLaunchPath(filePath)
230
- if (!app || !this.observedApps.has(app)) return false
231
- const paths = this.changedPaths.get(app)
232
- if (!paths) return false
233
- const resolved = path.resolve(filePath)
234
- const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
235
- const index = this.changedPathIndex.get(app)
236
- if (!index) return false
237
- const key = pathKey(resolved)
238
- const exact = index.exact.get(key)
239
- const descendants = index.descendants.get(key)
240
- if (!exact && !descendants) return false
241
- const matches = new Set(descendants || [])
242
- if (exact) matches.add(exact)
243
- for (const candidate of matches) {
244
- paths.delete(candidate)
245
- this.removeChangedPathFromIndex(index, appRoot, candidate)
246
- }
247
- if (!paths.size) this.discardChangedPaths(app)
248
- return true
256
+ if (collected && collected.size) this.changedPaths.set(app, collected)
257
+ else this.changedPaths.delete(app)
249
258
  }
250
259
 
251
260
  discardChangedPaths(app) {
252
261
  this.changedPaths.delete(app)
253
- this.changedPathIndex.delete(app)
254
262
  }
255
263
 
256
264
  consumeChangedPaths(app, paths) {
257
265
  const collected = this.changedPaths.get(app)
258
266
  if (!collected) return
259
- const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
260
- const index = this.changedPathIndex.get(app)
261
267
  for (const filePath of paths || []) {
262
- const resolved = path.resolve(filePath)
263
- if (!collected.delete(resolved) || !index) continue
264
- this.removeChangedPathFromIndex(index, appRoot, resolved)
268
+ collected.delete(path.resolve(filePath))
265
269
  }
266
270
  if (!collected.size) this.discardChangedPaths(app)
267
271
  }
@@ -890,7 +894,7 @@ class AutomaticScans {
890
894
  })
891
895
  }
892
896
 
893
- async verifiedAutomaticHash(active, entry, memory, verifiedHashes, counts) {
897
+ async currentAutomaticEntry(active, entry, counts) {
894
898
  this.checkpoint(active)
895
899
  let current
896
900
  try {
@@ -911,7 +915,20 @@ class AutomaticScans {
911
915
  active.app, entry, "snapshot-changed", counts)
912
916
  return null
913
917
  }
914
- const currentEntry = Object.assign({}, entry, fileSnapshot(current))
918
+ return {
919
+ stat: current,
920
+ entry: Object.assign({}, entry, fileSnapshot(current))
921
+ }
922
+ }
923
+
924
+ async verifiedAutomaticHash(
925
+ active, entry, memory, verifiedHashes, counts, prepared = null
926
+ ) {
927
+ const checked = prepared || await this.currentAutomaticEntry(
928
+ active, entry, counts)
929
+ if (!checked) return null
930
+ const current = checked.stat
931
+ const currentEntry = checked.entry
915
932
  const key = this.automaticHashKey(currentEntry)
916
933
  const cachedSnapshot = {
917
934
  size: entry.cached_size,
@@ -931,10 +948,11 @@ class AutomaticScans {
931
948
  if (reusable) {
932
949
  counts.hash_reuses += 1
933
950
  memory.set(key, reusable)
934
- verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
951
+ const verifiedEntry = Object.assign({}, currentEntry, {
935
952
  hash: reusable
936
- }))
937
- return reusable
953
+ })
954
+ verifiedHashes.set(pathKey(entry.path), verifiedEntry)
955
+ return verifiedEntry
938
956
  }
939
957
  let verified
940
958
  try {
@@ -961,25 +979,36 @@ class AutomaticScans {
961
979
  counts.hashed += 1
962
980
  counts.hash_bytes += Math.max(0, Number(verified.result.size) || 0)
963
981
  memory.set(key, verified.result.hash)
964
- verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
982
+ const verifiedEntry = Object.assign({}, currentEntry, {
965
983
  hash: verified.result.hash
966
- }))
967
- return verified.result.hash
984
+ })
985
+ verifiedHashes.set(pathKey(entry.path), verifiedEntry)
986
+ return verifiedEntry
968
987
  }
969
988
 
970
989
  async runPrecheck(active) {
971
990
  const app = active.app
972
991
  const root = path.resolve(this.vault.kernel.homedir, "api", app)
992
+ const startedAt = Date.now()
993
+ active.startedAt = startedAt
994
+ const markStage = (stage, entry = null) => {
995
+ active.stage = stage
996
+ active.stagePath = entry && entry.path ? entry.path : null
997
+ active.stageKind = entry && entry.kind ? entry.kind : null
998
+ }
999
+ markStage("app-root-check")
973
1000
  if (!await this.appRootIsAvailable(app)) {
974
1001
  const error = new Error("That app is no longer available.")
975
1002
  error.code = "ENOENT"
976
1003
  throw error
977
1004
  }
978
- const startedAt = Date.now()
979
1005
  const counts = {
980
1006
  files: 0,
981
1007
  bytes: 0,
982
1008
  candidates: 0,
1009
+ registry_pages: 0,
1010
+ candidate_checks: 0,
1011
+ peer_checks: 0,
983
1012
  path_errors: 0,
984
1013
  hashed: 0,
985
1014
  hash_bytes: 0,
@@ -987,6 +1016,7 @@ class AutomaticScans {
987
1016
  hash_failures: 0,
988
1017
  unstable_hashes: 0
989
1018
  }
1019
+ markStage("threshold-read")
990
1020
  const threshold = await this.candidateThreshold()
991
1021
  const paths = [...new Set(active.paths || [])].filter((filePath) =>
992
1022
  typeof filePath === "string" && inside(root, filePath))
@@ -998,6 +1028,7 @@ class AutomaticScans {
998
1028
  changed_paths: paths.length,
999
1029
  policy: "metadata-prefilter-sha256"
1000
1030
  })
1031
+ markStage("staging-reset")
1001
1032
  await this.vault.registry.beginAutomaticPrecheck(app)
1002
1033
  try {
1003
1034
  this.checkpoint(active)
@@ -1017,9 +1048,11 @@ class AutomaticScans {
1017
1048
  return true
1018
1049
  }
1019
1050
  }
1051
+ markStage("changed-parent-metadata")
1020
1052
  const safePaths = await this.changedPathsWithSafeParents(
1021
1053
  paths, root, metadataOptions, () => this.checkpoint(active))
1022
1054
  this.checkpoint(active)
1055
+ markStage("changed-path-metadata")
1023
1056
  const safeStats = await this.statChangedPaths(
1024
1057
  safePaths, metadataOptions, () => this.checkpoint(active))
1025
1058
  const statsByPath = new Map(safePaths.map((filePath, index) =>
@@ -1046,107 +1079,164 @@ class AutomaticScans {
1046
1079
  gid: stat.gid
1047
1080
  })
1048
1081
  }
1049
- counts.candidates = candidates.length
1050
1082
  if (candidates.length) {
1083
+ markStage("staging-write")
1051
1084
  await this.vault.registry.stageAutomaticPrecheckFiles(app, candidates)
1052
1085
  }
1053
1086
  this.checkpoint(active)
1087
+ markStage("app-root-recheck")
1054
1088
  if (!await this.appRootIsAvailable(app)) {
1055
1089
  const error = new Error("That app is no longer available.")
1056
1090
  error.code = "ENOENT"
1057
1091
  throw error
1058
1092
  }
1059
- const verifiedMatches = []
1093
+ this.log("verification-started", {
1094
+ app,
1095
+ files: counts.files,
1096
+ bytes: counts.bytes,
1097
+ threshold_candidates: candidates.length,
1098
+ duration_ms: Date.now() - startedAt
1099
+ })
1100
+ let proof = null
1060
1101
  let group = null
1102
+ const acceptProof = (candidate, peer, hash) => {
1103
+ proof = { path: candidate.path, hash }
1104
+ this.log("first-proof-exit", {
1105
+ app,
1106
+ candidate_path: candidate.path,
1107
+ peer_path: peer.path,
1108
+ peer_kind: peer.kind,
1109
+ size: candidate.size,
1110
+ duration_ms: Date.now() - startedAt,
1111
+ registry_pages: counts.registry_pages,
1112
+ candidate_checks: counts.candidate_checks,
1113
+ peer_checks: counts.peer_checks,
1114
+ hashed: counts.hashed,
1115
+ hash_bytes: counts.hash_bytes,
1116
+ hash_reuses: counts.hash_reuses
1117
+ })
1118
+ }
1061
1119
  const preparePeers = () => {
1062
1120
  if (!group || group.unmatched) return
1063
1121
  group.unmatched = new Map()
1064
- group.unmatchedIdentities = new Map()
1065
1122
  for (const [hash, value] of group.candidatesByHash) {
1066
- if (value.identities.size > 1) {
1067
- for (const candidate of value.candidates) {
1068
- group.matched.add(pathKey(candidate.path))
1069
- }
1070
- } else {
1071
- const identity = value.identities.values().next().value
1072
- group.unmatched.set(hash, {
1073
- identity,
1074
- candidates: value.candidates
1075
- })
1076
- group.unmatchedIdentities.set(
1077
- identity,
1078
- (group.unmatchedIdentities.get(identity) || 0) + 1)
1079
- }
1123
+ group.unmatched.set(hash, {
1124
+ identity: value.identities.values().next().value,
1125
+ candidate: value.candidate
1126
+ })
1080
1127
  }
1081
1128
  }
1082
- const finishGroup = () => {
1083
- if (!group) return
1084
- preparePeers()
1085
- for (const [hash, value] of group.candidatesByHash) {
1086
- for (const candidate of value.candidates) {
1087
- if (!group.matched.has(pathKey(candidate.path))) continue
1088
- verifiedMatches.push({ path: candidate.path, hash })
1129
+ const hashCandidates = async (peerIdentity = null) => {
1130
+ if (!group || group.candidatesHashed) return !!group
1131
+ const identities = new Set(group.candidates.map((candidate) =>
1132
+ this.automaticIdentityKey(candidate.entry)))
1133
+ if (identities.size < 2 &&
1134
+ (!peerIdentity || identities.has(peerIdentity))) {
1135
+ return false
1136
+ }
1137
+ group.candidatesHashed = true
1138
+ counts.candidates += group.candidates.length
1139
+ for (const candidate of group.candidates) {
1140
+ this.checkpoint(active)
1141
+ markStage("candidate-hash", candidate.entry)
1142
+ const verified = await this.verifiedAutomaticHash(
1143
+ active,
1144
+ candidate.entry,
1145
+ group.memory,
1146
+ verifiedHashes,
1147
+ counts,
1148
+ candidate
1149
+ )
1150
+ if (!verified) continue
1151
+ const hash = verified.hash
1152
+ let value = group.candidatesByHash.get(hash)
1153
+ if (!value) {
1154
+ value = { candidate: verified, identities: new Set() }
1155
+ group.candidatesByHash.set(hash, value)
1156
+ }
1157
+ value.identities.add(this.automaticIdentityKey(verified))
1158
+ if (value.identities.size > 1) {
1159
+ acceptProof(value.candidate, verified, hash)
1160
+ return true
1089
1161
  }
1090
1162
  }
1163
+ preparePeers()
1164
+ return true
1165
+ }
1166
+ const finishGroup = async () => {
1167
+ if (!group) return false
1168
+ await hashCandidates()
1169
+ if (proof) return true
1091
1170
  group = null
1171
+ return false
1172
+ }
1173
+ const complete = async () => {
1174
+ this.checkpoint(active)
1175
+ markStage("result-signature")
1176
+ const result = await this.vault.registry.automaticPrecheckResult(
1177
+ app, proof ? [proof] : [])
1178
+ this.checkpoint(active)
1179
+ return Object.assign({}, counts, {
1180
+ signature: result.signature,
1181
+ verified_files: Math.max(0, Number(result.files) || 0),
1182
+ duration_ms: Date.now() - startedAt
1183
+ })
1092
1184
  }
1093
1185
  let cursor = null
1094
1186
  do {
1187
+ markStage("candidate-query")
1188
+ counts.registry_pages += 1
1095
1189
  const page = await this.vault.registry.automaticPrecheckEntries(
1096
1190
  app, cursor, ENTRY_BATCH_SIZE)
1097
1191
  this.checkpoint(active)
1098
1192
  for (const entry of page.entries) {
1099
1193
  const key = `${entry.dev}\0${entry.size}`
1100
1194
  if (!group || group.key !== key) {
1101
- finishGroup()
1195
+ if (await finishGroup()) return await complete()
1102
1196
  group = {
1103
1197
  key,
1198
+ candidates: [],
1199
+ candidatesHashed: false,
1104
1200
  candidatesByHash: new Map(),
1105
- matched: new Set(),
1106
1201
  unmatched: null,
1107
- unmatchedIdentities: null,
1108
1202
  memory: new Map()
1109
1203
  }
1110
1204
  }
1111
1205
  if (entry.kind === "changed") {
1112
- const candidate = entry
1113
1206
  this.checkpoint(active)
1114
- const hash = await this.verifiedAutomaticHash(
1115
- active, candidate, group.memory, verifiedHashes, counts)
1116
- if (!hash) continue
1117
- let value = group.candidatesByHash.get(hash)
1118
- if (!value) {
1119
- value = { candidates: [], identities: new Set() }
1120
- group.candidatesByHash.set(hash, value)
1121
- }
1122
- value.candidates.push(candidate)
1123
- value.identities.add(this.automaticIdentityKey(candidate))
1207
+ markStage("candidate-metadata", entry)
1208
+ counts.candidate_checks += 1
1209
+ const candidate = await this.currentAutomaticEntry(
1210
+ active, entry, counts)
1211
+ if (candidate) group.candidates.push(candidate)
1124
1212
  continue
1125
1213
  }
1126
- preparePeers()
1214
+ if (!group.candidatesHashed) await hashCandidates()
1215
+ if (proof) return await complete()
1216
+ if (group.candidatesHashed && !group.unmatched.size) continue
1217
+ markStage("peer-metadata", entry)
1218
+ counts.peer_checks += 1
1219
+ const peer = await this.currentAutomaticEntry(active, entry, counts)
1220
+ if (!peer) continue
1221
+ const identity = this.automaticIdentityKey(peer.entry)
1222
+ if (!await hashCandidates(identity)) continue
1223
+ if (proof) return await complete()
1127
1224
  if (!group.unmatched.size) continue
1225
+ if (![...group.unmatched.values()].some((wanted) =>
1226
+ wanted.identity !== identity)) continue
1128
1227
  this.checkpoint(active)
1129
- const identity = this.automaticIdentityKey(entry)
1130
- if (group.unmatchedIdentities.size === 1 &&
1131
- group.unmatchedIdentities.has(identity)) continue
1132
- const hash = await this.verifiedAutomaticHash(
1133
- active, entry, group.memory, verifiedHashes, counts)
1134
- const wanted = hash && group.unmatched.get(hash)
1228
+ markStage("peer-hash", entry)
1229
+ const verified = await this.verifiedAutomaticHash(
1230
+ active, entry, group.memory, verifiedHashes, counts, peer)
1231
+ const wanted = verified && group.unmatched.get(verified.hash)
1135
1232
  if (!wanted || wanted.identity === identity) {
1136
1233
  continue
1137
1234
  }
1138
- for (const candidate of wanted.candidates) {
1139
- group.matched.add(pathKey(candidate.path))
1140
- }
1141
- group.unmatched.delete(hash)
1142
- const remaining = group.unmatchedIdentities.get(wanted.identity) - 1
1143
- if (remaining) {
1144
- group.unmatchedIdentities.set(wanted.identity, remaining)
1145
- } else {
1146
- group.unmatchedIdentities.delete(wanted.identity)
1147
- }
1235
+ acceptProof(wanted.candidate, verified, verified.hash)
1236
+ return await complete()
1148
1237
  }
1149
1238
  if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
1239
+ markStage("hash-cache-write")
1150
1240
  await this.vault.registry.rememberHashCache(
1151
1241
  [...verifiedHashes.values()])
1152
1242
  verifiedHashes.clear()
@@ -1154,24 +1244,26 @@ class AutomaticScans {
1154
1244
  }
1155
1245
  cursor = page.next_cursor
1156
1246
  } while (cursor)
1157
- finishGroup()
1158
- this.checkpoint(active)
1159
- const result = await this.vault.registry.automaticPrecheckResult(
1160
- app, verifiedMatches)
1161
- this.checkpoint(active)
1162
- return Object.assign({}, counts, {
1163
- signature: result.signature,
1164
- verified_files: Math.max(0, Number(result.files) || 0),
1165
- duration_ms: Date.now() - startedAt
1166
- })
1247
+ await finishGroup()
1248
+ return await complete()
1167
1249
  } finally {
1168
1250
  try {
1169
1251
  if (verifiedHashes.size) {
1170
- await this.vault.registry.rememberHashCache(
1171
- [...verifiedHashes.values()])
1252
+ try {
1253
+ await this.vault.registry.rememberHashCache(
1254
+ [...verifiedHashes.values()])
1255
+ } catch (error) {
1256
+ markStage("hash-cache-write")
1257
+ throw error
1258
+ }
1172
1259
  }
1173
1260
  } finally {
1174
- await this.vault.registry.abortAutomaticPrecheck(app)
1261
+ try {
1262
+ await this.vault.registry.abortAutomaticPrecheck(app)
1263
+ } catch (error) {
1264
+ markStage("staging-cleanup")
1265
+ throw error
1266
+ }
1175
1267
  }
1176
1268
  }
1177
1269
  }
@@ -1275,10 +1367,22 @@ class AutomaticScans {
1275
1367
  ? (error.code === "EVAULTCANCELLED" ? "cancelled" : "failed")
1276
1368
  : "complete",
1277
1369
  cancel_reason: reason,
1278
- duration_ms: result && result.duration_ms,
1370
+ duration_ms: result
1371
+ ? result.duration_ms
1372
+ : error && active.startedAt
1373
+ ? Date.now() - active.startedAt
1374
+ : undefined,
1375
+ failure_stage: error && active.stage,
1376
+ failure_path: error && (error.path || active.stagePath),
1377
+ failure_kind: error && active.stageKind,
1378
+ error_code: error && error.code,
1379
+ error_syscall: error && error.syscall,
1279
1380
  files: result && result.files,
1280
1381
  bytes: result && result.bytes,
1281
1382
  candidates: result && result.candidates,
1383
+ registry_pages: result && result.registry_pages,
1384
+ candidate_checks: result && result.candidate_checks,
1385
+ peer_checks: result && result.peer_checks,
1282
1386
  verified_files: result && result.verified_files,
1283
1387
  hashed: result && result.hashed,
1284
1388
  hash_bytes: result && result.hash_bytes,