@cat-factory/app 0.225.0 → 0.226.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.
@@ -22,22 +22,18 @@ type Entry =
22
22
  // clock-fallback `occurredAt`).
23
23
  const entries = computed<Entry[]>(() =>
24
24
  [
25
- ...props.failures.map(
26
- (failure, i): Entry => ({
27
- kind: 'failure',
28
- key: `failure-${i}`,
29
- occurredAt: failure.occurredAt,
30
- failure,
31
- }),
32
- ),
33
- ...props.outputs.map(
34
- (output, i): Entry => ({
35
- kind: 'success',
36
- key: `success-${i}`,
37
- occurredAt: output.occurredAt,
38
- output,
39
- }),
40
- ),
25
+ ...props.failures.map((failure, i): Entry => ({
26
+ kind: 'failure',
27
+ key: `failure-${i}`,
28
+ occurredAt: failure.occurredAt,
29
+ failure,
30
+ })),
31
+ ...props.outputs.map((output, i): Entry => ({
32
+ kind: 'success',
33
+ key: `success-${i}`,
34
+ occurredAt: output.occurredAt,
35
+ output,
36
+ })),
41
37
  ].sort((a, b) => b.occurredAt - a.occurredAt),
42
38
  )
43
39
  </script>
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { RiskPolicy } from '~/types/merge'
3
- import { resolveRiskPolicyPicker } from '~/components/riskPolicy/RiskPolicyPicker.logic'
3
+ import {
4
+ refusedRiskPolicySelections,
5
+ resolveRiskPolicyPicker,
6
+ } from '~/components/riskPolicy/RiskPolicyPicker.logic'
4
7
 
5
8
  // The resolver reads ids only, so the fixture carries just enough to be identifiable.
6
9
  const policy = (id: string, name: string) => ({ id, name }) as unknown as RiskPolicy
@@ -73,3 +76,60 @@ describe('resolveRiskPolicyPicker', () => {
73
76
  expect(state).toEqual({ policy: null, viaWorkspaceDefault: false })
74
77
  })
75
78
  })
79
+
80
+ describe('refusedRiskPolicySelections', () => {
81
+ const rolePolicy = (id: string, over: Partial<RiskPolicy>) =>
82
+ ({ id, name: id, classRules: {}, classRulesByRole: {}, dryRunRoles: [], ...over }) as RiskPolicy
83
+
84
+ const sandboxed = rolePolicy('mp_sandboxed', { dryRunRoles: ['member'] })
85
+ const open = rolePolicy('mp_open', {})
86
+ const member = { role: 'member' as const, managesPolicy: false }
87
+
88
+ it('marks the rows a member may not move a sandboxed task to', () => {
89
+ // The picker must not offer what the backend refuses, including the "workspace default" row,
90
+ // which is a real policy (here: the open one) and not the absence of a choice.
91
+ const refused = refusedRiskPolicySelections({
92
+ options: [sandboxed, open],
93
+ defaultPolicy: open,
94
+ modelValue: 'mp_sandboxed',
95
+ actor: member,
96
+ })
97
+ expect(refused.get('mp_open')).toBe('relaxes_role_sandbox')
98
+ expect(refused.get('')).toBe('relaxes_role_sandbox')
99
+ expect(refused.has('mp_sandboxed')).toBe(false) // the row already selected
100
+ })
101
+
102
+ it('judges a task that picked nothing against the workspace default, as the engine does', () => {
103
+ // The create-form shape: nothing selected yet, so the policy being moved AWAY from is the one
104
+ // the task would have been governed by.
105
+ const refused = refusedRiskPolicySelections({
106
+ options: [sandboxed, open],
107
+ defaultPolicy: sandboxed,
108
+ modelValue: '',
109
+ actor: member,
110
+ })
111
+ expect(refused.get('mp_open')).toBe('relaxes_role_sandbox')
112
+ expect(refused.has('mp_sandboxed')).toBe(false)
113
+ })
114
+
115
+ it('refuses nothing to an editor who manages the policy library', () => {
116
+ const refused = refusedRiskPolicySelections({
117
+ options: [sandboxed, open],
118
+ defaultPolicy: open,
119
+ modelValue: 'mp_sandboxed',
120
+ actor: { role: 'admin', managesPolicy: true },
121
+ })
122
+ expect(refused.size).toBe(0)
123
+ })
124
+
125
+ it('refuses nothing between policies that treat every initiator alike', () => {
126
+ // Every built-in ships with an empty role layer, so the common case offers the whole library.
127
+ const refused = refusedRiskPolicySelections({
128
+ options: [open, rolePolicy('mp_other', {})],
129
+ defaultPolicy: open,
130
+ modelValue: 'mp_open',
131
+ actor: member,
132
+ })
133
+ expect(refused.size).toBe(0)
134
+ })
135
+ })
@@ -6,6 +6,11 @@
6
6
  // riskPolicies store's `resolve()` falls back to the workspace default for both an empty id
