@cat-factory/app 0.195.0 → 0.195.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.
Files changed (31) hide show
  1. package/app/components/board/nodes/BlockNode.vue +23 -4
  2. package/app/components/initiative/InitiativePlanDecision.vue +134 -0
  3. package/app/components/initiative/InitiativePlanNotice.vue +69 -0
  4. package/app/components/initiative/InitiativePlanReview.vue +278 -254
  5. package/app/components/initiative/InitiativeTrackerWindow.vue +72 -25
  6. package/app/components/judge/JudgeResultView.vue +5 -1
  7. package/app/components/merge/MergeEffortChips.vue +5 -1
  8. package/app/components/panels/ReportsPanel.vue +5 -2
  9. package/app/components/panels/ReportsSpendBreakdown.vue +5 -1
  10. package/app/components/panels/StepEffortReport.vue +11 -2
  11. package/app/components/panels/StepFragmentAdherence.vue +7 -2
  12. package/app/components/panels/StepMetadataCard.vue +4 -1
  13. package/app/components/pipeline/EstimateThresholdFields.vue +74 -0
  14. package/app/components/pipeline/OutputBudgetInput.vue +1 -0
  15. package/app/components/pipeline/PipelineBuilder.vue +48 -96
  16. package/app/components/settings/ConsensusGroupsSection.vue +12 -3
  17. package/app/utils/estimateGating.spec.ts +44 -0
  18. package/app/utils/estimateGating.ts +60 -0
  19. package/app/utils/initiative.spec.ts +43 -0
  20. package/app/utils/initiative.ts +23 -0
  21. package/i18n/locales/de.json +28 -4
  22. package/i18n/locales/en.json +28 -4
  23. package/i18n/locales/es.json +28 -4
  24. package/i18n/locales/fr.json +28 -4
  25. package/i18n/locales/he.json +28 -4
  26. package/i18n/locales/it.json +28 -4
  27. package/i18n/locales/ja.json +28 -4
  28. package/i18n/locales/pl.json +28 -4
  29. package/i18n/locales/tr.json +28 -4
  30. package/i18n/locales/uk.json +28 -4
  31. package/package.json +1 -1
@@ -75,6 +75,21 @@ const FRAME_LABEL_KEYS: Record<BlockStatus, string> = {
75
75
  done: 'board.frame.status.done',
76
76
  }
77
77
  const statusLabel = computed(() => t(FRAME_LABEL_KEYS[frameStatus.value]))
78
+ // The badge is a ROLLUP over the frame's children, not a status anybody set on the service, and
79
+ // the label alone never says which child produced it — "Needs attention" names no task, and
80
+ // "Live" reads as a health check rather than "caught up". So each status gets its own tooltip
81
+ // naming what it is rolled up from. `pr_ready`/`done` are unreachable here (`frameStatus` caps
82
+ // below `done`: a service is long-lived and never finishes) but the Record stays exhaustive over
83
+ // `BlockStatus` for the same reason the label map does, and mirrors that map's aliasing.
84
+ const FRAME_STATUS_HINT_KEYS: Record<BlockStatus, string> = {
85
+ planned: 'board.frame.statusHint.planned',
86
+ ready: 'board.frame.statusHint.ready',
87
+ in_progress: 'board.frame.statusHint.in_progress',
88
+ blocked: 'board.frame.statusHint.blocked',
89
+ pr_ready: 'board.frame.statusHint.pr_ready',
90
+ done: 'board.frame.statusHint.done',
91
+ }
92
+ const statusHint = computed(() => t(FRAME_STATUS_HINT_KEYS[frameStatus.value]))
78
93
 
79
94
  const selected = computed(() => ui.selectedBlockId === props.id)
80
95
  // Services are always expanded to their task canvas, at every zoom level: there is no
