@cat-factory/app 0.169.0 → 0.170.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.
package/README.md CHANGED
@@ -56,9 +56,10 @@ over the WebSocket. How that sync works is written up in
56
56
  ## Interface modes (basic / advanced)
57
57
 
58
58
  The SPA renders at one of two **interface tiers**. `basic` (the default) is the everyday
59
- surface: the power-user nav destinations are hidden and the run/pipeline options that only
60
- exist to override a workspace-level default are left at that default. `advanced` shows
61
- everything. The tier resolves in a fixed order, first match wins:
59
+ surface: the run/pipeline options that only exist to override a workspace-level default are
60
+ left at that default, and the nav is trimmed to the destinations that are the **only route**
61
+ to their capability. `advanced` shows everything. The tier resolves in a fixed order, first
62
+ match wins:
62
63
 
63
64
  1. **`NUXT_PUBLIC_UI_MODE`** (`basic` | `advanced`) — the deployment pin. Like
64
65
  `NUXT_PUBLIC_API_BASE` it is baked in at **build** time (`ssr: false`), and while it is
@@ -81,7 +82,15 @@ hoc where it can be avoided:
81
82
  - **A nav destination** declares `advanced: true` in `app/modular/nav-contributions.ts`. The
82
83
  shared `navSlotFilter` drops it in basic mode across all three shells (sidebar, command
83
84
  palette, toolbar), independently of its RBAC `gate` — both must pass. A consumer module's
84
- own contributions take the same flag.
85
+ own contributions take the same flag. The bar for setting it is **route count, not how
86
+ advanced the surface feels**: hiding the only way to reach a capability removes the
87
+ capability from the tier, while hiding a shortcut into a surface a basic destination also
88
+ opens removes nothing. So the flag belongs on a surface that sits beside the delivery path
89
+ (Sandbox, Kaizen), on a palette shortcut into a Workspace-settings tab, or on a knob the
90
+ Integrations hub already offers — and never on a sole route (the pipeline builder, the
91
+ fragment library, the infrastructure/PREnv windows, the operator + reports views).
92
+ `nav-contributions.spec.ts` pins the advanced set against a table of each item's
93
+ alternative route, so promoting one forces that claim to be written down.
85
94
  - **A less-used option inside a surface** reads `useUiModeStore().isAdvanced`. Hide, never
86
95
  disable, and only ever hide an OVERRIDE: what remains must be exactly the default the hidden
87
96
  field would have shown, so a basic-mode user never gets different behaviour from an advanced
@@ -99,11 +99,36 @@ describe('navSlotFilter', () => {
99
99
  expect(kept).toContain('integrations-hub')
100
100
  expect(kept).toContain('workspace-settings')
101
101
  expect(kept).toContain('model-config')
102
- // ...and the authoring / operator surfaces don't.
103
- expect(kept).not.toContain('build-pipeline')
104
- expect(kept).not.toContain('fragments')
102
+ // ...and so does every destination that is the SOLE route to its capability, however deep
103
+ // it feels: authoring a flow, the standards library, the PREnv/runner plumbing, and the
104
+ // aggregate run-health / spend views.
105
+ expect(kept).toContain('build-pipeline')
106
+ expect(kept).toContain('fragments')
107
+ expect(kept).toContain('infrastructure')
108
+ expect(kept).toContain('environment-setup')
109
+ expect(kept).toContain('operator-dashboard')
110
+ expect(kept).toContain('reports')
111
+ // What drops is beside the delivery path (experimentation) or a shortcut into a surface
112
+ // basic mode already reaches another way.
105
113
  expect(kept).not.toContain('sandbox')
106
- expect(kept).not.toContain('operator-dashboard')
114
+ expect(kept).not.toContain('kaizen')
115
+ expect(kept).not.toContain('merge-thresholds')
116
+ })
117
+
118
+ it('never makes an advanced item the only route to its capability', () => {
119
+ // The tier's governing rule (see the catalog comment): every advanced destination either
120
+ // sits beside the delivery path, or is a shortcut into a surface a basic destination also
121
+ // opens. Spelled out here as a table so promoting an item to `advanced` forces an explicit
122
+ // claim about how basic mode still reaches it, rather than a silent capability loss.
123
+ const ALTERNATIVE_ROUTE: Record<string, string> = {
124
+ sandbox: 'none needed - experimentation surface, not on the delivery path',
125
+ kaizen: 'none needed - self-grading history, not on the delivery path',
126
+ 'merge-thresholds': 'workspace-settings -> Merge tab',
127
+ 'service-fragment-defaults': 'workspace-settings -> Service best practices tab',
128
+ 'local-models': 'integrations-hub -> Local runners',
129
+ }
130
+ const advanced = NAV_CONTRIBUTIONS.filter((i) => i.advanced).map((i) => i.id)
131
+ expect(advanced.sort()).toEqual(Object.keys(ALTERNATIVE_ROUTE).sort())
107
132
  })
108
133
 