7
7
  // and a dangling one; this mirrors that, so the picker can never tell the user "no risk policy
8
8
  // configured" about a task the default is quietly governing.
9
+ import {
10
+ refuseRiskPolicySelection,
11
+ type BlockEditActor,
12
+ type RiskPolicySelectionRefusal,
13
+ } from '@cat-factory/contracts'
9
14
  import type { RiskPolicy } from '~/types/merge'
10
15
 
11
16
  export interface RiskPolicyPickerState {
@@ -45,3 +50,38 @@ export function resolveRiskPolicyPicker(input: RiskPolicyPickerInput): RiskPolic
45
50
  if (named) return { policy: named, viaWorkspaceDefault: false }
46
51
  return { policy: input.defaultPolicy, viaWorkspaceDefault: !!input.defaultPolicy }
47
52
  }
53
+
54
+ /**
55
+ * Which of the offered policies this user may not move THIS task to, keyed by option id (`''`
56
+ * for the "workspace default" row), with the reason.
57
+ *
58
+ * A task's policy decides whether its runs are sandboxed for the initiator's role and how their
59
+ * auto-merge is narrowed, so the backend refuses a selection that would relax what the selector's
60
+ * own role is held to (ADR 0037). The picker applies the SAME contracts rule rather than offering
61
+ * a row and handing back a 403: an authoring surface that offers what the engine discards is
62
+ * telling someone they made a choice they did not make.
63
+ *
64
+ * The policy being moved AWAY from is resolved exactly as the engine resolves it: the named
65
+ * option, else the workspace default. That is what makes the same call correct on the create form
66
+ * (nothing picked yet, so the default is what would have governed the task) and in the inspector.
67
+ */
68
+ export function refusedRiskPolicySelections(input: {
69
+ options: readonly RiskPolicy[]
70
+ defaultPolicy: RiskPolicy | null
71
+ modelValue: string
72
+ actor: BlockEditActor
73
+ }): Map<string, RiskPolicySelectionRefusal> {
74
+ const refusals = new Map<string, RiskPolicySelectionRefusal>()
75
+ const from =
76
+ (input.modelValue ? input.options.find((p) => p.id === input.modelValue) : undefined) ??
77
+ input.defaultPolicy
78
+ if (!from) return refusals
79
+ const judge = (id: string, to: RiskPolicy | null) => {
80
+ if (!to) return
81
+ const refusal = refuseRiskPolicySelection({ from, to, actor: input.actor })
82
+ if (refusal) refusals.set(id, refusal)
83
+ }
84
+ judge('', input.defaultPolicy)
85
+ for (const policy of input.options) judge(policy.id, policy)
86
+ return refusals
87
+ }
@@ -14,7 +14,10 @@
14
14
  import { computed, ref } from 'vue'
15
15
  import type { RiskPolicy } from '~/types/merge'
16
16
  import RiskPolicyPreview from '~/components/riskPolicy/RiskPolicyPreview.vue'
