@cat-factory/app 0.199.0 → 0.200.0

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 (32) hide show
  1. package/app/components/foundational/FoundationalContractSummary.vue +36 -0
  2. package/app/components/foundational/FoundationalServiceCatalogList.vue +170 -0
  3. package/app/components/foundational/FoundationalServiceManager.vue +111 -0
  4. package/app/components/foundational/FoundationalServicePanel.vue +37 -0
  5. package/app/components/foundational/FoundationalServiceRegistry.vue +339 -0
  6. package/app/components/foundational/FoundationalServiceSources.vue +398 -0
  7. package/app/components/foundational/FoundationalSuppressions.vue +75 -0
  8. package/app/components/layout/AccountFoundationalSettings.vue +25 -0
  9. package/app/components/settings/AccountSettingsPanel.vue +17 -1
  10. package/app/composables/api/foundationalServices.ts +131 -0
  11. package/app/composables/useApi.ts +2 -0
  12. package/app/composables/useNavContributions.ts +1 -0
  13. package/app/composables/usePipelineErrorToast.ts +4 -0
  14. package/app/modular/nav-contributions.spec.ts +6 -0
  15. package/app/modular/nav-contributions.ts +26 -0
  16. package/app/pages/index.vue +4 -0
  17. package/app/stores/foundationalServices.spec.ts +121 -0
  18. package/app/stores/foundationalServices.ts +276 -0
  19. package/app/stores/ui/modals.ts +12 -0
  20. package/app/types/domain.ts +1 -0
  21. package/app/types/foundationalServices.ts +32 -0
  22. package/i18n/locales/de.json +144 -6
  23. package/i18n/locales/en.json +144 -6
  24. package/i18n/locales/es.json +144 -6
  25. package/i18n/locales/fr.json +144 -6
  26. package/i18n/locales/he.json +144 -6
  27. package/i18n/locales/it.json +144 -6
  28. package/i18n/locales/ja.json +144 -6
  29. package/i18n/locales/pl.json +144 -6
  30. package/i18n/locales/tr.json +144 -6
  31. package/i18n/locales/uk.json +144 -6
  32. package/package.json +2 -2
@@ -49,6 +49,7 @@ export function useNavContributions() {
49
49
  kaizen: () => ui.openKaizen(),
50
50
  infrastructure: () => ui.openInfrastructure(),
51
51
  fragmentLibrary: () => ui.openFragmentLibrary(),
52
+ foundationalServices: () => ui.openFoundationalServices(),
52
53
  mergeThresholds: () => ui.openWorkspaceSettings('merge'),
53
54
  workspaceSettings: () => ui.openWorkspaceSettings(),
54
55
  modelConfiguration: () => ui.openModelConfig(),
@@ -224,6 +224,10 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
224
224
  titleKey: 'errors.conflict.title.foundational_service_exists',
225
225
  descriptionKey: 'errors.conflict.description.foundational_service_exists',
226
226
  },
227
+ foundational_service_not_inherited: {
228
+ titleKey: 'errors.conflict.title.foundational_service_not_inherited',
229
+ descriptionKey: 'errors.conflict.description.foundational_service_not_inherited',
230
+ },
227
231
  pipeline_schedule_intake_unconfigured: {
228
232
  titleKey: 'errors.conflict.title.pipeline_schedule_intake_unconfigured',
229
233
  descriptionKey: 'errors.conflict.description.pipeline_schedule_intake_unconfigured',
@@ -124,6 +124,7 @@ describe('navSlotFilter', () => {
124
124
  expect(kept).not.toContain('bootstrap-repo')
125
125
  expect(kept).not.toContain('operator-dashboard')
126
126
  expect(kept).not.toContain('reports')
127
+ expect(kept).not.toContain('foundational-services')
127
128
  })
128
129
 
129
130
  it('states, per advanced item, whether basic mode still reaches its capability', () => {
@@ -166,6 +167,10 @@ describe('navSlotFilter', () => {
166
167
  kind: 'out-of-tier',
167
168
  why: 'deployment-wide spend/activity rollup - an operator job, not a delivery one',
168
169
  },
170
+ 'foundational-services': {
171
+ kind: 'out-of-tier',
172
+ why: 'org-wide platform inventory, set up once - a board delivers fine with none',
173
+ },
169
174
  }
170
175
  const advanced = NAV_CONTRIBUTIONS.filter((i) => i.advanced).map((i) => i.id)
171
176
  expect(advanced.sort()).toEqual(Object.keys(REASON).sort())
@@ -344,6 +349,7 @@ describe('nav grouping helpers', () => {
344
349
  'keyboard-shortcuts',
345
350
  'ui-mode',
346
351
  'tutorial',
352
+ 'foundational-services',
347
353
  ])
348
354
  })
349
355
 
@@ -168,6 +168,7 @@ export const NAV_ACTIONS = [
168
168
  'kaizen',
169
169
  'infrastructure',
170
170
  'fragmentLibrary',
171
+ 'foundationalServices',
171
172
  'mergeThresholds',
172
173
  'workspaceSettings',
173
174
  'modelConfiguration',
@@ -365,6 +366,31 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
365
366
  keywordsKey: 'layout.commandBar.keywords.fragments',
366
367
  },
367
368
  },