109
134
  it('keeps the tier switch itself reachable in basic mode', () => {
@@ -119,15 +144,19 @@ describe('navSlotFilter', () => {
119
144
  })
120
145
 
121
146
  it('keeps the tier and the permission axes independent — both must pass', () => {
122
- // `build-pipeline` is advanced AND needs board.write: neither axis alone reveals it.
147
+ // `sandbox` is advanced AND needs integrations.manage: neither axis alone reveals it.
123
148
  const advancedOnly: NavGates = { ...NO_GATES, advancedMode: true }
124
- expect(ids(navSlotFilter(slots(), { gates: advancedOnly }))).not.toContain('build-pipeline')
149
+ expect(ids(navSlotFilter(slots(), { gates: advancedOnly }))).not.toContain('sandbox')
125
150
 
126
- const permissionOnly: NavGates = { ...NO_GATES, canWriteBoard: true, advancedMode: false }
127
- expect(ids(navSlotFilter(slots(), { gates: permissionOnly }))).not.toContain('build-pipeline')
151
+ const permissionOnly: NavGates = {
152
+ ...NO_GATES,
153
+ canManageIntegrations: true,
154
+ advancedMode: false,
155
+ }
156
+ expect(ids(navSlotFilter(slots(), { gates: permissionOnly }))).not.toContain('sandbox')
128
157
 
129
- const both: NavGates = { ...NO_GATES, canWriteBoard: true, advancedMode: true }
130
- expect(ids(navSlotFilter(slots(), { gates: both }))).toContain('build-pipeline')
158
+ const both: NavGates = { ...NO_GATES, canManageIntegrations: true, advancedMode: true }
159
+ expect(ids(navSlotFilter(slots(), { gates: both }))).toContain('sandbox')
131
160
  })
132
161
 
133
162
  it('leaves at least one sidebar destination in every basic-mode section it keeps', () => {
@@ -153,13 +153,24 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
153
153
  * with accounts enabled).
154
154
  * - one icon per destination across shells.
155
155
  *
156
- * `advanced: true` marks the power-user half, hidden in basic interface mode. The line
157
- * drawn here is "would a team shipping their first task open this?": connecting a repo or
158
- * an integration, picking models, and the workspace/account settings stay; AUTHORING the
159
- * machinery those defaults come from (the pipeline builder, the fragment library, merge
160
- * thresholds, service fragment defaults), the experimentation surfaces (sandbox, Kaizen),
161
- * the infrastructure/environment plumbing, per-user local models, and the operator-tier
162
- * dashboards do not.
156
+ * `advanced: true` marks the power-user half, hidden in basic interface mode. The line is
157
+ * drawn by ROUTE COUNT, not by how advanced a surface feels: hiding the only way to reach a
158
+ * capability removes the capability from the tier, while hiding a shortcut to a surface basic
159
+ * mode already reaches removes nothing. So an item is advanced only when both of these hold —
160
+ * it is not the sole route to its capability, and nothing on the delivery path needs it:
161
+ *
162
+ * - `sandbox` / `kaizen` — experimentation and self-grading surfaces, beside the delivery
163
+ * path rather than on it.
164
+ * - `merge-thresholds` / `service-fragment-defaults` — palette shortcuts into Workspace
165
+ * settings tabs (Merge, Service best practices), which basic mode reaches via
166
+ * `workspace-settings`.
167
+ * - `local-models` — a per-user endpoint knob the Integrations hub already offers.
168
+ *
169
+ * Everything else stays in basic BECAUSE it is a sole route: authoring a flow
170
+ * (`build-pipeline`), the standards/skills library (`fragments`, whose only other route is a
171
+ * button two levels into Workspace settings), the PREnv + runner plumbing (`infrastructure`,
172
+ * `environment-setup`), and the operator/cost views that are the only aggregate read of run
173
+ * health and spend (`operator-dashboard`, `reports`).
163
174
  */
164
175
  export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
165
176
  {
@@ -167,7 +178,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
167
178
  labelKey: 'nav.buildPipeline',
168
179
  icon: 'i-lucide-workflow',
169
180
  surfaces: S('sidebar', 'command'),
170
- advanced: true,
171
181
  gate: (g) => g.canWriteBoard,
172
182
  action: 'buildPipeline',
173
183
  testId: 'nav-build-pipeline',
@@ -253,7 +263,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
253
263
  labelKey: 'nav.infrastructure',
254
264
  icon: 'i-lucide-server-cog',
255
265
  surfaces: S('sidebar'),
256
- advanced: true,
257
266
  gate: (g) => g.infrastructureAvailable,
258
267
  action: 'infrastructure',
259
268
  testId: 'nav-infrastructure',
@@ -264,7 +273,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
264
273
  labelKey: 'nav.environmentSetup',
265
274
  icon: 'i-lucide-flask-conical',
266
275
  surfaces: S('sidebar'),
267
- advanced: true,
268
276
  gate: (g) => g.infrastructureAvailable,
269
277
  action: 'environmentSetup',
270
278
  testId: 'nav-environment-setup',
@@ -275,7 +283,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
275
283
  labelKey: 'nav.contextFragments',
276
284
  icon: 'i-lucide-book-marked',
277
285
  surfaces: S('sidebar', 'command'),
278
- advanced: true,
279
286
  gate: (g) => g.libraryAvailable && g.canManageSettings,
280
287
  action: 'fragmentLibrary',
281
288
  testId: 'nav-fragments',
@@ -381,7 +388,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
381
388
  labelKey: 'nav.operatorDashboard',
382
389
  icon: 'i-lucide-gauge',
383
390
  surfaces: S('sidebar'),
384
- advanced: true,
385
391
  gate: (g) => g.accountsEnabled && g.isAccountAdmin,
386
392
  action: 'operatorDashboard',
387
393
  testId: 'nav-operator-dashboard',
@@ -392,7 +398,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
392
398
  labelKey: 'nav.reports',
393
399
  icon: 'i-lucide-chart-column',
394
400
  surfaces: S('sidebar'),
395
- advanced: true,
396
401
  gate: (g) => g.accountsEnabled && g.isAccountAdmin,
397
402
  action: 'reports',
398
403
  testId: 'nav-reports',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.169.0",
3
+ "version": "0.170.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",