17
- import { resolveRiskPolicyPicker } from '~/components/riskPolicy/RiskPolicyPicker.logic'
17
+ import {
18
+ refusedRiskPolicySelections,
19
+ resolveRiskPolicyPicker,
20
+ } from '~/components/riskPolicy/RiskPolicyPicker.logic'
18
21
 
19
22
  const props = withDefaults(
20
23
  defineProps<{
@@ -37,6 +40,7 @@ const props = withDefaults(
37
40
 
38
41
  const emit = defineEmits<{ 'update:modelValue': [string] }>()
39
42
  const { t } = useI18n()
43
+ const access = useWorkspaceAccess()
40
44
 
41
45
  const open = ref(false)
42
46
  // The row the pointer or the keyboard is currently on, driving the right-column preview.
@@ -56,6 +60,39 @@ const preview = computed(() =>
56
60
  }),
57
61
  )
58
62
 
63
+ /**
64
+ * The policies this user may not move THIS task to, and why (ADR 0037): a selection may not
65
+ * relax what the selector's own role is held to, and the backend refuses one that does. Offering
66
+ * the row anyway would be telling someone they made a choice the engine then discards.
67
+ *
68
+ * Empty on every workspace whose policies treat each initiator alike (which is every built-in),
69
+ * and always empty for someone holding `settings.manage`, who owns the library.
70
+ */
71
+ const refused = computed(() =>
72
+ refusedRiskPolicySelections({
73
+ options: props.options,
74
+ defaultPolicy: props.defaultPolicy,
75
+ modelValue: props.modelValue,
76
+ actor: { role: access.role.value, managesPolicy: access.canManageSettings.value },
77
+ }),
78
+ )
79
+
80
+ /** The reason the previewed row is refused, if it is: the pane explains what the row cannot. */
81
+ const previewRefusal = computed(() => refused.value.get(activeId.value ?? props.modelValue))
82
+
83
+ /**
84
+ * The same reason as a tooltip on the row itself, `undefined` when the row is selectable.
85
+ *
86
+ * A refused row is `aria-disabled`, NOT `disabled`: a disabled button is unfocusable, so keyboard
87
+ * users could never land on it, and landing on it is precisely what puts the reason in the detail
88
+ * pane (the `@focus` handler below drives the preview). Refusing the click is `choose`'s job, so
89
+ * the row stays reachable by both routes and explains itself on both.
90
+ */
91
+ function refusalText(id: string): string | undefined {
92
+ const refusal = refused.value.get(id)
93
+ return refusal ? t(`riskPolicy.picker.refused.${refusal}`) : undefined
94
+ }
95
+
59
96
  /**
60
97
  * Tabbing BETWEEN two rows fires `focusout` on the one being left, so only focus leaving the
61
98
  * panel altogether may drop the preview back to the selection.
@@ -68,6 +105,7 @@ function onPanelFocusOut(event: FocusEvent) {
68
105
  }
69
106
 
70
107
  function choose(id: string) {
108
+ if (refused.value.has(id)) return
71
109
  emit('update:modelValue', id)
72
110
  open.value = false
73
111
  }
@@ -101,14 +139,22 @@ function choose(id: string) {
101
139
  <li>
102
140
  <button
103
141
  type="button"
104
- class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm hover:bg-slate-800/60"
105
- :class="modelValue ? 'text-slate-300' : 'text-slate-100'"
142
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm"
143
+ :class="[
144
+ refused.has('') ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-800/60',
145
+ modelValue ? 'text-slate-300' : 'text-slate-100',
146
+ ]"
147
+ :aria-disabled="refused.has('')"
148
+ :title="refusalText('')"
106
149
  data-testid="risk-policy-option-none"
107
150
  @mouseenter="activeId = ''"
108
151
  @focus="activeId = ''"
109
152
  @click="choose('')"
110
153
  >
111
- <UIcon name="i-lucide-rotate-ccw" class="h-4 w-4 shrink-0 text-slate-400" />
154
+ <UIcon
155
+ :name="refused.has('') ? 'i-lucide-lock' : 'i-lucide-rotate-ccw'"
156
+ class="h-4 w-4 shrink-0 text-slate-400"
157
+ />
112
158
  <span class="flex-1 truncate">{{ noneLabel }}</span>
113
159
  <UIcon
114
160
  v-if="!modelValue"
@@ -120,14 +166,22 @@ function choose(id: string) {
120
166
  <li v-for="p in options" :key="p.id">
121
167
  <button
122
168
  type="button"
123
- class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm hover:bg-slate-800/60"
124
- :class="modelValue === p.id ? 'text-slate-100' : 'text-slate-300'"
169
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm"
170
+ :class="[
171
+ refused.has(p.id) ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-800/60',
172
+ modelValue === p.id ? 'text-slate-100' : 'text-slate-300',
173
+ ]"
174
+ :aria-disabled="refused.has(p.id)"
175
+ :title="refusalText(p.id)"
125
176
  :data-testid="`risk-policy-option-${p.id}`"
126
177
  @mouseenter="activeId = p.id"
127
178
  @focus="activeId = p.id"
128
179
  @click="choose(p.id)"
129
180
  >
130
- <UIcon name="i-lucide-git-merge" class="h-4 w-4 shrink-0 text-slate-400" />
181
+ <UIcon
182
+ :name="refused.has(p.id) ? 'i-lucide-lock' : 'i-lucide-git-merge'"
183
+ class="h-4 w-4 shrink-0 text-slate-400"
184
+ />
131
185
  <span class="flex-1 truncate">{{ p.name }}</span>
132
186
  <UIcon
133
187
  v-if="modelValue === p.id"
@@ -141,6 +195,13 @@ function choose(id: string) {
141
195
  <!-- right: what the active (or selected) policy actually does -->
142
196
  <div class="w-1/2 overflow-y-auto p-3">
143
197
  <template v-if="preview.policy">
198
+ <p
199
+ v-if="previewRefusal"
200
+ class="mb-2 text-[11px] leading-snug text-amber-400"
201
+ data-testid="risk-policy-refusal"
202
+ >
203
+ {{ t(`riskPolicy.picker.refused.${previewRefusal}`) }}
204
+ </p>
144
205
  <p
145
206
  v-if="preview.viaWorkspaceDefault"
146
207
  class="mb-2 text-[11px] leading-snug text-slate-500"
@@ -6156,7 +6156,11 @@
6156
6156
  },
6157
6157
  "picker": {
6158
6158
  "workspaceDefaultCaption": "Gilt, weil diese Aufgabe keine eigene Richtlinie wählt.",
6159
- "noneHint": "Keine Risikorichtlinie konfiguriert. Jeder Pull Request wartet auf eine menschliche Prüfung."
6159
+ "noneHint": "Keine Risikorichtlinie konfiguriert. Jeder Pull Request wartet auf eine menschliche Prüfung.",
6160
+ "refused": {
6161
+ "relaxes_role_sandbox": "Für deine Rolle nicht verfügbar: Läufe dieser Aufgabe laufen in der Sandbox, und diese Richtlinie würde sie mergen lassen. Eine Workspace-Administration kann das ändern.",
6162
+ "relaxes_role_class_rule": "Für deine Rolle nicht verfügbar: Diese Richtlinie merged Änderungen automatisch, die du bei dieser Aufgabe prüfen musst. Eine Workspace-Administration kann das ändern."
6163
+ }
6160
6164
  },
6161
6165
  "preview": {
6162
6166
  "defaultBadge": "Standard",
@@ -4744,7 +4744,11 @@
4744
4744
  },
4745
4745
  "picker": {
4746
4746
  "workspaceDefaultCaption": "Applied because this task picks no policy of its own.",
4747
- "noneHint": "No risk policy configured. Every pull request waits for a human review."
4747
+ "noneHint": "No risk policy configured. Every pull request waits for a human review.",
4748
+ "refused": {
4749
+ "relaxes_role_sandbox": "Not available for your role: this task’s runs are sandboxed, and this policy would let them merge. A workspace admin can change it.",
4750
+ "relaxes_role_class_rule": "Not available for your role: this policy auto-merges changes you are held to review on this task. A workspace admin can change it."
4751
+ }
4748
4752
  },
4749
4753
  "preview": {
4750
4754
  "defaultBadge": "Default",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "Se aplica porque esta tarea no elige una política propia.",
6026
- "noneHint": "No hay ninguna política de riesgo configurada. Cada pull request espera una revisión humana."
6026
+ "noneHint": "No hay ninguna política de riesgo configurada. Cada pull request espera una revisión humana.",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "No disponible para tu rol: las ejecuciones de esta tarea están en un entorno aislado y esta política permitiría fusionarlas. Un administrador del espacio de trabajo puede cambiarlo.",
6029
+ "relaxes_role_class_rule": "No disponible para tu rol: esta política fusiona automáticamente cambios que tú debes revisar en esta tarea. Un administrador del espacio de trabajo puede cambiarlo."
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "Predeterminada",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "Appliquée parce que cette tâche ne choisit aucune politique.",
6026
- "noneHint": "Aucune politique de risque configurée. Chaque pull request attend une revue humaine."
6026
+ "noneHint": "Aucune politique de risque configurée. Chaque pull request attend une revue humaine.",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "Indisponible pour votre rôle : les exécutions de cette tâche sont isolées, et cette politique les laisserait fusionner. Un administrateur de l’espace de travail peut le changer.",
6029
+ "relaxes_role_class_rule": "Indisponible pour votre rôle : cette politique fusionne automatiquement des changements que vous devez relire sur cette tâche. Un administrateur de l’espace de travail peut le changer."
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "Par défaut",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "חלה מפני שהמשימה הזו לא בוחרת מדיניות משלה.",
6026
- "noneHint": "לא הוגדרה מדיניות סיכון. כל בקשת משיכה ממתינה לסקירה אנושית."
6026
+ "noneHint": "לא הוגדרה מדיניות סיכון. כל בקשת משיכה ממתינה לסקירה אנושית.",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "לא זמין לתפקיד שלך: ההרצות של משימה זו מבודדות, והמדיניות הזו הייתה מאפשרת למזג אותן. מנהל סביבת העבודה יכול לשנות זאת.",
6029
+ "relaxes_role_class_rule": "לא זמין לתפקיד שלך: המדיניות הזו ממזגת אוטומטית שינויים שאתה נדרש לבדוק במשימה זו. מנהל סביבת העבודה יכול לשנות זאת."
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "ברירת מחדל",
@@ -6156,7 +6156,11 @@
6156
6156
  },
6157
6157
  "picker": {
6158
6158
  "workspaceDefaultCaption": "Si applica perche questa attivita non sceglie un criterio proprio.",
6159
- "noneHint": "Nessun criterio di rischio configurato. Ogni pull request attende una revisione umana."
6159
+ "noneHint": "Nessun criterio di rischio configurato. Ogni pull request attende una revisione umana.",
6160
+ "refused": {
6161
+ "relaxes_role_sandbox": "Non disponibile per il tuo ruolo: le esecuzioni di questa attività sono isolate e questo criterio ne consentirebbe il merge. Un amministratore dello spazio di lavoro può cambiarlo.",
6162
+ "relaxes_role_class_rule": "Non disponibile per il tuo ruolo: questo criterio unisce automaticamente modifiche che devi revisionare in questa attività. Un amministratore dello spazio di lavoro può cambiarlo."
6163
+ }
6160
6164
  },
6161
6165
  "preview": {
6162
6166
  "defaultBadge": "Predefinito",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "このタスクが独自のポリシーを選んでいないため、これが適用されます。",
6026
- "noneHint": "リスクポリシーが設定されていません。すべてのプルリクエストは人のレビューを待ちます。"
6026
+ "noneHint": "リスクポリシーが設定されていません。すべてのプルリクエストは人のレビューを待ちます。",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "あなたのロールでは選べません。このタスクの実行はサンドボックスで動作しますが、このポリシーではマージが許可されます。ワークスペース管理者が変更できます。",
6029
+ "relaxes_role_class_rule": "あなたのロールでは選べません。このポリシーは、このタスクであなたのレビューが必要な変更を自動マージします。ワークスペース管理者が変更できます。"
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "既定",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "Stosowana, ponieważ to zadanie nie wybiera własnej zasady.",
6026
- "noneHint": "Nie skonfigurowano żadnej zasady ryzyka. Każdy pull request czeka na ludzką recenzję."
6026
+ "noneHint": "Nie skonfigurowano żadnej zasady ryzyka. Każdy pull request czeka na ludzką recenzję.",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "Niedostępne dla twojej roli: uruchomienia tego zadania działają w piaskownicy, a ta zasada pozwoliłaby je scalić. Administrator obszaru roboczego może to zmienić.",
6029
+ "relaxes_role_class_rule": "Niedostępne dla twojej roli: ta zasada automatycznie scala zmiany, które musisz przejrzeć w tym zadaniu. Administrator obszaru roboczego może to zmienić."
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "Domyślna",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "Bu görev kendine bir ilke seçmediği için uygulanır.",
6026
- "noneHint": "Yapılandırılmış risk ilkesi yok. Her pull request insan incelemesini bekler."
6026
+ "noneHint": "Yapılandırılmış risk ilkesi yok. Her pull request insan incelemesini bekler.",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "Rolün için kullanılamaz: bu görevin çalışmaları yalıtılmış çalışır ve bu ilke birleştirmelerine izin verirdi. Bir çalışma alanı yöneticisi bunu değiştirebilir.",
6029
+ "relaxes_role_class_rule": "Rolün için kullanılamaz: bu ilke, bu görevde incelemen gereken değişiklikleri otomatik birleştirir. Bir çalışma alanı yöneticisi bunu değiştirebilir."
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "Varsayılan",
@@ -6023,7 +6023,11 @@
6023
6023
  },
6024
6024
  "picker": {
6025
6025
  "workspaceDefaultCaption": "Застосовується, бо це завдання не обрало власної політики.",
6026
- "noneHint": "Політику ризику не налаштовано. Кожен pull request чекає на людську перевірку."
6026
+ "noneHint": "Політику ризику не налаштовано. Кожен pull request чекає на людську перевірку.",
6027
+ "refused": {
6028
+ "relaxes_role_sandbox": "Недоступно для вашої ролі: запуски цього завдання ізольовані, а ця політика дозволила б їх злиття. Адміністратор робочого простору може це змінити.",
6029
+ "relaxes_role_class_rule": "Недоступно для вашої ролі: ця політика автоматично зливає зміни, які ви маєте перевіряти в цьому завданні. Адміністратор робочого простору може це змінити."
6030
+ }
6027
6031
  },
6028
6032
  "preview": {
6029
6033
  "defaultBadge": "Типова",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.225.0",
3
+ "version": "0.226.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@modular-frontend/core": "^0.6.0",
21
+ "@modular-frontend/core": "0.6.0",
22
22
  "@modular-vue/core": "^1.5.0",
23
23
  "@modular-vue/journeys": "^1.4.0",
24
24
  "@modular-vue/nuxt": "^0.4.1",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.242.0"
43
+ "@cat-factory/contracts": "0.243.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",
@@ -53,7 +53,7 @@
53
53
  "vue-tsc": "^3.3.9"
54
54
  },
55
55
  "peerDependencies": {
56
- "nuxt": "^4.5.0"
56
+ "nuxt": "^4.5.1"
57
57
  },
58
58
  "scripts": {
59
59
  "postinstall": "nuxt prepare",