Replace implemented column with CMMI maturity level

Drop the boolean implemented/not-implemented state in favor of a
mandatory CMMI maturity level enum (NONE, INITIAL, MANAGED, DEFINED,
QUANTITATIVELY_MANAGED, OPTIMIZING) stored as a Postgres enum type.

The migration backfills existing rows (NOT_IMPLEMENTED → NONE,
IMPLEMENTED → INITIAL), makes the column NOT NULL, and drops the old
implemented column and its enum type.

- maturityLevel is required on CreateControlInput and non-nullable (!)
  in the GraphQL schema
- CLI displays human-readable CMMI labels instead of raw enum tokens
- SOA table and published document use a single Maturity column in
  place of the old Implemented + Maturity columns
- Remove ControlImplementationState type and all implemented references
  across backend, frontend, CLI, MCP, n8n, and E2E tests

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-20 20:48:23 +02:00
parent da91afc2a7
commit e1148f812e
32 changed files with 271 additions and 586 deletions

View File

@@ -19,14 +19,11 @@ import {
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui";
export const MATURITY_LEVEL_UNSET = "__unset__" as const;
export function ControlMaturityLevelOptions() {
const { __ } = useTranslate();
return (
<>
<Option value={MATURITY_LEVEL_UNSET}>{__("Not set")}</Option>
{controlMaturityLevels.map(level => (
<Option key={level} value={level}>
{getControlMaturityLevelLabel(__, level)}

View File

@@ -118,7 +118,6 @@ export const frameworkControlNodeQuery = graphql`
sectionTitle
description
bestPractice
implemented
notImplementedJustification
maturityLevel
canUpdate: permission(action: "core:control:update")

View File

@@ -383,25 +383,17 @@ export default function FrameworkControlPage({ queryRef }: Props) {
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Implemented")}</span>
<Badge variant={control.implemented === "IMPLEMENTED" ? "success" : "warning"} size="sm">
{control.implemented === "IMPLEMENTED" ? __("Implemented") : __("Not Implemented")}
<span className="text-sm text-txt-secondary">{__("Maturity level")}</span>
<Badge variant="neutral" size="sm">
{getControlMaturityLevelLabel(__, control.maturityLevel ?? "NONE")}
</Badge>
</div>
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
{control.maturityLevel === "NONE" && control.notImplementedJustification && (
<div>
<span className="text-xs text-txt-secondary">{__("Justification for non-implementation")}</span>
<div className="text-sm mt-0.5 whitespace-pre-wrap">{control.notImplementedJustification}</div>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Maturity level")}</span>
<Badge variant="neutral" size="sm">
{control.maturityLevel
? getControlMaturityLevelLabel(__, control.maturityLevel)
: __("Not set")}
</Badge>
</div>
</div>
</Card>
<div className="mb-4">

View File

@@ -32,10 +32,7 @@ import { graphql } from "relay-runtime";
import { z } from "zod";
import type { FrameworkControlDialogFragment$key } from "#/__generated__/core/FrameworkControlDialogFragment.graphql";
import {
ControlMaturityLevelOptions,
MATURITY_LEVEL_UNSET,
} from "#/components/form/ControlMaturityLevelOptions";
import { ControlMaturityLevelOptions } from "#/components/form/ControlMaturityLevelOptions";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
@@ -53,7 +50,6 @@ const controlFragment = graphql`
description
sectionTitle
bestPractice
implemented
notImplementedJustification
maturityLevel
}
@@ -89,10 +85,7 @@ const schema = z.object({
description: z.string().optional().nullable(),
sectionTitle: z.string(),
bestPractice: z.boolean(),
implemented: z.enum(["IMPLEMENTED", "NOT_IMPLEMENTED"]),
notImplementedJustification: z.string().optional().nullable(),
maturityLevel: z.enum([
MATURITY_LEVEL_UNSET,
"NONE",
"INITIAL",
"MANAGED",
@@ -100,6 +93,7 @@ const schema = z.object({
"QUANTITATIVELY_MANAGED",
"OPTIMIZING",
]),
notImplementedJustification: z.string().optional().nullable(),
});
export function FrameworkControlDialog(props: Props) {
@@ -124,9 +118,8 @@ export function FrameworkControlDialog(props: Props) {
description: frameworkControl?.description ?? "",
sectionTitle: frameworkControl?.sectionTitle ?? "",
bestPractice: frameworkControl?.bestPractice ?? true,
implemented: frameworkControl?.implemented ?? "IMPLEMENTED",
maturityLevel: frameworkControl?.maturityLevel ?? "INITIAL",
notImplementedJustification: frameworkControl?.notImplementedJustification ?? "",
maturityLevel: frameworkControl?.maturityLevel ?? MATURITY_LEVEL_UNSET,
}),
[frameworkControl],
);
@@ -141,14 +134,9 @@ export function FrameworkControlDialog(props: Props) {
}, [defaultValues, reset]);
const bestPracticeValue = watch("bestPractice");
const implementedValue = watch("implemented");
const maturityLevelValue = watch("maturityLevel");
const onSubmit = async (data: z.infer<typeof schema>) => {
const maturityLevel = data.maturityLevel === MATURITY_LEVEL_UNSET
? null
: data.maturityLevel;
if (frameworkControl) {
await mutate({
variables: {
@@ -158,9 +146,8 @@ export function FrameworkControlDialog(props: Props) {
description: data.description || null,
sectionTitle: data.sectionTitle,
bestPractice: data.bestPractice,
implemented: data.implemented,
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
maturityLevel,
maturityLevel: data.maturityLevel,
notImplementedJustification: data.maturityLevel === "NONE" ? (data.notImplementedJustification || null) : null,
},
},
});
@@ -173,9 +160,8 @@ export function FrameworkControlDialog(props: Props) {
description: data.description || null,
sectionTitle: data.sectionTitle,
bestPractice: data.bestPractice ?? true,
implemented: data.implemented ?? "IMPLEMENTED",
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
maturityLevel,
maturityLevel: data.maturityLevel,
notImplementedJustification: data.maturityLevel === "NONE" ? (data.notImplementedJustification || null) : null,
},
connections: [props.connectionId!],
},
@@ -232,23 +218,6 @@ export function FrameworkControlDialog(props: Props) {
/>
<span className="text-sm">{__("Best Practice")}</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={implementedValue === "IMPLEMENTED"}
onChange={checked =>
setValue("implemented", checked ? "IMPLEMENTED" : "NOT_IMPLEMENTED")}
/>
<span className="text-sm">{__("Implemented")}</span>
</label>
{implementedValue === "NOT_IMPLEMENTED" && (
<Textarea
id="notImplementedJustification"
variant="ghost"
autogrow
placeholder={__("Justification for non-implementation")}
{...register("notImplementedJustification")}
/>
)}
<div className="flex items-center gap-2">
<span className="text-sm">{__("Maturity level")}</span>
<Select
@@ -260,6 +229,15 @@ export function FrameworkControlDialog(props: Props) {
<ControlMaturityLevelOptions />
</Select>
</div>
{maturityLevelValue === "NONE" && (
<Textarea
id="notImplementedJustification"
variant="ghost"
autogrow
placeholder={__("Justification for non-implementation")}
{...register("notImplementedJustification")}
/>
)}
</div>
</DialogContent>
<DialogFooter>

View File

@@ -93,7 +93,6 @@ const createApplicabilityStatementMutation = graphql`
sectionTitle
name
bestPractice
implemented
notImplementedJustification
regulatory
contractual

View File

@@ -86,7 +86,6 @@ export const controlsFragment = graphql`
sectionTitle
name
bestPractice
implemented
notImplementedJustification
maturityLevel
regulatory
@@ -151,7 +150,6 @@ export default function StatementOfApplicabilityControlsTab({
applicability: edge.node.applicability,
justification: edge.node.justification,
bestPractice: edge.node.control.bestPractice,
implemented: edge.node.control.implemented,
notImplementedJustification: edge.node.control.notImplementedJustification,
maturityLevel: edge.node.control.maturityLevel,
regulatory: edge.node.control.regulatory,
@@ -226,7 +224,6 @@ export default function StatementOfApplicabilityControlsTab({
<Th className="w-[9%]">{__("Framework")}</Th>
<Th className="w-[17%]">{__("Control")}</Th>
<Th className="w-[12%]">{__("Applicability")}</Th>
<Th className="w-[12%]">{__("Implemented")}</Th>
<Th className="w-[14%]">{__("Maturity")}</Th>
<Th className="w-[7%]">{__("Regulatory")}</Th>
<Th className="w-[7%]">{__("Contractual")}</Th>
@@ -241,7 +238,7 @@ export default function StatementOfApplicabilityControlsTab({
{linkedControls.length === 0 && (
<Tr>
<Td
colSpan={canUpdate || canDelete ? 10 : 9}
colSpan={canUpdate || canDelete ? 9 : 8}
className="text-center text-txt-secondary py-12"
>
{__("No controls linked")}
@@ -293,12 +290,12 @@ export default function StatementOfApplicabilityControlsTab({
: (
<div className="space-y-1">
<Badge
variant={control.implemented === "IMPLEMENTED" ? "success" : "danger"}
variant={control.maturityLevel !== "NONE" ? "success" : "neutral"}
size="sm"
>
{control.implemented === "IMPLEMENTED" ? __("Yes") : __("No")}
{getControlMaturityLevelLabel(__, control.maturityLevel)}
</Badge>
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
{control.maturityLevel === "NONE" && control.notImplementedJustification && (
<p className="text-xs text-txt-secondary break-words">
{control.notImplementedJustification}
</p>
@@ -306,17 +303,6 @@ export default function StatementOfApplicabilityControlsTab({
</div>
)}
</Td>
<Td>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
<Badge variant="neutral" size="sm">
{control.maturityLevel
? getControlMaturityLevelLabel(__, control.maturityLevel)
: __("Not set")}
</Badge>
)}
</Td>
<Td>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>