@cat-factory/app 0.199.0 → 0.200.2
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.
- package/app/components/foundational/FoundationalContractSummary.vue +36 -0
- package/app/components/foundational/FoundationalServiceCatalogList.vue +170 -0
- package/app/components/foundational/FoundationalServiceManager.vue +111 -0
- package/app/components/foundational/FoundationalServicePanel.vue +37 -0
- package/app/components/foundational/FoundationalServiceRegistry.vue +339 -0
- package/app/components/foundational/FoundationalServiceSources.vue +398 -0
- package/app/components/foundational/FoundationalSuppressions.vue +75 -0
- package/app/components/layout/AccountFoundationalSettings.vue +25 -0
- package/app/components/layout/AccountSkillSettings.vue +1 -1
- package/app/components/settings/AccountSettingsPanel.vue +17 -1
- package/app/components/skills/SkillLibraryManager.vue +1 -1
- package/app/composables/api/foundationalServices.ts +131 -0
- package/app/composables/api/skills.ts +1 -1
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useNavContributions.ts +1 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/modular/nav-contributions.spec.ts +6 -0
- package/app/modular/nav-contributions.ts +26 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/foundationalServices.spec.ts +121 -0
- package/app/stores/foundationalServices.ts +276 -0
- package/app/stores/skillLibrary.ts +1 -1
- package/app/stores/skills.spec.ts +1 -1
- package/app/stores/skills.ts +1 -1
- package/app/stores/ui/modals.ts +12 -0
- package/app/types/domain.ts +1 -0
- package/app/types/foundationalServices.ts +32 -0
- package/app/types/skills.ts +1 -1
- package/i18n/locales/de.json +146 -6
- package/i18n/locales/en.json +146 -6
- package/i18n/locales/es.json +146 -6
- package/i18n/locales/fr.json +146 -6
- package/i18n/locales/he.json +146 -6
- package/i18n/locales/it.json +146 -6
- package/i18n/locales/ja.json +146 -6
- package/i18n/locales/pl.json +146 -6
- package/i18n/locales/tr.json +146 -6
- package/i18n/locales/uk.json +146 -6
- package/package.json +2 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// One service's contract MANIFEST — id, format, size and the indexed operation names, never a
|
|
3
|
+
// document body (backend/docs/adr/0031-foundational-services.md). This is exactly what an Architect's
|
|
4
|
+
// catalog carries, so rendering the same fields here is what makes the surface answer "what will
|
|
5
|
+
// the design actually see?".
|
|
6
|
+
//
|
|
7
|
+
// `omittedOperations` is rendered rather than dropped: the operation list is CAPPED, and a reader
|
|
8
|
+
// who assumed it was complete would conclude the missing endpoints do not exist.
|
|
9
|
+
import type { ApiContractFormat, ApiContractSummary } from '~/types/domain'
|
|
10
|
+
|
|
11
|
+
defineProps<{
|
|
12
|
+
contracts: ApiContractSummary[]
|
|
13
|
+
/** Exhaustive format → translated label map, owned by the caller so the keys stay literal. */
|
|
14
|
+
formatLabel: Record<ApiContractFormat, string>
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const { t, n } = useI18n()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div v-if="contracts.length" class="mt-1 flex flex-col gap-1">
|
|
22
|
+
<div v-for="c in contracts" :key="c.contractId" class="text-[11px] text-slate-500">
|
|
23
|
+
<span class="text-slate-400">{{ c.title }}</span>
|
|
24
|
+
<span class="ms-1">({{ formatLabel[c.format] }}, {{ n(c.size) }})</span>
|
|
25
|
+
<span v-if="c.operations.length" class="ms-1 font-mono text-slate-500">
|
|
26
|
+
{{ c.operations.join(' · ') }}
|
|
27
|
+
</span>
|
|
28
|
+
<span v-if="c.omittedOperations > 0" class="ms-1 text-amber-500/80">
|
|
29
|
+
{{ t('foundational.contracts.omitted', { count: c.omittedOperations }) }}
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<p v-else class="mt-1 text-[11px] text-amber-500/80">
|
|
34
|
+
{{ t('foundational.contracts.none') }}
|
|
35
|
+
</p>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The MERGED catalog an Architect is actually handed for this board — account ⊕ workspace, the
|
|
3
|
+
// workspace winning by id (backend/docs/adr/0031-foundational-services.md). Workspace scope only: an
|
|
4
|
+
// account has no tier above it to merge with.
|
|
5
|
+
//
|
|
6
|
+
// This view is where the two board-level decisions live, and they are deliberately different
|
|
7
|
+
// actions rather than one "remove":
|
|
8
|
+
// - SUPPRESS an inherited account service — the board opts out, nothing is destroyed, and it is
|
|
9
|
+
// reversible with restore. The account keeps the service for every other board.
|
|
10
|
+
// - a service the board REGISTERED itself is edited or deleted in the other tab; suppressing it
|
|
11
|
+
// would be an obscure spelling of delete, and the backend refuses it as such.
|
|
12
|
+
//
|
|
13
|
+
// A contract body is fetched only when a human expands one, through the SAME lazy read a consumer
|
|
14
|
+
// dispatch makes — so what is inspected here is what an agent would be given, and merely opening
|
|
15
|
+
// the catalog transfers no documents.
|
|
16
|
+
import { computed, reactive, ref } from 'vue'
|
|
17
|
+
import type { ApiContractFormat, FoundationalServiceTier } from '~/types/domain'
|
|
18
|
+
import { useFoundationalServicesStore } from '~/stores/foundationalServices'
|
|
19
|
+
import FoundationalContractSummary from '~/components/foundational/FoundationalContractSummary.vue'
|
|
20
|
+
|
|
21
|
+
const catalog = useFoundationalServicesStore()
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
const { t } = useI18n()
|
|
24
|
+
|
|
25
|
+
// Exhaustive maps of literal `t(...)` keys, so a new tier/format fails the typed-key guard.
|
|
26
|
+
const tierLabel = computed<Record<FoundationalServiceTier, string>>(() => ({
|
|
27
|
+
account: t('foundational.tier.account'),
|
|
28
|
+
workspace: t('foundational.tier.workspace'),
|
|
29
|
+
}))
|
|
30
|
+
const formatLabel = computed<Record<ApiContractFormat, string>>(() => ({
|
|
31
|
+
openapi: t('foundational.format.openapi'),
|
|
32
|
+
'toad-contract': t('foundational.format.toadContract'),
|
|
33
|
+
'lokalise-api-contract': t('foundational.format.lokaliseApiContract'),
|
|
34
|
+
}))
|
|
35
|
+
// `as const` keeps the literal colour names assignable to UBadge's `color` union.
|
|
36
|
+
const tierColor = {
|
|
37
|
+
account: 'info',
|
|
38
|
+
workspace: 'primary',
|
|
39
|
+
} as const satisfies Record<FoundationalServiceTier, string>
|
|
40
|
+
|
|
41
|
+
function notifyError(title: string, e: unknown) {
|
|
42
|
+
toast.add({
|
|
43
|
+
title,
|
|
44
|
+
description: e instanceof Error ? e.message : String(e),
|
|
45
|
+
icon: 'i-lucide-triangle-alert',
|
|
46
|
+
color: 'error',
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const busyRows = reactive(new Set<string>())
|
|
51
|
+
const rowBusy = (key: string) => busyRows.has(key)
|
|
52
|
+
async function withRow(key: string, fn: () => Promise<void>) {
|
|
53
|
+
if (busyRows.has(key)) return
|
|
54
|
+
busyRows.add(key)
|
|
55
|
+
try {
|
|
56
|
+
await fn()
|
|
57
|
+
} finally {
|
|
58
|
+
busyRows.delete(key)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Service ids whose contract documents the user has expanded. */
|
|
63
|
+
const expanded = ref<string[]>([])
|
|
64
|
+
|
|
65
|
+
async function toggleContracts(serviceId: string) {
|
|
66
|
+
if (expanded.value.includes(serviceId)) {
|
|
67
|
+
expanded.value = expanded.value.filter((id) => id !== serviceId)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
await withRow(`docs:${serviceId}`, async () => {
|
|
71
|
+
try {
|
|
72
|
+
await catalog.contractsFor(serviceId)
|
|
73
|
+
expanded.value = [...expanded.value, serviceId]
|
|
74
|
+
} catch (e) {
|
|
75
|
+
notifyError(t('foundational.toast.contractsFailed'), e)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function suppress(serviceId: string) {
|
|
81
|
+
await withRow(`suppress:${serviceId}`, async () => {
|
|
82
|
+
try {
|
|
83
|
+
await catalog.suppress(serviceId)
|
|
84
|
+
toast.add({ title: t('foundational.toast.suppressed'), icon: 'i-lucide-eye-off' })
|
|
85
|
+
} catch (e) {
|
|
86
|
+
notifyError(t('foundational.toast.suppressFailed'), e)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<div class="flex flex-col gap-3" data-testid="foundational-catalog">
|
|
94
|
+
<p class="text-xs text-slate-500">{{ t('foundational.catalog.intro') }}</p>
|
|
95
|
+
|
|
96
|
+
<div
|
|
97
|
+
v-for="s in catalog.resolved"
|
|
98
|
+
:key="s.id"
|
|
99
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
100
|
+
>
|
|
101
|
+
<div class="flex items-start gap-2">
|
|
102
|
+
<UIcon name="i-lucide-boxes" class="mt-0.5 h-4 w-4 shrink-0 text-sky-400" />
|
|
103
|
+
<div class="min-w-0 flex-1">
|
|
104
|
+
<p class="truncate text-sm font-medium text-slate-100">
|
|
105
|
+
{{ s.name }}
|
|
106
|
+
<code class="ms-1 text-[11px] text-slate-500">{{ s.id }}</code>
|
|
107
|
+
</p>
|
|
108
|
+
<p class="text-xs text-slate-400">{{ s.summary }}</p>
|
|
109
|
+
<div v-if="s.capabilities.length" class="mt-1 flex flex-wrap gap-1">
|
|
110
|
+
<UBadge v-for="c in s.capabilities" :key="c" size="xs" variant="subtle" color="neutral">
|
|
111
|
+
{{ c }}
|
|
112
|
+
</UBadge>
|
|
113
|
+
</div>
|
|
114
|
+
<FoundationalContractSummary :contracts="s.contracts" :format-label="formatLabel" />
|
|
115
|
+
|
|
116
|
+
<div class="mt-2 flex items-center gap-2">
|
|
117
|
+
<UButton
|
|
118
|
+
v-if="s.contracts.length"
|
|
119
|
+
size="xs"
|
|
120
|
+
variant="ghost"
|
|
121
|
+
:loading="rowBusy(`docs:${s.id}`)"
|
|
122
|
+
@click="toggleContracts(s.id)"
|
|
123
|
+
>
|
|
124
|
+
{{
|
|
125
|
+
expanded.includes(s.id)
|
|
126
|
+
? t('foundational.catalog.hideDocuments')
|
|
127
|
+
: t('foundational.catalog.showDocuments')
|
|
128
|
+
}}
|
|
129
|
+
</UButton>
|
|
130
|
+
</div>
|
|
131
|
+
<!-- The lazy read, rendered verbatim: this is the text a consumer step receives. -->
|
|
132
|
+
<div v-if="expanded.includes(s.id)" class="mt-2 flex flex-col gap-2">
|
|
133
|
+
<div
|
|
134
|
+
v-for="doc in catalog.contractBodies[s.id] ?? []"
|
|
135
|
+
:key="doc.contractId"
|
|
136
|
+
class="rounded-md border border-slate-800 bg-slate-950/60 p-2"
|
|
137
|
+
>
|
|
138
|
+
<p class="mb-1 text-[11px] text-slate-400">
|
|
139
|
+
{{ doc.title }}
|
|
140
|
+
<span v-if="doc.path" class="ms-1 font-mono text-slate-600">{{ doc.path }}</span>
|
|
141
|
+
</p>
|
|
142
|
+
<pre class="max-h-64 overflow-auto text-[11px] text-slate-300">{{ doc.body }}</pre>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex shrink-0 flex-col items-end gap-1">
|
|
147
|
+
<UBadge size="xs" :color="tierColor[s.tier]" variant="subtle">
|
|
148
|
+
{{ tierLabel[s.tier] }}
|
|
149
|
+
</UBadge>
|
|
150
|
+
<!-- Only an INHERITED entry can be suppressed; the board's own row is managed in the
|
|
151
|
+
registry tab, where deleting it is the honest action. -->
|
|
152
|
+
<UButton
|
|
153
|
+
v-if="s.tier === 'account'"
|
|
154
|
+
icon="i-lucide-eye-off"
|
|
155
|
+
size="xs"
|
|
156
|
+
variant="ghost"
|
|
157
|
+
:loading="rowBusy(`suppress:${s.id}`)"
|
|
158
|
+
:title="t('foundational.catalog.suppress')"
|
|
159
|
+
:data-testid="`foundational-suppress-${s.id}`"
|
|
160
|
+
@click="suppress(s.id)"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<p v-if="!catalog.resolved.length" class="text-sm text-slate-500">
|
|
167
|
+
{{ t('foundational.catalog.empty') }}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
</template>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Foundational-services manager (backend/docs/adr/0031-foundational-services.md), reused at two
|
|
3
|
+
// scopes: a board (`workspace`) and an `account`. Register the shared capabilities the
|
|
4
|
+
// organisation already runs, attach their API contracts (uploaded or synced from a repo), and —
|
|
5
|
+
// at the workspace scope only — review the MERGED catalog an Architect is handed and opt the
|
|
6
|
+
// board out of anything it should not design against.
|
|
7
|
+
//
|
|
8
|
+
// The tab split is the feature's own split, not a layout choice: "Catalog" is what an agent sees
|
|
9
|
+
// (identity + operation names, never a document body), while "This tier" is what this owner
|
|
10
|
+
// actually registers. An account has no tier above it, so it gets no catalog tab and no
|
|
11
|
+
// suppression controls.
|
|
12
|
+
import { computed, ref, watch } from 'vue'
|
|
13
|
+
import type { FoundationalServiceOwnerKind } from '~/types/domain'
|
|
14
|
+
import {
|
|
15
|
+
useFoundationalServices,
|
|
16
|
+
useFoundationalServicesStore,
|
|
17
|
+
} from '~/stores/foundationalServices'
|
|
18
|
+
import FoundationalServiceCatalogList from '~/components/foundational/FoundationalServiceCatalogList.vue'
|
|
19
|
+
import FoundationalServiceRegistry from '~/components/foundational/FoundationalServiceRegistry.vue'
|
|
20
|
+
import FoundationalServiceSources from '~/components/foundational/FoundationalServiceSources.vue'
|
|
21
|
+
import FoundationalSuppressions from '~/components/foundational/FoundationalSuppressions.vue'
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(
|
|
24
|
+
defineProps<{
|
|
25
|
+
kind: FoundationalServiceOwnerKind
|
|
26
|
+
ownerId: string
|
|
27
|
+
/** Whether to show the merged-catalog tab (workspace scope only). */
|
|
28
|
+
showCatalog?: boolean
|
|
29
|
+
}>(),
|
|
30
|
+
{ showCatalog: false },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// The workspace scope follows the active board (singleton, shared with the navbar); the account
|
|
34
|
+
// scope uses an owner-keyed store so each account is isolated.
|
|
35
|
+
const catalog =
|
|
36
|
+
props.kind === 'workspace'
|
|
37
|
+
? useFoundationalServicesStore()
|
|
38
|
+
: useFoundationalServices(props.kind, props.ownerId)
|
|
39
|
+
const github = useGitHubStore()
|
|
40
|
+
const { t } = useI18n()
|
|
41
|
+
|
|
42
|
+
watch(
|
|
43
|
+
() => props.ownerId,
|
|
44
|
+
() => {
|
|
45
|
+
void catalog.probe()
|
|
46
|
+
// The repo pickers need the active board's installation state; probe once so they light up.
|
|
47
|
+
void github.probe()
|
|
48
|
+
},
|
|
49
|
+
{ immediate: true },
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
type Tab = 'catalog' | 'registry' | 'sources'
|
|
53
|
+
const tab = ref<Tab>(props.showCatalog ? 'catalog' : 'registry')
|
|
54
|
+
|
|
55
|
+
const ownerLabel = computed(() =>
|
|
56
|
+
props.kind === 'workspace' ? t('foundational.owner.workspace') : t('foundational.owner.account'),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const tabs = computed(() => {
|
|
60
|
+
const items = [
|
|
61
|
+
{ value: 'registry' as const, label: ownerLabel.value, slot: 'registry' },
|
|
62
|
+
{ value: 'sources' as const, label: t('foundational.tab.sources'), slot: 'sources' },
|
|
63
|
+
]
|
|
64
|
+
if (!props.showCatalog) return items
|
|
65
|
+
return [
|
|
66
|
+
{ value: 'catalog' as const, label: t('foundational.tab.catalog'), slot: 'catalog' },
|
|
67
|
+
...items,
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const activeTab = computed({
|
|
72
|
+
get: () => tab.value,
|
|
73
|
+
set: (v: string) => {
|
|
74
|
+
tab.value = v as Tab
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<div class="flex flex-col gap-4" data-testid="foundational-manager">
|
|
81
|
+
<!-- The catalog is opt-in; if a deployment has not wired it, say so rather than offering
|
|
82
|
+
forms that would fail with a raw 503. -->
|
|
83
|
+
<div
|
|
84
|
+
v-if="catalog.available === false"
|
|
85
|
+
class="rounded-md border border-slate-800 bg-slate-900/40 p-3 text-sm text-slate-400"
|
|
86
|
+
>
|
|
87
|
+
{{ t('foundational.unavailable') }}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<UTabs
|
|
91
|
+
v-else
|
|
92
|
+
v-model="activeTab"
|
|
93
|
+
:items="tabs"
|
|
94
|
+
variant="link"
|
|
95
|
+
:ui="{ root: 'gap-4', list: 'overflow-x-auto' }"
|
|
96
|
+
>
|
|
97
|
+
<template #catalog>
|
|
98
|
+
<div class="flex flex-col gap-4">
|
|
99
|
+
<FoundationalServiceCatalogList />
|
|
100
|
+
<FoundationalSuppressions />
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
103
|
+
<template #registry>
|
|
104
|
+
<FoundationalServiceRegistry :kind="props.kind" :owner-id="props.ownerId" />
|
|
105
|
+
</template>
|
|
106
|
+
<template #sources>
|
|
107
|
+
<FoundationalServiceSources :kind="props.kind" :owner-id="props.ownerId" />
|
|
108
|
+
</template>
|
|
109
|
+
</UTabs>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The board (workspace-tier) foundational-services modal
|
|
3
|
+
// (backend/docs/adr/0031-foundational-services.md). A thin shell around the shared manager at the
|
|
4
|
+
// active board's scope — including the merged catalog, so the account ⊕ workspace inheritance an
|
|
5
|
+
// Architect designs against is visible and the board can opt out of an inherited service.
|
|
6
|
+
// Opened from the navbar / command bar via the ui store.
|
|
7
|
+
import FoundationalServiceManager from '~/components/foundational/FoundationalServiceManager.vue'
|
|
8
|
+
|
|
9
|
+
const ui = useUiStore()
|
|
10
|
+
const workspace = useWorkspaceStore()
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
const open = computed({
|
|
14
|
+
get: () => ui.foundationalServicesOpen,
|
|
15
|
+
set: (v: boolean) => {
|
|
16
|
+
if (!v) ui.closeFoundationalServices()
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<UModal
|
|
23
|
+
v-model:open="open"
|
|
24
|
+
:title="t('foundational.panel.title')"
|
|
25
|
+
:description="t('foundational.panel.subtitle')"
|
|
26
|
+
:ui="{ content: 'max-w-3xl' }"
|
|
27
|
+
>
|
|
28
|
+
<template #body>
|
|
29
|
+
<FoundationalServiceManager
|
|
30
|
+
v-if="workspace.workspaceId"
|
|
31
|
+
kind="workspace"
|
|
32
|
+
:owner-id="workspace.workspaceId"
|
|
33
|
+
show-catalog
|
|
34
|
+
/>
|
|
35
|
+
</template>
|
|
36
|
+
</UModal>
|
|
37
|
+
</template>
|