ultra-builder-pro-cli 0.25.1 → 0.26.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 (243) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +94 -0
  3. package/.ultra-template/decisions/.gitkeep +1 -0
  4. package/.ultra-template/evidence/.gitkeep +1 -0
  5. package/.ultra-template/north-star.md +46 -0
  6. package/.ultra-template/research/.gitkeep +1 -0
  7. package/.ultra-template/specs/discovery.md +2 -2
  8. package/.ultra-template/tasks.json +3 -0
  9. package/.ultra-template/test-report.json +27 -0
  10. package/CHANGELOG.md +107 -1
  11. package/README.md +217 -579
  12. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  13. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  14. package/adapters/_shared/file-ops.cjs +76 -2
  15. package/adapters/_shared/host-profile.cjs +97 -0
  16. package/adapters/_shared/plugin-core.cjs +204 -0
  17. package/adapters/_shared/runtime-assets.cjs +31 -96
  18. package/adapters/claude.js +92 -291
  19. package/adapters/codex.js +309 -842
  20. package/adapters/grok.js +143 -1469
  21. package/adapters/kimi.js +124 -1053
  22. package/adapters/opencode.js +230 -742
  23. package/bin/delegate-worker.cjs +328 -0
  24. package/bin/delegate.cjs +463 -0
  25. package/bin/install.js +38 -11
  26. package/docs/ARCHITECTURE.md +125 -216
  27. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  28. package/docs/DECISIONS.md +77 -87
  29. package/docs/PHILOSOPHY.md +208 -0
  30. package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
  32. package/docs/SKILL-AUTHORING.md +94 -73
  33. package/docs/WORKFLOW-LIFECYCLE.md +97 -189
  34. package/hooks/README.md +19 -37
  35. package/hooks/_common.py +113 -0
  36. package/hooks/adapters/codex.py +9 -16
  37. package/hooks/adapters/grok.py +5 -5
  38. package/hooks/adapters/kimi.py +8 -14
  39. package/hooks/block_dangerous_commands.py +201 -0
  40. package/hooks/compact_context.py +65 -0
  41. package/hooks/mid_workflow_recall.py +25 -0
  42. package/hooks/post_edit_guard.py +105 -0
  43. package/hooks/session_context.py +30 -0
  44. package/package.json +17 -52
  45. package/skills/ultra-change/SKILL.md +115 -50
  46. package/skills/ultra-change/references/change-contract.md +68 -3
  47. package/skills/ultra-delegate/SKILL.md +89 -0
  48. package/skills/ultra-delegate/references/delegation-contract.md +59 -0
  49. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  50. package/skills/ultra-deliver/SKILL.md +98 -42
  51. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  52. package/skills/ultra-dev/SKILL.md +115 -62
  53. package/skills/ultra-dev/references/debugging.md +22 -0
  54. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  55. package/skills/ultra-grilling/SKILL.md +118 -0
  56. package/skills/ultra-grilling/references/reframing.md +72 -0
  57. package/skills/ultra-init/SKILL.md +102 -37
  58. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  59. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  60. package/skills/ultra-plan/SKILL.md +101 -68
  61. package/skills/ultra-research/SKILL.md +68 -48
  62. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  63. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  64. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  65. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  66. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  67. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  68. package/skills/ultra-research/references/10-user-personas.md +3 -2
  69. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  70. package/skills/ultra-research/references/20-user-stories.md +4 -3
  71. package/skills/ultra-research/references/21-features-scope.md +3 -2
  72. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  73. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  74. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  75. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  76. package/skills/ultra-research/references/40-deployment.md +3 -3
  77. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  78. package/skills/ultra-research/references/99-synthesis.md +7 -7
  79. package/skills/ultra-review/SKILL.md +84 -32
  80. package/skills/ultra-review/references/code.md +17 -0
  81. package/skills/ultra-review/references/comments.md +12 -0
  82. package/skills/ultra-review/references/design.md +12 -0
  83. package/skills/ultra-review/references/errors.md +13 -0
  84. package/skills/ultra-review/references/spec.md +13 -0
  85. package/skills/ultra-review/references/tests.md +12 -0
  86. package/skills/ultra-review/references/worker-packet.md +40 -0
  87. package/skills/ultra-review/scripts/review_wait.py +26 -15
  88. package/skills/ultra-status/SKILL.md +61 -36
  89. package/skills/ultra-tdd/SKILL.md +81 -0
  90. package/skills/ultra-tdd/references/templates/README.md +20 -0
  91. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  92. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  93. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  94. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  95. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  96. package/skills/ultra-tdd/references/test-execution.md +18 -0
  97. package/skills/ultra-test/SKILL.md +75 -30
  98. package/skills/ultra-test/references/export-syntax.md +18 -0
  99. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  100. package/skills/ultra-think/SKILL.md +49 -27
  101. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  102. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  103. package/.ultra-template/docs/research/README.md +0 -55
  104. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  105. package/.ultra-template/reports/templates/test-report.json +0 -18
  106. package/.ultra-template/tasks/tasks.json +0 -15
  107. package/.ultra-template/templates/task-context.md +0 -19
  108. package/adapters/_shared/codex-assets.cjs +0 -725
  109. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  110. package/adapters/_shared/interaction-contract.cjs +0 -129
  111. package/adapters/_shared/path-rewrite.cjs +0 -79
  112. package/adapters/_shared/runtime-native.cjs +0 -132
  113. package/adapters/_shared/settings-merge.cjs +0 -76
  114. package/agents/code-reviewer.md +0 -42
  115. package/agents/debugger.md +0 -40
  116. package/agents/review-code.md +0 -49
  117. package/agents/review-comments.md +0 -38
  118. package/agents/review-coordinator.md +0 -56
  119. package/agents/review-design.md +0 -40
  120. package/agents/review-errors.md +0 -43
  121. package/agents/review-spec.md +0 -39
  122. package/agents/review-tests.md +0 -40
  123. package/agents/tdd-runner.md +0 -38
  124. package/bin/orchestrator.js +0 -383
  125. package/commands/ultra-change.md +0 -10
  126. package/commands/ultra-deliver.md +0 -10
  127. package/commands/ultra-dev.md +0 -10
  128. package/commands/ultra-doctor.md +0 -10
  129. package/commands/ultra-init.md +0 -10
  130. package/commands/ultra-plan.md +0 -10
  131. package/commands/ultra-research.md +0 -10
  132. package/commands/ultra-review.md +0 -10
  133. package/commands/ultra-status.md +0 -10
  134. package/commands/ultra-test.md +0 -10
  135. package/commands/ultra-think.md +0 -10
  136. package/docs/AGENT-CONTEXT.md +0 -191
  137. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  138. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  139. package/docs/ROADMAP.md +0 -101
  140. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  141. package/hooks/active_task_context.py +0 -152
  142. package/hooks/context_envelope.py +0 -100
  143. package/hooks/health_check.py +0 -196
  144. package/hooks/pre_stop_check.py +0 -66
  145. package/hooks/runtime_paths.py +0 -396
  146. package/hooks/subagent_tracker.py +0 -88
  147. package/hooks/workflow_checkpoint.py +0 -76
  148. package/hooks/workflow_context.py +0 -48
  149. package/hooks/workflow_resume.py +0 -84
  150. package/mcp-server/hook-context.cjs +0 -38
  151. package/mcp-server/hook-event.cjs +0 -34
  152. package/mcp-server/lib/archive-journal.cjs +0 -1119
  153. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  154. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  155. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  156. package/mcp-server/lib/canonical-json.cjs +0 -19
  157. package/mcp-server/lib/change-authority.cjs +0 -26
  158. package/mcp-server/lib/change-packet.cjs +0 -790
  159. package/mcp-server/lib/change-workflow.cjs +0 -65
  160. package/mcp-server/lib/context-envelope.cjs +0 -581
  161. package/mcp-server/lib/context-paths.cjs +0 -142
  162. package/mcp-server/lib/decision-records.cjs +0 -423
  163. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  164. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  165. package/mcp-server/lib/doctor.cjs +0 -627
  166. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  167. package/mcp-server/lib/init-project.cjs +0 -1021
  168. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  169. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  170. package/mcp-server/lib/managed-file-write.cjs +0 -172
  171. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  172. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  173. package/mcp-server/lib/plan-store.cjs +0 -794
  174. package/mcp-server/lib/prd-parser.cjs +0 -83
  175. package/mcp-server/lib/pricing.cjs +0 -44
  176. package/mcp-server/lib/project-context.cjs +0 -178
  177. package/mcp-server/lib/projector.cjs +0 -386
  178. package/mcp-server/lib/provider-refs.cjs +0 -42
  179. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  180. package/mcp-server/lib/runtime-state.cjs +0 -222
  181. package/mcp-server/lib/safe-project-file.cjs +0 -385
  182. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  183. package/mcp-server/lib/state-authority.cjs +0 -95
  184. package/mcp-server/lib/state-db.cjs +0 -1580
  185. package/mcp-server/lib/state-ops.cjs +0 -1428
  186. package/mcp-server/lib/task-contract.cjs +0 -404
  187. package/mcp-server/lib/task-expander.cjs +0 -105
  188. package/mcp-server/lib/task-ledger.cjs +0 -1964
  189. package/mcp-server/lib/telemetry.cjs +0 -85
  190. package/mcp-server/lib/topo.cjs +0 -131
  191. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  192. package/mcp-server/lib/worker-packet.cjs +0 -358
  193. package/mcp-server/server.cjs +0 -437
  194. package/orchestrator/auto-merge.cjs +0 -128
  195. package/orchestrator/daemon.cjs +0 -264
  196. package/orchestrator/dispatch-rules.cjs +0 -78
  197. package/orchestrator/parallel-orchestrator.cjs +0 -903
  198. package/orchestrator/planner/plan-builder.cjs +0 -125
  199. package/orchestrator/recovery.cjs +0 -188
  200. package/orchestrator/session-close-journal-worker.cjs +0 -5
  201. package/orchestrator/session-close-journal.cjs +0 -1014
  202. package/orchestrator/session-runner.cjs +0 -1153
  203. package/orchestrator/worktree-manager.cjs +0 -282
  204. package/settings.json +0 -7
  205. package/skills/cc-collab/SKILL.md +0 -42
  206. package/skills/code-review-expert/SKILL.md +0 -43
  207. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  208. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  209. package/skills/code-review-expert/references/removal-plan.md +0 -24
  210. package/skills/codex-collab/SKILL.md +0 -49
  211. package/skills/codex-collab/evals/evals.json +0 -35
  212. package/skills/integration-rules/SKILL.md +0 -32
  213. package/skills/security-rules/SKILL.md +0 -38
  214. package/skills/testing-rules/SKILL.md +0 -32
  215. package/skills/ultra-doctor/SKILL.md +0 -41
  216. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  217. package/skills/ultra-verify/SKILL.md +0 -46
  218. package/skills/ultra-verify/evals/evals.json +0 -46
  219. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  220. package/skills/ultra-verify/references/evidence-status.md +0 -21
  221. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  222. package/spec/cli-protocol.md +0 -102
  223. package/spec/mcp-tools.yaml +0 -312
  224. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  225. package/spec/schemas/change-delta.v1.schema.json +0 -153
  226. package/spec/schemas/command-manifest.schema.json +0 -29
  227. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  228. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  229. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  230. package/spec/schemas/mcp-tools.schema.json +0 -136
  231. package/spec/schemas/skill-manifest.schema.json +0 -24
  232. package/spec/schemas/state-db.sql +0 -641
  233. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  234. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  235. package/spec/schemas/test-report.v1.schema.json +0 -225
  236. package/ultra-tools/cli.cjs +0 -129
  237. package/ultra-tools/commands/db.cjs +0 -226
  238. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  239. package/ultra-tools/commands/migrate.cjs +0 -1386
  240. package/ultra-tools/commands/session.cjs +0 -213
  241. package/ultra-tools/commands/status.cjs +0 -559
  242. package/ultra-tools/commands/system.cjs +0 -498
  243. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,1232 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- const ops = require('./state-ops.cjs');
