@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.
- package/README.md +50 -4
- package/app/components/board/AddTaskModal.vue +54 -78
- package/app/components/board/CreateInitiativeModal.vue +8 -6
- package/app/components/{board/InitiativePresetFields.vue → common/DescriptorFields.vue} +57 -63
- package/app/components/inputGate/InputGateNotice.vue +1 -1
- package/app/components/panels/AgentStepDetail.vue +2 -2
- package/app/components/panels/inspector/ServiceTestConfig.vue +1 -1
- package/app/components/panels/inspector/TaskExecution.vue +2 -2
- package/app/components/settings/WorkspaceSettingsPanel.vue +1 -1
- package/app/composables/api/inputGate.ts +1 -1
- package/app/modular/nav-contributions.spec.ts +1 -1
- package/app/modular/nav-contributions.ts +1 -1
- package/app/stores/inputGate.ts +1 -1
- package/app/stores/ui/resultViews.ts +1 -1
- package/app/types/domain.ts +6 -0
- package/app/utils/descriptorFields.spec.ts +126 -0
- package/app/utils/descriptorFields.ts +104 -0
- package/app/utils/initiative.ts +0 -30
- package/app/utils/inputGate.ts +1 -1
- package/app/utils/pipelineRender.spec.ts +1 -1
- package/app/utils/pipelineRender.ts +1 -1
- package/i18n/i18n.config.ts +7 -21
- package/i18n/locales/de.json +3 -2
- package/i18n/locales/en.json +6 -2
- package/i18n/locales/es.json +18 -5
- package/i18n/locales/fr.json +18 -5
- package/i18n/locales/he.json +88 -86
- package/i18n/locales/it.json +3 -2
- package/i18n/locales/ja.json +3 -2
- package/i18n/locales/pl.json +21 -8
- package/i18n/locales/tr.json +3 -2
- package/i18n/locales/uk.json +21 -8
- package/i18n/plural-forms.spec.ts +76 -0
- package/i18n/plural-rules.spec.ts +98 -0
- package/i18n/plural-rules.ts +123 -0
- 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
|
|
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
|
|
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
|
*/
|
package/app/stores/inputGate.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
6
6
|
import { useExecutionStore } from '~/stores/execution'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* The PRE-
|
|
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-
|
|
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') {
|
package/app/types/domain.ts
CHANGED
|
@@ -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
|
+
}
|
package/app/utils/initiative.ts
CHANGED
|
@@ -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,
|
package/app/utils/inputGate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Which PRE-
|
|
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-
|
|
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-
|
|
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.
|
package/i18n/i18n.config.ts
CHANGED
|
@@ -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)
|
|
8
|
-
//
|
|
9
|
-
|
|
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
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
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
|
package/i18n/locales/de.json
CHANGED
|
@@ -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.",
|
package/i18n/locales/en.json
CHANGED
|
@@ -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",
|
package/i18n/locales/es.json
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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",
|