@cat-factory/app 0.216.0 → 0.217.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 (36) hide show
  1. package/README.md +50 -4
  2. package/app/components/board/AddTaskModal.vue +54 -78
  3. package/app/components/board/CreateInitiativeModal.vue +8 -6
  4. package/app/components/{board/InitiativePresetFields.vue → common/DescriptorFields.vue} +57 -63
  5. package/app/components/inputGate/InputGateNotice.vue +1 -1
  6. package/app/components/panels/AgentStepDetail.vue +2 -2
  7. package/app/components/panels/inspector/ServiceTestConfig.vue +1 -1
  8. package/app/components/panels/inspector/TaskExecution.vue +2 -2
  9. package/app/components/settings/WorkspaceSettingsPanel.vue +1 -1
  10. package/app/composables/api/inputGate.ts +1 -1
  11. package/app/modular/nav-contributions.spec.ts +1 -1
  12. package/app/modular/nav-contributions.ts +1 -1
  13. package/app/stores/inputGate.ts +1 -1
  14. package/app/stores/ui/resultViews.ts +1 -1
  15. package/app/types/domain.ts +6 -0
  16. package/app/utils/descriptorFields.spec.ts +126 -0
  17. package/app/utils/descriptorFields.ts +104 -0
  18. package/app/utils/initiative.ts +0 -30
  19. package/app/utils/inputGate.ts +1 -1
  20. package/app/utils/pipelineRender.spec.ts +1 -1
  21. package/app/utils/pipelineRender.ts +1 -1
  22. package/i18n/i18n.config.ts +7 -21
  23. package/i18n/locales/de.json +3 -2
  24. package/i18n/locales/en.json +6 -2
  25. package/i18n/locales/es.json +18 -5
  26. package/i18n/locales/fr.json +18 -5
  27. package/i18n/locales/he.json +88 -86
  28. package/i18n/locales/it.json +3 -2
  29. package/i18n/locales/ja.json +3 -2
  30. package/i18n/locales/pl.json +21 -8
  31. package/i18n/locales/tr.json +3 -2
  32. package/i18n/locales/uk.json +21 -8
  33. package/i18n/plural-forms.spec.ts +76 -0
  34. package/i18n/plural-rules.spec.ts +98 -0
  35. package/i18n/plural-rules.ts +123 -0
  36. package/package.json +2 -2