369
+ {
370
+ // Registering the shared capabilities the ORGANISATION already runs, so a design consumes
371
+ // them instead of proposing a rebuild. Advanced: the everyday delivery loop (plan a task,
372
+ // run it, review and merge it) never touches this — it is org-wide platform configuration,
373
+ // set up once by whoever knows the estate, and a board can deliver its whole backlog with an
374
+ // empty catalog. Beside the fragment library in `workspaceContext`, because both answer
375
+ // "what standing context does an agent get?".
376
+ id: 'foundational-services',
377
+ labelKey: 'nav.foundationalServices',
378
+ icon: 'i-lucide-boxes',
379
+ surfaces: S('sidebar', 'command'),
380
+ advanced: true,
381
+ gate: (g) => g.canManageSettings,
382
+ action: 'foundationalServices',
383
+ testId: 'nav-foundational-services',
384
+ sidebar: { group: 'workspaceContext', order: 20 },
385
+ command: {
386
+ // Appended after the pre-existing workspace commands rather than interleaved beside the
387
+ // fragment library, so the palette order people already know is unchanged.
388
+ group: 'workspace',
389
+ order: 110,
390
+ labelKey: 'layout.commandBar.cmd.foundationalServices',
391
+ keywordsKey: 'layout.commandBar.keywords.foundationalServices',
392
+ },
393
+ },
368
394
  {
369
395
  id: 'merge-thresholds',
370
396
  labelKey: 'layout.commandBar.cmd.mergeThresholds',
@@ -73,6 +73,9 @@ const SlackPanel = defineAsyncComponent(() => import('~/components/slack/SlackPa
73
73
  const FragmentLibraryPanel = defineAsyncComponent(
74
74
  () => import('~/components/fragments/FragmentLibraryPanel.vue'),
75
75
  )
76
+ const FoundationalServicePanel = defineAsyncComponent(
77
+ () => import('~/components/foundational/FoundationalServicePanel.vue'),
78
+ )
76
79
  // Startup advisory for invalid / outdated pipelines — only mounted while open (auto-opened
77
80
  // at most once per session by the watcher below), so it stays out of the initial bundle.
78
81
  const PipelineHealthModal = defineAsyncComponent(
@@ -487,6 +490,7 @@ watch(
487
490
  <GitHubPanel v-if="ui.githubOpen" />
488
491
  <SlackPanel v-if="ui.slackOpen" />
489
492
  <FragmentLibraryPanel v-if="ui.fragmentLibraryOpen" />
493
+ <FoundationalServicePanel v-if="ui.foundationalServicesOpen" />
490
494
  <PipelineHealthModal v-if="ui.pipelineHealthOpen" />
491
495
  <RiskPolicyHealthModal v-if="ui.riskPolicyHealthOpen" />
492
496
  <ModelPresetHealthModal v-if="ui.modelPresetHealthOpen" />
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { useFoundationalServicesStore } from '~/stores/foundationalServices'
3
+ import { useWorkspaceStore } from '~/stores/workspace'
4
+
5
+ // The two store rules that the surface's correctness rests on, and that neither the backend
6
+ // tests nor a component test would catch:
7
+ //
8
+ // - opening the catalog must transfer NO contract document. The whole two-table split exists so
9
+ // a catalog read costs identity + operation names, and a store that eagerly hydrated bodies
10
+ // would quietly undo it on the one surface a human uses to check what an agent sees.
11
+ // - a suppress/restore must refresh the OPT-OUT list as well as the catalog. The two are
12
+ // complements — an entry leaves one exactly as it enters the other — so refreshing only the
13
+ // catalog leaves the way BACK stale, which is the failure the pair exists to prevent.
14
+
15
+ const SERVICE = {
16
+ id: 'file-storage',
17
+ ownerKind: 'account' as const,
18
+ name: 'File Storage',
19
+ summary: 'Stores uploads.',
20
+ description: '',
21
+ capabilities: [],
22
+ contracts: [
23
+ {
24
+ contractId: 'openapi',
25
+ format: 'openapi',
26
+ title: 'HTTP API',
27
+ size: 42,
28
+ path: null,
29
+ operations: ['GET /files'],
30
+ omittedOperations: 0,
31
+ },
32
+ ],
33
+ sourceId: null,
34
+ sourcePath: null,
35
+ pinnedCommit: null,
36
+ createdAt: 1,
37
+ updatedAt: 1,
38
+ }
39
+
40
+ function api(over: Record<string, unknown> = {}) {
41
+ return {
42
+ listFoundationalServices: vi.fn(() => Promise.resolve([])),
43
+ getResolvedFoundationalServices: vi.fn(() =>
44
+ Promise.resolve([{ ...SERVICE, tier: 'account' }]),
45
+ ),
46
+ listFoundationalServiceSuppressions: vi.fn(() => Promise.resolve([])),
47
+ listFoundationalSources: vi.fn(() => Promise.resolve([])),
48
+ getFoundationalServiceContracts: vi.fn(() =>
49
+ Promise.resolve([{ ...SERVICE.contracts[0], body: 'openapi: 3.0.3' }]),
50
+ ),
51
+ suppressFoundationalService: vi.fn(() => Promise.resolve(undefined)),
52
+ restoreFoundationalService: vi.fn(() => Promise.resolve(undefined)),
53
+ ...over,
54
+ }
55
+ }
56
+
57
+ describe('foundational-services store', () => {
58
+ beforeEach(() => {
59
+ useWorkspaceStore().workspaceId = 'ws1'
60
+ })
61
+
62
+ it('probes the catalog without fetching a single contract document', async () => {
63
+ const client = api()
64
+ vi.stubGlobal('useApi', () => client)
65
+ const store = useFoundationalServicesStore()
66
+ await store.probe()
67
+
68
+ expect(store.resolved).toHaveLength(1)
69
+ // The manifest rode the catalog read — the body did not.
70
+ expect(store.resolved[0]?.contracts[0]?.operations).toEqual(['GET /files'])
71
+ expect(client.getFoundationalServiceContracts).not.toHaveBeenCalled()
72
+ expect(store.contractBodies).toEqual({})
73
+ })
74
+
75
+ it('fetches a document only on demand, then serves it from the session cache', async () => {
76
+ const client = api()
77
+ vi.stubGlobal('useApi', () => client)
78
+ const store = useFoundationalServicesStore()
79
+ await store.probe()
80
+
81
+ await store.contractsFor('file-storage')
82
+ await store.contractsFor('file-storage')
83
+ expect(client.getFoundationalServiceContracts).toHaveBeenCalledTimes(1)
84
+ expect(store.contractBodies['file-storage']?.[0]?.body).toBe('openapi: 3.0.3')
85
+ })
86
+
87
+ it('resets the repo-source flag too when a re-probe finds the catalog gone', async () => {
88
+ // `sourcesAvailable` gates an affordance rather than content, so a probe that leaves it at a
89
+ // previous `true` would offer repo-source linking against an owner whose catalog is now
90
+ // unreachable. Every view the probe owns has to come back down together.
91
+ const client = api()
92
+ vi.stubGlobal('useApi', () => client)
93
+ const store = useFoundationalServicesStore()
94
+ await store.probe()
95
+ expect(store.sourcesAvailable).toBe(true)
96
+
97
+ client.listFoundationalServices.mockRejectedValueOnce(new Error('503'))
98
+ useWorkspaceStore().workspaceId = 'ws2'
99
+ await store.probe()
100
+
101
+ expect(store.available).toBe(false)
102
+ expect(store.sourcesAvailable).toBe(false)
103
+ expect(store.sources).toEqual([])
104
+ })
105
+
106
+ it('refreshes the opt-out list alongside the catalog on suppress and restore', async () => {
107
+ const client = api()
108
+ vi.stubGlobal('useApi', () => client)
109
+ const store = useFoundationalServicesStore()
110
+ await store.probe()
111
+ const afterProbe = client.listFoundationalServiceSuppressions.mock.calls.length
112
+
113
+ await store.suppress('file-storage')
114
+ await store.restore('file-storage')
115
+
116
+ // Once per write: a suppression that only refreshed the catalog would leave the restore
117
+ // control missing for the very service just hidden.
118
+ expect(client.listFoundationalServiceSuppressions.mock.calls.length).toBe(afterProbe + 2)
119
+ expect(client.getResolvedFoundationalServices.mock.calls.length).toBe(afterProbe + 2)
120
+ })
121
+ })
@@ -0,0 +1,276 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ ApiContractDocument,
5
+ CreateFoundationalServiceInput,
6
+ FoundationalService,
7
+ FoundationalServiceOwnerKind,
8
+ FoundationalServiceSource,
9
+ FoundationalServiceSuppression,
10
+ FoundationalServiceSyncResult,
11
+ LinkFoundationalServiceSourceInput,
12
+ ResolvedFoundationalService,
13
+ UpdateFoundationalServiceInput,
14
+ } from '~/types/domain'
15
+ import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
16
+ import { useWorkspaceStore } from '~/stores/workspace'
17
+
18
+ /**
19
+ * The foundational-services catalog for one owner — a board (`workspace`) or an `account`
20
+ * (backend/docs/adr/0031-foundational-services.md). Holds that owner's own (raw) tier of registered
21
+ * services, its linked repo sources, and — at the **workspace** tier only — the merged catalog an
22
+ * Architect actually sees (account ⊕ workspace, workspace winning).
23
+ *
24
+ * Three things about this store follow the feature's design rather than the fragment library's:
25
+ *
26
+ * - **Contract documents are never held here.** The catalog reads carry a contract MANIFEST
27
+ * (id, format, size, operation names) and no bodies, because that is the whole reason a design
28
+ * prompt scales with the number of an org's services rather than the size of its specs. A body
29
+ * is fetched on demand, per service, by {@link contractsFor}, and cached only for the session.
30
+ * - **`suppress` / `restore` are workspace-only**, and they are not "delete". Suppression opts a
31
+ * board out of an INHERITED account service and destroys nothing; deleting removes the board's
32
+ * own registration and its uploaded documents.
33
+ * - **`sourcesAvailable` is the finer gate**, exactly as in the skill library: the catalog works
34
+ * without the GitHub integration (contracts can be uploaded directly), while the repo-source
35
+ * routes 503 without it.
36
+ */
37
+ function foundationalServicesSetup(
38
+ kind: FoundationalServiceOwnerKind,
39
+ resolveOwnerId: () => string | null,
40
+ ) {
41
+ const api = useApi()
42
+
43
+ /** The merged/resolved catalog only exists at the workspace tier. */
44
+ const hasResolved = kind === 'workspace'
45
+
46
+ /** null = not probed yet; true/false = the catalog is on/off for this deployment. */
47
+ const available = ref<boolean | null>(null)
48
+ /** false when the GitHub integration is off: the catalog works, repo sources do not. */
49
+ const sourcesAvailable = ref(true)
50
+ /** This owner's own registered services (its tier, raw). */
51
+ const services = ref<FoundationalService[]>([])
52
+ /** The merged catalog an Architect sees (workspace tier only; empty otherwise). */
53
+ const resolved = ref<ResolvedFoundationalService[]>([])
54
+ /**
55
+ * What this board is opted OUT of (workspace tier only; empty otherwise). Its own read because
56
+ * a suppressed id is by construction absent from {@link resolved} — without it, suppression
57
+ * would be a one-way door.
58
+ */
59
+ const suppressions = ref<FoundationalServiceSuppression[]>([])
60
+ /** Linked repo sources for this owner. */
61
+ const sources = ref<FoundationalServiceSource[]>([])
62
+ /** Per-source "changes available" flag from the last status check. */
63
+ const sourceChanges = ref<Record<string, boolean>>({})
64
+ /**
65
+ * Session cache of fetched contract DOCUMENTS, keyed by service id. Populated only by an
66
+ * explicit {@link contractsFor}, so opening the surface never transfers a document body — the
67
+ * property the whole two-table split exists to guarantee.
68
+ */
69
+ const contractBodies = ref<Record<string, ApiContractDocument[]>>({})
70
+
71
+ /** How many entries of the merged catalog this board inherits rather than owns. */
72
+ const inheritedCount = computed(
73
+ () => resolved.value.filter((entry) => entry.tier === 'account').length,
74
+ )
75
+
76
+ function requireOwnerId(): string {
77
+ const id = resolveOwnerId()
78
+ if (!id) throw new Error('No foundational-services owner')
79
+ return id
80
+ }
81
+
82
+ function requireWorkspaceId(): string {
83
+ if (!hasResolved) throw new Error('Foundational-service inheritance is workspace-scoped')
84
+ return requireOwnerId()
85
+ }
86
+
87
+ /** Probe the feature + load this owner's tier, sources and (ws) the merged catalog. */
88
+ async function runProbe() {
89
+ const id = resolveOwnerId()
90
+ if (!id) return
91
+ try {
92
+ const [tier, merged, opted] = await Promise.all([
93
+ api.listFoundationalServices(kind, id),
94
+ hasResolved
95
+ ? api.getResolvedFoundationalServices(id)
96
+ : Promise.resolve([] as ResolvedFoundationalService[]),
97
+ hasResolved
98
+ ? api.listFoundationalServiceSuppressions(id)
99
+ : Promise.resolve([] as FoundationalServiceSuppression[]),
100
+ ])
101
+ services.value = tier
102
+ resolved.value = merged
103
+ suppressions.value = opted
104
+ available.value = true
105
+ } catch {
106
+ // Reset EVERY view, `sourcesAvailable` included. Leaving it at its previous value would
107
+ // let a re-probe of an owner whose catalog is now unreachable keep claiming the repo-source
108
+ // half is wired, which is the one flag here that gates an affordance rather than content.
109
+ available.value = false
110
+ services.value = []
111
+ resolved.value = []
112
+ suppressions.value = []
113
+ sources.value = []
114
+ sourceChanges.value = {}
115
+ sourcesAvailable.value = false
116
+ return
117
+ }
118
+ // Repo sources need the GitHub integration; a 503 here hides only the linking UI — the
119
+ // catalog read above already succeeded, so the feature itself is on.
120
+ try {
121
+ sources.value = await api.listFoundationalSources(kind, id)
122
+ sourcesAvailable.value = true
123
+ } catch {
124
+ sources.value = []
125
+ sourcesAvailable.value = false
126
+ }
127
+ }
128
+ // Single-flight the probe keyed on the owner id, so a panel-open fan-out loads once per owner.
129
+ const { probe, ensureProbed } = useSingleFlightProbe(runProbe, () => resolveOwnerId())
130
+
131
+ async function reloadTier() {
132
+ services.value = await api.listFoundationalServices(kind, requireOwnerId())
133
+ }
134
+
135
+ async function refreshResolved() {
136
+ if (!hasResolved) return
137
+ const id = requireOwnerId()
138
+ // Both in one pass: a write that changes the merge routinely changes the opt-out list too
139
+ // (suppressing removes an entry from one and adds it to the other), and refreshing only the
140
+ // catalog would leave the way BACK stale — the exact state the pair exists to avoid.
141
+ const [merged, opted] = await Promise.all([
142
+ api.getResolvedFoundationalServices(id),
143
+ api.listFoundationalServiceSuppressions(id),
144
+ ])
145
+ resolved.value = merged
146
+ suppressions.value = opted
147
+ }
148
+
149
+ /** Every write invalidates both views: a tier edit changes what the merge resolves to. */
150
+ async function reload() {
151
+ await Promise.all([reloadTier(), refreshResolved()])
152
+ }
153
+
154
+ async function create(input: CreateFoundationalServiceInput) {
155
+ await api.createFoundationalService(kind, requireOwnerId(), input)
156
+ await reload()
157
+ }
158
+
159
+ async function update(serviceId: string, patch: UpdateFoundationalServiceInput) {
160
+ await api.updateFoundationalService(kind, requireOwnerId(), serviceId, patch)
161
+ // A contract replacement changes the stored bodies, so drop the cached copy rather than
162
+ // letting a stale document be shown as what a consumer would receive.
163
+ delete contractBodies.value[serviceId]
164
+ await reload()
165
+ }
166
+
167
+ /** Remove this tier's OWN registration (and its uploaded documents). */
168
+ async function remove(serviceId: string) {
169
+ await api.deleteFoundationalService(kind, requireOwnerId(), serviceId)
170
+ delete contractBodies.value[serviceId]
171
+ await reload()
172
+ }
173
+
174
+ /** Opt this board out of an inherited ACCOUNT service. Destroys nothing; reversible. */
175
+ async function suppress(serviceId: string) {
176
+ await api.suppressFoundationalService(requireWorkspaceId(), serviceId)
177
+ await reload()
178
+ }
179
+
180
+ /** Lift a suppression, so the board inherits the account service again. */
181
+ async function restore(serviceId: string) {
182
+ await api.restoreFoundationalService(requireWorkspaceId(), serviceId)
183
+ await reload()
184
+ }
185
+
186
+ /**
187
+ * Fetch (and cache for the session) one service's full contract documents — the same lazy read
188
+ * a consumer dispatch makes, so what a human inspects here is what an agent would be handed.
189
+ */
190
+ async function contractsFor(serviceId: string): Promise<ApiContractDocument[]> {
191
+ const cached = contractBodies.value[serviceId]
192
+ if (cached) return cached
193
+ const documents = await api.getFoundationalServiceContracts(requireWorkspaceId(), serviceId)
194
+ contractBodies.value = { ...contractBodies.value, [serviceId]: documents }
195
+ return documents
196
+ }
197
+
198
+ async function reloadSources() {
199
+ sources.value = await api.listFoundationalSources(kind, requireOwnerId())
200
+ }
201
+
202
+ async function linkSource(input: LinkFoundationalServiceSourceInput) {
203
+ const source = await api.linkFoundationalSource(kind, requireOwnerId(), input)
204
+ sources.value = [source, ...sources.value]
205
+ // Sync immediately so the linked repo's services land in the catalog rather than waiting for
206
+ // the autorefresh sweep — a freshly linked source that shows nothing reads as a broken link.
207
+ await syncSource(source.id)
208
+ return source
209
+ }
210
+
211
+ async function unlinkSource(sourceId: string) {
212
+ await api.unlinkFoundationalSource(kind, requireOwnerId(), sourceId)
213
+ sources.value = sources.value.filter((s) => s.id !== sourceId)
214
+ delete sourceChanges.value[sourceId]
215
+ await reload()
216
+ }
217
+
218
+ /** Resync a source's definitions into the catalog, then refresh both views. */
219
+ async function syncSource(sourceId: string): Promise<FoundationalServiceSyncResult> {
220
+ const result = await api.syncFoundationalSource(kind, requireOwnerId(), sourceId)
221
+ delete sourceChanges.value[sourceId]
222
+ await Promise.all([reloadSources(), reload()])
223
+ return result
224
+ }
225
+
226
+ /** The cheap commit-version "check for changes" for a source; caches the flag. */
227
+ async function checkSource(sourceId: string) {
228
+ const status = await api.foundationalSourceStatus(kind, requireOwnerId(), sourceId)
229
+ sourceChanges.value = { ...sourceChanges.value, [sourceId]: status.changed }
230
+ return status
231
+ }
232
+
233
+ return {
234
+ kind,
235
+ hasResolved,
236
+ available,
237
+ sourcesAvailable,
238
+ services,
239
+ resolved,
240
+ suppressions,
241
+ sources,
242
+ sourceChanges,
243
+ contractBodies,
244
+ inheritedCount,
245
+ probe,
246
+ ensureProbed,
247
+ create,
248
+ update,
249
+ remove,
250
+ suppress,
251
+ restore,
252
+ contractsFor,
253
+ linkSource,
254
+ unlinkSource,
255
+ syncSource,
256
+ checkSource,
257
+ }
258
+ }
259
+
260
+ /**
261
+ * The workspace-tier catalog for the **active** board — a singleton that resolves the owner
262
+ * lazily, so it follows board switches and is shared by the navbar and the board modal.
263
+ */
264
+ export const useFoundationalServicesStore = defineStore('foundationalServices', () =>
265
+ foundationalServicesSetup('workspace', () => useWorkspaceStore().workspaceId),
266
+ )
267
+
268
+ /**
269
+ * An owner-keyed catalog store, used for the **account** tier (and reusable for any explicit
270
+ * owner). Keyed by `(kind, ownerId)` so each account gets isolated state.
271
+ */
272
+ export function useFoundationalServices(kind: FoundationalServiceOwnerKind, ownerId: string) {
273
+ return defineStore(`foundationalServices:${kind}:${ownerId}`, () =>
274
+ foundationalServicesSetup(kind, () => ownerId),
275
+ )()
276
+ }
@@ -344,6 +344,9 @@ function createOverlayModals() {
344
344
  // Prompt-fragment library panel (manage the board's best-practice catalog +
345
345
  // linked guideline repos; ADR 0006).
346
346
  const fragmentLibraryOpen = ref(false)
347
+ // Foundational-services panel (register the shared capabilities the org already runs, and see
348
+ // the merged catalog an Architect designs against; backend/docs/adr/0031-foundational-services.md).
349
+ const foundationalServicesOpen = ref(false)
347
350
  // Command bar (⌘K) — searchable launcher for every navbar action.
348
351
  const commandBarOpen = ref(false)
349
352
  // Keyboard-shortcuts cheatsheet (?) — a modal listing every global shortcut.
@@ -373,6 +376,12 @@ function createOverlayModals() {
373
376
  function closeFragmentLibrary() {
374
377
  fragmentLibraryOpen.value = false
375
378
  }
379
+ function openFoundationalServices() {
380
+ foundationalServicesOpen.value = true
381
+ }
382
+ function closeFoundationalServices() {
383
+ foundationalServicesOpen.value = false
384
+ }
376
385
  function openCommandBar() {
377
386
  commandBarOpen.value = true
378
387
  }
@@ -411,6 +420,7 @@ function createOverlayModals() {
411
420
  bootstrapOpen,
412
421
  addServiceOpen,
413
422
  fragmentLibraryOpen,
423
+ foundationalServicesOpen,
414
424
  commandBarOpen,
415
425
  shortcutsHelpOpen,
416
426
  mobileNavOpen,
@@ -421,6 +431,8 @@ function createOverlayModals() {
421
431
  closeAddService,
422
432
  openFragmentLibrary,
423
433
  closeFragmentLibrary,
434
+ openFoundationalServices,
435
+ closeFoundationalServices,
424
436
  openCommandBar,
425
437
  closeCommandBar,
426
438
  toggleCommandBar,
@@ -177,6 +177,7 @@ export type * from './execution'
177
177
  export type * from './models'
178
178
  export type * from './fragments'
179
179
  export type * from './skills'
180
+ export type * from './foundationalServices'
180
181
  export type * from './documents'
181
182
  export type * from './tasks'
182
183
  export type * from './bugHunt'
@@ -0,0 +1,32 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Foundational services (backend/docs/adr/0031-foundational-services.md). Mirrors the
3
+ // `@cat-factory/contracts` schemas: the tiered catalog of shared capabilities an
4
+ // organisation already runs (file storage, notifications, audit …), each with its
5
+ // API contracts, plus the repo sources that feed it.
6
+ //
7
+ // Note `ApiContractSummary` (no body) and `ApiContractDocument` (with body) are two
8
+ // separate types on purpose — the catalog read never carries a document, and the
9
+ // management surface reads one only when a human opens it.
10
+ // ---------------------------------------------------------------------------
11
+ //
12
+ // All wire shapes are sourced from @cat-factory/contracts (single source of truth).
13
+
14
+ export type {
15
+ ApiContractDocument,
16
+ ApiContractFormat,
17
+ ApiContractSummary,
18
+ CreateFoundationalServiceInput,
19
+ FoundationalService,
20
+ FoundationalServiceOwnerKind,
21
+ FoundationalServiceSelection,
22
+ FoundationalServiceSource,
23
+ FoundationalServiceSourceMode,
24
+ FoundationalServiceSourceStatus,
25
+ FoundationalServiceSuppression,
26
+ FoundationalServiceSyncResult,
27
+ FoundationalServiceTier,
28
+ LinkFoundationalServiceSourceInput,
29
+ ResolvedFoundationalService,
30
+ UpdateFoundationalServiceInput,
31
+ UploadApiContract,
32
+ } from '@cat-factory/contracts'