@a5c-ai/krate 5.0.1-staging.04a3db697

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 (246) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3067 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +2955 -0
  8. package/dist/krate-summary.json +722 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/product-requirements.md +62 -0
  108. package/docs/roadmap-mvp.md +87 -0
  109. package/docs/system-requirements.md +90 -0
  110. package/docs/tests/README.md +53 -0
  111. package/docs/tests/agent-qa-plan.md +63 -0
  112. package/docs/tests/browser-ui-tests.md +62 -0
  113. package/docs/tests/ci-quality-gates.md +48 -0
  114. package/docs/tests/coverage-model.md +64 -0
  115. package/docs/tests/e2e-scenario-tests.md +53 -0
  116. package/docs/tests/fixtures-test-data.md +63 -0
  117. package/docs/tests/observability-reliability-tests.md +54 -0
  118. package/docs/tests/product-test-matrix.md +145 -0
  119. package/docs/tests/qa-adoption-roadmap.md +130 -0
  120. package/docs/tests/qa-automation-plan.md +101 -0
  121. package/docs/tests/security-compliance-tests.md +57 -0
  122. package/docs/tests/test-framework-tools.md +88 -0
  123. package/docs/tests/test-suite-layout.md +121 -0
  124. package/docs/tests/unit-integration-tests.md +48 -0
  125. package/docs/todo-kyverno +714 -0
  126. package/docs/todos.md +4 -0
  127. package/docs/user-stories.md +78 -0
  128. package/examples/minikube-demo.yaml +190 -0
  129. package/examples/oam-application.yaml +23 -0
  130. package/examples/policy-kyverno-pr-title.yaml +18 -0
  131. package/package.json +63 -0
  132. package/scripts/build.mjs +29 -0
  133. package/scripts/setup-minikube.mjs +65 -0
  134. package/scripts/smoke.mjs +37 -0
  135. package/scripts/validate-doc-coverage.mjs +152 -0
  136. package/scripts/validate-package.mjs +93 -0
  137. package/scripts/validate-ui.mjs +236 -0
  138. package/src/agent-adapter-controller.js +169 -0
  139. package/src/agent-approval-controller.js +170 -0
  140. package/src/agent-context-bundles.js +242 -0
  141. package/src/agent-dispatch-controller.js +209 -0
  142. package/src/agent-gateway-config-controller.js +147 -0
  143. package/src/agent-memory-controller.js +357 -0
  144. package/src/agent-memory-import.js +327 -0
  145. package/src/agent-memory-query.js +292 -0
  146. package/src/agent-memory-repository-source-controller.js +255 -0
  147. package/src/agent-mux-client.js +280 -0
  148. package/src/agent-permission-review.js +250 -0
  149. package/src/agent-project-controller.js +117 -0
  150. package/src/agent-provider-config-controller.js +150 -0
  151. package/src/agent-secret-config-grant-controller.js +282 -0
  152. package/src/agent-session-transcript-controller.js +189 -0
  153. package/src/agent-stack-controller.js +347 -0
  154. package/src/agent-subagent-controller.js +160 -0
  155. package/src/agent-transport-binding-controller.js +121 -0
  156. package/src/agent-trigger-controller.js +321 -0
  157. package/src/agent-workspace-controller.js +447 -0
  158. package/src/agent-writeback-controller.js +302 -0
  159. package/src/api-controller.js +541 -0
  160. package/src/argocd-gitops.js +43 -0
  161. package/src/async-controller.js +207 -0
  162. package/src/audit-controller.js +191 -0
  163. package/src/auth.js +307 -0
  164. package/src/component-catalog.js +41 -0
  165. package/src/control-plane.js +136 -0
  166. package/src/controller-client.js +50 -0
  167. package/src/controller-ui.js +551 -0
  168. package/src/data-plane.js +178 -0
  169. package/src/event-bus.js +61 -0
  170. package/src/external/conflict-controller.js +225 -0
  171. package/src/external/github/auth.js +96 -0
  172. package/src/external/github/cicd.js +180 -0
  173. package/src/external/github/git-forge.js +240 -0
  174. package/src/external/github/index.js +144 -0
  175. package/src/external/github/issue-tracking.js +163 -0
  176. package/src/external/provider-adapter.js +161 -0
  177. package/src/external/provider-resource-factory.js +161 -0
  178. package/src/external/sync-controller.js +235 -0
  179. package/src/external/webhook-controller.js +144 -0
  180. package/src/external/write-controller.js +283 -0
  181. package/src/gitea-backend.js +95 -0
  182. package/src/gitea-service.js +173 -0
  183. package/src/handoff.js +98 -0
  184. package/src/hooks-events.js +63 -0
  185. package/src/http-server.js +377 -0
  186. package/src/identity-policy.js +86 -0
  187. package/src/index.js +55 -0
  188. package/src/kubernetes-controller-async.js +511 -0
  189. package/src/kubernetes-controller.js +878 -0
  190. package/src/kubernetes-resource-gateway.js +48 -0
  191. package/src/operations.js +112 -0
  192. package/src/org-scoping.js +5 -0
  193. package/src/resource-model.js +221 -0
  194. package/src/runners-ci.js +48 -0
  195. package/src/runtime.js +196 -0
  196. package/src/snapshot-cache.js +157 -0
  197. package/src/web-ui.js +40 -0
  198. package/tests/agent-adapter-controller.test.js +361 -0
  199. package/tests/agent-approval-controller.test.js +173 -0
  200. package/tests/agent-context-bundles.test.js +278 -0
  201. package/tests/agent-dispatch-controller.test.js +315 -0
  202. package/tests/agent-gateway-config-controller.test.js +386 -0
  203. package/tests/agent-memory-controller.test.js +308 -0
  204. package/tests/agent-memory-import-snapshot.test.js +477 -0
  205. package/tests/agent-memory-query.test.js +404 -0
  206. package/tests/agent-memory-repository-source.test.js +514 -0
  207. package/tests/agent-mux-client.test.js +204 -0
  208. package/tests/agent-permission-review-v2.test.js +317 -0
  209. package/tests/agent-permission-review.test.js +209 -0
  210. package/tests/agent-project-controller.test.js +302 -0
  211. package/tests/agent-provider-config-controller.test.js +376 -0
  212. package/tests/agent-resources.test.js +228 -0
  213. package/tests/agent-secret-config-grant.test.js +231 -0
  214. package/tests/agent-session-transcript-controller.test.js +499 -0
  215. package/tests/agent-stack-controller.test.js +221 -0
  216. package/tests/agent-subagent-controller.test.js +201 -0
  217. package/tests/agent-transport-binding-controller.test.js +294 -0
  218. package/tests/agent-trigger-controller.test.js +211 -0
  219. package/tests/agent-trigger-routes.test.js +190 -0
  220. package/tests/agent-trigger-sources.test.js +245 -0
  221. package/tests/agent-workspace-controller.test.js +181 -0
  222. package/tests/agent-writeback.test.js +292 -0
  223. package/tests/approval-persistence.test.js +171 -0
  224. package/tests/async-controller.test.js +252 -0
  225. package/tests/audit-controller.test.js +227 -0
  226. package/tests/deployment.test.js +396 -0
  227. package/tests/e2e/lifecycle.test.js +117 -0
  228. package/tests/external-github-forge.test.js +560 -0
  229. package/tests/external-github-issues-cicd.test.js +520 -0
  230. package/tests/external-integration.test.js +470 -0
  231. package/tests/external-persistence.test.js +340 -0
  232. package/tests/external-provider-adapter.test.js +365 -0
  233. package/tests/external-resource-model.test.js +215 -0
  234. package/tests/external-webhook-sync.test.js +287 -0
  235. package/tests/external-write-conflict.test.js +353 -0
  236. package/tests/gitea-service.test.js +253 -0
  237. package/tests/health-check-real.test.js +165 -0
  238. package/tests/integration/full-flow.test.js +266 -0
  239. package/tests/krate.test.js +727 -0
  240. package/tests/memory-search-wiring.test.js +270 -0
  241. package/tests/org-scoping.test.js +687 -0
  242. package/tests/session-cookie-hmac.test.js +151 -0
  243. package/tests/snapshot-performance.test.js +247 -0
  244. package/tests/sse-events.test.js +107 -0
  245. package/tests/workspace-volumes.test.js +312 -0
  246. package/tests/writeback-persistence.test.js +207 -0