@@ -111,7 +111,7 @@ describe('navSlotFilter', () => {
111
111
  // Model providers are NOT an integration: the split is what makes the engines findable.
112
112
  expect(kept).toContain('model-providers')
113
113
  // ...and so does everything the everyday delivery loop runs on, however deep it feels:
114
- // authoring a flow, the standards library, and the PREnv/runner plumbing.
114
+ // authoring a flow, the standards library, and the ephemeral-env/runner plumbing.
115
115
  expect(kept).toContain('build-pipeline')
116
116
  expect(kept).toContain('fragments')
117
117
  // Also the only route to the guided per-service Compose environment setup, which folded
@@ -279,7 +279,7 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
279
279
  *
280
280
  * Everything else stays in basic because the delivery loop needs it: authoring a flow
281
281
  * (`build-pipeline`), adding a repo (`add-from-repo`), the standards/skills library
282
- * (`fragments`), the PREnv + runner plumbing (`infrastructure`, which is also the only route
282
+ * (`fragments`), the ephemeral-env + runner plumbing (`infrastructure`, which is also the only route
283
283
  * to the guided per-service Compose environment setup), and the workspace/model configuration
284
284
  * a run actually reads (`workspace-settings`, `model-config`).
285
285
  */
@@ -6,7 +6,7 @@ import { useWorkspaceStore } from '~/stores/workspace'
6
6
  import { useExecutionStore } from '~/stores/execution'
7
7
 
8
8
  /**
9
- * The PRE-TOKEN INPUT GATE's action surface. The verdict itself lives on the run
9
+ * The PRE-DISPATCH INPUT GATE's action surface. The verdict itself lives on the run
10
10
  * (`instance.inputGate`) and is kept fresh by the execution stream, so this store only wraps the
11
11
  * `resolve` action, tracks the in-flight state so the notice can disable its buttons, and
12
12
  * reflects the returned verdict back so the UI updates before the stream echoes it.
@@ -86,7 +86,7 @@ export function createUiResultViews() {
86
86
  : step
87
87
  ? (park ?? agentKindMeta(step.agentKind).resultView)
88
88
  : undefined
89
- // The PRE-TOKEN INPUT GATE is the one dedicated park with no window of its own: it is
89
+ // The PRE-DISPATCH INPUT GATE is the one dedicated park with no window of its own: it is
90
90
  // answered by an inline notice, which the generic step detail renders. Routing to the
91
91
  // step's usual result view instead would open a window about work that has not run.
92
92
  if (park === 'input-gate') {
@@ -67,6 +67,12 @@ export type {
67
67
  TaskTypePresentation,
68
68
  TaskTypeFieldDescriptor,
69
69
  TaskTypeFieldOption,
70
+ // The shared descriptor-driven form vocabulary (`contracts/src/form-fields.ts`): one field
71
+ // shape and one filled-value bag behind both the initiative-preset form and a custom task
72
+ // type's per-case form, so `DescriptorFields.vue` renders either.
73
+ DescriptorField,
74
+ DescriptorFieldValue,
75
+ DescriptorFieldValues,
70
76
  Pipeline,
71
77
  PipelinePurpose,
72
78
  SpendStatus,
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { DescriptorField } from '~/types/domain'
3
+ import {
4
+ defaultDescriptorValues,
5
+ descriptorGroupValue,
6
+ setDescriptorCheckbox,
7
+ setDescriptorValue,
8
+ toggleDescriptorGroupValue,
9
+ } from './descriptorFields'
10
+
11
+ const field = (over: Partial<DescriptorField> & Pick<DescriptorField, 'key'>): DescriptorField => ({
12
+ label: over.key,
13
+ ...over,
14
+ })
15
+
16
+ describe('defaultDescriptorValues', () => {
17
+ it('seeds each declared default in its own contract shape', () => {
18
+ // The form model is the wire shape, so a default has to arrive typed: the shared validator
19
+ // refuses a `'3'` where a `number` field is declared, and would refuse it at create time too.
20
+ expect(
21
+ defaultDescriptorValues([
22
+ field({ key: 'style', type: 'select', default: 'collection' }),
23
+ field({ key: 'depth', type: 'number', default: '3' }),
24
+ field({ key: 'gate', type: 'checkbox', default: 'true' }),
25
+ field({ key: 'ops', type: 'checkbox-group', defaultValues: ['create', 'list'] }),
26
+ field({ key: 'dir', type: 'path', default: 'docs' }),
27
+ ]),
28
+ ).toEqual({
29
+ style: 'collection',
30
+ depth: 3,
31
+ gate: true,
32
+ ops: ['create', 'list'],
33
+ dir: 'docs',
34
+ })
35
+ })
36
+
37
+ it('leaves a field with no meaningful default ABSENT rather than blank', () => {
38
+ // Absent is what validation reads as unset, so seeding `''`/`[]`/`false` would both freeze an
39
+ // empty answer and (for a required field) look filled to nothing that checks it.
40
+ expect(
41
+ defaultDescriptorValues([
42
+ field({ key: 'entity', type: 'text' }),
43
+ field({ key: 'notes', type: 'textarea', default: '' }),
44
+ field({ key: 'gate', type: 'checkbox' }),
45
+ field({ key: 'gateOff', type: 'checkbox', default: 'false' }),
46
+ field({ key: 'ops', type: 'checkbox-group', defaultValues: [] }),
47
+ field({ key: 'depth', type: 'number', default: 'not-a-number' }),
48
+ ]),
49
+ ).toEqual({})
50
+ })
51
+
52
+ it('copies a multi-select default, so editing the form cannot mutate the descriptor', () => {
53
+ const ops = field({ key: 'ops', type: 'checkbox-group', defaultValues: ['create'] })
54
+ const seeded = defaultDescriptorValues([ops])
55
+ ;(seeded.ops as string[]).push('delete')
56
+ expect(ops.defaultValues).toEqual(['create'])
57
+ })
58
+ })
59
+
60
+ describe('setDescriptorValue', () => {
61
+ it('drops a value the shared rules read as unset rather than freezing it', () => {
62
+ // Absent is what `validateDescriptorFields` treats as unfilled and what
63
+ // `sanitizeDescriptorFields` refuses to freeze, so the form must not hold one either: a
64
+ // cleared field that stayed as `''`/`[]`/`false` would reach the wire as a collected answer.
65
+ expect(setDescriptorValue({ entity: 'Order' }, 'entity', '')).toEqual({})
66
+ expect(setDescriptorValue({ ops: ['create'] }, 'ops', [])).toEqual({})
67
+ expect(setDescriptorValue({ gate: true }, 'gate', false)).toEqual({})
68
+ expect(setDescriptorValue({ entity: 'Order' }, 'entity', undefined)).toEqual({})
69
+ })
70
+
71
+ it('keeps a numeric 0 but drops a half-typed number', () => {
72
+ // `0` is a real answer. `NaN` is what `Number('')`/a partial entry yields, and it serialises to
73
+ // `null` on the wire, which the value schema refuses: the submit would fail with a raw schema
74
+ // error naming nothing the user typed.
75
+ expect(setDescriptorValue({}, 'depth', 0)).toEqual({ depth: 0 })
76
+ expect(setDescriptorValue({ depth: 3 }, 'depth', Number.NaN)).toEqual({})
77
+ })
78
+
79
+ it('does not mutate the bag it was given', () => {
80
+ const before = { entity: 'Order' }
81
+ expect(setDescriptorValue(before, 'style', 'action')).toEqual({
82
+ entity: 'Order',
83
+ style: 'action',
84
+ })
85
+ expect(before).toEqual({ entity: 'Order' })
86
+ })
87
+ })
88
+
89
+ describe('setDescriptorCheckbox', () => {
90
+ it('persists an explicit false ONLY for a default-ON toggle', () => {
91
+ // A default-ON checkbox is the one field where absent and `false` are opposite facts: a
92
+ // consumer reads the opt-out as `inputs[key] !== false` (the tech-migration preset's
93
+ // `humanReview`), so dropping the `false` would make the toggle dead.
94
+ const onByDefault = field({ key: 'humanReview', type: 'checkbox', default: 'true' })
95
+ expect(setDescriptorCheckbox({}, onByDefault, false)).toEqual({ humanReview: false })
96
+ expect(setDescriptorCheckbox({}, onByDefault, true)).toEqual({ humanReview: true })
97
+ // A default-OFF checkbox never freezes a redundant `false`: absent already means unchecked.
98
+ const offByDefault = field({ key: 'breaking', type: 'checkbox' })
99
+ expect(setDescriptorCheckbox({ breaking: true }, offByDefault, false)).toEqual({})
100
+ })
101
+ })
102
+
103
+ describe('toggleDescriptorGroupValue', () => {
104
+ it('adds, removes and de-duplicates without freezing an empty selection', () => {
105
+ expect(toggleDescriptorGroupValue({}, 'ops', 'create', true)).toEqual({ ops: ['create'] })
106
+ expect(toggleDescriptorGroupValue({ ops: ['create'] }, 'ops', 'create', true)).toEqual({
107
+ ops: ['create'],
108
+ })
109
+ expect(toggleDescriptorGroupValue({ ops: ['create', 'list'] }, 'ops', 'create', false)).toEqual(
110
+ {
111
+ ops: ['list'],
112
+ },
113
+ )
114
+ // Unchecking the last option leaves the key ABSENT, not an empty array.
115
+ expect(toggleDescriptorGroupValue({ ops: ['create'] }, 'ops', 'create', false)).toEqual({})
116
+ })
117
+
118
+ it('reads a wrong-shaped stored value as an empty selection', () => {
119
+ // A bag can arrive from a probe prefill or a since-changed descriptor, so the reader narrows
120
+ // rather than assuming: a scalar under a multi-select key must not throw in the renderer.
121
+ expect(descriptorGroupValue({ ops: 'create' }, 'ops')).toEqual([])
122
+ expect(toggleDescriptorGroupValue({ ops: 'create' }, 'ops', 'list', true)).toEqual({
123
+ ops: ['list'],
124
+ })
125
+ })
126
+ })
@@ -0,0 +1,104 @@
1
+ import type { DescriptorField, DescriptorFieldValue, DescriptorFieldValues } from '~/types/domain'
2
+
3
+ // Form-side helpers over the shared descriptor-field vocabulary (`contracts/src/form-fields.ts`),
4
+ // used by every surface that renders one through `DescriptorFields.vue`: an initiative preset's
5
+ // create form and a reusable operation's per-case form on a custom task type.
6
+ //
7
+ // The RULES (visibility, validation, sanitization, prose rendering) live in contracts, because the
8
+ // server has to agree about them. What lives here is what only a FORM decides: which values to
9
+ // start it with, and how one edit changes the bag. Both are pure functions over the value bag
10
+ // rather than methods inside the SFC, so the mutation rules a wrong answer would freeze on an
11
+ // entity are unit-testable without mounting a component.
12
+
13
+ /**
14
+ * The initial, typed values a field list implies: its declared DEFAULTS folded into the
15
+ * `DescriptorFieldValues` shape the renderer and the wire contract expect (`checkbox-group` to
16
+ * `string[]`, `checkbox` to a boolean, `number` to a number, everything else a string). Only fields
17
+ * with a meaningful default are seeded, so an unfilled optional field stays absent (which is what
18
+ * validation reads as unset) and never freezes an empty value. A repo-detection probe's prefill and
19
+ * the user's own edits layer on top.
20
+ */
21
+ export function defaultDescriptorValues(fields: readonly DescriptorField[]): DescriptorFieldValues {
22
+ const values: DescriptorFieldValues = {}
23
+ for (const field of fields) {
24
+ if (field.type === 'checkbox-group') {
25
+ if (field.defaultValues?.length) values[field.key] = [...field.defaultValues]
26
+ } else if (field.type === 'checkbox') {
27
+ if (field.default === 'true') values[field.key] = true
28
+ } else if (field.type === 'number') {
29
+ const parsed = Number(field.default)
30
+ if (field.default !== undefined && field.default !== '' && Number.isFinite(parsed)) {
31
+ values[field.key] = parsed
32
+ }
33
+ } else if (field.default) {
34
+ values[field.key] = field.default
35
+ }
36
+ }
37
+ return values
38
+ }
39
+
40
+ /**
41
+ * A value that must stay ABSENT from the bag rather than freeze on the entity: an unchecked
42
+ * (`false`) checkbox, a blank string, an empty multi-select, or a number that is not one (a
43
+ * half-typed `number` input reads as `NaN`, which serialises to `null` on the wire and is refused
44
+ * by the value schema, so the form must never carry it). A numeric `0` is a real answer and is kept.
45
+ *
46
+ * The same judgement the shared `validateDescriptorFields` / `sanitizeDescriptorFields` make about
47
+ * an unset value, applied at the moment of the edit so the model never holds one at all.
48
+ */
49
+ function isEmptyDescriptorValue(value: DescriptorFieldValue): boolean {
50
+ if (typeof value === 'number') return !Number.isFinite(value)
51
+ return value === false || value === '' || (Array.isArray(value) && value.length === 0)
52
+ }
53
+
54
+ /**
55
+ * One field's value set immutably on the bag, DROPPING an empty one so a cleared field never
56
+ * freezes an empty `''`/`[]`/`false` (mirroring `ProviderConnectionTab`'s delete-when-blank).
57
+ */
58
+ export function setDescriptorValue(
59
+ values: DescriptorFieldValues,
60
+ key: string,
61
+ value: DescriptorFieldValue | undefined,
62
+ ): DescriptorFieldValues {
63
+ const next = { ...values }
64
+ if (value === undefined || isEmptyDescriptorValue(value)) delete next[key]
65
+ else next[key] = value
66
+ return next
67
+ }
68
+
69
+ /**
70
+ * A checkbox's value set on the bag. A checkbox whose descriptor default is ON (`default: 'true'`)
71
+ * must be able to persist an explicit `false`: {@link setDescriptorValue} otherwise drops it (an off
72
+ * box "stays unset"), which for a default-ON field is indistinguishable from "untouched, still on",
73
+ * so a consumer reading the opt-out as `humanReview !== false` (`seedMigrationPlan`) could never
74
+ * observe the unchecked state and the toggle would be dead. A default-OFF checkbox keeps the
75
+ * drop-when-false behaviour (absent === unchecked), so it never freezes a redundant `false`.
76
+ */
77
+ export function setDescriptorCheckbox(
78
+ values: DescriptorFieldValues,
79
+ field: DescriptorField,
80
+ checked: boolean,
81
+ ): DescriptorFieldValues {
82
+ if (!checked && field.default === 'true') return { ...values, [field.key]: false }
83
+ return setDescriptorValue(values, field.key, checked)
84
+ }
85
+
86
+ /** One `checkbox-group` field's current value, as the `string[]` the renderer and wire expect. */
87
+ export function descriptorGroupValue(values: DescriptorFieldValues, key: string): string[] {
88
+ const value = values[key]
89
+ return Array.isArray(value) ? value : []
90
+ }
91
+
92
+ /** One option toggled on/off in a `checkbox-group` field's value (deduped, order-preserving). */
93
+ export function toggleDescriptorGroupValue(
94
+ values: DescriptorFieldValues,
95
+ key: string,
96
+ option: string,
97
+ checked: boolean,
98
+ ): DescriptorFieldValues {
99
+ const current = descriptorGroupValue(values, key)
100
+ const next = checked
101
+ ? [...new Set([...current, option])]
102
+ : current.filter((entry) => entry !== option)
103
+ return setDescriptorValue(values, key, next)
104
+ }
@@ -4,8 +4,6 @@ import type {
4
4
  InitiativeItem,
5
5
  InitiativeItemStatus,
6
6
  InitiativePhase,
7
- InitiativePresetDescriptor,
8
- InitiativePresetInputs,
9
7
  InitiativeQa,
10
8
  InitiativeStatus,
11
9
  } from '~/types/domain'
@@ -140,34 +138,6 @@ export const INITIATIVE_FOLLOWUP_STATUS_CHIPS: Record<InitiativeFollowUp['status
140
138
  dismissed: 'neutral',
141
139
  }
142
140
 
143
- /**
144
- * The initial, typed create-form values a preset descriptor implies — its field DEFAULTS folded
145
- * into the `InitiativePresetInputs` shape the renderer + wire contract expect (`checkbox-group` →
146
- * `string[]`, `checkbox` → boolean, `number` → number, everything else a string). Only fields with
147
- * a meaningful default are seeded, so unfilled optional fields stay absent (equivalent to unset for
148
- * validation) and never freeze an empty value. The probe prefill and the user's edits layer on top.
149
- */
150
- export function defaultPresetInputs(
151
- descriptor: InitiativePresetDescriptor,
152
- ): InitiativePresetInputs {
153
- const inputs: InitiativePresetInputs = {}
154
- for (const field of descriptor.fields) {
155
- if (field.type === 'checkbox-group') {
156
- if (field.defaultValues?.length) inputs[field.key] = [...field.defaultValues]
157
- } else if (field.type === 'checkbox') {
158
- if (field.default === 'true') inputs[field.key] = true
159
- } else if (field.type === 'number') {
160
- const parsed = Number(field.default)
161
- if (field.default !== undefined && field.default !== '' && Number.isFinite(parsed)) {
162
- inputs[field.key] = parsed
163
- }
164
- } else if (field.default) {
165
- inputs[field.key] = field.default
166
- }
167
- }
168
- return inputs
169
- }
170
-
171
141
  /** Completion rollup across an initiative's items, or null when there are none. */
172
142
  export function initiativeProgress(
173
143
  items: InitiativeItem[] | undefined,
@@ -1,4 +1,4 @@
1
- // Which PRE-TOKEN INPUT GATE verdicts a run surfaces, and how they are presented.
1
+ // Which PRE-DISPATCH INPUT GATE verdicts a run surfaces, and how they are presented.
2
2
  //
3
3
  // The gate records a verdict for EVERY disposition, including the ones where it did nothing, so
4
4
  // "has a verdict" is not the same question as "has something to tell a human". This is the one
@@ -79,7 +79,7 @@ describe('dedicatedParkView', () => {
79
79
  expect(dedicatedParkView(step({}), run())).toBeNull()
80
80
  })
81
81
 
82
- // The PRE-TOKEN INPUT GATE parks whatever step 0 happens to be and leaves nothing
82
+ // The PRE-DISPATCH INPUT GATE parks whatever step 0 happens to be and leaves nothing
83
83
  // kind-specific on the step, so it is recognised off the RUN. The generic approve resolver
84
84
  // refuses it server-side: approving it would mark the run's first working step done and skip
85
85
  // the work the run exists to do.
@@ -150,7 +150,7 @@ export function dedicatedParkView(
150
150
  step: PipelineStep,
151
151
  instance: ExecutionInstance | null | undefined,
152
152
  ): 'follow-ups' | 'fork-decision' | 'input-gate' | null {
153
- // The PRE-TOKEN INPUT GATE parks whatever step 0 happens to be, so it leaves nothing on the
153
+ // The PRE-DISPATCH INPUT GATE parks whatever step 0 happens to be, so it leaves nothing on the
154
154
  // STEP to recognise it by: its verdict is a fact about the RUN. Checked first, and off the
155
155
  // instance: approving it generically would mark the run's first working step done and skip
156
156
  // the work the run exists to do.
@@ -4,32 +4,18 @@
4
4
  //
5
5
  // Locale MESSAGES are NOT defined here — they live in `i18n/locales/*.json` so the
6
6
  // module can deep-merge them across the `extends` layer chain. This file carries only
7
- // the runtime vue-i18n behaviour (fallback, number/date formats) shared by every locale.
8
- // Slavic one/few/many plural selector (CLDR rule for Polish & Ukrainian), returning the
9
- // 0|1|2 index into a 3-form `"one | few | many"` message. vue-i18n's BUILT-IN pluralizer
10
- // only ever picks index 0 (n===1) or 1/2 by a non-Slavic rule, so without this the pl/uk
11
- // 3-form catalog entries (e.g. board.toolbar.decisionWord "decyzja | decyzje | decyzji")
12
- // render the WRONG form for counts like 2-4 and 22-24. `choicesLength` is unused — the
13
- // three forms are assumed; en/es/fr keep the default 2-form behaviour (not listed here).
14
- const slavicPluralRule = (choice: number): number => {
15
- const n = Math.abs(choice)
16
- const mod10 = n % 10
17
- const mod100 = n % 100
18
- if (n === 1) return 0 // one
19
- if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 1 // few
20
- return 2 // many (incl. 0, 5-21, …)
21
- }
7
+ // the runtime vue-i18n behaviour (fallback, plural selectors, number/date formats)
8
+ // shared by every locale.
9
+ import { pluralRules } from './plural-rules'
22
10
 
23
11
  export default defineI18nConfig(() => ({
24
12
  legacy: false,
25
13
  fallbackLocale: 'en',
26
14
 
27
- // Per-locale plural selectors. Only the Slavic locales need overriding; the others use
28
- // vue-i18n's default (correct for their 2-form catalogs).
29
- pluralRules: {
30
- pl: slavicPluralRule,
31
- uk: slavicPluralRule,
32
- },
15
+ // Per-locale plural selectors for the locales vue-i18n's built-in pluralizer gets wrong
16
+ // (Slavic one/few/many, Hebrew one/two/other). Everything else keeps the default. The
17
+ // slot contract each catalog entry declares by its form count lives in `plural-rules.ts`.
18
+ pluralRules,
33
19
 
34
20
  // Locale-aware number/currency formatting. Use `$n(value, 'currency')` etc. at call
35
21
  // sites instead of a raw `Intl.NumberFormat`; `$n`/`$d` are thin `Intl` wrappers so
@@ -2671,6 +2671,7 @@
2671
2671
  "continue": "Weiter",
2672
2672
  "submit": "Aufgabe hinzufügen",
2673
2673
  "addFailedTitle": "Aufgabe konnte nicht hinzugefügt werden",
2674
+ "customFieldsInvalid": "Das Formular dieses Aufgabentyps hat sich seit dem Öffnen geändert, daher passen die Antworten nicht mehr zu den Angaben, die es verlangt. Schließen Sie den Dialog und öffnen Sie ihn erneut, um die aktuellen Felder auszufüllen.",
2674
2675
  "linkFailed": "Aufgabe hinzugefügt, aber {count} Anhang konnte nicht verknüpft werden | Aufgabe hinzugefügt, aber {count} Anhänge konnten nicht verknüpft werden",
2675
2676
  "review": {
2676
2677
  "prUrl": "Pull Request",
@@ -4814,7 +4815,6 @@
4814
4815
  "titlePlaceholder": "z. B. Die API auf das neue Auth-Modell migrieren",
4815
4816
  "goalField": "Ziel",
4816
4817
  "goalPlaceholder": "Beschreiben Sie das Ziel, die Einschränkungen und den groben Umfang. Der Planer verfeinert dies zu einem mehrphasigen Plan.",
4817
- "pathInvalid": "Geben Sie einen Pfad innerhalb des Repositorys an (kein \"..\", keine absoluten Pfade und keine Backslashes).",
4818
4818
  "hint": "Es läuft noch nichts: Führen Sie nach dem Erstellen die Initiative-Planning-Pipeline auf dem Block aus. Sie analysiert die Codebasis und entwirft den mehrphasigen Plan zu Ihrer Freigabe.",
4819
4819
  "submit": "Initiative erstellen",
4820
4820
  "failedTitle": "Die Initiative konnte nicht erstellt werden",
@@ -5027,7 +5027,8 @@
5027
5027
  "confirm": "Verwerfen",
5028
5028
  "keep": "Weiter bearbeiten"
5029
5029
  },
5030
- "edit": "Bearbeiten"
5030
+ "edit": "Bearbeiten",
5031
+ "pathInvalid": "Geben Sie einen Pfad innerhalb des Repositorys an (kein \"..\", keine absoluten Pfade und keine Backslashes)."
5031
5032
  },
5032
5033
  "access": {
5033
5034
  "noBoardWrite": "Nur-Lese-Zugriff: Sie können dieses Board ansehen, aber nicht bearbeiten.",
@@ -98,7 +98,8 @@
98
98
  "confirm": "Discard",
99
99
  "keep": "Keep editing"
100
100
  },
101
- "edit": "Edit"
101
+ "edit": "Edit",
102
+ "pathInvalid": "Enter a path inside the repository (no \"..\", absolute paths, or backslashes)."
102
103
  },
103
104
  "access": {
104
105
  "noBoardWrite": "Read-only access: you can view this board but can't edit it.",
@@ -325,6 +326,10 @@
325
326
  "continue": "Continue",
326
327
  "submit": "Add task",
327
328
  "addFailedTitle": "Could not add task",
329
+ "customFieldsInvalid": "This task type's form has changed since you opened it, so the answers no longer match what it asks for. Close and reopen the dialog to fill in the current fields.",
330
+ "@customFieldsInvalid": {
331
+ "description": "Shown when the server refuses a created task because the collected values for a deployment-registered custom task type contradict the type's declared form (a required answer missing, a value outside its options). In practice this means the deployment re-registered the type while the dialog was open."
332
+ },
328
333
  "linkFailed": "Task added, but {count} attachment could not be linked | Task added, but {count} attachments could not be linked",
329
334
  "@linkFailed": {
330
335
  "description": "Count-based: how many context attachments (docs/issues) failed to link after the task was created (count is always >= 1). Provide ALL plural forms your language needs (English has 2; Polish/Ukrainian need 3 - one/few/many - via the custom pluralRules in i18n.config.ts)."
@@ -6137,7 +6142,6 @@
6137
6142
  "titlePlaceholder": "e.g. Migrate the API to the new auth model",
6138
6143
  "goalField": "Goal",
6139
6144
  "goalPlaceholder": "Describe the goal, constraints and rough scope. The planner refines this into a multi-phase plan.",
6140
- "pathInvalid": "Enter a path inside the repository (no \"..\", absolute paths, or backslashes).",
6141
6145
  "hint": "Nothing runs yet: after creating, run the Initiative Planning pipeline on the block. It analyses the codebase and drafts the multi-phase plan for your approval.",
6142
6146
  "submit": "Create initiative",
6143
6147
  "failedTitle": "Could not create the initiative",
@@ -83,7 +83,8 @@
83
83
  "confirm": "Descartar",
84
84
  "keep": "Seguir editando"
85
85
  },
86
- "edit": "Editar"
86
+ "edit": "Editar",
87
+ "pathInvalid": "Introduce una ruta dentro del repositorio (sin \"..\", rutas absolutas ni barras invertidas)."
87
88
  },
88
89
  "access": {
89
90
  "noBoardWrite": "Acceso de solo lectura: puedes ver este tablero pero no editarlo.",
@@ -298,6 +299,7 @@
298
299
  "continue": "Continuar",
299
300
  "submit": "Añadir tarea",
300
301
  "addFailedTitle": "No se pudo añadir la tarea",
302
+ "customFieldsInvalid": "El formulario de este tipo de tarea ha cambiado desde que lo abriste, por lo que las respuestas ya no coinciden con lo que pide. Cierra y vuelve a abrir el diálogo para rellenar los campos actuales.",
301
303
  "linkFailed": "Tarea añadida, pero no se pudo vincular {count} adjunto | Tarea añadida, pero no se pudieron vincular {count} adjuntos",
302
304
  "review": {
303
305
  "prUrl": "Pull request",
@@ -3224,13 +3226,25 @@
3224
3226
  "executionBackend": {
3225
3227
  "label": "Dónde se ejecutan los agentes",
3226
3228
  "local-docker": "Docker local (host)",
3229
+ "local-dockerDesc": "Ejecuta cada agente en un contenedor sobre el daemon de Docker de esta máquina.",
3227
3230
  "cloudflare-containers": "Cloudflare Containers (integrado)",
3228
- "runner-pool": "Pool de ejecutores autoalojado"
3231
+ "cloudflare-containersDesc": "Ejecuta los agentes en la plataforma de contenedores por ejecución integrada de Cloudflare.",
3232
+ "kubernetes": "Clúster de Kubernetes",
3233
+ "kubernetesDesc": "Ejecuta cada agente como un pod en un clúster de Kubernetes que tú gestionas.",
3234
+ "runner-pool": "Pool de ejecutores personalizado (HTTP)",
3235
+ "runner-poolDesc": "Envía los agentes a tu propio planificador detrás de una API de manifiestos HTTP.",
3236
+ "k3s": "Kubernetes local (k3s)",
3237
+ "k3sDesc": "Precargado para un clúster k3s/k3d local en esta máquina: solo tienes que pegar un token."
3229
3238
  },
3230
3239
  "testEnvBackend": {
3231
3240
  "label": "Dónde se ejecutan los entornos de prueba",
3232
- "local-compose": "docker-compose en contenedor",
3233
- "environment-provider": "Proveedor de entornos",
3241
+ "local-compose": "docker-compose en contenedor (dependencias del Tester)",
3242
+ "local-composeDesc": "Levanta las dependencias del Tester con docker-compose dentro del contenedor de la ejecución. Para un entorno de vista previa completo de la app, usa la opción de entorno de vista previa de Docker Compose de abajo.",
3243
+ "kubernetes": "Clúster de Kubernetes",
3244
+ "kubernetesDesc": "Aprovisiona un namespace por PR en un clúster de Kubernetes que tú gestionas. Ideal para apps nativas de Kubernetes o si ya tienes un clúster en marcha.",
3245
+ "environment-provider": "Proveedor HTTP personalizado",
3246
+ "environment-providerDesc": "Aprovisiona entornos efímeros a través de tu propia API de gestión HTTP. Ideal si ya cuentas con herramientas propias de entornos de vista previa.",
3247
+ "compose": "Entorno de vista previa de Docker Compose",
3234
3248
  "composeDesc": "Levanta el docker-compose.yml del propio repositorio en un daemon de Docker local como URL del Tester. Ideal para apps locales basadas en Compose. Admite imágenes prediseñadas o compilación desde el código fuente; requiere un daemon de Docker, por lo que solo en despliegues locales."
3235
3249
  },
3236
3250
  "dockerComposeInfo": "Los servicios de Docker Compose se levantan en el Docker local del runtime: no se necesita conexión (el contenedor de la ejecución debe admitir Docker-in-Docker). Configura el servicio, el puerto y el origen de la imagen (usar prediseñada o compilar desde el código fuente) en los ajustes de entorno del servicio.",
@@ -5930,7 +5944,6 @@
5930
5944
  "titlePlaceholder": "p. ej. Migrar la API al nuevo modelo de autenticacion",
5931
5945
  "goalField": "Objetivo",
5932
5946
  "goalPlaceholder": "Describe el objetivo, las restricciones y el alcance aproximado. El planificador lo refina en un plan multifase.",
5933
- "pathInvalid": "Introduce una ruta dentro del repositorio (sin \"..\", rutas absolutas ni barras invertidas).",
5934
5947
  "hint": "Todavia no se ejecuta nada: tras crearla, ejecuta el pipeline de planificacion de iniciativas sobre el bloque. Analiza el codigo y redacta el plan multifase para tu aprobacion.",
5935
5948
  "submit": "Crear iniciativa",
5936
5949
  "failedTitle": "No se pudo crear la iniciativa",
@@ -83,7 +83,8 @@
83
83
  "confirm": "Ignorer",
84
84
  "keep": "Continuer l'édition"
85
85
  },
86
- "edit": "Modifier"
86
+ "edit": "Modifier",
87
+ "pathInvalid": "Saisissez un chemin a l'interieur du depot (pas de \"..\", de chemins absolus ni d'antislashs)."
87
88
  },
88
89
  "access": {
89
90
  "noBoardWrite": "Accès en lecture seule : vous pouvez consulter ce tableau mais pas le modifier.",
@@ -298,6 +299,7 @@
298
299
  "continue": "Continuer",
299
300
  "submit": "Ajouter la tâche",
300
301
  "addFailedTitle": "Impossible d’ajouter la tâche",
302
+ "customFieldsInvalid": "Le formulaire de ce type de tâche a changé depuis son ouverture : les réponses ne correspondent donc plus à ce qui est demandé. Fermez puis réouvrez la boîte de dialogue pour remplir les champs actuels.",
301
303
  "linkFailed": "Tâche ajoutée, mais {count} pièce jointe n’a pas pu être liée | Tâche ajoutée, mais {count} pièces jointes n’ont pas pu être liées",
302
304
  "review": {
303
305
  "prUrl": "Pull request",
@@ -3224,13 +3226,25 @@
3224
3226
  "executionBackend": {
3225
3227
  "label": "Où s'exécutent les agents",
3226
3228
  "local-docker": "Docker local (hôte)",
3229
+ "local-dockerDesc": "Exécute chaque agent dans un conteneur sur le démon Docker de cette machine.",
3227
3230
  "cloudflare-containers": "Cloudflare Containers (intégré)",
3228
- "runner-pool": "Pool d'exécuteurs auto-hébergé"
3231
+ "cloudflare-containersDesc": "Exécute les agents sur la plateforme de conteneurs par exécution intégrée à Cloudflare.",
3232
+ "kubernetes": "Cluster Kubernetes",
3233
+ "kubernetesDesc": "Exécute chaque agent en tant que pod dans un cluster Kubernetes que vous gérez.",
3234
+ "runner-pool": "Pool d'exécuteurs personnalisé (HTTP)",
3235
+ "runner-poolDesc": "Envoie les agents vers votre propre ordonnanceur derrière une API de manifestes HTTP.",
3236
+ "k3s": "Kubernetes local (k3s)",
3237
+ "k3sDesc": "Préconfiguré pour un cluster k3s/k3d local sur cette machine : il suffit de coller un jeton."
3229
3238
  },
3230
3239
  "testEnvBackend": {
3231
3240
  "label": "Où s'exécutent les environnements de test",
3232
- "local-compose": "docker-compose dans le conteneur",
3233
- "environment-provider": "Fournisseur d'environnements",
3241
+ "local-compose": "docker-compose dans le conteneur (dépendances du Testeur)",
3242
+ "local-composeDesc": "Démarre les dépendances du Testeur avec docker-compose à l'intérieur du conteneur de l'exécution. Pour un environnement de prévisualisation complet de l'application, utilisez l'option d'environnement de prévisualisation Docker Compose ci-dessous.",
3243
+ "kubernetes": "Cluster Kubernetes",
3244
+ "kubernetesDesc": "Provisionne un namespace par PR dans un cluster Kubernetes que vous gérez. Idéal pour les applications natives Kubernetes ou si vous exploitez déjà un cluster.",
3245
+ "environment-provider": "Fournisseur HTTP personnalisé",
3246
+ "environment-providerDesc": "Provisionne des environnements éphémères via votre propre API de gestion HTTP. Idéal si vous disposez déjà d'un outillage dédié aux environnements de prévisualisation.",
3247
+ "compose": "Environnement de prévisualisation Docker Compose",
3234
3248
  "composeDesc": "Démarre le docker-compose.yml du dépôt sur un démon Docker local comme URL du Testeur. Idéal pour les applications locales basées sur Compose. Prend en charge les images préconstruites ou la construction depuis les sources ; nécessite un démon Docker, donc uniquement pour les déploiements locaux."
3235
3249
  },
3236
3250
  "dockerComposeInfo": "Les services Docker Compose démarrent sur le Docker local du runtime : aucune connexion requise (le conteneur d’exécution doit prendre en charge Docker-in-Docker). Configurez le service, le port et la source de l’image (image préconstruite ou construction depuis les sources) dans les paramètres d’environnement du service.",
@@ -5930,7 +5944,6 @@
5930
5944
  "titlePlaceholder": "p. ex. Migrer l'API vers le nouveau modele d'authentification",
5931
5945
  "goalField": "Objectif",
5932
5946
  "goalPlaceholder": "Decrivez l'objectif, les contraintes et le perimetre approximatif. Le planificateur l'affine en un plan multiphase.",
5933
- "pathInvalid": "Saisissez un chemin a l'interieur du depot (pas de \"..\", de chemins absolus ni d'antislashs).",
5934
5947
  "hint": "Rien ne s'execute encore : apres la creation, lancez le pipeline de planification d'initiative sur le bloc. Il analyse le code et redige le plan multiphase pour votre approbation.",
5935
5948
  "submit": "Creer l'initiative",
5936
5949
  "failedTitle": "Impossible de creer l'initiative",