sentinelayer-cli 0.6.2 → 0.8.1

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 (280) hide show
  1. package/README.md +1009 -996
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +64 -63
  6. package/src/agents/ai-governance/index.js +12 -0
  7. package/src/agents/ai-governance/tools/base.js +171 -0
  8. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  9. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  10. package/src/agents/ai-governance/tools/index.js +52 -0
  11. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  12. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  13. package/src/agents/backend/index.js +12 -0
  14. package/src/agents/backend/tools/base.js +189 -0
  15. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  16. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  17. package/src/agents/backend/tools/index.js +87 -0
  18. package/src/agents/backend/tools/retry-audit.js +132 -0
  19. package/src/agents/backend/tools/timeout-audit.js +144 -0
  20. package/src/agents/code-quality/index.js +12 -0
  21. package/src/agents/code-quality/tools/base.js +159 -0
  22. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  23. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  24. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  25. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  26. package/src/agents/code-quality/tools/index.js +89 -0
  27. package/src/agents/data-layer/index.js +12 -0
  28. package/src/agents/data-layer/tools/base.js +181 -0
  29. package/src/agents/data-layer/tools/index-audit.js +165 -0
  30. package/src/agents/data-layer/tools/index.js +83 -0
  31. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  32. package/src/agents/data-layer/tools/query-explain.js +120 -0
  33. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  34. package/src/agents/documentation/index.js +12 -0
  35. package/src/agents/documentation/tools/api-diff.js +91 -0
  36. package/src/agents/documentation/tools/base.js +151 -0
  37. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  38. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  39. package/src/agents/documentation/tools/index.js +52 -0
  40. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  41. package/src/agents/envelope/fix-cycle.js +45 -0
  42. package/src/agents/envelope/index.js +31 -0
  43. package/src/agents/envelope/loop.js +150 -0
  44. package/src/agents/envelope/pulse.js +18 -0
  45. package/src/agents/envelope/stream.js +40 -0
  46. package/src/agents/infrastructure/index.js +12 -0
  47. package/src/agents/infrastructure/tools/base.js +171 -0
  48. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  49. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  50. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  51. package/src/agents/infrastructure/tools/index.js +52 -0
  52. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  53. package/src/agents/jules/config/definition.js +160 -160
  54. package/src/agents/jules/config/system-prompt.js +182 -182
  55. package/src/agents/jules/error-intake.js +51 -51
  56. package/src/agents/jules/fix-cycle.js +17 -17
  57. package/src/agents/jules/loop.js +460 -450
  58. package/src/agents/jules/pulse.js +10 -10
  59. package/src/agents/jules/stream.js +187 -186
  60. package/src/agents/jules/swarm/file-scanner.js +74 -74
  61. package/src/agents/jules/swarm/index.js +11 -11
  62. package/src/agents/jules/swarm/orchestrator.js +362 -362
  63. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  64. package/src/agents/jules/swarm/sub-agent.js +315 -309
  65. package/src/agents/jules/tools/aidenid-email.js +189 -189
  66. package/src/agents/jules/tools/auth-audit.js +1708 -1691
  67. package/src/agents/jules/tools/dispatch.js +340 -335
  68. package/src/agents/jules/tools/file-edit.js +2 -2
  69. package/src/agents/jules/tools/file-read.js +2 -2
  70. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  71. package/src/agents/jules/tools/glob.js +2 -2
  72. package/src/agents/jules/tools/grep.js +2 -2
  73. package/src/agents/jules/tools/index.js +29 -29
  74. package/src/agents/jules/tools/path-guards.js +2 -2
  75. package/src/agents/jules/tools/runtime-audit.js +507 -507
  76. package/src/agents/jules/tools/shell.js +2 -2
  77. package/src/agents/jules/tools/url-policy.js +100 -100
  78. package/src/agents/mode.js +113 -0
  79. package/src/agents/observability/index.js +12 -0
  80. package/src/agents/observability/tools/alert-audit.js +39 -0
  81. package/src/agents/observability/tools/base.js +181 -0
  82. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  83. package/src/agents/observability/tools/index.js +54 -0
  84. package/src/agents/observability/tools/log-schema-check.js +74 -0
  85. package/src/agents/observability/tools/span-coverage.js +74 -0
  86. package/src/agents/persona-visuals.js +102 -61
  87. package/src/agents/release/index.js +12 -0
  88. package/src/agents/release/tools/base.js +181 -0
  89. package/src/agents/release/tools/changelog-diff.js +86 -0
  90. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  91. package/src/agents/release/tools/index.js +61 -0
  92. package/src/agents/release/tools/rollback-verify.js +129 -0
  93. package/src/agents/release/tools/semver-check.js +109 -0
  94. package/src/agents/reliability/index.js +12 -0
  95. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  96. package/src/agents/reliability/tools/base.js +181 -0
  97. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  98. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  99. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  100. package/src/agents/reliability/tools/index.js +87 -0
  101. package/src/agents/run-persona.js +109 -0
  102. package/src/agents/security/index.js +12 -0
  103. package/src/agents/security/tools/authz-audit.js +134 -0
  104. package/src/agents/security/tools/base.js +190 -0
  105. package/src/agents/security/tools/crypto-review.js +175 -0
  106. package/src/agents/security/tools/index.js +97 -0
  107. package/src/agents/security/tools/sast-scan.js +175 -0
  108. package/src/agents/security/tools/secrets-scan.js +216 -0
  109. package/src/agents/shared-tools/dispatch-core.js +320 -315
  110. package/src/agents/shared-tools/file-edit.js +180 -180
  111. package/src/agents/shared-tools/file-read.js +100 -100
  112. package/src/agents/shared-tools/glob.js +168 -168
  113. package/src/agents/shared-tools/grep.js +228 -228
  114. package/src/agents/shared-tools/index.js +46 -46
  115. package/src/agents/shared-tools/path-guards.js +161 -161
  116. package/src/agents/shared-tools/shell.js +383 -383
  117. package/src/agents/supply-chain/index.js +12 -0
  118. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  119. package/src/agents/supply-chain/tools/base.js +151 -0
  120. package/src/agents/supply-chain/tools/index.js +52 -0
  121. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  122. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  123. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  124. package/src/agents/testing/index.js +12 -0
  125. package/src/agents/testing/tools/base.js +202 -0
  126. package/src/agents/testing/tools/coverage-gap.js +144 -0
  127. package/src/agents/testing/tools/flake-detect.js +125 -0
  128. package/src/agents/testing/tools/index.js +85 -0
  129. package/src/agents/testing/tools/mutation-test.js +143 -0
  130. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  131. package/src/ai/aidenid.js +1021 -1009
  132. package/src/ai/client.js +553 -553
  133. package/src/ai/domain-target-store.js +268 -268
  134. package/src/ai/identity-store.js +270 -270
  135. package/src/ai/proxy.js +137 -137
  136. package/src/ai/site-store.js +145 -145
  137. package/src/audit/agents/architecture.js +180 -180
  138. package/src/audit/agents/compliance.js +179 -179
  139. package/src/audit/agents/documentation.js +165 -165
  140. package/src/audit/agents/performance.js +145 -145
  141. package/src/audit/agents/security.js +215 -215
  142. package/src/audit/agents/testing.js +172 -172
  143. package/src/audit/orchestrator.js +557 -557
  144. package/src/audit/package.js +204 -204
  145. package/src/audit/registry.js +284 -284
  146. package/src/audit/replay.js +103 -103
  147. package/src/auth/gate.js +428 -371
  148. package/src/auth/http.js +681 -611
  149. package/src/auth/service.js +1106 -1106
  150. package/src/auth/session-store.js +813 -813
  151. package/src/cli.js +257 -252
  152. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  153. package/src/commands/ai/provision-governance.js +1272 -1272
  154. package/src/commands/ai/shared.js +147 -147
  155. package/src/commands/ai.js +11 -11
  156. package/src/commands/apply.js +12 -12
  157. package/src/commands/audit.js +1171 -1166
  158. package/src/commands/auth.js +419 -419
  159. package/src/commands/chat.js +184 -191
  160. package/src/commands/config.js +184 -184
  161. package/src/commands/cost.js +311 -311
  162. package/src/commands/daemon/core.js +850 -850
  163. package/src/commands/daemon/extended.js +1048 -1048
  164. package/src/commands/daemon/shared.js +213 -213
  165. package/src/commands/daemon.js +11 -11
  166. package/src/commands/guide.js +174 -174
  167. package/src/commands/ingest.js +58 -58
  168. package/src/commands/init.js +55 -55
  169. package/src/commands/legacy-args.js +20 -10
  170. package/src/commands/mcp.js +461 -461
  171. package/src/commands/omargate.js +63 -29
  172. package/src/commands/persona.js +65 -20
  173. package/src/commands/plugin.js +260 -260
  174. package/src/commands/policy.js +132 -132
  175. package/src/commands/prompt.js +238 -238
  176. package/src/commands/review.js +704 -704
  177. package/src/commands/scan.js +865 -872
  178. package/src/commands/session.js +1238 -0
  179. package/src/commands/spec.js +771 -716
  180. package/src/commands/swarm.js +651 -651
  181. package/src/commands/telemetry.js +202 -202
  182. package/src/commands/watch.js +511 -511
  183. package/src/config/agent-dictionary.js +182 -182
  184. package/src/config/io.js +56 -56
  185. package/src/config/paths.js +18 -18
  186. package/src/config/schema.js +55 -55
  187. package/src/config/service.js +184 -184
  188. package/src/coord/events-log.js +141 -0
  189. package/src/coord/handshake.js +719 -0
  190. package/src/coord/index.js +35 -0
  191. package/src/coord/paths.js +84 -0
  192. package/src/coord/priority.js +62 -0
  193. package/src/coord/tarjan.js +157 -0
  194. package/src/cost/budget.js +235 -235
  195. package/src/cost/history.js +188 -188
  196. package/src/cost/tokenizer.js +160 -0
  197. package/src/cost/tracker.js +232 -171
  198. package/src/daemon/artifact-lineage.js +896 -534
  199. package/src/daemon/assignment-ledger.js +1083 -770
  200. package/src/daemon/ast-drift.js +496 -0
  201. package/src/daemon/ast-parser-layer.js +258 -258
  202. package/src/daemon/budget-governor.js +633 -633
  203. package/src/daemon/callgraph-overlay.js +646 -646
  204. package/src/daemon/error-worker.js +1209 -626
  205. package/src/daemon/fix-cycle.js +384 -377
  206. package/src/daemon/hybrid-mapper.js +929 -929
  207. package/src/daemon/ingest-refresh.js +79 -11
  208. package/src/daemon/jira-lifecycle.js +767 -632
  209. package/src/daemon/operator-control.js +657 -657
  210. package/src/daemon/pulse.js +327 -327
  211. package/src/daemon/reliability-lane.js +471 -471
  212. package/src/daemon/scope-engine.js +1068 -0
  213. package/src/daemon/watchdog.js +971 -971
  214. package/src/events/schema.js +190 -0
  215. package/src/guide/generator.js +316 -316
  216. package/src/ingest/engine.js +933 -918
  217. package/src/ingest/ownership.js +380 -0
  218. package/src/interactive/index.js +97 -97
  219. package/src/legacy-cli.js +3228 -2994
  220. package/src/mcp/registry.js +695 -695
  221. package/src/memory/blackboard.js +301 -301
  222. package/src/memory/retrieval.js +581 -581
  223. package/src/orchestrator/kai-chen.js +126 -0
  224. package/src/plugin/manifest.js +553 -553
  225. package/src/policy/packs.js +144 -144
  226. package/src/prompt/generator.js +136 -118
  227. package/src/review/ai-review.js +672 -679
  228. package/src/review/compliance-pack.js +389 -0
  229. package/src/review/investor-dd-config.js +54 -0
  230. package/src/review/investor-dd-file-loop.js +303 -0
  231. package/src/review/investor-dd-file-router.js +406 -0
  232. package/src/review/investor-dd-html-report.js +233 -0
  233. package/src/review/investor-dd-notification.js +120 -0
  234. package/src/review/investor-dd-orchestrator.js +405 -0
  235. package/src/review/investor-dd-persona-runner.js +275 -0
  236. package/src/review/live-validator.js +253 -0
  237. package/src/review/local-review.js +1351 -1305
  238. package/src/review/omargate-interactive.js +68 -68
  239. package/src/review/omargate-orchestrator.js +492 -300
  240. package/src/review/persona-prompts.js +484 -296
  241. package/src/review/reconciliation-rules.js +329 -0
  242. package/src/review/replay.js +235 -235
  243. package/src/review/report.js +664 -664
  244. package/src/review/reproducibility-chain.js +136 -0
  245. package/src/review/scan-modes.js +147 -42
  246. package/src/review/spec-binding.js +487 -487
  247. package/src/scaffold/generator.js +67 -67
  248. package/src/scaffold/templates.js +150 -150
  249. package/src/scan/generator.js +418 -418
  250. package/src/scan/gh-secrets.js +107 -107
  251. package/src/session/agent-registry.js +359 -0
  252. package/src/session/analytics.js +479 -0
  253. package/src/session/daemon.js +1396 -0
  254. package/src/session/file-locks.js +666 -0
  255. package/src/session/paths.js +37 -0
  256. package/src/session/recap.js +567 -0
  257. package/src/session/redact.js +82 -0
  258. package/src/session/runtime-bridge.js +762 -0
  259. package/src/session/scoring.js +406 -0
  260. package/src/session/setup-guides.js +304 -0
  261. package/src/session/store.js +704 -0
  262. package/src/session/stream.js +333 -0
  263. package/src/session/sync.js +753 -0
  264. package/src/session/tasks.js +1054 -0
  265. package/src/session/templates.js +188 -0
  266. package/src/spec/generator.js +619 -519
  267. package/src/spec/regenerate.js +237 -237
  268. package/src/spec/templates.js +91 -91
  269. package/src/swarm/dashboard.js +247 -247
  270. package/src/swarm/factory.js +363 -363
  271. package/src/swarm/pentest.js +934 -934
  272. package/src/swarm/registry.js +419 -419
  273. package/src/swarm/report.js +158 -158
  274. package/src/swarm/runtime.js +569 -576
  275. package/src/swarm/scenario-dsl.js +272 -272
  276. package/src/telemetry/ledger.js +302 -302
  277. package/src/telemetry/session-tracker.js +234 -234
  278. package/src/telemetry/sync.js +203 -203
  279. package/src/ui/command-hints.js +13 -13
  280. package/src/ui/markdown.js +220 -220
