Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforcing the Requirement Identification principle #418

Merged
merged 1 commit into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions components/XDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const props = defineProps<{

const dataTable = ref<DataTable>(),
createDisabled = ref(false),
sortField = ref<string | undefined>('name'),
confirm = useConfirm(),
createDialog = ref<Dialog>(),
createDialogVisible = ref(false),
Expand Down Expand Up @@ -190,8 +189,7 @@ const onEditDialogCancel = () => {
</template>
</Toolbar>
<DataTable ref="dataTable" :value="props.datasource as unknown as any[]" dataKey="id" v-model:filters="filters"
:globalFilterFields="Object.keys(props.datasource?.[0] ?? {})" :sortField="sortField" :sortOrder="1"
:loading="props.loading" stripedRows>
:globalFilterFields="Object.keys(props.datasource?.[0] ?? {})" :loading="props.loading" stripedRows>
<Column
v-for="key of Object.keys(props.viewModel).filter(k => props.viewModel[k as keyof RowType] !== 'hidden')"
:key="key" :field="key" :header="camelCaseToTitle(key)" sortable>
Expand Down
6 changes: 6 additions & 0 deletions domain/relations/RequirementRelation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export abstract class RequirementRelation extends BaseEntity {
@Property({ type: 'uuid', primary: true })
id: string;

/**
* The left-hand side of the relation
*/
@ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] })
left: Requirement

/**
* The right-hand side of the relation
*/
@ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] })
right: Requirement
}
6 changes: 6 additions & 0 deletions domain/requirements/Assumption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const assumptionReqIdPrefix = 'E.4.' as const;
export type AssumptionReqId = `${typeof assumptionReqIdPrefix}${number}`;

/**
* Posited property of the environment
*/
Expand All @@ -12,4 +15,7 @@ export class Assumption extends Requirement {
super(props);
this.req_type = ReqType.ASSUMPTION;
}

override get reqId(): AssumptionReqId | undefined { return super.reqId as AssumptionReqId | undefined }
override set reqId(value: AssumptionReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { ConstraintCategory } from './ConstraintCategory.js';
import { type Properties } from '../types/index.js';
import { ReqType } from './ReqType.js';

export const constraintReqIdPrefix = 'E.3.' as const;
export type ConstraintReqId = `${typeof constraintReqIdPrefix}${number}`;

/**
* A Constraint is a property imposed by the environment
*/
Expand All @@ -15,6 +18,9 @@ export class Constraint extends Requirement {
this.req_type = ReqType.CONSTRAINT;
}

override get reqId(): ConstraintReqId | undefined { return super.reqId as ConstraintReqId | undefined }
override set reqId(value: ConstraintReqId | undefined) { super.reqId = value }

/**
* Category of the constraint
*/
Expand Down
6 changes: 6 additions & 0 deletions domain/requirements/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const effectReqIdPrefix = 'E.5.' as const;
export type EffectReqId = `${typeof effectReqIdPrefix}${number}`;

/**
* Environment property affected by the system
*/
Expand All @@ -12,4 +15,7 @@ export class Effect extends Requirement {
super(props);
this.req_type = ReqType.EFFECT;
}

override get reqId(): EffectReqId | undefined { return super.reqId as EffectReqId | undefined }
override set reqId(value: EffectReqId | undefined) { super.reqId = value }
}
8 changes: 7 additions & 1 deletion domain/requirements/EnvironmentComponent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Entity, ManyToOne } from "@mikro-orm/core";
import { Entity } from "@mikro-orm/core";
import { Component } from "./Component.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const environmentComponentReqIdPrefix = 'E.2.' as const;
export type EnvironmentComponentReqId = `${typeof environmentComponentReqIdPrefix}${number}`;

/**
* Represents a component that is part of an environment.
*/
Expand All @@ -12,4 +15,7 @@ export class EnvironmentComponent extends Component {
super(props);
this.req_type = ReqType.ENVIRONMENT_COMPONENT;
}

override get reqId(): EnvironmentComponentReqId | undefined { return super.reqId as EnvironmentComponentReqId | undefined }
override set reqId(value: EnvironmentComponentReqId | undefined) { super.reqId = value }
}
21 changes: 21 additions & 0 deletions domain/requirements/Epic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Entity } from "@mikro-orm/core";
import { Scenario } from "./Scenario.js";
import { ReqType } from "./ReqType.js";

export const epicReqIdPrefix = 'G.5.' as const;
export type EpicReqId = `${typeof epicReqIdPrefix}${number}`;