package/src/runtime.js ADDED
@@ -0,0 +1,196 @@
1
+ import { ControlPlane } from './control-plane.js';
2
+ import { GiteaGitService, GiteaRepositoryStore } from './data-plane.js';
3
+ import { WebhookBus } from './hooks-events.js';
4
+ import { createAdmissionPolicy, mapOidcIdentity } from './identity-policy.js';
5
+ import { createInviteResource, createTeamResource, createAuthProviderResources, mapLoginProfileToKrateIdentity } from './auth.js';
6
+ import { createResource, clone } from './resource-model.js';
7
+ import { RunnerScheduler } from './runners-ci.js';
8
+
9
+ const DEFAULT_ORG = 'default';
10
+ const DEFAULT_NAMESPACE = 'krate-org-default';
11
+
12
+ export function createDefaultKrateUsers() {
13
+ return {
14
+ platformEngineer: mapOidcIdentity({ subject: 'user:platform', email: 'platform@example.com', groups: ['krate:platform-engineers'] }),
15
+ developer: mapOidcIdentity({ subject: 'user:dev', email: 'dev@example.com', groups: ['krate:developers'] }),
16
+ repoAdmin: mapOidcIdentity({ subject: 'user:admin', email: 'admin@example.com', groups: ['krate:repo-admins'] })
17
+ };
18
+ }
19
+
20
+ export class KrateRuntime {
21
+ constructor({ organizationRef = DEFAULT_ORG, namespace = DEFAULT_NAMESPACE, users = createDefaultKrateUsers(), controlPlane, git, runners, webhooks } = {}) {
22
+ this.organizationRef = organizationRef;
23
+ this.namespace = namespace;
24
+ this.users = users;
25
+ this.controlPlane = controlPlane || new ControlPlane();
26
+ this.controlPlane.addAdmissionPolicy(createAdmissionPolicy({
27
+ name: 'pr-title-required',
28
+ mode: 'enforce',
29
+ match: ({ resource }) => resource.kind === 'PullRequest',
30
+ validate: ({ resource }) => Boolean(resource.spec.title && resource.spec.title.length >= 8),
31
+ message: 'PullRequest spec.title must be descriptive'
32
+ }));
33
+ this.git = git || new GiteaGitService({ controlPlane: this.controlPlane, stores: [new GiteaRepositoryStore({ name: 'gitea-primary', receivePackReady: true })] });
34
+ this.runners = runners || new RunnerScheduler({ controlPlane: this.controlPlane });
35
+ this.webhooks = webhooks || new WebhookBus({ controlPlane: this.controlPlane });
36
+ this.seeded = false;
37
+ }
38
+
39
+ static fromSnapshot(snapshot, options = {}) {
40
+ const runtime = new KrateRuntime(options);
41
+ runtime.importSnapshot(snapshot);
42
+ return runtime;
43
+ }
44
+
45
+ bootstrap({ repository = 'krate-demo', webhookUrl = 'https://hooks.example.test/krate' } = {}) {
46
+ if (this.seeded) return this.snapshot();
47
+ const { platformEngineer, repoAdmin } = this.users;
48
+ this.git.createRepository({ name: repository, namespace: this.namespace, organizationRef: this.organizationRef }, repoAdmin);
49
+ this.controlPlane.create(createResource('BranchProtection', { name: 'main-protection', namespace: this.namespace }, { organizationRef: this.organizationRef, refs: ['refs/heads/main'], requirePullRequest: true, requiredChecks: ['test'], requiredApprovals: 1 }), repoAdmin);
50
+ this.controlPlane.create(createResource('RefPolicy', { name: 'deny-internal-refs', namespace: this.namespace }, { organizationRef: this.organizationRef, deny: ['refs/internal/'] }), repoAdmin);
51
+ this.runners.createRunnerPool({ name: 'trusted-linux', namespace: this.namespace, organizationRef: this.organizationRef, warmReplicas: 1, maxReplicas: 4 }, platformEngineer);
52
+ this.webhooks.subscribe({ name: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, url: webhookUrl, events: ['pullrequest.created', 'pullrequest.merged'] }, repoAdmin);
53
+ this.controlPlane.create(createTeamResource({ name: 'maintainers', organizationRef: this.organizationRef, displayName: 'Maintainers', members: ['admin'], maintainers: ['admin'], repositoryGrants: [{ repository, permission: 'admin' }], namespace: this.namespace }), platformEngineer);
54
+ const identity = mapLoginProfileToKrateIdentity({ provider: 'sso', subject: 'user:admin', email: 'admin@example.com', displayName: 'Admin', username: 'admin', teams: ['maintainers'], admin: true, namespace: this.namespace, organizationRef: this.organizationRef });
55
+ this.controlPlane.create(identity.user, platformEngineer);
56
+ this.controlPlane.create(identity.mapping, platformEngineer);
57
+ this.controlPlane.create(createInviteResource({ email: 'new-user@example.com', role: 'member', teams: ['maintainers'], invitedBy: 'admin', namespace: this.namespace, organizationRef: this.organizationRef }), repoAdmin);
58
+ for (const provider of createAuthProviderResources(undefined, this.namespace, this.organizationRef)) this.controlPlane.create(provider, platformEngineer);
59
+ this.seeded = true;
60
+ return this.snapshot();
61
+ }
62
+
63
+ exportSnapshot() {
64
+ return {
65
+ apiVersion: 'krate.a5c.ai/v1alpha1',
66
+ kind: 'KrateRuntimeSnapshot',
67
+ namespace: this.namespace,
68
+ organizationRef: this.organizationRef,
69
+ seeded: this.seeded,
70
+ controlPlane: this.controlPlane.exportSnapshot(),
71
+ git: this.git.snapshot?.() || null
72
+ };
73
+ }
74
+
75
+ importSnapshot(snapshot) {
76
+ const controlPlaneSnapshot = snapshot?.controlPlane || snapshot;
77
+ this.controlPlane.importSnapshot(controlPlaneSnapshot);
78
+ if (snapshot?.git) this.git.importSnapshot(snapshot.git);
79
+ this.seeded = Boolean(snapshot?.seeded || this.controlPlane.list('Repository', { namespace: this.namespace }).items.length);
80
+ return this.snapshot();
81
+ }
82
+
83
+ createRepository({ name, visibility = 'private' }, user = this.users.repoAdmin) {
84
+ return this.git.createRepository({ name, namespace: this.namespace, organizationRef: this.organizationRef, visibility }, user);
85
+ }
86
+
87
+ createPullRequest({ repository, sourceRef = 'refs/heads/feature', targetRef = 'refs/heads/main', title, actor = this.users.developer, fork = false }) {
88
+ this.#requireRepository(repository);
89
+ const index = this.controlPlane.list('PullRequest', { namespace: this.namespace }).items.length + 1;
90
+ const name = `pr-${index}`;
91
+ const pullRequest = this.controlPlane.create(createResource('PullRequest', { name, namespace: this.namespace, labels: { repository } }, {
92
+ organizationRef: this.organizationRef,
93
+ repository,
94
+ sourceRef,
95
+ targetRef,
96
+ title,
97
+ author: actor.name,
98
+ checks: [],
99
+ requiredApprovals: this.#requiredApprovals(targetRef)
100
+ }, { phase: 'Open', approvals: 0, mergeable: false }), actor);
101
+ const pipelineRun = this.runners.startPipeline({ name: `pipeline-${name}`, namespace: this.namespace, organizationRef: this.organizationRef, repository, ref: `refs/pull/${index}/head`, actor, fork, steps: ['checkout', 'test'] }, actor);
102
+ this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.created', payload: { pullRequest: name, repository, title } }, this.users.repoAdmin);
103
+ return { pullRequest, pipeline: pipelineRun.pipeline, jobs: pipelineRun.jobs };
104
+ }
105
+
106
+ completePipeline({ pipeline, phase = 'Succeeded', failedStep = null }, user = this.users.developer) {
107
+ const existing = this.controlPlane.get('Pipeline', this.namespace, pipeline);
108
+ if (!existing) throw new Error(`Pipeline ${pipeline} not found`);
109
+ const jobs = this.controlPlane.list('Job', { namespace: this.namespace, labels: { pipeline } }).items.map((job) => {
110
+ const jobPhase = failedStep && job.spec.step === failedStep ? 'Failed' : phase;
111
+ return this.controlPlane.patchStatus('Job', this.namespace, job.metadata.name, { phase: jobPhase, finishedAt: new Date().toISOString() }, user);
112
+ });
113
+ const pipelinePhase = jobs.some((job) => job.status.phase === 'Failed') ? 'Failed' : phase;
114
+ const updatedPipeline = this.controlPlane.patchStatus('Pipeline', this.namespace, pipeline, { phase: pipelinePhase, currentStep: null, finishedAt: new Date().toISOString() }, user);
115
+ this.#refreshPullRequestMergeability(existing.spec.repository);
116
+ return { pipeline: updatedPipeline, jobs };
117
+ }
118
+
119
+ addReview({ pullRequest, decision = 'approved', body = 'Looks good', reviewer = this.users.repoAdmin }) {
120
+ const pr = this.#requirePullRequest(pullRequest);
121
+ const reviewCount = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest } }).items.length + 1;
122
+ const review = this.controlPlane.create(createResource('Review', { name: `${pullRequest}-review-${reviewCount}`, namespace: this.namespace, labels: { pullRequest, decision } }, {
123
+ organizationRef: this.organizationRef,
124
+ pullRequest,
125
+ repository: pr.spec.repository,
126
+ reviewer: reviewer.name,
127
+ decision,
128
+ body
129
+ }, { phase: decision === 'approved' ? 'Approved' : 'ChangesRequested' }), reviewer);
130
+ this.#refreshPullRequestMergeability(pr.spec.repository);
131
+ return review;
132
+ }
133
+
134
+ mergePullRequest({ pullRequest, actor = this.users.repoAdmin }) {
135
+ const pr = this.#requirePullRequest(pullRequest);
136
+ const refreshed = this.#refreshPullRequestMergeability(pr.spec.repository).find((candidate) => candidate.metadata.name === pullRequest) || pr;
137
+ if (!refreshed.status.mergeable) throw new Error(`PullRequest ${pullRequest} is not mergeable`);
138
+ const gitEvent = this.git.receivePack({ repository: refreshed.spec.repository, namespace: this.namespace, ref: refreshed.spec.targetRef, newRev: this.#syntheticRevision(pullRequest), actor });
139
+ const merged = this.controlPlane.patchStatus('PullRequest', this.namespace, pullRequest, { phase: 'Merged', mergeable: false, mergedAt: new Date().toISOString(), mergeCommit: gitEvent.newRev }, actor);
140
+ const delivery = this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.merged', payload: { pullRequest, repository: refreshed.spec.repository, mergeCommit: gitEvent.newRev } }, this.users.repoAdmin);
141
+ return { pullRequest: merged, gitEvent, delivery };
142
+ }
143
+
144
+ snapshot() {
145
+ const kinds = ['Organization', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'RunnerPool', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookSubscription', 'WebhookDelivery'];
146
+ const resources = Object.fromEntries(kinds.map((kind) => [kind, this.controlPlane.list(kind, { namespace: this.namespace }).items]));
147
+ return {
148
+ namespace: this.namespace,
149
+ export: this.exportSnapshot(),
150
+ storage: this.controlPlane.storageReport(),
151
+ resources,
152
+ events: clone(this.controlPlane.events),
153
+ auditLog: clone(this.controlPlane.auditLog)
154
+ };
155
+ }
156
+
157
+ #requireRepository(repository) {
158
+ const existing = this.controlPlane.get('Repository', this.namespace, repository);
159
+ if (!existing) throw new Error(`Repository ${repository} not found`);
160
+ return existing;
161
+ }
162
+
163
+ #requirePullRequest(pullRequest) {
164
+ const existing = this.controlPlane.get('PullRequest', this.namespace, pullRequest);
165
+ if (!existing) throw new Error(`PullRequest ${pullRequest} not found`);
166
+ return existing;
167
+ }
168
+
169
+ #requiredApprovals(targetRef) {
170
+ const branchProtection = this.controlPlane.list('BranchProtection', { namespace: this.namespace }).items.find((policy) => (policy.spec.refs || []).includes(targetRef));
171
+ return branchProtection?.spec.requiredApprovals || 0;
172
+ }
173
+
174
+ #refreshPullRequestMergeability(repository) {
175
+ const pullRequests = this.controlPlane.list('PullRequest', { namespace: this.namespace, labels: { repository } }).items;
176
+ return pullRequests.map((pr) => {
177
+ if (pr.status.phase !== 'Open') return pr;
178
+ const reviews = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest: pr.metadata.name } }).items;
179
+ const approvals = reviews.filter((review) => review.spec.decision === 'approved').length;
180
+ const pipelines = this.controlPlane.list('Pipeline', { namespace: this.namespace, labels: { repository } }).items.filter((pipeline) => pipeline.metadata.name === `pipeline-${pr.metadata.name}`);
181
+ const checksPassed = pipelines.length > 0 && pipelines.every((pipeline) => pipeline.status.phase === 'Succeeded');
182
+ return this.controlPlane.patchStatus('PullRequest', this.namespace, pr.metadata.name, { approvals, checksPassed, mergeable: approvals >= (pr.spec.requiredApprovals || 0) && checksPassed }, this.users.repoAdmin);
183
+ });
184
+ }
185
+
186
+ #syntheticRevision(seed) {
187
+ const source = Buffer.from(`${seed}:${Date.now()}`).toString('hex');
188
+ return source.padEnd(40, '0').slice(0, 40);
189
+ }
190
+ }
191
+
192
+ export function createKrateRuntime(options = {}) {
193
+ const runtime = new KrateRuntime(options);
194
+ if (options.bootstrap !== false) runtime.bootstrap(options.bootstrapOptions);
195
+ return runtime;
196
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Snapshot cache with:
3
+ * - Per-org storage (multiple orgs cached simultaneously)
4
+ * - stale-while-revalidate: return stale data immediately, refresh in background
5
+ * - Configurable TTL via KRATE_SNAPSHOT_CACHE_TTL_MS
6
+ */
7
+
8
+ export const CACHE_TTL_MS = Number(process.env.KRATE_SNAPSHOT_CACHE_TTL_MS || 30_000);
9
+
10
+ /**
11
+ * Per-org cache map. Key = org string (or '' for no-org).
12
+ * Value = { data, timestamp, revalidating: boolean }
13
+ */
14
+ const orgCacheMap = new Map();
15
+
16
+ // Legacy single-org cache kept for backward compatibility with controller-client.js
17
+ let snapshotCache = { data: null, timestamp: 0, org: null };
18
+
19
+ export function getSnapshotCache() {
20
+ return snapshotCache;
21
+ }
22
+
23
+ export function setSnapshotCache(data, org) {
24
+ snapshotCache = { data, timestamp: Date.now(), org };
25
+ // Also update per-org map
26
+ const key = org ?? '';
27
+ const entry = orgCacheMap.get(key) || {};
28
+ orgCacheMap.set(key, { ...entry, data, timestamp: Date.now(), revalidating: false });
29
+ }
30
+
31
+ export function clearSnapshotCache() {
32
+ snapshotCache = { data: null, timestamp: 0, org: null };
33
+ orgCacheMap.clear();
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Per-org cache API
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Get the cached snapshot for a specific org.
42
+ * Returns null when nothing is cached or when the TTL has expired.
43
+ *
44
+ * @param {string|null} org
45
+ * @returns {{ data: object, timestamp: number, revalidating: boolean } | null}
46
+ */
47
+ export function getOrgCache(org) {
48
+ const key = org ?? '';
49
+ const entry = orgCacheMap.get(key);
50
+ if (!entry || !entry.data) return null;
51
+ return entry;
52
+ }
53
+
54
+ /**
55
+ * Store a snapshot for a specific org.
56
+ *
57
+ * @param {object} data
58
+ * @param {string|null} org
59
+ */
60
+ export function setOrgCache(data, org) {
61
+ const key = org ?? '';
62
+ orgCacheMap.set(key, { data, timestamp: Date.now(), revalidating: false });
63
+ // Keep legacy cache in sync for the most recently written org
64
+ snapshotCache = { data, timestamp: Date.now(), org };
65
+ }
66
+
67
+ /**
68
+ * Clear the cache entry for a specific org only.
69
+ *
70
+ * @param {string|null} org
71
+ */
72
+ export function clearOrgCache(org) {
73
+ orgCacheMap.delete(org ?? '');
74
+ if (snapshotCache.org === org) {
75
+ snapshotCache = { data: null, timestamp: 0, org: null };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Return all orgs currently in the cache (for introspection / debugging).
81
+ *
82
+ * @returns {string[]}
83
+ */
84
+ export function cachedOrgs() {
85
+ return [...orgCacheMap.keys()];
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // stale-while-revalidate helpers
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Check whether the cache entry for the given org is fresh.
94
+ *
95
+ * @param {string|null} org
96
+ * @param {number} [ttlMs] - defaults to CACHE_TTL_MS
97
+ * @returns {boolean}
98
+ */
99
+ export function isCacheFresh(org, ttlMs = CACHE_TTL_MS) {
100
+ const entry = getOrgCache(org);
101
+ if (!entry) return false;
102
+ return (Date.now() - entry.timestamp) < ttlMs;
103
+ }
104
+
105
+ /**
106
+ * stale-while-revalidate: return stale data immediately if available, then
107
+ * trigger revalidateFn() in the background to refresh the cache entry.
108
+ *
109
+ * @param {string|null} org - Organization key for the cache
110
+ * @param {Function} revalidateFn - Async function that returns fresh data
111
+ * @param {object} [swrOptions]
112
+ * @param {number} [swrOptions.ttlMs] - Fresh TTL in ms (default: CACHE_TTL_MS)
113
+ * @param {number} [swrOptions.staleMs] - Max staleness before we block on revalidate (default: 5 × ttlMs)
114
+ * @returns {Promise<object>} Either the stale cached value or the freshly fetched one
115
+ */
116
+ export async function staleWhileRevalidate(org, revalidateFn, swrOptions = {}) {
117
+ const ttlMs = swrOptions.ttlMs ?? CACHE_TTL_MS;
118
+ const staleMs = swrOptions.staleMs ?? ttlMs * 5;
119
+ const key = org ?? '';
120
+ const entry = orgCacheMap.get(key);
121
+ const now = Date.now();
122
+
123
+ const isFresh = entry && entry.data && (now - entry.timestamp) < ttlMs;
124
+ const isStale = entry && entry.data && (now - entry.timestamp) < staleMs;
125
+
126
+ if (isFresh) {
127
+ // Cache is fresh: return immediately
128
+ return entry.data;
129
+ }
130
+
131
+ if (isStale && !entry.revalidating) {
132
+ // Stale but still usable: return immediately and revalidate in background
133
+ orgCacheMap.set(key, { ...entry, revalidating: true });
134
+ Promise.resolve().then(async () => {
135
+ try {
136
+ const fresh = await revalidateFn();
137
+ setOrgCache(fresh, org);
138
+ } catch {
139
+ // On background refresh error, clear the revalidating flag so a future
140
+ // request can try again
141
+ const current = orgCacheMap.get(key);
142
+ if (current) orgCacheMap.set(key, { ...current, revalidating: false });
143
+ }
144
+ });
145
+ return entry.data;
146
+ }
147
+
148
+ if (isStale && entry.revalidating) {
149
+ // Another caller is already refreshing: return stale data now
150
+ return entry.data;
151
+ }
152
+
153
+ // No usable cache: block on the revalidation
154
+ const fresh = await revalidateFn();
155
+ setOrgCache(fresh, org);
156
+ return fresh;
157
+ }
package/src/web-ui.js ADDED
@@ -0,0 +1,40 @@
1
+ import { createResource } from './resource-model.js';
2
+ import { toResourceYaml } from './identity-policy.js';
3
+
4
+ export function createPullRequestReviewModel({ pullRequest, changedFiles = [], pipelineRuns = [] }) {
5
+ return { layout: 'three-pane-review', panes: ['file-tree', 'diff-and-comments', 'conversation-ci'], keyboardShortcuts: ['j/k file navigation', 'n/p comment navigation', 'a add suggestion', 'm merge'], pullRequest, changedFiles, pipelineRuns, yaml: toResourceYaml(pullRequest) };
6
+ }
7
+
8
+ export function createFailingRunModel({ pipeline, jobs }) {
9
+ const failedJobs = jobs.filter((job) => job.status.phase === 'Failed');
10
+ return { layout: 'live-run-debugger', stream: 'sse', pipeline, failedJobs, actions: ['copy failure', 'find similar runs', 'rerun from step'], similarRunSelector: failedJobs.map((job) => job.metadata.labels).filter(Boolean) };
11
+ }
12
+
13
+ export function createRunnerPoolEditor(pool) {
14
+ return { layout: 'split-form-yaml', fields: ['image', 'resources', 'nodeSelector', 'warmReplicas', 'maxReplicas', 'trustTier', 'cache'], resource: pool, yaml: toResourceYaml(pool), saveModes: ['apply', 'copy kubectl', 'open platform-config PR'] };
15
+ }
16
+
17
+ export function createWebhookInspector({ subscription, deliveries }) {
18
+ return { layout: 'webhook-inspector', subscription, deliveries, columns: ['phase', 'latency', 'attempts', 'response', 'signature'], actions: ['send test delivery', 'inspect headers/body/response', 'replay'] };
19
+ }
20
+
21
+ export function createPolicyRolloutModel(policy) {
22
+ return { layout: 'policy-authoring', modes: ['template', 'CEL/raw'], rollout: ['preview', 'audit', 'enforce'], policy, yaml: toResourceYaml(policy) };
23
+ }
24
+
25
+ export function createTriageView({ name, namespace = 'krate-org-default', organizationRef = 'default', selector }) {
26
+ return createResource('View', { name, namespace, labels: { purpose: 'triage' } }, { organizationRef, selector, columns: ['kind', 'repository', 'priority', 'assignee', 'status'], shareable: true }, { saved: true });
27
+ }
28
+
29
+ export function createDashboard({ repositories, pullRequests, pipelines, runnerPools, webhookDeliveries }) {
30
+ return {
31
+ product: 'Krate',
32
+ principles: ['Kubernetes is the backend', 'CRDs are contracts', 'GitOps transparency'],
33
+ repositories,
34
+ pullRequests,
35
+ pipelines,
36
+ runnerPools,
37
+ webhookDeliveries,
38
+ excellentFlows: ['Open and review a PR', 'Debug a failing run', 'Configure a runner pool', 'Add a webhook and verify it works', 'Write a PR policy with audit-to-enforce rollout', 'Cross-repo triage with saved filters']
39
+ };
40
+ }