8
- const contextPaths = require('./context-paths.cjs');
9
- const {
10
- openStableProjectRead,
11
- readStableProjectFile,
12
- walkStableProjectTree,
13
- } = require('./safe-project-file.cjs');
14
-
15
- const ENDPOINT_TYPES = new Set([
16
- 'artifact', 'project', 'baseline', 'change', 'task', 'workflow', 'external',
17
- ]);
18
- const OWNER_TYPES = new Set(['project', 'baseline', 'change', 'task', 'workflow']);
19
- const STATUSES = new Set(['current', 'stale', 'terminal', 'archived']);
20
- const ARTIFACT_INPUT_FIELDS = new Set([
21
- 'id', 'owner_type', 'owner_id', 'change_id', 'task_id', 'kind', 'path',
22
- 'content_digest', 'expected_before_digest', 'source_refs', 'consumer_refs',
23
- 'provenance', 'metadata', 'status',
24
- ]);
25
- const DIGEST = /^[0-9a-f]{64}$/;
26
- const OWNER_TABLES = Object.freeze({
27
- baseline: 'baselines',
28
- change: 'changes',
29
- task: 'tasks',
30
- workflow: 'workflow_runs',
31
- });
32
- const STATIC_EXEMPT_PATHS = new Set([
33
- '.ultra/docs/research/README.md',
34
- '.ultra/reports/templates/delivery-report.json',
35
- '.ultra/reports/templates/test-report.json',
36
- '.ultra/templates/task-context.md',
37
- '.ultra/tasks/tasks.json',
38
- '.ultra/changes/active/.gitkeep',
39
- '.ultra/changes/archive/.gitkeep',
40
- ]);
41
- const PROVISIONAL_BASELINE_SPEC_PATHS = new Set([
42
- '.ultra/specs/discovery.md',
43
- '.ultra/specs/product.md',
44
- '.ultra/specs/architecture.md',
45
- '.ultra/specs/research-distillate.md',
46
- ]);
47
-
48
- class ArtifactRegistryError extends Error {
49
- constructor(code, message, details = undefined) {
50
- super(message);
51
- this.name = 'ArtifactRegistryError';
52
- this.code = code;
53
- if (details !== undefined) this.details = details;
54
- }
55
- }
56
-
57
- function artifactText(value, field, { optional = false, nullable = false } = {}) {
58
- if (value === undefined && optional) return undefined;
59
- if (value === null && nullable) return null;
60
- if (typeof value !== 'string' || value.trim() === '') {
61
- throw new ArtifactRegistryError(
62
- 'VALIDATION_ERROR',
63
- `${field} must be a non-empty string${nullable ? ' or null' : ''}`,
64
- );
65
- }
66
- return value.trim();
67
- }
68
-
69
- function artifactDigest(value, field, { nullable = false } = {}) {
70
- if (value === undefined) return undefined;
71
- if (value === null && nullable) return null;
72
- if (typeof value !== 'string' || !DIGEST.test(value)) {
73
- throw new ArtifactRegistryError(
74
- 'VALIDATION_ERROR',
75
- `${field} must be a lowercase SHA-256 digest${nullable ? ' or null' : ''}`,
76
- );
77
- }
78
- return value;
79
- }
80
-
81
- function assertJsonValue(value, field, seen = new Set()) {
82
- if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
83
- if (typeof value === 'number') {
84
- if (Number.isFinite(value)) return;
85
- throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
86
- }
87
- if (typeof value !== 'object') {
88
- throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
89
- }
90
- if (seen.has(value)) {
91
- throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must not contain circular references`);
92
- }
93
- if (!Array.isArray(value)) {
94
- const prototype = Object.getPrototypeOf(value);
95
- if (prototype !== Object.prototype && prototype !== null) {
96
- throw new ArtifactRegistryError(
97
- 'VALIDATION_ERROR', `${field} must contain only JSON objects`,
98
- );
99
- }
100
- }
101
- seen.add(value);
102
- if (Array.isArray(value)) {
103
- value.forEach((item, index) => assertJsonValue(item, `${field}[${index}]`, seen));
104
- } else {
105
- Object.entries(value).forEach(([key, item]) => (
106
- assertJsonValue(item, `${field}.${key}`, seen)
107
- ));
108
- }
109
- seen.delete(value);
110
- }
111
-
112
- function normalizeJsonObject(value, field) {
113
- if (value === undefined) return {};
114
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
115
- throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must be a JSON object`);
116
- }
117
- assertJsonValue(value, field);
118
- return JSON.parse(JSON.stringify(value));
119
- }
120
-
121
- function parseJson(value, fallback) {
122
- if (value == null || value === '') return fallback;
123
- try { return JSON.parse(value); } catch { return fallback; }
124
- }
125
-
126
- function normalizeRelativePath(value) {
127
- if (typeof value !== 'string' || value.trim() === '') {
128
- throw new ArtifactRegistryError('VALIDATION_ERROR', 'artifact path is required');
129
- }
130
- const raw = value.trim().replaceAll('\\', '/');
131
- if (path.posix.isAbsolute(raw)) {
132
- throw new ArtifactRegistryError('ARTIFACT_PATH_INVALID', 'artifact path must be project-relative');
133
- }
134
- const normalized = path.posix.normalize(raw).replace(/^\.\//, '');
135
- if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
136
- throw new ArtifactRegistryError(
137
- 'ARTIFACT_PATH_INVALID', `artifact path escapes the project: ${value}`,
138
- );
139
- }
140
- return normalized;
141
- }
142
-
143
- function isExemptArtifactPath(value) {
144
- const relative = normalizeRelativePath(value);
145
- return relative === '.ultra/.runtime'
146
- || relative.startsWith('.ultra/.runtime/')
147
- || relative === '.ultra/scratch'
148
- || relative.startsWith('.ultra/scratch/')
149
- || STATIC_EXEMPT_PATHS.has(relative);
150
- }
151
-
152
- function isGeneratedProjectionPath(value) {
153
- const relative = normalizeRelativePath(value);
154
- return relative === '.ultra/.runtime/projections'
155
- || relative.startsWith('.ultra/.runtime/projections/')
156
- || relative === '.ultra/tasks/contexts'
157
- || relative.startsWith('.ultra/tasks/contexts/');
158
- }
159
-
160
- function artifactPathError(message, details = undefined) {
161
- return new ArtifactRegistryError('ARTIFACT_PATH_INVALID', message, details);
162
- }
163
-
164
- function isWithinPath(root, candidate) {
165
- return candidate === root || candidate.startsWith(`${root}${path.sep}`);
166
- }
167
-
168
- function statIdentity(stat) {
169
- return {
170
- dev: String(stat.dev),
171
- ino: String(stat.ino),
172
- mode: stat.mode,
173
- };
174
- }
175
-
176
- function sameIdentity(left, right) {
177
- return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;
178
- }
179
-
180
- function inspectResolvedArtifactPath(resolved, { requireFile = false } = {}) {
181
- const identities = [];
182
- let rootStat;
183
- try {
184
- rootStat = fs.lstatSync(resolved.physicalRoot);
185
- } catch (cause) {
186
- throw artifactPathError(
187
- `artifact project root is unavailable: ${resolved.root}`,
188
- { cause: cause.message },
189
- );
190
- }
191
- if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
192
- throw artifactPathError(`artifact project root is not a regular directory: ${resolved.root}`);
193
- }
194
- identities.push({
195
- path: resolved.physicalRoot,
196
- kind: 'directory',
197
- ...statIdentity(rootStat),
198
- });
199
-
200
- const components = resolved.relative.split('/').filter((part) => part && part !== '.');
201
- let current = resolved.physicalRoot;
202
- for (let index = 0; index < components.length; index += 1) {
203
- current = path.join(current, components[index]);
204
- const final = index === components.length - 1;
205
- let stat;
206
- try {
207
- stat = fs.lstatSync(current);
208
- } catch (cause) {
209
- if (cause?.code === 'ENOENT' && final && !requireFile) {
210
- return { exists: false, identities, final: null };
211
- }
212
- if (cause?.code === 'ENOENT' && final) {
213
- throw new ArtifactRegistryError(
214
- 'ARTIFACT_FILE_MISSING',
215
- `artifact file does not exist: ${resolved.relative}`,
216
- );
217
- }
218
- throw artifactPathError(
219
- `artifact ancestor is unavailable: ${resolved.relative}`,
220
- { component: components.slice(0, index + 1).join('/'), cause: cause.message },
221
- );
222
- }
223
- if (stat.isSymbolicLink()) {
224
- throw artifactPathError(
225
- `artifact path contains a symbolic link: ${resolved.relative}`,
226
- { component: components.slice(0, index + 1).join('/') },
227
- );
228
- }
229
- if (final ? !stat.isFile() : !stat.isDirectory()) {
230
- throw artifactPathError(
231
- `artifact path contains a non-regular ${final ? 'file' : 'ancestor'}: ${resolved.relative}`,
232
- { component: components.slice(0, index + 1).join('/') },
233
- );
234
- }
235
- let physical;
236
- try {
237
- physical = fs.realpathSync.native(current);
238
- } catch (cause) {
239
- throw artifactPathError(
240
- `artifact path cannot be resolved safely: ${resolved.relative}`,
241
- { component: components.slice(0, index + 1).join('/'), cause: cause.message },
242
- );
243
- }
244
- if (!isWithinPath(resolved.physicalRoot, physical)) {
245
- throw artifactPathError(
246
- `artifact path escapes the physical project root: ${resolved.relative}`,
247
- { component: components.slice(0, index + 1).join('/'), physical },
248
- );
249
- }
250
- identities.push({
251
- path: current,
252
- kind: final ? 'file' : 'directory',
253
- ...statIdentity(stat),
254
- });
255
- }
256
- const final = identities.at(-1);
257
- if (!final || final.kind !== 'file') {
258
- if (requireFile) {
259
- throw new ArtifactRegistryError(
260
- 'ARTIFACT_FILE_MISSING',
261
- `artifact file does not exist: ${resolved.relative}`,
262
- );
263
- }
264
- return { exists: false, identities, final: null };
265
- }
266
- return { exists: true, identities, final };
267
- }
268
-
269
- function resolveArtifactFile(rootDir, relative) {
270
- const root = path.resolve(rootDir);
271
- const normalized = normalizeRelativePath(relative);
272
- let physicalRoot;
273
- try {
274
- physicalRoot = fs.realpathSync.native(root);
275
- } catch (cause) {
276
- throw artifactPathError(
277
- `artifact project root is unavailable: ${root}`,
278
- { cause: cause.message },
279
- );
280
- }
281
- const file = path.resolve(physicalRoot, normalized);
282
- if (!isWithinPath(physicalRoot, file)) {
283
- throw artifactPathError(`artifact path escapes the project: ${relative}`);
284
- }
285
- const resolved = {
286
- root,
287
- physicalRoot,
288
- file,
289
- relative: normalized,
290
- };
291
- inspectResolvedArtifactPath(resolved);
292
- return resolved;
293
- }
294
-
295
- function digestFile(file) {
296
- let fd;
297
- try {
298
- fd = fs.openSync(
299
- file,
300
- fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0),
301
- );
302
- const stat = fs.fstatSync(fd);
303
- if (!stat.isFile()) {
304
- throw artifactPathError(`artifact must be a regular file: ${file}`);
305
- }
306
- return crypto.createHash('sha256').update(fs.readFileSync(fd)).digest('hex');
307
- } catch (error) {
308
- if (error instanceof ArtifactRegistryError) throw error;
309
- throw artifactPathError(`artifact cannot be read safely: ${file}`, {
310
- cause: error.message,
311
- });
312
- } finally {
313
- if (fd !== undefined) fs.closeSync(fd);
314
- }
315
- }
316
-
317
- function openStableArtifactRead(resolved) {
318
- const translate = (error) => {
319
- if (error instanceof ArtifactRegistryError) throw error;
320
- if (error?.code === 'PROJECT_FILE_MISSING') {
321
- throw new ArtifactRegistryError(
322
- 'ARTIFACT_FILE_MISSING',
323
- `artifact file does not exist: ${resolved.relative}`,
324
- );
325
- }
326
- if (error?.code === 'PROJECT_FILE_CHANGED') {
327
- throw new ArtifactRegistryError(
328
- 'ARTIFACT_DIGEST_CONFLICT',
329
- `artifact bytes changed while publication was in progress: ${resolved.relative}`,
330
- error.details,
331
- );
332
- }
333
- throw artifactPathError(
334
- `artifact cannot be opened without following links: ${resolved.relative}`,
335
- { cause: error.message },
336
- );
337
- };
338
- try {
339
- const reader = openStableProjectRead(resolved.root, resolved.relative);
340
- return {
341
- digest: reader.digest,
342
- verify() {
343
- try { reader.verify(); } catch (error) { translate(error); }
344
- },
345
- close: reader.close,
346
- };
347
- } catch (error) {
348
- translate(error);
349
- }
350
- }
351
-
352
- function stableArtifactDigest(resolved) {
353
- const reader = openStableArtifactRead(resolved);
354
- try {
355
- reader.verify();
356
- return reader.digest;
357
- } finally {
358
- reader.close();
359
- }
360
- }
361
-
362
- function endpointExists(db, type, id) {
363
- if (!ENDPOINT_TYPES.has(type) || typeof id !== 'string' || id.trim() === '') return false;
364
- if (type === 'external') return true;
365
- if (type === 'project') return id === 'project';
366
- const table = type === 'artifact' ? 'artifacts' : OWNER_TABLES[type];
367
- if (!table) return false;
368
- return Boolean(db.prepare(`SELECT 1 FROM ${table} WHERE id = ? LIMIT 1`).get(id));
369
- }
370
-
371
- function normalizeEndpoint(endpoint, field) {
372
- if (!endpoint || typeof endpoint !== 'object' || Array.isArray(endpoint)) {
373
- throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must be an object`);
374
- }
375
- const unknown = Object.keys(endpoint)
376
- .filter((key) => !['type', 'id', 'relation'].includes(key));
377
- if (unknown.length > 0) {
378
- throw new ArtifactRegistryError(
379
- 'VALIDATION_ERROR', `${field}.${unknown[0]} is not allowed`,
380
- );
381
- }
382
- const type = typeof endpoint.type === 'string' ? endpoint.type.trim() : '';
383
- const id = typeof endpoint.id === 'string' ? endpoint.id.trim() : '';
384
- const relation = typeof endpoint.relation === 'string' ? endpoint.relation.trim() : '';
385
- if (!ENDPOINT_TYPES.has(type) || !id || !relation) {
386
- throw new ArtifactRegistryError(
387
- 'VALIDATION_ERROR', `${field} requires a supported type, id, and relation`,
388
- );
389
- }
390
- return { type, id, relation };
391
- }
392
-
393
- function normalizeArtifactInput(input) {
394
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
395
- throw new ArtifactRegistryError('VALIDATION_ERROR', 'artifact input is required');
396
- }
397
- const unknown = Object.keys(input).filter((field) => !ARTIFACT_INPUT_FIELDS.has(field));
398
- if (unknown.length > 0) {
399
- throw new ArtifactRegistryError(
400
- 'VALIDATION_ERROR', `artifact.${unknown[0]} is not allowed`,
401
- );
402
- }
403
- const status = input.status === undefined
404
- ? 'current'
405
- : artifactText(input.status, 'artifact.status');
406
- if (!STATUSES.has(status)) {
407
- throw new ArtifactRegistryError('VALIDATION_ERROR', `invalid artifact status: ${status}`);
408
- }
409
- const normalizeRefs = (value, field) => {
410
- if (value === undefined) return [];
411
- if (!Array.isArray(value)) {
412
- throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must be an array`);
413
- }
414
- return value.map((item, index) => normalizeEndpoint(item, `${field}[${index}]`));
415
- };
416
- return {
417
- id: artifactText(input.id, 'artifact.id', { optional: true }),
418
- ownerType: artifactText(input.owner_type, 'artifact.owner_type'),
419
- ownerId: artifactText(input.owner_id, 'artifact.owner_id'),
420
- changeId: artifactText(
421
- input.change_id,
422
- 'artifact.change_id',
423
- { optional: true, nullable: true },
424
- ),
425
- taskId: artifactText(
426
- input.task_id,
427
- 'artifact.task_id',
428
- { optional: true, nullable: true },
429
- ),
430
- kind: artifactText(input.kind, 'artifact.kind'),
431
- path: artifactText(input.path, 'artifact.path'),
432
- status,
433
- contentDigest: artifactDigest(input.content_digest, 'artifact.content_digest'),
434
- expectedBeforeDigest: artifactDigest(
435
- input.expected_before_digest,
436
- 'artifact.expected_before_digest',
437
- { nullable: true },
438
- ),
439
- sourceRefs: normalizeRefs(input.source_refs, 'artifact.source_refs'),
440
- consumerRefs: normalizeRefs(input.consumer_refs, 'artifact.consumer_refs'),
441
- provenance: normalizeJsonObject(input.provenance, 'artifact.provenance'),
442
- metadata: normalizeJsonObject(input.metadata, 'artifact.metadata'),
443
- };
444
- }
445
-
446
- function assertEndpoint(db, endpoint, field) {
447
- const normalized = normalizeEndpoint(endpoint, field);
448
- const { type, id } = normalized;
449
- if (!endpointExists(db, type, id)) {
450
- throw new ArtifactRegistryError(
451
- 'ARTIFACT_ENDPOINT_MISSING', `${field} endpoint does not exist: ${type}:${id}`,
452
- { type, id },
453
- );
454
- }
455
- return normalized;
456
- }
457
-
458
- function assertOwner(db, ownerType, ownerId) {
459
- if (!OWNER_TYPES.has(ownerType)) {
460
- throw new ArtifactRegistryError(
461
- 'VALIDATION_ERROR', `unsupported artifact owner_type: ${ownerType}`,
462
- );
463
- }
464
- if (!endpointExists(db, ownerType, ownerId)) {
465
- throw new ArtifactRegistryError(
466
- 'ARTIFACT_OWNER_MISSING', `artifact owner does not exist: ${ownerType}:${ownerId}`,
467
- { owner_type: ownerType, owner_id: ownerId },
468
- );
469
- }
470
- }
471
-
472
- function assertArtifactOwner(db, ownerType, ownerId) {
473
- assertOwner(db, ownerType, ownerId);
474
- }
475
-
476
- function assertDeclaredOwnerRefs(db, normalized) {
477
- const expectedTaskId = normalized.ownerType === 'task' ? normalized.ownerId : null;
478
- const expectedChangeId = normalized.ownerType === 'change'
479
- ? normalized.ownerId
480
- : normalized.ownerType === 'task'
481
- ? db.prepare('SELECT change_id FROM tasks WHERE id = ?').get(normalized.ownerId)?.change_id || null
482
- : normalized.ownerType === 'workflow'
483
- ? db.prepare('SELECT change_id FROM workflow_runs WHERE id = ?').get(normalized.ownerId)?.change_id || null
484
- : null;
485
- for (const [field, supplied, expected] of [
486
- ['change_id', normalized.changeId, expectedChangeId],
487
- ['task_id', normalized.taskId, expectedTaskId],
488
- ]) {
489
- if (supplied !== undefined && supplied !== expected) {
490
- throw new ArtifactRegistryError(
491
- 'ARTIFACT_AUTHORITY_CONFLICT',
492
- `artifact ${field} does not match its owner authority`,
493
- { field, supplied, expected },
494
- );
495
- }
496
- }
497
- return { changeId: expectedChangeId, taskId: expectedTaskId };
498
- }
499
-
500
- function edgesForArtifact(db, artifactId) {
501
- const sources = db.prepare(
502
- `SELECT source_type AS type, source_id AS id, relation
503
- FROM artifact_edges
504
- WHERE target_type = 'artifact' AND target_id = ?
505
- ORDER BY source_type, source_id, relation`,
506
- ).all(artifactId);
507
- const consumers = db.prepare(
508
- `SELECT target_type AS type, target_id AS id, relation
509
- FROM artifact_edges
510
- WHERE source_type = 'artifact' AND source_id = ?
511
- ORDER BY target_type, target_id, relation`,
512
- ).all(artifactId);
513
- return { sources, consumers };
514
- }
515
-
516
- function rowToArtifact(db, row) {
517
- if (!row) return null;
518
- const edges = edgesForArtifact(db, row.id);
519
- return {
520
- id: row.id,
521
- owner_type: row.owner_type,
522
- owner_id: row.owner_id,
523
- kind: row.kind,
524
- path: row.path,
525
- digest: row.digest || row.content_hash || null,
526
- before_digest: row.before_digest || null,
527
- after_digest: row.after_digest || row.digest || row.content_hash || null,
528
- managed: Boolean(row.managed),
529
- status: row.status,
530
- provenance: parseJson(row.provenance_json, {}),
531
- metadata: parseJson(row.metadata_json, {}),
532
- source_refs: edges.sources,
533
- consumer_refs: edges.consumers,
534
- created_at: row.created_at,
535
- updated_at: row.updated_at,
536
- };
537
- }
538
-
539
- function getArtifact(db, { id, path: artifactPath } = {}) {
540
- const hasId = typeof id === 'string' && id.trim() !== '';
541
- const hasPath = typeof artifactPath === 'string' && artifactPath.trim() !== '';
542
- if (hasId === hasPath) {
543
- throw new ArtifactRegistryError(
544
- 'VALIDATION_ERROR', 'provide exactly one of artifact id or path',
545
- );
546
- }
547
- let row;
548
- if (hasId) {
549
- row = db.prepare('SELECT * FROM artifacts WHERE id = ?').get(id.trim());
550
- } else {
551
- const relative = normalizeRelativePath(artifactPath);
552
- const matches = db.prepare(
553
- `SELECT * FROM artifacts WHERE path = ? AND status <> 'archived'
554
- ORDER BY CASE status WHEN 'current' THEN 0 WHEN 'terminal' THEN 1
555
- WHEN 'stale' THEN 2 ELSE 3 END, updated_at DESC, rowid DESC`,
556
- ).all(relative);
557
- if (matches.length > 1) {
558
- throw new ArtifactRegistryError(
559
- 'ARTIFACT_DUPLICATE_AUTHORITY',
560
- `artifact path has multiple active authorities: ${relative}`,
561
- { path: relative, artifact_ids: matches.map((item) => item.id) },
562
- );
563
- }
564
- [row] = matches;
565
- if (!row) {
566
- row = db.prepare(
567
- `SELECT * FROM artifacts WHERE path = ? AND status = 'archived'
568
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
569
- ).get(relative);
570
- }
571
- }
572
- if (!row) {
573
- throw new ArtifactRegistryError(
574
- 'ARTIFACT_NOT_FOUND', `artifact ${hasId ? id : artifactPath} not found`,
575
- );
576
- }
577
- return rowToArtifact(db, row);
578
- }
579
-
580
- function insertEdge(db, sourceType, sourceId, targetType, targetId, relation) {
581
- db.prepare(
582
- `INSERT INTO artifact_edges
583
- (source_type, source_id, target_type, target_id, relation)
584
- VALUES (?, ?, ?, ?, ?)`,
585
- ).run(sourceType, sourceId, targetType, targetId, relation);
586
- }
587
-
588
- function endpointKey(endpoint) {
589
- return `${endpoint.type}\u0000${endpoint.id}`;
590
- }
591
-
592
- function outgoingEdges(db, endpoint) {
593
- return db.prepare(
594
- `SELECT target_type AS type, target_id AS id, relation
595
- FROM artifact_edges
596
- WHERE source_type = ? AND source_id = ?
597
- ORDER BY target_type, target_id, relation`,
598
- ).all(endpoint.type, endpoint.id);
599
- }
600
-
601
- function graphReaches(db, start, target) {
602
- const targetKey = endpointKey(target);
603
- const queue = [...outgoingEdges(db, start)];
604
- const seen = new Set();
605
- while (queue.length > 0) {
606
- const endpoint = queue.shift();
607
- const key = endpointKey(endpoint);
608
- if (key === targetKey) return true;
609
- if (seen.has(key)) continue;
610
- seen.add(key);
611
- queue.push(...outgoingEdges(db, endpoint));
612
- }
613
- return false;
614
- }
615
-
616
- function graphCycleFrom(db, start) {
617
- return graphReaches(db, start, start);
618
- }
619
-
620
- function invalidateConsumersFromEndpointInTx(db, start, {
621
- prior_consumers: priorConsumers = [],
622
- reason = 'authority_digest_changed',
623
- exclude = [],
624
- } = {}) {
625
- const excluded = new Set(exclude.map(endpointKey));
626
- const queue = [...priorConsumers, ...outgoingEdges(db, start)];
627
- const seen = new Set();
628
- const invalidated = [];
629
- while (queue.length > 0) {
630
- const endpoint = queue.shift();
631
- const key = endpointKey(endpoint);
632
- if (seen.has(key) || excluded.has(key) || key === endpointKey(start)) continue;
633
- seen.add(key);
634
- if (endpoint.type === 'workflow') {
635
- // Workflow rows are retained only as pre-v0.24 audit history. Preserve
636
- // graph traversal for downstream current authorities, but never rewrite
637
- // the historical workflow row or claim that it was invalidated.
638
- queue.push(...outgoingEdges(db, endpoint));
639
- continue;
640
- }
641
- invalidated.push({ type: endpoint.type, id: endpoint.id });
642
- if (endpoint.type === 'artifact') {
643
- const row = db.prepare('SELECT status FROM artifacts WHERE id = ?').get(endpoint.id);
644
- if (!row) continue;
645
- if (row.status === 'current') {
646
- db.prepare(
647
- `UPDATE artifacts SET status = 'stale',
648
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
649
- ).run(endpoint.id);
650
- }
651
- }
652
- if (endpoint.type === 'task') {
653
- const task = db.prepare('SELECT id, stale FROM tasks WHERE id = ?').get(endpoint.id);
654
- if (task && !task.stale) {
655
- db.prepare(
656
- `UPDATE tasks SET stale = 1,
657
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
658
- ).run(endpoint.id);
659
- ops.appendEventInTx(db, {
660
- type: 'task_stale_marked',
661
- task_id: endpoint.id,
662
- payload: {
663
- source_type: start.type,
664
- source_id: start.id,
665
- ...(start.type === 'artifact' ? { artifact_id: start.id } : {}),
666
- reason,
667
- },
668
- });
669
- }
670
- }
671
- queue.push(...outgoingEdges(db, endpoint));
672
- }
673
- return invalidated;
674
- }
675
-
676
- function invalidateDownstreamInTx(db, artifactId, priorConsumers = []) {
677
- return invalidateConsumersFromEndpointInTx(
678
- db,
679
- { type: 'artifact', id: artifactId },
680
- {
681
- prior_consumers: priorConsumers,
682
- reason: 'artifact_digest_changed',
683
- },
684
- );
685
- }
686
-
687
- function preflightArtifactPublication(db, input, { rootDir = process.cwd() } = {}) {
688
- const normalized = normalizeArtifactInput(input);
689
- const {
690
- ownerType, ownerId, kind, status,
691
- } = normalized;
692
- assertOwner(db, ownerType, ownerId);
693
- assertDeclaredOwnerRefs(db, normalized);
694
- const resolved = resolveArtifactFile(rootDir, normalized.path);
695
- if (isExemptArtifactPath(resolved.relative) || isGeneratedProjectionPath(resolved.relative)) {
696
- throw new ArtifactRegistryError(
697
- 'ARTIFACT_PATH_EXEMPT',
698
- `runtime, scratch, and generated projection paths are not registry authority: ${resolved.relative}`,
699
- );
700
- }
701
- const explicitId = normalized.id || null;
702
- const existingById = explicitId
703
- ? db.prepare('SELECT * FROM artifacts WHERE id = ?').get(explicitId)
704
- : null;
705
- const existingByAuthority = db.prepare(
706
- `SELECT * FROM artifacts
707
- WHERE owner_type = ? AND owner_id = ? AND kind = ? AND path = ?`,
708
- ).get(ownerType, ownerId, kind, resolved.relative);
709
- if (explicitId && existingByAuthority && existingByAuthority.id !== explicitId) {
710
- throw new ArtifactRegistryError(
711
- 'ARTIFACT_AUTHORITY_CONFLICT',
712
- `artifact authority is already bound to id ${existingByAuthority.id}`,
713
- );
714
- }
715
- const existing = existingById || existingByAuthority;
716
- if (existing && (
717
- existing.owner_type !== ownerType || existing.owner_id !== ownerId
718
- || existing.kind !== kind || existing.path !== resolved.relative
719
- )) {
720
- throw new ArtifactRegistryError(
721
- 'ARTIFACT_AUTHORITY_CONFLICT',
722
- `artifact id ${existing.id} is already bound to another authority`,
723
- );
724
- }
725
- const artifactId = existing?.id || explicitId;
726
- const pathConflict = db.prepare(
727
- `SELECT id FROM artifacts
728
- WHERE path = ? AND status <> 'archived' AND (? IS NULL OR id <> ?)
729
- ORDER BY id LIMIT 1`,
730
- ).get(resolved.relative, artifactId, artifactId);
731
- if (status !== 'archived' && pathConflict) {
732
- throw new ArtifactRegistryError(
733
- 'ARTIFACT_AUTHORITY_CONFLICT',
734
- `artifact path already has active authority ${pathConflict.id}: ${resolved.relative}`,
735
- {
736
- path: resolved.relative,
737
- requested_id: artifactId,
738
- existing_id: pathConflict.id,
739
- },
740
- );
741
- }
742
- const beforeDigest = existing?.digest || existing?.content_hash || null;
743
- if (existing) {
744
- let actual;
745
- try {
746
- actual = stableArtifactDigest(resolved);
747
- } catch (error) {
748
- if (error.code === 'ARTIFACT_FILE_MISSING') {
749
- throw new ArtifactRegistryError(
750
- 'ARTIFACT_DIGEST_CONFLICT',
751
- `registered artifact bytes are missing before publication: ${resolved.relative}`,
752
- { expected: beforeDigest, actual: null },
753
- );
754
- }
755
- throw error;
756
- }
757
- if (beforeDigest && actual !== beforeDigest) {
758
- throw new ArtifactRegistryError(
759
- 'ARTIFACT_DIGEST_CONFLICT',
760
- `registered artifact changed outside its authority before publication: ${resolved.relative}`,
761
- { expected: beforeDigest, actual },
762
- );
763
- }
764
- } else {
765
- const state = inspectResolvedArtifactPath(resolved);
766
- if (state.exists) {
767
- stableArtifactDigest(resolved);
768
- }
769
- }
770
- return {
771
- artifact_id: artifactId,
772
- path: resolved.relative,
773
- expected_before_digest: beforeDigest,
774
- };
775
- }
776
-
777
- function recordArtifactInTx(db, input, { rootDir = process.cwd() } = {}) {
778
- const normalized = normalizeArtifactInput(input);
779
- const {
780
- ownerType, ownerId, kind, status,
781
- sourceRefs, consumerRefs, provenance, metadata,
782
- } = normalized;
783
- assertOwner(db, ownerType, ownerId);
784
- const ownerRefs = assertDeclaredOwnerRefs(db, normalized);
785
- const resolved = resolveArtifactFile(rootDir, normalized.path);
786
- if (isExemptArtifactPath(resolved.relative) || isGeneratedProjectionPath(resolved.relative)) {
787
- throw new ArtifactRegistryError(
788
- 'ARTIFACT_PATH_EXEMPT',
789
- `runtime, scratch, and generated projection paths are not registry authority: ${resolved.relative}`,
790
- );
791
- }
792
- const stableRead = openStableArtifactRead(resolved);
793
- try {
794
- const explicitId = normalized.id || null;
795
- const existingById = explicitId
796
- ? db.prepare('SELECT * FROM artifacts WHERE id = ?').get(explicitId)
797
- : null;
798
- const existingByAuthority = db.prepare(
799
- `SELECT * FROM artifacts
800
- WHERE owner_type = ? AND owner_id = ? AND kind = ? AND path = ?`,
801
- ).get(ownerType, ownerId, kind, resolved.relative);
802
- if (explicitId && existingByAuthority && existingByAuthority.id !== explicitId) {
803
- throw new ArtifactRegistryError(
804
- 'ARTIFACT_AUTHORITY_CONFLICT',
805
- `artifact authority is already bound to id ${existingByAuthority.id}`,
806
- { requested_id: explicitId, existing_id: existingByAuthority.id },
807
- );
808
- }
809
- const existing = existingById || existingByAuthority;
810
- if (existing && (
811
- existing.owner_type !== ownerType || existing.owner_id !== ownerId
812
- || existing.kind !== kind || existing.path !== resolved.relative
813
- )) {
814
- throw new ArtifactRegistryError(
815
- 'ARTIFACT_AUTHORITY_CONFLICT',
816
- `artifact id ${existing.id} is already bound to another authority`,
817
- );
818
- }
819
- const artifactId = existing?.id || explicitId || `art-${crypto.randomUUID().slice(0, 12)}`;
820
- const activePathRows = db.prepare(
821
- "SELECT id FROM artifacts WHERE path = ? AND status <> 'archived' ORDER BY id",
822
- ).all(resolved.relative);
823
- const pathConflict = activePathRows.find((row) => row.id !== artifactId);
824
- if (status !== 'archived' && pathConflict) {
825
- throw new ArtifactRegistryError(
826
- 'ARTIFACT_AUTHORITY_CONFLICT',
827
- `artifact path already has active authority ${pathConflict.id}: ${resolved.relative}`,
828
- {
829
- path: resolved.relative,
830
- requested_id: artifactId,
831
- existing_id: pathConflict.id,
832
- },
833
- );
834
- }
835
- for (const [field, refs] of [['source_refs', sourceRefs], ['consumer_refs', consumerRefs]]) {
836
- refs.forEach((ref, index) => {
837
- if (ref.type === 'artifact' && ref.id === artifactId) {
838
- throw new ArtifactRegistryError(
839
- 'ARTIFACT_GRAPH_SELF_EDGE',
840
- `${field}[${index}] cannot reference artifact ${artifactId} itself`,
841
- { artifact_id: artifactId, field, index },
842
- );
843
- }
844
- assertEndpoint(db, ref, `${field}[${index}]`);
845
- });
846
- }
847
- const beforeDigest = existing?.digest || existing?.content_hash || null;
848
- if (normalized.expectedBeforeDigest !== undefined
849
- && normalized.expectedBeforeDigest !== beforeDigest) {
850
- throw new ArtifactRegistryError(
851
- 'ARTIFACT_DIGEST_CONFLICT',
852
- `artifact ${artifactId} changed since it was read`,
853
- { expected: normalized.expectedBeforeDigest, actual: beforeDigest },
854
- );
855
- }
856
- const afterDigest = stableRead.digest;
857
- if (normalized.contentDigest !== undefined && normalized.contentDigest !== afterDigest) {
858
- throw new ArtifactRegistryError(
859
- 'ARTIFACT_DIGEST_CONFLICT',
860
- `artifact bytes do not match the supplied digest: ${resolved.relative}`,
861
- { expected: normalized.contentDigest, actual: afterDigest },
862
- );
863
- }
864
- const changed = beforeDigest !== afterDigest;
865
- const priorConsumers = existing ? edgesForArtifact(db, artifactId).consumers : [];
866
- const ownerChangeId = ownerRefs.changeId;
867
- const now = new Date().toISOString();
868
- let invalidated = [];
869
- db.prepare(
870
- `INSERT INTO artifacts
871
- (id, owner_type, owner_id, change_id, task_id, kind, path, digest,
872
- content_hash, before_digest, after_digest, provenance_json, metadata_json,
873
- managed, status, created_at, updated_at)
874
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
875
- ON CONFLICT(id) DO UPDATE SET
876
- owner_type = excluded.owner_type, owner_id = excluded.owner_id,
877
- change_id = excluded.change_id, task_id = excluded.task_id,
878
- kind = excluded.kind, path = excluded.path, digest = excluded.digest,
879
- content_hash = excluded.content_hash, before_digest = excluded.before_digest,
880
- after_digest = excluded.after_digest, provenance_json = excluded.provenance_json,
881
- metadata_json = excluded.metadata_json, managed = 1, status = excluded.status,
882
- updated_at = excluded.updated_at`,
883
- ).run(
884
- artifactId, ownerType, ownerId,
885
- ownerChangeId,
886
- ownerRefs.taskId,
887
- kind, resolved.relative, afterDigest, afterDigest, beforeDigest, afterDigest,
888
- JSON.stringify(provenance), JSON.stringify(metadata), status,
889
- existing?.created_at || now, now,
890
- );
891
- db.prepare(
892
- `DELETE FROM artifact_edges
893
- WHERE (source_type = 'artifact' AND source_id = ?)
894
- OR (target_type = 'artifact' AND target_id = ?)`,
895
- ).run(artifactId, artifactId);
896
- for (const ref of sourceRefs) {
897
- insertEdge(db, ref.type, ref.id, 'artifact', artifactId, ref.relation);
898
- }
899
- for (const ref of consumerRefs) {
900
- insertEdge(db, 'artifact', artifactId, ref.type, ref.id, ref.relation);
901
- }
902
- if (graphCycleFrom(db, { type: 'artifact', id: artifactId })) {
903
- throw new ArtifactRegistryError(
904
- 'ARTIFACT_GRAPH_CYCLE',
905
- `artifact dependency edges create a cycle through ${artifactId}`,
906
- { artifact_id: artifactId },
907
- );
908
- }
909
- if (beforeDigest && changed) {
910
- invalidated = invalidateDownstreamInTx(db, artifactId, priorConsumers);
911
- }
912
- ops.appendEventInTx(db, {
913
- type: 'artifact_recorded',
914
- change_id: ownerChangeId,
915
- task_id: ownerType === 'task' ? ownerId : null,
916
- payload: {
917
- artifact_id: artifactId, path: resolved.relative, before_digest: beforeDigest,
918
- after_digest: afterDigest, changed,
919
- },
920
- });
921
- if (beforeDigest && changed) {
922
- ops.appendEventInTx(db, {
923
- type: 'spec_changed',
924
- change_id: ownerChangeId,
925
- task_id: ownerType === 'task' ? ownerId : null,
926
- payload: {
927
- artifact_id: artifactId, path: resolved.relative,
928
- before_digest: beforeDigest, after_digest: afterDigest,
929
- sections: [resolved.relative], invalidated,
930
- },
931
- });
932
- }
933
- stableRead.verify();
934
- return {
935
- artifact: getArtifact(db, { id: artifactId }),
936
- changed,
937
- invalidated,
938
- };
939
- } finally {
940
- stableRead.close();
941
- }
942
- }
943
-
944
- function recordArtifact(db, input, options = {}) {
945
- return ops.tx(db, () => recordArtifactInTx(db, input, options));
946
- }
947
-
948
- function moveArtifactInTx(db, id, nextPath, { rootDir = process.cwd() } = {}) {
949
- const row = db.prepare('SELECT * FROM artifacts WHERE id = ?').get(id);
950
- if (!row) {
951
- throw new ArtifactRegistryError('ARTIFACT_NOT_FOUND', `artifact ${id} not found`);
952
- }
953
- const resolved = resolveArtifactFile(rootDir, nextPath);
954
- if (isExemptArtifactPath(resolved.relative) || isGeneratedProjectionPath(resolved.relative)) {
955
- throw new ArtifactRegistryError(
956
- 'ARTIFACT_PATH_EXEMPT',
957
- `artifact cannot move into runtime, scratch, or generated projections: ${resolved.relative}`,
958
- );
959
- }
960
- const conflict = db.prepare(
961
- "SELECT id FROM artifacts WHERE path = ? AND status <> 'archived' AND id <> ? LIMIT 1",
962
- ).get(resolved.relative, id);
963
- if (row.status !== 'archived' && conflict) {
964
- throw new ArtifactRegistryError(
965
- 'ARTIFACT_AUTHORITY_CONFLICT',
966
- `artifact path already has active authority ${conflict.id}: ${resolved.relative}`,
967
- );
968
- }
969
- const stableRead = openStableArtifactRead(resolved);
970
- try {
971
- const actual = stableRead.digest;
972
- const expected = row.digest || row.content_hash;
973
- if (expected && actual !== expected) {
974
- throw new ArtifactRegistryError(
975
- 'ARTIFACT_DIGEST_CONFLICT',
976
- `artifact move target digest does not match ${id}`,
977
- { expected, actual },
978
- );
979
- }
980
- db.prepare(
981
- `UPDATE artifacts SET path = ?,
982
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
983
- ).run(resolved.relative, id);
984
- ops.appendEventInTx(db, {
985
- type: 'artifact_moved',
986
- change_id: row.change_id || (row.owner_type === 'change' ? row.owner_id : null),
987
- task_id: row.owner_type === 'task' ? row.owner_id : null,
988
- payload: { artifact_id: id, from: row.path, to: resolved.relative },
989
- });
990
- stableRead.verify();
991
- return getArtifact(db, { id });
992
- } finally {
993
- stableRead.close();
994
- }
995
- }
996
-
997
- function setArtifactStatusInTx(db, id, status) {
998
- if (!STATUSES.has(status)) {
999
- throw new ArtifactRegistryError('VALIDATION_ERROR', `invalid artifact status: ${status}`);
1000
- }
1001
- const row = db.prepare('SELECT * FROM artifacts WHERE id = ?').get(id);
1002
- if (!row) {
1003
- throw new ArtifactRegistryError('ARTIFACT_NOT_FOUND', `artifact ${id} not found`);
1004
- }
1005
- if (status !== 'archived') {
1006
- const conflict = db.prepare(
1007
- "SELECT id FROM artifacts WHERE path = ? AND status <> 'archived' AND id <> ? LIMIT 1",
1008
- ).get(row.path, id);
1009
- if (conflict) {
1010
- throw new ArtifactRegistryError(
1011
- 'ARTIFACT_AUTHORITY_CONFLICT',
1012
- `artifact path already has active authority ${conflict.id}: ${row.path}`,
1013
- );
1014
- }
1015
- }
1016
- db.prepare(
1017
- `UPDATE artifacts SET status = ?,
1018
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
1019
- ).run(status, id);
1020
- return getArtifact(db, { id });
1021
- }
1022
-
1023
- function collectJsonPaths(value, output) {
1024
- if (Array.isArray(value)) {
1025
- for (const item of value) collectJsonPaths(item, output);
1026
- return;
1027
- }
1028
- if (!value || typeof value !== 'object') return;
1029
- for (const [key, item] of Object.entries(value)) {
1030
- if (['path', 'ref', 'manifest_path', 'report_path'].includes(key)
1031
- && typeof item === 'string' && item.startsWith('.ultra/')) {
1032
- try { output.add(normalizeRelativePath(item)); } catch { /* diagnosed elsewhere */ }
1033
- } else {
1034
- collectJsonPaths(item, output);
1035
- }
1036
- }
1037
- }
1038
-
1039
- function knownArtifactPaths(db) {
1040
- const known = new Set(
1041
- db.prepare('SELECT path FROM artifacts').all().map((row) => normalizeRelativePath(row.path)),
1042
- );
1043
- const jsonQueries = [
1044
- ['SELECT spec_refs_json, evidence_json FROM baselines', ['spec_refs_json', 'evidence_json']],
1045
- ['SELECT outputs_json FROM workflow_steps', ['outputs_json']],
1046
- ['SELECT checkpoint_json FROM decision_threads', ['checkpoint_json']],
1047
- ];
1048
- for (const [sql, fields] of jsonQueries) {
1049
- for (const row of db.prepare(sql).all()) {
1050
- for (const field of fields) collectJsonPaths(parseJson(row[field], null), known);
1051
- }
1052
- }
1053
- for (const row of db.prepare('SELECT manifest_path FROM context_snapshots').all()) {
1054
- if (row.manifest_path) known.add(normalizeRelativePath(row.manifest_path));
1055
- }
1056
- for (const row of db.prepare('SELECT id, context_file FROM tasks').all()) {
1057
- known.add(normalizeRelativePath(
1058
- contextPaths.resolveContextPath('.', row.context_file, { taskId: row.id }).relative,
1059
- ));
1060
- }
1061
- return known;
1062
- }
1063
-
1064
- function listUltraFiles(rootDir) {
1065
- return walkStableProjectTree(rootDir, '.ultra', {
1066
- ignore(relative, entry, stat) {
1067
- return (stat.isDirectory()
1068
- && (relative === '.ultra/.runtime' || relative === '.ultra/scratch'))
1069
- || (stat.isFile() && ['.DS_Store', '.gitkeep'].includes(entry.name));
1070
- },
1071
- });
1072
- }
1073
-
1074
- function hasReachableConsumerSink(db, artifactId) {
1075
- const queue = [...outgoingEdges(db, { type: 'artifact', id: artifactId })];
1076
- const seen = new Set();
1077
- while (queue.length > 0) {
1078
- const endpoint = queue.shift();
1079
- const key = endpointKey(endpoint);
1080
- if (seen.has(key)) continue;
1081
- seen.add(key);
1082
- if (!endpointExists(db, endpoint.type, endpoint.id)) continue;
1083
- if (endpoint.type === 'task' || endpoint.type === 'workflow') return true;
1084
- if (endpoint.type === 'artifact') {
1085
- const row = db.prepare(
1086
- 'SELECT status, metadata_json FROM artifacts WHERE id = ?',
1087
- ).get(endpoint.id);
1088
- const metadata = parseJson(row?.metadata_json, {});
1089
- if (row && (row.status === 'terminal' || metadata.terminal_role === true)) return true;
1090
- }
1091
- queue.push(...outgoingEdges(db, endpoint));
1092
- }
1093
- return false;
1094
- }
1095
-
1096
- function inspectArtifactHealth(db, { rootDir = process.cwd() } = {}) {
1097
- const issues = [];
1098
- const rows = db.prepare('SELECT * FROM artifacts ORDER BY path, id').all();
1099
- const byPath = new Map();
1100
- const push = (code, details = {}) => issues.push({ code, ...details });
1101
- for (const row of rows) {
1102
- const relative = normalizeRelativePath(row.path);
1103
- if (row.status !== 'archived') {
1104
- if (!byPath.has(relative)) byPath.set(relative, []);
1105
- byPath.get(relative).push(row.id);
1106
- }
1107
- let resolved;
1108
- try {
1109
- resolved = resolveArtifactFile(rootDir, relative);
1110
- } catch (error) {
1111
- push(
1112
- error.code === 'ARTIFACT_PATH_INVALID'
1113
- ? 'ARTIFACT_PATH_UNSAFE'
1114
- : 'ARTIFACT_MISSING',
1115
- { artifact_id: row.id, path: relative },
1116
- );
1117
- continue;
1118
- }
1119
- try {
1120
- const expected = row.digest || row.content_hash;
1121
- const actual = stableArtifactDigest(resolved);
1122
- if (row.status === 'stale' || (expected && expected !== actual)) {
1123
- push('ARTIFACT_STALE', {
1124
- artifact_id: row.id, path: relative, expected_digest: expected || null,
1125
- actual_digest: actual,
1126
- });
1127
- }
1128
- } catch (error) {
1129
- push(
1130
- error.code === 'ARTIFACT_PATH_INVALID'
1131
- ? 'ARTIFACT_PATH_UNSAFE'
1132
- : 'ARTIFACT_MISSING',
1133
- { artifact_id: row.id, path: relative },
1134
- );
1135
- }
1136
- if (!endpointExists(db, row.owner_type, row.owner_id)) {
1137
- push('ARTIFACT_OWNER_MISSING', {
1138
- artifact_id: row.id, path: relative,
1139
- owner_type: row.owner_type, owner_id: row.owner_id,
1140
- });
1141
- }
1142
- if (!row.managed) {
1143
- push('ARTIFACT_COMPATIBILITY_UNMANAGED', {
1144
- artifact_id: row.id, path: relative,
1145
- });
1146
- }
1147
- if (row.managed && row.status === 'current') {
1148
- const metadata = parseJson(row.metadata_json, {});
1149
- if (metadata.terminal_role !== true && !hasReachableConsumerSink(db, row.id)) {
1150
- push('ARTIFACT_NO_CONSUMER', { artifact_id: row.id, path: relative });
1151
- }
1152
- if (graphCycleFrom(db, { type: 'artifact', id: row.id })) {
1153
- push('ARTIFACT_GRAPH_CYCLE', { artifact_id: row.id, path: relative });
1154
- }
1155
- }
1156
- }
1157
- for (const [artifactPath, ids] of byPath) {
1158
- if (ids.length > 1) {
1159
- push('ARTIFACT_DUPLICATE_AUTHORITY', {
1160
- path: artifactPath, artifact_ids: ids,
1161
- });
1162
- }
1163
- }
1164
- for (const edge of db.prepare('SELECT * FROM artifact_edges ORDER BY id').all()) {
1165
- const sourceExists = endpointExists(db, edge.source_type, edge.source_id);
1166
- const targetExists = endpointExists(db, edge.target_type, edge.target_id);
1167
- if (!sourceExists || !targetExists) {
1168
- push('ARTIFACT_EDGE_DANGLING', {
1169
- edge_id: edge.id,
1170
- source: { type: edge.source_type, id: edge.source_id },
1171
- target: { type: edge.target_type, id: edge.target_id },
1172
- });
1173
- }
1174
- }
1175
- const known = knownArtifactPaths(db);
1176
- const readyBaseline = Boolean(db.prepare(
1177
- "SELECT 1 FROM baselines WHERE status = 'ready' LIMIT 1",
1178
- ).get());
1179
- const tree = listUltraFiles(rootDir);
1180
- for (const issue of tree.unsafe) {
1181
- push('ARTIFACT_TREE_UNSAFE', issue);
1182
- }
1183
- for (const relative of tree.files) {
1184
- if (isExemptArtifactPath(relative) || known.has(relative)) continue;
1185
- if (!readyBaseline && PROVISIONAL_BASELINE_SPEC_PATHS.has(relative)) continue;
1186
- if (relative.startsWith(`${contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE}/`)) {
1187
- let contents;
1188
- try {
1189
- contents = readStableProjectFile(rootDir, relative).bytes;
1190
- } catch (error) {
1191
- push('ARTIFACT_TREE_UNSAFE', { path: relative, reason: error.message });
1192
- continue;
1193
- }
1194
- if (contextPaths.isGeneratedContextContents(contents)) {
1195
- push('ARTIFACT_GHOST_PROJECTION', { path: relative });
1196
- }
1197
- }
1198
- push('ARTIFACT_UNREGISTERED', { path: relative });
1199
- }
1200
- const counts = {};
1201
- for (const issue of issues) counts[issue.code] = (counts[issue.code] || 0) + 1;
1202
- return {
1203
- status: issues.length === 0 ? 'pass' : 'fail',
1204
- registered: rows.length,
1205
- managed: rows.filter((row) => Boolean(row.managed)).length,
1206
- unmanaged: rows.filter((row) => !row.managed).length,
1207
- issues,
1208
- counts,
1209
- };
1210
- }
1211
-
1212
- module.exports = {
1213
- ArtifactRegistryError,
1214
- ENDPOINT_TYPES,
1215
- OWNER_TYPES,
1216
- STATUSES,
1217
- assertArtifactOwner,
1218
- digestFile,
1219
- getArtifact,
1220
- inspectArtifactHealth,
1221
- isExemptArtifactPath,
1222
- isGeneratedProjectionPath,
1223
- normalizeRelativePath,
1224
- preflightArtifactPublication,
1225
- recordArtifact,
1226
- recordArtifactInTx,
1227
- moveArtifactInTx,
1228
- setArtifactStatusInTx,
1229
- invalidateDownstreamInTx,
1230
- invalidateConsumersFromEndpointInTx,
1231
- outgoingEdges,
1232
- };