/**
* An Epic is a collection of Use Cases and User Stories all directed towards a common goal.
* Ex: "decrease the percentage of of fraudulent sellers by 20%"
*/
@Entity({ discriminatorValue: ReqType.EPIC })
export class Epic extends Scenario {
constructor(props: Omit<Epic, 'id' | 'req_type'>) {
super(props);
this.req_type = ReqType.EPIC;
}

override get reqId(): EpicReqId | undefined { return super.reqId as EpicReqId | undefined }
override set reqId(value: EpicReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/FunctionalBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Functionality } from "./Functionality.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const functionalBehaviorReqIdPrefix = 'S.2.' as const;
export type FunctionalBehaviorReqId = `${typeof functionalBehaviorReqIdPrefix}${number}`;

/**
* FunctionalBehavior specifies **what** behavior the system should exhibit, i.e.,
* the results or effects of the system's operation.
Expand All @@ -14,4 +17,7 @@ export class FunctionalBehavior extends Functionality {
super(props);
this.req_type = ReqType.FUNCTIONAL_BEHAVIOR;
}

override get reqId(): FunctionalBehaviorReqId | undefined { return super.reqId as FunctionalBehaviorReqId | undefined }
override set reqId(value: FunctionalBehaviorReqId | undefined) { super.reqId = value }
}
8 changes: 7 additions & 1 deletion domain/requirements/GlossaryTerm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Entity, ManyToOne } from "@mikro-orm/core";
import { Entity } from "@mikro-orm/core";
import { Component } from "./Component.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const glossaryTermReqIdPrefix = 'E.1.' as const;
export type GlossaryTermReqId = `${typeof glossaryTermReqIdPrefix}${number}`;

/**
* A word or phrase that is part of a glossary. Provides a definition for the term
*/
Expand All @@ -12,4 +15,7 @@ export class GlossaryTerm extends Component {
super(props);
this.req_type = ReqType.GLOSSARY_TERM;
}

override get reqId(): GlossaryTermReqId | undefined { return super.reqId as GlossaryTermReqId | undefined }
override set reqId(value: GlossaryTermReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Invariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const invariantReqIdPrefix = 'E.6.' as const;
export type InvariantReqId = `${typeof invariantReqIdPrefix}${number}`;

/**
* Environment property that must be maintained.
* It exists as both an assumption and an effect.
Expand All @@ -14,4 +17,7 @@ export class Invariant extends Requirement {
super(props);
this.req_type = ReqType.INVARIANT;
}

override get reqId(): InvariantReqId | undefined { return super.reqId as InvariantReqId | undefined }
override set reqId(value: InvariantReqId | undefined) { super.reqId = value }
}
3 changes: 2 additions & 1 deletion domain/requirements/Justification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

/**
* Explanation of a project or system property in reference to a goal or environment property
* Explanation of a project or system property in reference to a goal or environment property.
* A requirement is justified if it helps to achieve a goal or to satisfy an environment property (constraint).
*/
@Entity({ discriminatorValue: ReqType.JUSTIFICATION })
export class Justification extends MetaRequirement {
Expand Down
6 changes: 6 additions & 0 deletions domain/requirements/Limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const limitReqIdPrefix = 'G.6.' as const;
export type LimitReqId = `${typeof limitReqIdPrefix}${number}`;

/**
* An Exclusion from the scope of requirements
*/
Expand All @@ -12,4 +15,7 @@ export class Limit extends Requirement {
super(props);
this.req_type = ReqType.LIMIT;
}

override get reqId(): LimitReqId | undefined { return super.reqId as LimitReqId | undefined }
override set reqId(value: LimitReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/NonFunctionalBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Functionality } from "./Functionality.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const nonFunctionalBehaviorReqIdPrefix = 'S.2.' as const;
export type NonFunctionalBehaviorReqId = `${typeof nonFunctionalBehaviorReqIdPrefix}${number}`;

/**
* NonFunctionalBehavior is a type of Behavior that is not directly related to the functionality of a system.
* It specifies **how** the system should behave, i.e., the qualities that the system must exhibit.
Expand All @@ -14,4 +17,7 @@ export class NonFunctionalBehavior extends Functionality {
super(props);
this.req_type = ReqType.NON_FUNCTIONAL_BEHAVIOR;
}

override get reqId(): NonFunctionalBehaviorReqId | undefined { return super.reqId as NonFunctionalBehaviorReqId | undefined }
override set reqId(value: NonFunctionalBehaviorReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Obstacle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Goal } from "./Goal.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const obstacleReqIdPrefix = 'G.2.' as const;
export type ObstacleReqId = `${typeof obstacleReqIdPrefix}${number}`;

/**
* Obstacles are the challenges that prevent the goals from being achieved.
*/
Expand All @@ -12,4 +15,7 @@ export class Obstacle extends Goal {
super(props);
this.req_type = ReqType.OBSTACLE;
}

override get reqId(): ObstacleReqId | undefined { return super.reqId as ObstacleReqId | undefined }
override set reqId(value: ObstacleReqId | undefined) { super.reqId = value }
}
7 changes: 7 additions & 0 deletions domain/requirements/Outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Goal } from "./Goal.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

// FIXME: The Context and overall objective entry is an Outcome, but the req_id is G.1.0
export const outcomeReqIdPrefix = 'G.3.' as const;
export type OutcomeReqId = `${typeof outcomeReqIdPrefix}${number}`;

/**
* A result desired by an organization
*/
Expand All @@ -12,4 +16,7 @@ export class Outcome extends Goal {
super(props);
this.req_type = ReqType.OUTCOME;
}

override get reqId(): OutcomeReqId | undefined { return super.reqId as OutcomeReqId | undefined }
override set reqId(value: OutcomeReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Actor } from "./Actor.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const personReqIdPrefix = 'P.1.' as const;
export type PersonReqId = `${typeof personReqIdPrefix}${number}`;

/**
* A person is a member of the Project staff
*/
Expand All @@ -14,6 +17,9 @@ export class Person extends Actor {
this.email = email;
}

override get reqId(): PersonReqId | undefined { return super.reqId as PersonReqId | undefined }
override set reqId(value: PersonReqId | undefined) { super.reqId = value }

/**
* Email address of the person
*/
Expand Down
1 change: 1 addition & 0 deletions domain/requirements/ReqType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ReqType {
CONSTRAINT = 'constraint',
EFFECT = 'effect',
ENVIRONMENT_COMPONENT = 'environment_component',
EPIC = 'epic',
EXAMPLE = 'example',
FUNCTIONAL_BEHAVIOR = 'functional_behavior',
FUNCTIONALITY = 'functionality',
Expand Down
13 changes: 13 additions & 0 deletions domain/requirements/Requirement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { type Properties } from '../types/index.js';
import { ReqType } from './ReqType.js';
import { AppUser } from '../application/AppUser.js';

export type ReqIdPrefix = `${'P' | 'E' | 'G' | 'S'}.${number}.`
export type ReqId = `${ReqIdPrefix}${number}`

/**
* A Requirement is a statement that specifies a property.
*/
Expand Down Expand Up @@ -33,6 +36,16 @@ export abstract class Requirement extends BaseEntity {
@Property({ type: 'uuid', primary: true })
id: string;

private _reqId?: ReqId

/**
* The user-friendly identifier of the requirement that is unique within its parent
*/
// This is nullable because MetaRequirements, Silence, and Noise do not have a reqId
@Property({ type: 'text', nullable: true })
get reqId(): ReqId | undefined { return this._reqId }
set reqId(value: ReqId | undefined) { this._reqId = value }

// A property is a Predicate formalizing its associated statement.
// see: https://github.com/final-hill/cathedral/issues/368
// property!: string
Expand Down
10 changes: 9 additions & 1 deletion domain/requirements/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { Example } from "./Example.js";
import { Stakeholder } from "./Stakeholder.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";
import { Outcome } from "./Outcome.js";

/**
* A Scenario specifies system behavior by describing paths
* of interaction between actors and the system.
*/
@Entity({ abstract: true, discriminatorValue: ReqType.SCENARIO })
export abstract class Scenario extends Example {
constructor({ primaryActor, ...rest }: Properties<Omit<Scenario, 'id' | 'req_type'>>) {
constructor({ primaryActor, outcome, ...rest }: Properties<Omit<Scenario, 'id' | 'req_type'>>) {
super(rest);
this.primaryActor = primaryActor;
this.outcome = outcome;
this.req_type = ReqType.SCENARIO;
}

Expand All @@ -21,4 +23,10 @@ export abstract class Scenario extends Example {
*/
@ManyToOne({ entity: () => Stakeholder })
primaryActor?: Stakeholder;

/**
* The outcome (goal) that the scenario is aiming to achieve.
*/
@ManyToOne({ entity: () => Outcome })
outcome?: Outcome;
}
8 changes: 7 additions & 1 deletion domain/requirements/Stakeholder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Entity, Enum, ManyToOne, Property } from "@mikro-orm/core";
import { Entity, Enum, Property } from "@mikro-orm/core";
import { Component } from "./Component.js";
import { StakeholderCategory } from "./StakeholderCategory.js";
import { StakeholderSegmentation } from "./StakeholderSegmentation.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const stakeholderReqIdPrefix = 'G.7.' as const;
export type StakeholderReqId = `${typeof stakeholderReqIdPrefix}${number}`;

/**
* A human actor who may affect or be affected by a project or its associated system
*/
Expand All @@ -19,6 +22,9 @@ export class Stakeholder extends Component {
this.category = props.category;
}

override get reqId(): StakeholderReqId | undefined { return super.reqId as StakeholderReqId | undefined }
override set reqId(value: StakeholderReqId | undefined) { super.reqId = value }

/**
* The segmentation of the stakeholder.
*/
Expand Down
Loading