Products & Services Semantic Definition Proposal
Status: PROPOSAL — requires product-owner approval; not semantic authority Date: 2026-10-15
This document converts the current Products & Services implementation gap into an explicit approval proposal. It does not publish a Definition, change a capability profile, provision a Business, mutate STAGING or add a new architecture decision number.
The proposal follows the canonical owners:
- [Objects and Types](../architecture/objects-and-types.md) owns the universal
- [Business Model](../architecture/business-model.md) owns Business-scoped
- [Operation Contract](../architecture/operation-contract.md) owns governed
- [Views and Content](../architecture/views-and-content.md) owns persisted
- [Identity and Governance](../architecture/identity-and-governance.md) owns
Object, Type, value and validation contract.
configuration and connected business meaning.
executable Operations and their boundary to Commands.
Presentation, forms and model-projection UI.
Business scoping, authority, Policy and isolation.
1. Current-state evidence
Authoritative capability configuration
generic-business-capability-profile.json declares:
{
"key": "products-services",
"label": "Products & Services",
"records": []
}
The persisted Business Presentation profile contains a generic Products & Services Presentation node, but it has no record collection or record editing binding. The deployed page therefore presents only the data-defined status text that record Operations are not configured.
Existing reusable Type
CapabilityCompositionVocabulary::OFFERING_TYPE already defines the Type:
a8100000-0000-4000-8000-000000000001
Its existing descriptor is:
| Property | Type | Required | |---|---|---:| | name | string | yes | | offering_kind | string | yes | | description | string | no | | lifecycle | string | yes | | commercial_context | string | no |
The descriptor rejects additional properties. It is installed through the existing capability-composition vocabulary and uses the universal Object/Type model. No new Product or Service Type is required by the current repository evidence.
Existing generic lifecycle mechanism
BusinessRecordService already validates configured record values against the persisted Type descriptor and submits create/update effects through the existing CommandHandler, with Business scope, expected versions, transaction handling, revisions and provenance.
BusinessPresentationProvisioningService already derives persisted Business-scoped View Definitions and Presentation Object graphs from the authoritative capability profile. It is the appropriate reconciliation owner for this record binding.
The repository does not establish a Product/Service-specific delete, archive, pricing, tax, inventory, fulfilment or service-delivery Operation.
2. Recommended semantic model
Recommendation: one Offering Type with a discriminator
Reuse OFFERING_TYPE for both products and services. The offering_kind property distinguishes the configured offering kind.
This is ESTABLISHED at the Type-structure level because the Type already exists and already contains offering_kind. The allowed values themselves are RECOMMENDED, not established business requirements.
Recommended initial values:
product
service
These values require approval before they become an authoritative validation vocabulary. Until approved, the Type's existing string validation remains the only established constraint.
Why this model is recommended
- Reuses an existing platform-owned Type and revision.
- Avoids duplicate Product and Service persistence models.
- Preserves one Object identity and one generic `/objects` interface.
- Allows future offering kinds without a second semantic store.
- Keeps shared fields and generic Presentation bindings simple.
- Matches the existing `OFFERING_TYPE` descriptor and prior repository
vocabulary.
Alternative: separate Product and Service Types
Not recommended. Separate Types would duplicate validation, Presentation, authorization targets and provisioning configuration without any established semantic distinction requiring different persistence or Operation contracts. Separate Types may become justified later only if approved requirements show different invariants or lifecycles that cannot be represented by the shared Offering Type.
Alternative: free-form capability fields
Rejected. A capability-local field map would bypass the universal Type contract and make validation, authorization and Presentation resolution capability-specific.
3. Proposed field schema
The following uses the existing Type descriptor; it does not add new runtime validation.
Essential fields
| Field | Established/recommended status | Presentation | |---|---|---| | name | ESTABLISHED by OFFERING_TYPE and required | Required text | | offering_kind | ESTABLISHED as a required string field; product/service values are RECOMMENDED | Required select after value approval, otherwise required text | | lifecycle | ESTABLISHED as a required string field; vocabulary is UNRESOLVED | Required text until lifecycle vocabulary is approved |
Optional fields
| Field | Status | Presentation | |---|---|---| | description | ESTABLISHED by the existing Type descriptor | Optional textarea | | commercial_context | ESTABLISHED by the existing Type descriptor | Optional textarea |
Explicitly excluded from this proposal
The following are not inferred or added:
- Price or price lists
- Currency
- Tax treatment
- Stock or inventory
- SKU or catalogue identity
- Service delivery model
- Capacity, scheduling or fulfilment
- Subscription or entitlement semantics
- Product variants or bundles
Those concepts require separate requirements and semantic definitions if they become necessary.
4. Lifecycle proposal
The existing Type requires a lifecycle string but does not establish a controlled vocabulary or a transition model.
| Choice | Classification | |---|---| | Retain lifecycle as a required string field | ESTABLISHED by the existing Type | | Introduce draft, active, retired values | RECOMMENDED, requires approval | | Make lifecycle a state-transition definition | UNRESOLVED; requires approved State/Workflow semantics | | Add archive/delete now | NOT PROPOSED; no established Operation or retention requirement |
The minimum implementation should preserve the existing string field and avoid pretending that lifecycle values are authoritative until approved.
5. Operation and authorization matrix
| Capability | Mechanism | Status | |---|---|---| | Inspect Offering Objects | AuthorizedObjectReader through /objects | ESTABLISHED | | Create Offering Objects | Generic create effect through CommandHandler | ESTABLISHED | | Update Offering Objects | Generic update effect with expected version | ESTABLISHED | | Validate values | Existing Type descriptor and TypeDescriptorValidator | ESTABLISHED | | Delete | No Operation proposed | UNRESOLVED / deferred | | Archive | No Operation proposed | UNRESOLVED / deferred | | Product-specific Operation | None required for the minimum record lifecycle | ESTABLISHED by absence of requirement |
Business authorization must continue to require:
- Authenticated User.
- Active Business Membership.
- Applicable AuthorityGrant and Policy.
- Target Type authorization for `OFFERING_TYPE`.
- Business-owned Object scope.
- Exact expected Object revision on update.
- Existing Command transaction, revision and provenance evidence.
No direct repository write, special HTTP mutation path or Product/Service mutation handler is proposed.
6. Capability and Presentation configuration
Capability profile binding
The Products & Services entry should reference the existing Offering Type:
{
"key": "products-services",
"label": "Products & Services",
"records": [
{
"key": "offering",
"label": "Products & Services",
"type_id": "a8100000-0000-4000-8000-000000000001",
"view_id": "a2000000-0000-4000-8000-000000000004",
"fields": [
{ "name": "name", "label": "Name", "renderer": "text", "required": true },
{ "name": "offering_kind", "label": "Product or service", "renderer": "text", "required": true },
{ "name": "description", "label": "Description", "renderer": "textarea" },
{ "name": "lifecycle", "label": "Lifecycle", "renderer": "text", "required": true },
{ "name": "commercial_context", "label": "Commercial context", "renderer": "textarea" }
]
}
]
}
This is a proposed configuration shape, not yet an approved persisted change. a2000000-0000-4000-8000-000000000004 is a proposed unused View declaration identity and must be checked before implementation.
View Definition
The Business-scoped View Definition should use the same fields for list and detail presentation, with a shorter list:
- List: `name`, `offering_kind`, `lifecycle`
- Detail: all five fields
- No relationship sections in the minimum increment
- Generic create/update behavior through the existing record form and Command
pipeline
The existing reconciliation mechanism should derive and persist the View Definition and Presentation Object graph. No Products-specific renderer is permitted.
Form behavior
The existing generic object form can consume the persisted detail_fields binding. It should not introduce a Product-specific template. The form submits the existing generic create/update mutation path and preserves expected versions and provenance.
7. Provisioning and idempotency
Implementation, after approval, should:
1. Confirm or install the existing OFFERING_TYPE descriptor through CapabilityCompositionVocabulary::install. 2. Add the Offering record entry to generic-business-capability-profile.json. 3. Add the corresponding View declaration to generic-business-capability-configuration.json if that manifest remains the authoritative declaration source for the Business-scoped View. 4. Reconcile persisted View and Presentation Objects through BusinessPresentationProvisioningService. 5. Ensure the Business authorization target set includes OFFERING_TYPE. 6. Verify repeated reconciliation produces no duplicate Objects or revisions. 7. Verify a newly initialized Business can inspect, create and update Offering Objects only through canonical generic routes.
No records should be seeded merely to make the UI appear populated.
8. Decisions requiring approval
The following are the minimum unresolved product decisions:
1. Approve the shared OFFERING_TYPE model for both Products and Services. 2. Approve the product and service offering_kind values, or provide the authoritative vocabulary. 3. Approve whether lifecycle remains a free string initially or receives an approved controlled vocabulary. 4. Approve the proposed field labels and the proposed View declaration identity. 5. Confirm that pricing, currency, tax, inventory and service-delivery semantics are out of scope for this increment. 6. Confirm that delete/archive Operations are deferred.
No additional architecture decision number is needed. These are product configuration decisions under the existing settled Object, Business, Operation, Governance and Presentation contracts.
9. Implementation and regression plan
After approval:
Focused tests
- Assert `OFFERING_TYPE` exists with the established descriptor and revision.
- Assert the capability profile binds `products-services` to `OFFERING_TYPE`.
- Assert the persisted View/Form fields match the approved profile.
- Create an Offering through the generic Command path.
- Reject missing required fields and invalid Type values.
- Reject unauthorized and cross-Business mutations.
- Reject stale expected versions.
- Assert a successful update creates a revision and provenance.
- Assert canonical `/objects?type_id=...` and `/objects/{id}` rendering.
- Assert idempotent Business Presentation reconciliation.
- Preserve existing People, Organisation and Project tests.
Required validation
- PHP lint.
- Focused Type, Operation, provisioning, Presentation and authorization tests.
- Full PHPUnit against the isolated local test database.
- Package validation.
- HTTP acceptance.
- UI structure and shared renderer validation.
- `git diff --check`.
No STAGING database mutation is authorized by this proposal.