@@ -349,7 +364,7 @@ const ITEM_ICON: Record<string, string> = {
349
364
  </UBadge>
350
365
  </div>
351
366
  <div class="flex items-center justify-between">
352
- <UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
367
+ <UBadge :color="statusMeta.chip as any" variant="subtle" size="sm" :title="statusHint">{{
353
368
  statusLabel
354
369
  }}</UBadge>
355
370
  <span class="text-[11px] text-slate-400">{{
@@ -474,9 +489,13 @@ const ITEM_ICON: Record<string, string> = {
474
489
  </div>
475
490
  </div>
476
491
  <div class="flex items-center gap-1">
477
- <UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
478
- statusLabel
479
- }}</UBadge>
492
+ <UBadge
493
+ :color="statusMeta.chip as any"
494
+ variant="subtle"
495
+ size="sm"
496
+ :title="statusHint"
497
+ >{{ statusLabel }}</UBadge
498
+ >
480
499
  <!-- Board-authoring buttons (create task / from issue / recurring / initiative)
481
500
  are `board.write` — hidden for a read-only viewer, who keeps the badge +
482
501
  collapse toggle (view-only affordances). -->
@@ -0,0 +1,134 @@
1
+ <script setup lang="ts">
2
+ // The plan gate's DECISION half: the overall-feedback field plus the two commands that resolve the
3
+ // park — accept the plan, or send it back to the planner.
4
+ //
5
+ // It is its own component because the gate has two surfaces: the full document review
6
+ // (`InitiativePlanReview`, which owns the tracker window while a rendered plan is parked) and the
7
+ // compact notice a gate with no rendered plan falls back to (`InitiativePlanNotice`). Both must
8
+ // word the commands identically, gate them on the same RBAC fact, and refuse an empty send-back
9
+ // the same way — so the state machine and its markup live here once rather than in each.
10
+ //
11
+ // Deliberately NOT offered: "approve with corrections". The plan was ingested into the
12
+ // `initiatives` entity before this gate was raised, so the document is a VIEW of committed state —
13
+ // an edit typed over it would reach nothing, and the engine refuses it outright
14
+ // (`outputIsRendered` → 422). Requesting changes is the route for a correction, which is why an
15
+ // anchored comment is worth having: it quotes the planner's own text back to it on the re-plan.
16
+ import { computed, ref, watch } from 'vue'
17
+ import type { RequestStepChangesInput } from '@cat-factory/contracts'
18
+
19
+ const props = defineProps<{
20
+ /** The parked gate being resolved. */
21
+ approvalId: string
22
+ /** The run the gate belongs to, for the approve / request-changes commands. */
23
+ instanceId: string
24
+ /** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
25
+ canExecute: boolean
26
+ /**
27
+ * The anchored per-block comments to send with a send-back, from the surface that offers
28
+ * commenting. Absent on the notice surface, which has no document to anchor to.
29
+ */
30
+ comments?: RequestStepChangesInput['comments']
31
+ }>()
32
+
33
+ /** A send-back succeeded: the surface drops its anchored drafts (the feedback is cleared here). */
34
+ const emit = defineEmits<{ sent: [] }>()
35
+
36
+ const execution = useExecutionStore()
37
+ const { t } = useI18n()
38
+
39
+ const feedback = ref('')
40
+ const submitting = ref(false)
41
+
42
+ /** Changes can only be requested with something to act on — an empty send would re-plan blind. */
43
+ const canRequestChanges = computed(
44
+ () => !!feedback.value.trim() || (props.comments?.length ?? 0) > 0,
45
+ )
46
+
47
+ /** A fresh gate (a re-plan parked again) reviews the new plan clean, drafts dropped. */
48
+ watch(
49
+ () => props.approvalId,
50
+ () => {
51
+ feedback.value = ''
52
+ },
53
+ )
54
+
55
+ /**
56
+ * Accept the plan: the run advances to the committer, which persists it and arms the execution
57
+ * loop. The window stays open — the review disappears with the approval (live) and the tracker it
58
+ * gives the window back to is where the plan then executes.
59
+ */
60
+ async function approve() {
61
+ if (submitting.value || !props.canExecute) return
62
+ submitting.value = true
63
+ try {
64
+ await execution.approveStep(props.instanceId, props.approvalId)
65
+ } finally {
66
+ submitting.value = false
67
+ }
68
+ }
69
+
70
+ /** Send the plan back: the planner re-plans from the feedback + the anchored comments. */
71
+ async function requestChanges() {
72
+ if (submitting.value || !canRequestChanges.value || !props.canExecute) return
73
+ submitting.value = true
74
+ try {
75
+ const ok = await execution.requestStepChanges(props.instanceId, props.approvalId, {
76
+ feedback: feedback.value.trim() || undefined,
77
+ comments: props.comments,
78
+ })
79
+ if (ok) {
80
+ feedback.value = ''
81
+ emit('sent')
82
+ }
83
+ } finally {
84
+ submitting.value = false
85
+ }
86
+ }
87
+
88
+ const disabledTitle = computed(() => (props.canExecute ? undefined : t('access.noRunExecute')))
89
+ </script>
90
+
91
+ <template>
92
+ <div>
93
+ <UTextarea
94
+ v-model="feedback"
95
+ data-testid="initiative-plan-feedback"
96
+ :rows="2"
97
+ autoresize
98
+ size="sm"
99
+ class="w-full"
100
+ :placeholder="t('initiative.planReview.feedbackPlaceholder')"
101
+ />
102
+ <div class="mt-2 flex flex-wrap items-center gap-2">
103
+ <UButton
104
+ color="primary"
105
+ size="xs"
106
+ icon="i-lucide-check"
107
+ data-testid="initiative-plan-approve"
108
+ :loading="submitting"
109
+ :disabled="!canExecute"
110
+ :title="disabledTitle"
111
+ @click="approve"
112
+ >
113
+ {{ t('initiative.planReview.approve') }}
114
+ </UButton>
115
+ <UButton
116
+ color="warning"
117
+ variant="soft"
118
+ size="xs"
119
+ icon="i-lucide-rotate-ccw"
120
+ data-testid="initiative-plan-send-back"
121
+ :loading="submitting"
122
+ :disabled="!canRequestChanges || !canExecute"
123
+ :title="
124
+ canExecute && !canRequestChanges
125
+ ? t('initiative.planReview.needsFeedback')
126
+ : disabledTitle
127
+ "
128
+ @click="requestChanges"
129
+ >
130
+ {{ t('initiative.planReview.sendBack') }}
131
+ </UButton>
132
+ </div>
133
+ </div>
134
+ </template>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ // The plan gate with NO document to review: the compact rail that sits above the tracker's own
3
+ // sections, states that there is nothing to navigate, and still resolves the park.
4
+ //
5
+ // It is NOT a legacy shim for gates parked before the engine rendered plans. `outputIsRendered` is a
6
+ // fact about the STEP, and a plan gate can park without it today: the planner's post-completion
7
+ // resolver authors the rendering only once it has INGESTED the plan, so a run that reaches the gate
8
+ // without an ingest parks on the planner's transcript summary instead — a perfectly non-empty string
9
+ // that `planReviewDocument` correctly reads as no document (dressing one sentence up under a table
10
+ // of contents is the failure the whole review surface exists to end). Without this surface such a
11
+ // gate would have no resolving surface at all, which is the exact bug the plan-review e2e spec was
12
+ // written for.
13
+ //
14
+ // So it is deliberately NOT a takeover: unlike `InitiativePlanReview`, which replaces the tracker
15
+ // because the document repeats it, this one wants the tracker underneath — there the sections ARE
16
+ // the only rendering of the plan there is. Which is why it has to be TOLD whether they are actually
17
+ // on screen: it renders above the entity branch, because a gate can be parked while the entity is
18
+ // still loading, and pointing a reviewer at sections the window is not showing is worse than saying
19
+ // nothing.
20
+ import type { StepApproval } from '~/types/execution'
21
+ import InitiativePlanDecision from '~/components/initiative/InitiativePlanDecision.vue'
22
+
23
+ defineProps<{
24
+ /** The parked gate. */
25
+ approval: StepApproval
26
+ /** The run the gate belongs to, for the approve / request-changes commands. */
27
+ instanceId: string
28
+ /** Whether the viewer may resolve runs at all (RBAC); false renders the actions disabled. */
29
+ canExecute: boolean
30
+ /**
31
+ * Whether the tracker's own goal / phases / policy sections are rendered below this notice — i.e.
32
+ * whether the initiative entity has loaded. Only then may the notice point at them as the plan.
33
+ */
34
+ hasSections: boolean
35
+ }>()
36
+
37
+ const { t } = useI18n()
38
+ </script>
39
+
40
+ <template>
41
+ <section
42
+ class="mb-4 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3.5"
43
+ data-testid="initiative-plan-notice"
44
+ >
45
+ <header class="flex items-start gap-2.5">
46
+ <UIcon name="i-lucide-clipboard-check" class="mt-0.5 h-4 w-4 shrink-0 text-amber-300" />
47
+ <div class="min-w-0 flex-1">
48
+ <h3 class="text-[13px] font-semibold text-amber-200">
49
+ {{ t('initiative.planReview.title') }}
50
+ </h3>
51
+ <p class="mt-0.5 text-[12px] leading-relaxed text-amber-100/80">
52
+ {{ t('initiative.planReview.body') }}
53
+ </p>
54
+ <p class="mt-2 text-[12px] leading-relaxed text-amber-100/70">
55
+ {{ t('initiative.planReview.noDocument') }}
56
+ <template v-if="hasSections">
57
+ {{ t('initiative.planReview.noDocumentSections') }}
58
+ </template>
59
+ </p>
60
+ </div>
61
+ </header>
62
+ <InitiativePlanDecision
63
+ class="mt-3"
64
+ :approval-id="approval.id"
65
+ :instance-id="instanceId"
66
+ :can-execute="canExecute"
67
+ />
68
+ </section>
69
+ </template>