@@ -1,534 +1,896 @@
1
- import fsp from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- import { listAssignments, resolveAssignmentLedgerStorage } from "./assignment-ledger.js";
5
- import { getBudgetHealthColor, resolveOperatorControlStorage } from "./operator-control.js";
6
- import { listBudgetStates, resolveBudgetGovernorStorage } from "./budget-governor.js";
7
- import { listErrorQueue, resolveErrorDaemonStorage, WORK_ITEM_STATUSES } from "./error-worker.js";
8
- import { listJiraIssues, resolveJiraLifecycleStorage } from "./jira-lifecycle.js";
9
-
10
- const LINEAGE_SCHEMA_VERSION = "1.0.0";
11
- const WORK_ITEM_STATUS_SET = new Set(WORK_ITEM_STATUSES);
12
-
13
- function normalizeString(value) {
14
- return String(value || "").trim();
15
- }
16
-
17
- function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
18
- const normalized = normalizeString(value);
19
- if (!normalized) {
20
- return fallbackIso;
21
- }
22
- const epoch = Date.parse(normalized);
23
- if (!Number.isFinite(epoch)) {
24
- return fallbackIso;
25
- }
26
- return new Date(epoch).toISOString();
27
- }
28
-
29
- function normalizePositiveInteger(value, fieldName, fallbackValue) {
30
- if (value === undefined || value === null || normalizeString(value) === "") {
31
- return fallbackValue;
32
- }
33
- const normalized = Number(value);
34
- if (!Number.isFinite(normalized) || normalized <= 0) {
35
- throw new Error(`${fieldName} must be a positive integer.`);
36
- }
37
- return Math.floor(normalized);
38
- }
39
-
40
- function toPosixPath(value = "") {
41
- return String(value || "").replace(/\\/g, "/");
42
- }
43
-
44
- function toRelativePosix(baseDir, absolutePath) {
45
- const relative = path.relative(baseDir, absolutePath);
46
- return toPosixPath(relative);
47
- }
48
-
49
- async function writeJsonFile(filePath, payload = {}) {
50
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
51
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
52
- }
53
-
54
- async function appendJsonLine(filePath, payload = {}) {
55
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
56
- await fsp.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
57
- }
58
-
59
- async function readJsonFile(filePath, fallbackValue) {
60
- try {
61
- const raw = await fsp.readFile(filePath, "utf-8");
62
- return JSON.parse(raw);
63
- } catch (error) {
64
- if (error && typeof error === "object" && error.code === "ENOENT") {
65
- return fallbackValue;
66
- }
67
- throw error;
68
- }
69
- }
70
-
71
- async function readJsonFileOptional(filePath) {
72
- try {
73
- const raw = await fsp.readFile(filePath, "utf-8");
74
- return JSON.parse(raw);
75
- } catch (error) {
76
- if (error && typeof error === "object" && error.code === "ENOENT") {
77
- return null;
78
- }
79
- return null;
80
- }
81
- }
82
-
83
- async function readJsonFilesInDirectory(dirPath) {
84
- try {
85
- const entries = await fsp.readdir(dirPath, { withFileTypes: true });
86
- const files = entries
87
- .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
88
- .map((entry) => path.join(dirPath, entry.name))
89
- .sort((left, right) => left.localeCompare(right));
90
- const parsed = await Promise.all(
91
- files.map(async (filePath) => ({
92
- filePath,
93
- payload: await readJsonFileOptional(filePath),
94
- }))
95
- );
96
- return parsed.filter((entry) => entry.payload && typeof entry.payload === "object");
97
- } catch (error) {
98
- if (error && typeof error === "object" && error.code === "ENOENT") {
99
- return [];
100
- }
101
- throw error;
102
- }
103
- }
104
-
105
- function createInitialLineageIndex(nowIso = new Date().toISOString()) {
106
- return {
107
- schemaVersion: LINEAGE_SCHEMA_VERSION,
108
- generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
109
- lineageRunId: null,
110
- summary: {
111
- totalQueueItems: 0,
112
- totalWorkItemsIndexed: 0,
113
- statusCounts: {},
114
- activeAgentCount: 0,
115
- jiraLinkedCount: 0,
116
- budgetGuardedCount: 0,
117
- operatorCoveredCount: 0,
118
- },
119
- daemonArtifacts: {
120
- queuePath: null,
121
- statePath: null,
122
- streamPath: null,
123
- assignmentLedgerPath: null,
124
- assignmentEventsPath: null,
125
- jiraLifecyclePath: null,
126
- jiraEventsPath: null,
127
- budgetStatePath: null,
128
- budgetEventsPath: null,
129
- operatorStatePath: null,
130
- operatorEventsPath: null,
131
- },
132
- runs: {
133
- errorDaemonRuns: [],
134
- budgetChecks: [],
135
- operatorSnapshots: [],
136
- },
137
- workItems: [],
138
- };
139
- }
140
-
141
- function normalizeStatusList(statuses = []) {
142
- if (!Array.isArray(statuses)) {
143
- return [];
144
- }
145
- return statuses
146
- .map((status) => normalizeString(status).toUpperCase())
147
- .filter(Boolean)
148
- .filter((status) => WORK_ITEM_STATUS_SET.has(status));
149
- }
150
-
151
- function createLineageRunId(nowIso) {
152
- return `lineage-${nowIso.replace(/[:.]/g, "-")}`;
153
- }
154
-
155
- function summarizeStatusCounts(workItems = []) {
156
- const statusCounts = {};
157
- for (const item of workItems) {
158
- const status = normalizeString(item.workItemStatus).toUpperCase() || "UNKNOWN";
159
- statusCounts[status] = (statusCounts[status] || 0) + 1;
160
- }
161
- return statusCounts;
162
- }
163
-
164
- export async function resolveArtifactLineageStorage({
165
- targetPath = ".",
166
- outputDir = "",
167
- env,
168
- homeDir,
169
- } = {}) {
170
- const daemonStorage = await resolveErrorDaemonStorage({
171
- targetPath,
172
- outputDir,
173
- env,
174
- homeDir,
175
- });
176
- const lineageDir = path.join(daemonStorage.baseDir, "lineage");
177
- return {
178
- ...daemonStorage,
179
- lineageDir,
180
- lineageIndexPath: path.join(lineageDir, "lineage-index.json"),
181
- lineageEventsPath: path.join(lineageDir, "lineage-events.ndjson"),
182
- };
183
- }
184
-
185
- export async function buildArtifactLineageIndex({
186
- targetPath = ".",
187
- outputDir = "",
188
- env,
189
- homeDir,
190
- nowIso = new Date().toISOString(),
191
- } = {}) {
192
- const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
193
- const storage = await resolveArtifactLineageStorage({
194
- targetPath,
195
- outputDir,
196
- env,
197
- homeDir,
198
- });
199
- const assignmentStorage = await resolveAssignmentLedgerStorage({
200
- targetPath,
201
- outputDir,
202
- env,
203
- homeDir,
204
- });
205
- const jiraStorage = await resolveJiraLifecycleStorage({
206
- targetPath,
207
- outputDir,
208
- env,
209
- homeDir,
210
- });
211
- const budgetStorage = await resolveBudgetGovernorStorage({
212
- targetPath,
213
- outputDir,
214
- env,
215
- homeDir,
216
- });
217
- const operatorStorage = await resolveOperatorControlStorage({
218
- targetPath,
219
- outputDir,
220
- env,
221
- homeDir,
222
- });
223
-
224
- const [queue, assignments, issues, budgets, errorRuns, budgetRuns, operatorSnapshots] =
225
- await Promise.all([
226
- listErrorQueue({
227
- targetPath,
228
- outputDir,
229
- limit: 5000,
230
- env,
231
- homeDir,
232
- }),
233
- listAssignments({
234
- targetPath,
235
- outputDir,
236
- includeExpired: true,
237
- limit: 5000,
238
- env,
239
- homeDir,
240
- nowIso: normalizedNow,
241
- }),
242
- listJiraIssues({
243
- targetPath,
244
- outputDir,
245
- limit: 5000,
246
- env,
247
- homeDir,
248
- nowIso: normalizedNow,
249
- }),
250
- listBudgetStates({
251
- targetPath,
252
- outputDir,
253
- limit: 5000,
254
- env,
255
- homeDir,
256
- nowIso: normalizedNow,
257
- }),
258
- readJsonFilesInDirectory(storage.runsDir),
259
- readJsonFilesInDirectory(budgetStorage.budgetRunsDir),
260
- readJsonFilesInDirectory(operatorStorage.operatorSnapshotsDir),
261
- ]);
262
-
263
- const assignmentByWorkItem = new Map();
264
- for (const assignment of assignments.assignments) {
265
- if (!assignmentByWorkItem.has(assignment.workItemId)) {
266
- assignmentByWorkItem.set(assignment.workItemId, assignment);
267
- }
268
- }
269
- const issueByWorkItem = new Map();
270
- for (const issue of issues.issues) {
271
- if (!issueByWorkItem.has(issue.workItemId)) {
272
- issueByWorkItem.set(issue.workItemId, issue);
273
- }
274
- }
275
- const budgetByWorkItem = new Map();
276
- for (const record of budgets.records) {
277
- if (!budgetByWorkItem.has(record.workItemId)) {
278
- budgetByWorkItem.set(record.workItemId, record);
279
- }
280
- }
281
-
282
- const budgetRunsByWorkItem = new Map();
283
- for (const run of budgetRuns) {
284
- const workItemId = normalizeString(run.payload.workItemId);
285
- if (!workItemId) {
286
- continue;
287
- }
288
- if (!budgetRunsByWorkItem.has(workItemId)) {
289
- budgetRunsByWorkItem.set(workItemId, []);
290
- }
291
- budgetRunsByWorkItem.get(workItemId).push({
292
- runId: normalizeString(run.payload.runId) || path.basename(run.filePath, ".json"),
293
- generatedAt: normalizeIsoTimestamp(run.payload.generatedAt, normalizedNow),
294
- lifecycleState: normalizeString(run.payload.lifecycleState) || "WITHIN_BUDGET",
295
- action: normalizeString(run.payload.action) || "NONE",
296
- path: toRelativePosix(storage.outputRoot, run.filePath),
297
- });
298
- }
299
-
300
- const operatorSnapshotsByWorkItem = new Map();
301
- const operatorSnapshotSummaries = [];
302
- for (const snapshot of operatorSnapshots) {
303
- const runId = normalizeString(snapshot.payload.runId) || path.basename(snapshot.filePath, ".json");
304
- const generatedAt = normalizeIsoTimestamp(snapshot.payload.generatedAt, normalizedNow);
305
- const workItems = Array.isArray(snapshot.payload.workItems) ? snapshot.payload.workItems : [];
306
- operatorSnapshotSummaries.push({
307
- runId,
308
- generatedAt,
309
- path: toRelativePosix(storage.outputRoot, snapshot.filePath),
310
- visibleWorkItems: workItems.length,
311
- });
312
- for (const workItem of workItems) {
313
- const workItemId = normalizeString(workItem.workItemId);
314
- if (!workItemId) {
315
- continue;
316
- }
317
- if (!operatorSnapshotsByWorkItem.has(workItemId)) {
318
- operatorSnapshotsByWorkItem.set(workItemId, []);
319
- }
320
- operatorSnapshotsByWorkItem.get(workItemId).push({
321
- runId,
322
- generatedAt,
323
- path: toRelativePosix(storage.outputRoot, snapshot.filePath),
324
- budgetHealthColor: getBudgetHealthColor(workItem.budgetHealthColor),
325
- assignmentStatus: normalizeString(workItem.assignmentStatus) || "QUEUED",
326
- workItemStatus: normalizeString(workItem.workItemStatus) || "QUEUED",
327
- });
328
- }
329
- }
330
-
331
- const workItems = queue.items.map((queueItem) => {
332
- const assignment = assignmentByWorkItem.get(queueItem.workItemId) || null;
333
- const issue = issueByWorkItem.get(queueItem.workItemId) || null;
334
- const budget = budgetByWorkItem.get(queueItem.workItemId) || null;
335
- const budgetRunsForItem = budgetRunsByWorkItem.get(queueItem.workItemId) || [];
336
- const operatorSnapshotsForItem = operatorSnapshotsByWorkItem.get(queueItem.workItemId) || [];
337
- return {
338
- workItemId: queueItem.workItemId,
339
- workItemStatus: queueItem.status,
340
- severity: queueItem.severity,
341
- service: queueItem.service,
342
- endpoint: queueItem.endpoint,
343
- errorCode: queueItem.errorCode,
344
- message: queueItem.message,
345
- firstSeenAt: queueItem.firstSeenAt,
346
- lastSeenAt: queueItem.lastSeenAt,
347
- links: {
348
- agentIdentity: assignment?.assignedAgentIdentity || null,
349
- assignmentStatus: assignment?.status || null,
350
- assignmentStage: assignment?.stage || null,
351
- loopRunId: assignment?.runId || null,
352
- jiraIssueKey: issue?.issueKey || null,
353
- jiraStatus: issue?.status || null,
354
- budgetLifecycleState: budget?.lifecycleState || "WITHIN_BUDGET",
355
- budgetHealthColor: getBudgetHealthColor(budget?.lifecycleState || "WITHIN_BUDGET"),
356
- latestOperatorSnapshotRunId: operatorSnapshotsForItem.length > 0
357
- ? operatorSnapshotsForItem[0].runId
358
- : null,
359
- },
360
- artifacts: {
361
- queuePath: toRelativePosix(storage.outputRoot, storage.queuePath),
362
- assignmentLedgerPath: toRelativePosix(storage.outputRoot, assignmentStorage.ledgerPath),
363
- jiraLifecyclePath: toRelativePosix(storage.outputRoot, jiraStorage.lifecyclePath),
364
- budgetStatePath: toRelativePosix(storage.outputRoot, budgetStorage.budgetStatePath),
365
- operatorStatePath: toRelativePosix(storage.outputRoot, operatorStorage.operatorStatePath),
366
- budgetRuns: budgetRunsForItem,
367
- operatorSnapshots: operatorSnapshotsForItem,
368
- },
369
- updatedAt: queueItem.updatedAt,
370
- };
371
- });
372
-
373
- const lineageRunId = createLineageRunId(normalizedNow);
374
- const statusCounts = summarizeStatusCounts(workItems);
375
- const linkedAgentIdentities = new Set(
376
- workItems.map((item) => normalizeString(item.links.agentIdentity)).filter(Boolean)
377
- );
378
- const jiraLinkedCount = workItems.filter((item) => Boolean(item.links.jiraIssueKey)).length;
379
- const budgetGuardedCount = workItems.filter((item) => item.links.budgetLifecycleState !== "WITHIN_BUDGET").length;
380
- const operatorCoveredCount = workItems.filter(
381
- (item) => Array.isArray(item.artifacts.operatorSnapshots) && item.artifacts.operatorSnapshots.length > 0
382
- ).length;
383
-
384
- const index = {
385
- schemaVersion: LINEAGE_SCHEMA_VERSION,
386
- generatedAt: normalizedNow,
387
- lineageRunId,
388
- summary: {
389
- totalQueueItems: queue.totalCount,
390
- totalWorkItemsIndexed: workItems.length,
391
- statusCounts,
392
- activeAgentCount: linkedAgentIdentities.size,
393
- jiraLinkedCount,
394
- budgetGuardedCount,
395
- operatorCoveredCount,
396
- },
397
- daemonArtifacts: {
398
- queuePath: toRelativePosix(storage.outputRoot, storage.queuePath),
399
- statePath: toRelativePosix(storage.outputRoot, storage.statePath),
400
- streamPath: toRelativePosix(storage.outputRoot, storage.streamPath),
401
- assignmentLedgerPath: toRelativePosix(storage.outputRoot, assignmentStorage.ledgerPath),
402
- assignmentEventsPath: toRelativePosix(storage.outputRoot, assignmentStorage.eventsPath),
403
- jiraLifecyclePath: toRelativePosix(storage.outputRoot, jiraStorage.lifecyclePath),
404
- jiraEventsPath: toRelativePosix(storage.outputRoot, jiraStorage.eventsPath),
405
- budgetStatePath: toRelativePosix(storage.outputRoot, budgetStorage.budgetStatePath),
406
- budgetEventsPath: toRelativePosix(storage.outputRoot, budgetStorage.budgetEventsPath),
407
- operatorStatePath: toRelativePosix(storage.outputRoot, operatorStorage.operatorStatePath),
408
- operatorEventsPath: toRelativePosix(storage.outputRoot, operatorStorage.operatorEventsPath),
409
- },
410
- runs: {
411
- errorDaemonRuns: errorRuns
412
- .map((run) => ({
413
- runId: normalizeString(run.payload.runId) || path.basename(run.filePath, ".json"),
414
- generatedAt: normalizeIsoTimestamp(run.payload.generatedAt, normalizedNow),
415
- startOffset: Number(run.payload.startOffset || 0),
416
- endOffset: Number(run.payload.endOffset || 0),
417
- queueDepth: Number(run.payload.queueDepth || 0),
418
- path: toRelativePosix(storage.outputRoot, run.filePath),
419
- }))
420
- .sort((left, right) => (Date.parse(String(right.generatedAt || "")) || 0) - (Date.parse(String(left.generatedAt || "")) || 0)),
421
- budgetChecks: budgetRuns
422
- .map((run) => ({
423
- runId: normalizeString(run.payload.runId) || path.basename(run.filePath, ".json"),
424
- generatedAt: normalizeIsoTimestamp(run.payload.generatedAt, normalizedNow),
425
- workItemId: normalizeString(run.payload.workItemId) || null,
426
- action: normalizeString(run.payload.action) || "NONE",
427
- lifecycleState: normalizeString(run.payload.lifecycleState) || "WITHIN_BUDGET",
428
- path: toRelativePosix(storage.outputRoot, run.filePath),
429
- }))
430
- .sort((left, right) => (Date.parse(String(right.generatedAt || "")) || 0) - (Date.parse(String(left.generatedAt || "")) || 0)),
431
- operatorSnapshots: operatorSnapshotSummaries.sort(
432
- (left, right) =>
433
- (Date.parse(String(right.generatedAt || "")) || 0) - (Date.parse(String(left.generatedAt || "")) || 0)
434
- ),
435
- },
436
- workItems,
437
- };
438
-
439
- await Promise.all([
440
- writeJsonFile(storage.lineageIndexPath, index),
441
- appendJsonLine(storage.lineageEventsPath, {
442
- timestamp: normalizedNow,
443
- eventType: "lineage_build",
444
- lineageRunId,
445
- totalWorkItemsIndexed: workItems.length,
446
- statusCounts,
447
- jiraLinkedCount,
448
- budgetGuardedCount,
449
- operatorCoveredCount,
450
- }),
451
- ]);
452
-
453
- return {
454
- ...storage,
455
- lineageRunId,
456
- indexPath: storage.lineageIndexPath,
457
- eventPath: storage.lineageEventsPath,
458
- summary: index.summary,
459
- workItems: index.workItems,
460
- index,
461
- };
462
- }
463
-
464
- export async function listArtifactLineage({
465
- targetPath = ".",
466
- outputDir = "",
467
- statuses = [],
468
- workItemId = "",
469
- limit = 50,
470
- env,
471
- homeDir,
472
- nowIso = new Date().toISOString(),
473
- } = {}) {
474
- const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
475
- const normalizedLimit = normalizePositiveInteger(limit, "limit", 50);
476
- const normalizedStatuses = new Set(normalizeStatusList(statuses));
477
- const normalizedWorkItemId = normalizeString(workItemId);
478
- const storage = await resolveArtifactLineageStorage({
479
- targetPath,
480
- outputDir,
481
- env,
482
- homeDir,
483
- });
484
- let index = await readJsonFile(storage.lineageIndexPath, null);
485
- if (!index) {
486
- const built = await buildArtifactLineageIndex({
487
- targetPath,
488
- outputDir,
489
- env,
490
- homeDir,
491
- nowIso: normalizedNow,
492
- });
493
- index = built.index;
494
- }
495
- const normalizedIndex = {
496
- ...createInitialLineageIndex(normalizedNow),
497
- ...(index && typeof index === "object" ? index : {}),
498
- };
499
- const filtered = (Array.isArray(normalizedIndex.workItems) ? normalizedIndex.workItems : []).filter(
500
- (item) => {
501
- if (
502
- normalizedStatuses.size > 0 &&
503
- !normalizedStatuses.has(normalizeString(item.workItemStatus).toUpperCase())
504
- ) {
505
- return false;
506
- }
507
- if (normalizedWorkItemId && normalizeString(item.workItemId) !== normalizedWorkItemId) {
508
- return false;
509
- }
510
- return true;
511
- }
512
- );
513
- const sorted = [...filtered].sort((left, right) => {
514
- const leftEpoch = Date.parse(String(left.updatedAt || left.lastSeenAt || "")) || 0;
515
- const rightEpoch = Date.parse(String(right.updatedAt || right.lastSeenAt || "")) || 0;
516
- return rightEpoch - leftEpoch;
517
- });
518
- return {
519
- ...storage,
520
- generatedAt: normalizeIsoTimestamp(normalizedIndex.generatedAt, normalizedNow),
521
- lineageRunId: normalizeString(normalizedIndex.lineageRunId) || null,
522
- summary:
523
- normalizedIndex.summary && typeof normalizedIndex.summary === "object"
524
- ? normalizedIndex.summary
525
- : createInitialLineageIndex(normalizedNow).summary,
526
- totalCount: Array.isArray(normalizedIndex.workItems) ? normalizedIndex.workItems.length : 0,
527
- visibleCount: sorted.length,
528
- workItems: sorted.slice(0, normalizedLimit),
529
- runs:
530
- normalizedIndex.runs && typeof normalizedIndex.runs === "object"
531
- ? normalizedIndex.runs
532
- : createInitialLineageIndex(normalizedNow).runs,
533
- };
534
- }
1
+ import { createHash } from "node:crypto";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { listAssignments, resolveAssignmentLedgerStorage } from "./assignment-ledger.js";
6
+ import { getBudgetHealthColor, resolveOperatorControlStorage } from "./operator-control.js";
7
+ import { listBudgetStates, resolveBudgetGovernorStorage } from "./budget-governor.js";
8
+ import { listErrorQueue, resolveErrorDaemonStorage, WORK_ITEM_STATUSES } from "./error-worker.js";
9
+ import { listJiraIssues, resolveJiraLifecycleStorage } from "./jira-lifecycle.js";
10
+ import { resolveSessionPaths } from "../session/paths.js";
11
+
12
+ const LINEAGE_SCHEMA_VERSION = "1.0.0";
13
+ const WORK_ITEM_STATUS_SET = new Set(WORK_ITEM_STATUSES);
14
+
15
+ function normalizeString(value) {
16
+ return String(value || "").trim();
17
+ }
18
+
19
+ function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
20
+ const normalized = normalizeString(value);
21
+ if (!normalized) {
22
+ return fallbackIso;
23
+ }
24
+ const epoch = Date.parse(normalized);
25
+ if (!Number.isFinite(epoch)) {
26
+ return fallbackIso;
27
+ }
28
+ return new Date(epoch).toISOString();
29
+ }
30
+
31
+ function normalizePositiveInteger(value, fieldName, fallbackValue) {
32
+ if (value === undefined || value === null || normalizeString(value) === "") {
33
+ return fallbackValue;
34
+ }
35
+ const normalized = Number(value);
36
+ if (!Number.isFinite(normalized) || normalized <= 0) {
37
+ throw new Error(`${fieldName} must be a positive integer.`);
38
+ }
39
+ return Math.floor(normalized);
40
+ }
41
+
42
+ function toPosixPath(value = "") {
43
+ return String(value || "").replace(/\\/g, "/");
44
+ }
45
+
46
+ function toRelativePosix(baseDir, absolutePath) {
47
+ const relative = path.relative(baseDir, absolutePath);
48
+ return toPosixPath(relative);
49
+ }
50
+
51
+ function normalizeDateKey(value = "", fallbackIso = new Date().toISOString()) {
52
+ const normalized = normalizeString(value);
53
+ if (normalized && /^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
54
+ return normalized;
55
+ }
56
+ return normalizeIsoTimestamp(normalized, fallbackIso).slice(0, 10);
57
+ }
58
+
59
+ function buildDayKey(nowIso = new Date().toISOString()) {
60
+ return normalizeDateKey(nowIso, nowIso);
61
+ }
62
+
63
+ function normalizeStringArray(values = []) {
64
+ if (!Array.isArray(values)) {
65
+ return [];
66
+ }
67
+ return Array.from(
68
+ new Set(
69
+ values
70
+ .map((value) => normalizeString(value))
71
+ .filter(Boolean)
72
+ )
73
+ );
74
+ }
75
+
76
+ async function readFileBufferOptional(filePath) {
77
+ try {
78
+ return await fsp.readFile(filePath);
79
+ } catch (error) {
80
+ if (error && typeof error === "object" && error.code === "ENOENT") {
81
+ return null;
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ async function hashFileSha256(filePath) {
88
+ const payload = await fsp.readFile(filePath);
89
+ return createHash("sha256").update(payload).digest("hex");
90
+ }
91
+
92
+ function resolveWorkItemArtifactDir(storage, workItemId, date) {
93
+ return path.join(
94
+ storage.observabilityRoot,
95
+ normalizeDateKey(date, new Date().toISOString()),
96
+ normalizeString(workItemId)
97
+ );
98
+ }
99
+
100
+ function canonicalizeArtifactRecords(artifactFiles = []) {
101
+ return artifactFiles
102
+ .map((artifact) => ({
103
+ name: normalizeString(artifact.name),
104
+ path: toPosixPath(normalizeString(artifact.path)),
105
+ sha256: normalizeString(artifact.sha256).toLowerCase(),
106
+ sizeBytes: Number(artifact.sizeBytes || 0),
107
+ }))
108
+ .sort((left, right) => left.path.localeCompare(right.path));
109
+ }
110
+
111
+ function buildCloseoutAnchorPayload({
112
+ workItemId,
113
+ sessionId,
114
+ date,
115
+ artifacts = [],
116
+ sessionStream = null,
117
+ cosignAttestationRef = "",
118
+ sbomRef = "",
119
+ evidenceLinks = [],
120
+ } = {}) {
121
+ return {
122
+ workItemId: normalizeString(workItemId),
123
+ sessionId: normalizeString(sessionId) || null,
124
+ date: normalizeDateKey(date, new Date().toISOString()),
125
+ artifacts: canonicalizeArtifactRecords(artifacts),
126
+ sessionStream: sessionStream
127
+ ? {
128
+ path: toPosixPath(normalizeString(sessionStream.path)),
129
+ sha256: normalizeString(sessionStream.sha256).toLowerCase(),
130
+ sizeBytes: Number(sessionStream.sizeBytes || 0),
131
+ }
132
+ : null,
133
+ cosignAttestationRef: normalizeString(cosignAttestationRef) || null,
134
+ sbomRef: normalizeString(sbomRef) || null,
135
+ evidenceLinks: normalizeStringArray(evidenceLinks).sort((left, right) =>
136
+ left.localeCompare(right)
137
+ ),
138
+ };
139
+ }
140
+
141
+ function computeAnchorSha256(payload = {}) {
142
+ return createHash("sha256")
143
+ .update(JSON.stringify(payload))
144
+ .digest("hex");
145
+ }
146
+
147
+ function resolveArtifactAbsolutePath(storage, artifactPath = "") {
148
+ const normalized = normalizeString(artifactPath);
149
+ if (!normalized) {
150
+ return "";
151
+ }
152
+ if (path.isAbsolute(normalized)) {
153
+ return normalized;
154
+ }
155
+ return path.join(storage.outputRoot, normalized);
156
+ }
157
+
158
+ async function resolveSessionStreamDigest(sessionId, { targetPath = "." } = {}) {
159
+ const normalizedSessionId = normalizeString(sessionId);
160
+ if (!normalizedSessionId) {
161
+ return null;
162
+ }
163
+
164
+ const sessionPaths = resolveSessionPaths(normalizedSessionId, { targetPath });
165
+ const [rotatedBuffer, streamBuffer] = await Promise.all([
166
+ readFileBufferOptional(sessionPaths.rotatedStreamPath),
167
+ readFileBufferOptional(sessionPaths.streamPath),
168
+ ]);
169
+ if (!rotatedBuffer && !streamBuffer) {
170
+ return null;
171
+ }
172
+
173
+ const hash = createHash("sha256");
174
+ let totalBytes = 0;
175
+ if (rotatedBuffer) {
176
+ hash.update(rotatedBuffer);
177
+ totalBytes += rotatedBuffer.length;
178
+ }
179
+ if (streamBuffer) {
180
+ hash.update(streamBuffer);
181
+ totalBytes += streamBuffer.length;
182
+ }
183
+ return {
184
+ path: toPosixPath(path.relative(path.resolve(String(targetPath || ".")), sessionPaths.streamPath)),
185
+ sha256: hash.digest("hex"),
186
+ sizeBytes: totalBytes,
187
+ };
188
+ }
189
+
190
+ export async function writeCloseoutArtifact({
191
+ workItemId,
192
+ sessionId = "",
193
+ date = "",
194
+ targetPath = ".",
195
+ outputDir = "",
196
+ env,
197
+ homeDir,
198
+ nowIso = new Date().toISOString(),
199
+ cosignAttestationRef = "",
200
+ sbomRef = "",
201
+ evidenceLinks = [],
202
+ chainVerified = true,
203
+ } = {}) {
204
+ const normalizedWorkItemId = normalizeString(workItemId);
205
+ if (!normalizedWorkItemId) {
206
+ throw new Error("workItemId is required.");
207
+ }
208
+ const normalizedTargetPath = path.resolve(String(targetPath || "."));
209
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
210
+ const storage = await resolveArtifactLineageStorage({
211
+ targetPath: normalizedTargetPath,
212
+ outputDir,
213
+ env,
214
+ homeDir,
215
+ });
216
+ const dateKey = normalizeDateKey(date, normalizedNow);
217
+ const artifactDir = resolveWorkItemArtifactDir(storage, normalizedWorkItemId, dateKey);
218
+ await fsp.mkdir(artifactDir, { recursive: true });
219
+ const closeoutPath = path.join(artifactDir, "closeout.json");
220
+
221
+ const entries = await fsp.readdir(artifactDir, { withFileTypes: true }).catch((error) => {
222
+ if (error && typeof error === "object" && error.code === "ENOENT") {
223
+ return [];
224
+ }
225
+ throw error;
226
+ });
227
+ const artifactFiles = [];
228
+ for (const entry of entries) {
229
+ if (!entry.isFile()) {
230
+ continue;
231
+ }
232
+ const lower = entry.name.toLowerCase();
233
+ if (lower === "closeout.json") {
234
+ continue;
235
+ }
236
+ const absolutePath = path.join(artifactDir, entry.name);
237
+ const stat = await fsp.stat(absolutePath);
238
+ artifactFiles.push({
239
+ name: entry.name,
240
+ path: toRelativePosix(storage.outputRoot, absolutePath),
241
+ sha256: await hashFileSha256(absolutePath),
242
+ sizeBytes: Number(stat.size || 0),
243
+ });
244
+ }
245
+ const sessionStream = await resolveSessionStreamDigest(normalizeString(sessionId), {
246
+ targetPath: normalizedTargetPath,
247
+ });
248
+ const anchorPayload = buildCloseoutAnchorPayload({
249
+ workItemId: normalizedWorkItemId,
250
+ sessionId,
251
+ date: dateKey,
252
+ artifacts: artifactFiles,
253
+ sessionStream,
254
+ cosignAttestationRef,
255
+ sbomRef,
256
+ evidenceLinks,
257
+ });
258
+ const anchorSha256 = computeAnchorSha256(anchorPayload);
259
+ const payload = {
260
+ schemaVersion: "1.0.0",
261
+ generatedAt: normalizedNow,
262
+ chainVerified: Boolean(chainVerified),
263
+ workItemId: normalizedWorkItemId,
264
+ sessionId: normalizeString(sessionId) || null,
265
+ date: dateKey,
266
+ artifactDir: toRelativePosix(storage.outputRoot, artifactDir),
267
+ artifacts: canonicalizeArtifactRecords(artifactFiles),
268
+ sessionStream,
269
+ cosignAttestationRef: normalizeString(cosignAttestationRef) || null,
270
+ sbomRef: normalizeString(sbomRef) || null,
271
+ evidenceLinks: normalizeStringArray(evidenceLinks).sort((left, right) =>
272
+ left.localeCompare(right)
273
+ ),
274
+ anchorSha256,
275
+ };
276
+ await writeJsonFile(closeoutPath, payload);
277
+ return {
278
+ closeoutPath,
279
+ payload,
280
+ anchorSha256,
281
+ artifactCount: payload.artifacts.length,
282
+ };
283
+ }
284
+
285
+ export async function verifyArtifactChain({
286
+ workItemId,
287
+ date = "",
288
+ targetPath = ".",
289
+ outputDir = "",
290
+ env,
291
+ homeDir,
292
+ } = {}) {
293
+ const normalizedWorkItemId = normalizeString(workItemId);
294
+ if (!normalizedWorkItemId) {
295
+ throw new Error("workItemId is required.");
296
+ }
297
+ const normalizedTargetPath = path.resolve(String(targetPath || "."));
298
+ const storage = await resolveArtifactLineageStorage({
299
+ targetPath: normalizedTargetPath,
300
+ outputDir,
301
+ env,
302
+ homeDir,
303
+ });
304
+ const dateKey = normalizeDateKey(date, new Date().toISOString());
305
+ const artifactDir = resolveWorkItemArtifactDir(storage, normalizedWorkItemId, dateKey);
306
+ const closeoutPath = path.join(artifactDir, "closeout.json");
307
+ const closeout = await readJsonFile(closeoutPath, null);
308
+ if (!closeout || typeof closeout !== "object") {
309
+ throw new Error(`closeout.json was not found for work item '${normalizedWorkItemId}' on '${dateKey}'.`);
310
+ }
311
+
312
+ const mismatches = [];
313
+ const artifacts = Array.isArray(closeout.artifacts) ? closeout.artifacts : [];
314
+ for (const artifact of artifacts) {
315
+ const expectedPath = normalizeString(artifact.path);
316
+ const absolutePath = resolveArtifactAbsolutePath(storage, expectedPath);
317
+ if (!absolutePath) {
318
+ mismatches.push({
319
+ type: "artifact_path_missing",
320
+ path: expectedPath,
321
+ });
322
+ continue;
323
+ }
324
+ const buffer = await readFileBufferOptional(absolutePath);
325
+ if (!buffer) {
326
+ mismatches.push({
327
+ type: "artifact_missing",
328
+ path: expectedPath,
329
+ });
330
+ continue;
331
+ }
332
+ const actualSha256 = createHash("sha256").update(buffer).digest("hex");
333
+ if (normalizeString(artifact.sha256).toLowerCase() !== actualSha256) {
334
+ mismatches.push({
335
+ type: "artifact_sha_mismatch",
336
+ path: expectedPath,
337
+ expected: normalizeString(artifact.sha256).toLowerCase(),
338
+ actual: actualSha256,
339
+ });
340
+ }
341
+ }
342
+
343
+ let sessionStream = null;
344
+ const sessionId = normalizeString(closeout.sessionId);
345
+ if (sessionId) {
346
+ sessionStream = await resolveSessionStreamDigest(sessionId, {
347
+ targetPath: normalizedTargetPath,
348
+ });
349
+ if (closeout.sessionStream && sessionStream) {
350
+ const expectedStreamSha = normalizeString(closeout.sessionStream.sha256).toLowerCase();
351
+ if (expectedStreamSha !== normalizeString(sessionStream.sha256).toLowerCase()) {
352
+ mismatches.push({
353
+ type: "session_stream_sha_mismatch",
354
+ path: normalizeString(closeout.sessionStream.path),
355
+ expected: expectedStreamSha,
356
+ actual: normalizeString(sessionStream.sha256).toLowerCase(),
357
+ });
358
+ }
359
+ }
360
+ }
361
+
362
+ const recomputedAnchorPayload = buildCloseoutAnchorPayload({
363
+ workItemId: normalizedWorkItemId,
364
+ sessionId,
365
+ date: closeout.date,
366
+ artifacts,
367
+ sessionStream: sessionStream || closeout.sessionStream || null,
368
+ cosignAttestationRef: closeout.cosignAttestationRef,
369
+ sbomRef: closeout.sbomRef,
370
+ evidenceLinks: closeout.evidenceLinks,
371
+ });
372
+ const recomputedAnchorSha256 = computeAnchorSha256(recomputedAnchorPayload);
373
+ if (normalizeString(closeout.anchorSha256).toLowerCase() !== recomputedAnchorSha256) {
374
+ mismatches.push({
375
+ type: "anchor_sha_mismatch",
376
+ expected: normalizeString(closeout.anchorSha256).toLowerCase(),
377
+ actual: recomputedAnchorSha256,
378
+ });
379
+ }
380
+
381
+ return {
382
+ valid: mismatches.length === 0,
383
+ closeoutPath,
384
+ workItemId: normalizedWorkItemId,
385
+ date: normalizeDateKey(closeout.date, dateKey),
386
+ mismatches,
387
+ artifactCount: artifacts.length,
388
+ anchorSha256: normalizeString(closeout.anchorSha256).toLowerCase(),
389
+ recomputedAnchorSha256,
390
+ };
391
+ }
392
+
393
+ async function writeJsonFile(filePath, payload = {}) {
394
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
395
+ await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
396
+ }
397
+
398
+ async function appendJsonLine(filePath, payload = {}) {
399
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
400
+ await fsp.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
401
+ }
402
+
403
+ async function readJsonFile(filePath, fallbackValue) {
404
+ try {
405
+ const raw = await fsp.readFile(filePath, "utf-8");
406
+ return JSON.parse(raw);
407
+ } catch (error) {
408
+ if (error && typeof error === "object" && error.code === "ENOENT") {
409
+ return fallbackValue;
410
+ }
411
+ throw error;
412
+ }
413
+ }
414
+
415
+ async function readJsonFileOptional(filePath) {
416
+ try {
417
+ const raw = await fsp.readFile(filePath, "utf-8");
418
+ return JSON.parse(raw);
419
+ } catch (error) {
420
+ if (error && typeof error === "object" && error.code === "ENOENT") {
421
+ return null;
422
+ }
423
+ return null;
424
+ }
425
+ }
426
+
427
+ async function readJsonFilesInDirectory(dirPath) {
428
+ try {
429
+ const entries = await fsp.readdir(dirPath, { withFileTypes: true });
430
+ const files = entries
431
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
432
+ .map((entry) => path.join(dirPath, entry.name))
433
+ .sort((left, right) => left.localeCompare(right));
434
+ const parsed = await Promise.all(
435
+ files.map(async (filePath) => ({
436
+ filePath,
437
+ payload: await readJsonFileOptional(filePath),
438
+ }))
439
+ );
440
+ return parsed.filter((entry) => entry.payload && typeof entry.payload === "object");
441
+ } catch (error) {
442
+ if (error && typeof error === "object" && error.code === "ENOENT") {
443
+ return [];
444
+ }
445
+ throw error;
446
+ }
447
+ }
448
+
449
+ function createInitialLineageIndex(nowIso = new Date().toISOString()) {
450
+ return {
451
+ schemaVersion: LINEAGE_SCHEMA_VERSION,
452
+ generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
453
+ lineageRunId: null,
454
+ summary: {
455
+ totalQueueItems: 0,
456
+ totalWorkItemsIndexed: 0,
457
+ statusCounts: {},
458
+ activeAgentCount: 0,
459
+ jiraLinkedCount: 0,
460
+ budgetGuardedCount: 0,
461
+ operatorCoveredCount: 0,
462
+ },
463
+ daemonArtifacts: {
464
+ queuePath: null,
465
+ statePath: null,
466
+ streamPath: null,
467
+ assignmentLedgerPath: null,
468
+ assignmentEventsPath: null,
469
+ jiraLifecyclePath: null,
470
+ jiraEventsPath: null,
471
+ budgetStatePath: null,
472
+ budgetEventsPath: null,
473
+ operatorStatePath: null,
474
+ operatorEventsPath: null,
475
+ },
476
+ runs: {
477
+ errorDaemonRuns: [],
478
+ budgetChecks: [],
479
+ operatorSnapshots: [],
480
+ },
481
+ workItems: [],
482
+ };
483
+ }
484
+
485
+ function normalizeStatusList(statuses = []) {
486
+ if (!Array.isArray(statuses)) {
487
+ return [];
488
+ }
489
+ return statuses
490
+ .map((status) => normalizeString(status).toUpperCase())
491
+ .filter(Boolean)
492
+ .filter((status) => WORK_ITEM_STATUS_SET.has(status));
493
+ }
494
+
495
+ function createLineageRunId(nowIso) {
496
+ return `lineage-${nowIso.replace(/[:.]/g, "-")}`;
497
+ }
498
+
499
+ function summarizeStatusCounts(workItems = []) {
500
+ const statusCounts = {};
501
+ for (const item of workItems) {
502
+ const status = normalizeString(item.workItemStatus).toUpperCase() || "UNKNOWN";
503
+ statusCounts[status] = (statusCounts[status] || 0) + 1;
504
+ }
505
+ return statusCounts;
506
+ }
507
+
508
+ export async function resolveArtifactLineageStorage({
509
+ targetPath = ".",
510
+ outputDir = "",
511
+ env,
512
+ homeDir,
513
+ } = {}) {
514
+ const daemonStorage = await resolveErrorDaemonStorage({
515
+ targetPath,
516
+ outputDir,
517
+ env,
518
+ homeDir,
519
+ });
520
+ const lineageDir = path.join(daemonStorage.baseDir, "lineage");
521
+ return {
522
+ ...daemonStorage,
523
+ lineageDir,
524
+ lineageIndexPath: path.join(lineageDir, "lineage-index.json"),
525
+ lineageEventsPath: path.join(lineageDir, "lineage-events.ndjson"),
526
+ };
527
+ }
528
+
529
+ export async function buildArtifactLineageIndex({
530
+ targetPath = ".",
531
+ outputDir = "",
532
+ env,
533
+ homeDir,
534
+ nowIso = new Date().toISOString(),
535
+ } = {}) {
536
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
537
+ const storage = await resolveArtifactLineageStorage({
538
+ targetPath,
539
+ outputDir,
540
+ env,
541
+ homeDir,
542
+ });
543
+ const assignmentStorage = await resolveAssignmentLedgerStorage({
544
+ targetPath,
545
+ outputDir,
546
+ env,
547
+ homeDir,
548
+ });
549
+ const jiraStorage = await resolveJiraLifecycleStorage({
550
+ targetPath,
551
+ outputDir,
552
+ env,
553
+ homeDir,
554
+ });
555
+ const budgetStorage = await resolveBudgetGovernorStorage({
556
+ targetPath,
557
+ outputDir,
558
+ env,
559
+ homeDir,
560
+ });
561
+ const operatorStorage = await resolveOperatorControlStorage({
562
+ targetPath,
563
+ outputDir,
564
+ env,
565
+ homeDir,
566
+ });
567
+
568
+ const [queue, assignments, issues, budgets, errorRuns, budgetRuns, operatorSnapshots] =
569
+ await Promise.all([
570
+ listErrorQueue({
571
+ targetPath,
572
+ outputDir,
573
+ limit: 5000,
574
+ env,
575
+ homeDir,
576
+ }),
577
+ listAssignments({
578
+ targetPath,
579
+ outputDir,
580
+ includeExpired: true,
581
+ limit: 5000,
582
+ env,
583
+ homeDir,
584
+ nowIso: normalizedNow,
585
+ }),
586
+ listJiraIssues({
587
+ targetPath,
588
+ outputDir,
589
+ limit: 5000,
590
+ env,
591
+ homeDir,
592
+ nowIso: normalizedNow,
593
+ }),
594
+ listBudgetStates({
595
+ targetPath,
596
+ outputDir,
597
+ limit: 5000,
598
+ env,
599
+ homeDir,
600
+ nowIso: normalizedNow,
601
+ }),
602
+ readJsonFilesInDirectory(storage.runsDir),
603
+ readJsonFilesInDirectory(budgetStorage.budgetRunsDir),
604
+ readJsonFilesInDirectory(operatorStorage.operatorSnapshotsDir),
605
+ ]);
606
+
607
+ const assignmentByWorkItem = new Map();
608
+ for (const assignment of assignments.assignments) {
609
+ if (!assignmentByWorkItem.has(assignment.workItemId)) {
610
+ assignmentByWorkItem.set(assignment.workItemId, assignment);
611
+ }
612
+ }
613
+ const issueByWorkItem = new Map();
614
+ for (const issue of issues.issues) {
615
+ if (!issueByWorkItem.has(issue.workItemId)) {
616
+ issueByWorkItem.set(issue.workItemId, issue);
617
+ }
618
+ }
619
+ const budgetByWorkItem = new Map();
620
+ for (const record of budgets.records) {
621
+ if (!budgetByWorkItem.has(record.workItemId)) {
622
+ budgetByWorkItem.set(record.workItemId, record);
623
+ }
624
+ }
625
+
626
+ const budgetRunsByWorkItem = new Map();
627
+ for (const run of budgetRuns) {
628
+ const workItemId = normalizeString(run.payload.workItemId);
629
+ if (!workItemId) {
630
+ continue;
631
+ }
632
+ if (!budgetRunsByWorkItem.has(workItemId)) {
633
+ budgetRunsByWorkItem.set(workItemId, []);
634
+ }
635
+ budgetRunsByWorkItem.get(workItemId).push({
636
+ runId: normalizeString(run.payload.runId) || path.basename(run.filePath, ".json"),
637
+ generatedAt: normalizeIsoTimestamp(run.payload.generatedAt, normalizedNow),
638
+ lifecycleState: normalizeString(run.payload.lifecycleState) || "WITHIN_BUDGET",
639
+ action: normalizeString(run.payload.action) || "NONE",
640
+ path: toRelativePosix(storage.outputRoot, run.filePath),
641
+ });
642
+ }
643
+
644
+ const operatorSnapshotsByWorkItem = new Map();
645
+ const operatorSnapshotSummaries = [];
646
+ for (const snapshot of operatorSnapshots) {
647
+ const runId = normalizeString(snapshot.payload.runId) || path.basename(snapshot.filePath, ".json");
648
+ const generatedAt = normalizeIsoTimestamp(snapshot.payload.generatedAt, normalizedNow);
649
+ const workItems = Array.isArray(snapshot.payload.workItems) ? snapshot.payload.workItems : [];
650
+ operatorSnapshotSummaries.push({
651
+ runId,
652
+ generatedAt,
653
+ path: toRelativePosix(storage.outputRoot, snapshot.filePath),
654
+ visibleWorkItems: workItems.length,
655
+ });
656
+ for (const workItem of workItems) {
657
+ const workItemId = normalizeString(workItem.workItemId);
658
+ if (!workItemId) {
659
+ continue;
660
+ }
661
+ if (!operatorSnapshotsByWorkItem.has(workItemId)) {
662
+ operatorSnapshotsByWorkItem.set(workItemId, []);
663
+ }
664
+ operatorSnapshotsByWorkItem.get(workItemId).push({
665
+ runId,
666
+ generatedAt,
667
+ path: toRelativePosix(storage.outputRoot, snapshot.filePath),
668
+ budgetHealthColor: getBudgetHealthColor(workItem.budgetHealthColor),
669
+ assignmentStatus: normalizeString(workItem.assignmentStatus) || "QUEUED",
670
+ workItemStatus: normalizeString(workItem.workItemStatus) || "QUEUED",
671
+ });
672
+ }
673
+ }
674
+
675
+ const workItems = queue.items.map((queueItem) => {
676
+ const assignment = assignmentByWorkItem.get(queueItem.workItemId) || null;
677
+ const issue = issueByWorkItem.get(queueItem.workItemId) || null;
678
+ const budget = budgetByWorkItem.get(queueItem.workItemId) || null;
679
+ const budgetRunsForItem = budgetRunsByWorkItem.get(queueItem.workItemId) || [];
680
+ const operatorSnapshotsForItem = operatorSnapshotsByWorkItem.get(queueItem.workItemId) || [];
681
+ return {
682
+ workItemId: queueItem.workItemId,
683
+ workItemStatus: queueItem.status,
684
+ severity: queueItem.severity,
685
+ service: queueItem.service,
686
+ endpoint: queueItem.endpoint,
687
+ errorCode: queueItem.errorCode,
688
+ message: queueItem.message,
689
+ firstSeenAt: queueItem.firstSeenAt,
690
+ lastSeenAt: queueItem.lastSeenAt,
691
+ links: {
692
+ sessionId: assignment?.sessionId || null,
693
+ agentIdentity: assignment?.assignedAgentIdentity || null,
694
+ assignmentStatus: assignment?.status || null,
695
+ assignmentStage: assignment?.stage || null,
696
+ loopRunId: assignment?.runId || null,
697
+ jiraIssueKey: issue?.issueKey || null,
698
+ jiraStatus: issue?.status || null,
699
+ budgetLifecycleState: budget?.lifecycleState || "WITHIN_BUDGET",
700
+ budgetHealthColor: getBudgetHealthColor(budget?.lifecycleState || "WITHIN_BUDGET"),
701
+ latestOperatorSnapshotRunId: operatorSnapshotsForItem.length > 0
702
+ ? operatorSnapshotsForItem[0].runId
703
+ : null,
704
+ },
705
+ artifacts: {
706
+ queuePath: toRelativePosix(storage.outputRoot, storage.queuePath),
707
+ assignmentLedgerPath: toRelativePosix(storage.outputRoot, assignmentStorage.ledgerPath),
708
+ jiraLifecyclePath: toRelativePosix(storage.outputRoot, jiraStorage.lifecyclePath),
709
+ budgetStatePath: toRelativePosix(storage.outputRoot, budgetStorage.budgetStatePath),
710
+ operatorStatePath: toRelativePosix(storage.outputRoot, operatorStorage.operatorStatePath),
711
+ budgetRuns: budgetRunsForItem,
712
+ operatorSnapshots: operatorSnapshotsForItem,
713
+ },
714
+ updatedAt: queueItem.updatedAt,
715
+ };
716
+ });
717
+
718
+ await Promise.all(
719
+ workItems.map(async (workItem) => {
720
+ const closeout = await writeCloseoutArtifact({
721
+ workItemId: workItem.workItemId,
722
+ sessionId: normalizeString(workItem.links.sessionId),
723
+ date: buildDayKey(normalizedNow),
724
+ targetPath,
725
+ outputDir,
726
+ env,
727
+ homeDir,
728
+ nowIso: normalizedNow,
729
+ });
730
+ workItem.artifacts.closeoutPath = toRelativePosix(storage.outputRoot, closeout.closeoutPath);
731
+ workItem.artifacts.closeoutAnchorSha256 = closeout.anchorSha256;
732
+ })
733
+ );
734
+
735
+ const lineageRunId = createLineageRunId(normalizedNow);
736
+ const statusCounts = summarizeStatusCounts(workItems);
737
+ const linkedAgentIdentities = new Set(
738
+ workItems.map((item) => normalizeString(item.links.agentIdentity)).filter(Boolean)
739
+ );
740
+ const jiraLinkedCount = workItems.filter((item) => Boolean(item.links.jiraIssueKey)).length;
741
+ const budgetGuardedCount = workItems.filter((item) => item.links.budgetLifecycleState !== "WITHIN_BUDGET").length;
742
+ const operatorCoveredCount = workItems.filter(
743
+ (item) => Array.isArray(item.artifacts.operatorSnapshots) && item.artifacts.operatorSnapshots.length > 0
744
+ ).length;
745
+
746
+ const index = {
747
+ schemaVersion: LINEAGE_SCHEMA_VERSION,
748
+ generatedAt: normalizedNow,
749
+ lineageRunId,
750
+ summary: {
751
+ totalQueueItems: queue.totalCount,
752
+ totalWorkItemsIndexed: workItems.length,
753
+ statusCounts,
754
+ activeAgentCount: linkedAgentIdentities.size,
755
+ jiraLinkedCount,
756
+ budgetGuardedCount,
757
+ operatorCoveredCount,
758
+ },
759
+ daemonArtifacts: {
760
+ queuePath: toRelativePosix(storage.outputRoot, storage.queuePath),
761
+ statePath: toRelativePosix(storage.outputRoot, storage.statePath),
762
+ streamPath: toRelativePosix(storage.outputRoot, storage.streamPath),
763
+ assignmentLedgerPath: toRelativePosix(storage.outputRoot, assignmentStorage.ledgerPath),
764
+ assignmentEventsPath: toRelativePosix(storage.outputRoot, assignmentStorage.eventsPath),
765
+ jiraLifecyclePath: toRelativePosix(storage.outputRoot, jiraStorage.lifecyclePath),
766
+ jiraEventsPath: toRelativePosix(storage.outputRoot, jiraStorage.eventsPath),
767
+ budgetStatePath: toRelativePosix(storage.outputRoot, budgetStorage.budgetStatePath),
768
+ budgetEventsPath: toRelativePosix(storage.outputRoot, budgetStorage.budgetEventsPath),
769
+ operatorStatePath: toRelativePosix(storage.outputRoot, operatorStorage.operatorStatePath),
770
+ operatorEventsPath: toRelativePosix(storage.outputRoot, operatorStorage.operatorEventsPath),
771
+ },
772
+ runs: {
773
+ errorDaemonRuns: errorRuns
774
+ .map((run) => ({
775
+ runId: normalizeString(run.payload.runId) || path.basename(run.filePath, ".json"),
776
+ generatedAt: normalizeIsoTimestamp(run.payload.generatedAt, normalizedNow),
777
+ startOffset: Number(run.payload.startOffset || 0),
778
+ endOffset: Number(run.payload.endOffset || 0),
779
+ queueDepth: Number(run.payload.queueDepth || 0),
780
+ path: toRelativePosix(storage.outputRoot, run.filePath),
781
+ }))
782
+ .sort((left, right) => (Date.parse(String(right.generatedAt || "")) || 0) - (Date.parse(String(left.generatedAt || "")) || 0)),
783
+ budgetChecks: budgetRuns
784
+ .map((run) => ({
785
+ runId: normalizeString(run.payload.runId) || path.basename(run.filePath, ".json"),
786
+ generatedAt: normalizeIsoTimestamp(run.payload.generatedAt, normalizedNow),
787
+ workItemId: normalizeString(run.payload.workItemId) || null,
788
+ action: normalizeString(run.payload.action) || "NONE",
789
+ lifecycleState: normalizeString(run.payload.lifecycleState) || "WITHIN_BUDGET",
790
+ path: toRelativePosix(storage.outputRoot, run.filePath),
791
+ }))
792
+ .sort((left, right) => (Date.parse(String(right.generatedAt || "")) || 0) - (Date.parse(String(left.generatedAt || "")) || 0)),
793
+ operatorSnapshots: operatorSnapshotSummaries.sort(
794
+ (left, right) =>
795
+ (Date.parse(String(right.generatedAt || "")) || 0) - (Date.parse(String(left.generatedAt || "")) || 0)
796
+ ),
797
+ },
798
+ workItems,
799
+ };
800
+
801
+ await Promise.all([
802
+ writeJsonFile(storage.lineageIndexPath, index),
803
+ appendJsonLine(storage.lineageEventsPath, {
804
+ timestamp: normalizedNow,
805
+ eventType: "lineage_build",
806
+ lineageRunId,
807
+ totalWorkItemsIndexed: workItems.length,
808
+ statusCounts,
809
+ jiraLinkedCount,
810
+ budgetGuardedCount,
811
+ operatorCoveredCount,
812
+ }),
813
+ ]);
814
+
815
+ return {
816
+ ...storage,
817
+ lineageRunId,
818
+ indexPath: storage.lineageIndexPath,
819
+ eventPath: storage.lineageEventsPath,
820
+ summary: index.summary,
821
+ workItems: index.workItems,
822
+ index,
823
+ };
824
+ }
825
+
826
+ export async function listArtifactLineage({
827
+ targetPath = ".",
828
+ outputDir = "",
829
+ statuses = [],
830
+ workItemId = "",
831
+ limit = 50,
832
+ env,
833
+ homeDir,
834
+ nowIso = new Date().toISOString(),
835
+ } = {}) {
836
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
837
+ const normalizedLimit = normalizePositiveInteger(limit, "limit", 50);
838
+ const normalizedStatuses = new Set(normalizeStatusList(statuses));
839
+ const normalizedWorkItemId = normalizeString(workItemId);
840
+ const storage = await resolveArtifactLineageStorage({
841
+ targetPath,
842
+ outputDir,
843
+ env,
844
+ homeDir,
845
+ });
846
+ let index = await readJsonFile(storage.lineageIndexPath, null);
847
+ if (!index) {
848
+ const built = await buildArtifactLineageIndex({
849
+ targetPath,
850
+ outputDir,
851
+ env,
852
+ homeDir,
853
+ nowIso: normalizedNow,
854
+ });
855
+ index = built.index;
856
+ }
857
+ const normalizedIndex = {
858
+ ...createInitialLineageIndex(normalizedNow),
859
+ ...(index && typeof index === "object" ? index : {}),
860
+ };
861
+ const filtered = (Array.isArray(normalizedIndex.workItems) ? normalizedIndex.workItems : []).filter(
862
+ (item) => {
863
+ if (
864
+ normalizedStatuses.size > 0 &&
865
+ !normalizedStatuses.has(normalizeString(item.workItemStatus).toUpperCase())
866
+ ) {
867
+ return false;
868
+ }
869
+ if (normalizedWorkItemId && normalizeString(item.workItemId) !== normalizedWorkItemId) {
870
+ return false;
871
+ }
872
+ return true;
873
+ }
874
+ );
875
+ const sorted = [...filtered].sort((left, right) => {
876
+ const leftEpoch = Date.parse(String(left.updatedAt || left.lastSeenAt || "")) || 0;
877
+ const rightEpoch = Date.parse(String(right.updatedAt || right.lastSeenAt || "")) || 0;
878
+ return rightEpoch - leftEpoch;
879
+ });
880
+ return {
881
+ ...storage,
882
+ generatedAt: normalizeIsoTimestamp(normalizedIndex.generatedAt, normalizedNow),
883
+ lineageRunId: normalizeString(normalizedIndex.lineageRunId) || null,
884
+ summary:
885
+ normalizedIndex.summary && typeof normalizedIndex.summary === "object"
886
+ ? normalizedIndex.summary
887
+ : createInitialLineageIndex(normalizedNow).summary,
888
+ totalCount: Array.isArray(normalizedIndex.workItems) ? normalizedIndex.workItems.length : 0,
889
+ visibleCount: sorted.length,
890
+ workItems: sorted.slice(0, normalizedLimit),
891
+ runs:
892
+ normalizedIndex.runs && typeof normalizedIndex.runs === "object"
893
+ ? normalizedIndex.runs
894
+ : createInitialLineageIndex(normalizedNow).runs,
895
+ };
896
+ }