diff --git a/.github/workflows/capability-contract-test.yml b/.github/workflows/capability-contract-test.yml new file mode 100644 index 00000000..0e9cb4dc --- /dev/null +++ b/.github/workflows/capability-contract-test.yml @@ -0,0 +1,28 @@ +name: Capability core contract +on: + pull_request: + paths: + - 'tools/lint.py' + - 'tools/tests/test_capability_contract.py' + - 'notations/schemas/**' + - 'notations/vocabulary.yaml' + - 'notations/views/diagrams/05-capability-map.md' + - 'notations/views/diagrams/10-applications.md' + - 'notations/examples/capability-map/**' + - 'tests/fixtures/capability-management-cycle/**' + - '.github/workflows/capability-contract-test.yml' + workflow_dispatch: +permissions: + contents: read + +jobs: + contract: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install pyyaml + - name: Check positive, negative and compatibility cases + run: python -m unittest discover -s tools/tests -p test_capability_contract.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c08d354..8048b50d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ For each issue, include the file path or section reference, what you observed, a 4. **Run the relevant validators locally, before opening the PR.** This repo's own gate is not a single command — `.validators/lint.py` is what an *adopter* repo runs against its `canon/`; this repo has no `canon/` of its own to validate. What CI actually runs here, by what you touched: - Any `notations/**` spec or example, or `transitrix/skills/onboard/templates/**` — `node scripts/check-notations.mjs` (mechanical invariants: example extensions, `notation:` headers, internal link resolution, version-pin consistency). - A single view notation file (`*.transitrix.yaml`) — `npx @transitrix/cli validate ` (`npx.cmd` on Windows PowerShell with a restricted execution policy). - - `tools/lint.py` itself — the encoding regression test in `.github/workflows/lint-encoding-test.yml`. + - `tools/lint.py` itself — the encoding regression test in `.github/workflows/lint-encoding-test.yml`; for capability-map contracts also run `python -m unittest discover -s tools/tests -p test_capability_contract.py` (schema/vocabulary parity, native fixtures and negative controls). - A Claude Code Skill under `transitrix/skills//` — see "Adding or changing a Skill" below. - Anything else — check `.github/workflows/` for a job whose `paths:` trigger matches the files you touched; most packages and skills carry their own targeted test workflow. 5. **Update related docs.** If your change affects naming, layout, or rules, update the relevant file(s) under `method/` (naming: `method/03-modelling.md`; layout: `method/02-repository.md`), `method/00-glossary.md`, and `README.md` as needed. diff --git a/guides/capability-map-compatibility.md b/guides/capability-map-compatibility.md new file mode 100644 index 00000000..d2f86381 --- /dev/null +++ b/guides/capability-map-compatibility.md @@ -0,0 +1,45 @@ +# Capability-map compatibility checks + +This change corrects the core methodology. It adds optional native fields rather +than requiring a renderer-specific extension. No existing methodology version +pin changes until a release is prepared under CONTRACT §10. + +| Surface | Checked on 2026-10-23 | Limit | +| --- | --- | --- | +| Published additive JSON Schema | Draft-07 AJV with date-format assertions: nine fixture fragments accepted; four malformed controls refused | Validate the selected definition, not the full organisation envelope | +| `tools/lint.py` | Positive/negative integration cases, legacy omission, published schema/vocabulary parity and existing capability examples | New fields, mapping references, local keys, dates and view selections; no history reconstruction or renderer geometry | +| CLI `@transitrix/cli@2.11.0`, file scope | Native capability-map example returns zero errors and six `NOTATION-SKIP-001` warnings for unavailable maturity sidecars | The result says the file remains unvalidated; it is not full compatibility proof | +| CLI negative control | The same file with `management_cycle_stage: ESG` and `view_config.display.depth: 99` still returns zero errors | Released CLI does not enforce these new core contracts; run the reference linter and do not silently treat this acceptance as validation | +| DSM / Studio / standalone renderer | Not adopted by this source change | Separate consumer implementation and release evidence required | + +The reference validator preserves its existing single-file installation shape: + +```sh +python tools/lint.py +python -m unittest discover -s tools/tests -p test_capability_contract.py +python tools/tests/test_lint_encoding.py +``` + +The complete synthetic source model lives in +`tests/fixtures/capability-management-cycle/`. From that directory, run +`python ../../../tools/lint.py`. The view example is checked against that fixture +for drift. Past, present and future dates are accepted as selections; validating +a date does not establish that evidence exists for it. + +The new validator rejects malformed **new** native fields. Existing source data +without them remains valid; it is not classified from names, H/V orientation, +parent stages, tags or maturity. Existing direct application coverage fields +retain their previous validation boundary; new technical mappings always check +canonical CAPABILITY targets before display filtering. Common free-form tags +are not narrowed to ESG, MDM and Support. + +For proposals that placed these values under `extensions`, move +`extensions.management_cycle_stage` to the defined native field explicitly. +Reconcile conflicts before moving and retain the source history. Do not infer +stages from a legacy renderer's `layer`: canonical architectural `layer` remains +`business`. Native APPLICATION technical entry `key` projects to standalone +renderer `id`; source and adapter endpoint conventions require explicit date +conversion. This is a mapping boundary, not a new global technical TYPE. + +CLI/DSM/Studio adoption, whole-source temporal assessment, source-to-renderer +projection and product release remain separate from this methodology PR. diff --git a/guides/modelling-capabilities.md b/guides/modelling-capabilities.md index 73db084f..d3018b55 100644 --- a/guides/modelling-capabilities.md +++ b/guides/modelling-capabilities.md @@ -1,7 +1,7 @@ --- title: Modelling capabilities status: active -last_reviewed: 2026-09-14 +last_reviewed: 2026-10-23 audience: public license: MIT --- @@ -31,3 +31,34 @@ attribute_versions: ``` Before accepting the map, check IDs and references, admission and lifecycle, sidecar placement, and the evidence behind each assessment. Confirm the view displays the selected date's values. A rendered diagram alone does not establish model validity. Full sidecar rules and migration guidance are in the specification's attribute-history section. + +## Management cycle, tags and system coverage + +Use the capability element's optional `management_cycle_stage` to state its +management role. The five definitions and order live in +[capability-map §6a](../notations/views/diagrams/05-capability-map.md#6a-management-cycle-stage-and-tags). +Strategic includes strategic planning; Planning is tactical/operational. Keep +architectural `layer: business`. ESG, MDM and Support are ordinary tags, not stages +or automatic horizontal capabilities. Classify explicitly and never inherit. + +Record a system's direct `capabilities` on its APPLICATION element. Where the +source identifies technical abilities, record them as the application's native +`technical_capabilities` inventory, with unique local keys and explicit targets, +per [the applications contract](../notations/views/diagrams/10-applications.md#6a-technical-capabilities-and-business-coverage). +Neither field belongs in an `extensions` bag or solely inside a view. Missing +mapping means not specified. Do not infer mappings from containment or names. + +Choose stage grouping, tag filters, selected systems and date in the map's +[declared view_config](../notations/views/diagrams/05-capability-map.md#163-view_config-defaults). +Retain observation date, model-validity date, revision and target intent when +comparing snapshots. A red geometric intersection is an investigation aid, not +a finding of redundant functionality. Confirm source evidence before drawing +that conclusion. + +These core-contract corrections are prepared for review. Current standalone +rendering uses a documented JSON projection; this is not proof of native CLI, +DSM or Studio adoption. Verify consumer support before migrating source files. + +The core additive schemas and whole-model checks are described in the +[capability-map compatibility guide](capability-map-compatibility.md). They ship +with the methodology source; released consumers require separate adoption. diff --git a/notations/ELEMENT_PRIMITIVES.md b/notations/ELEMENT_PRIMITIVES.md index e2739332..b2087df0 100644 --- a/notations/ELEMENT_PRIMITIVES.md +++ b/notations/ELEMENT_PRIMITIVES.md @@ -450,6 +450,7 @@ Inline shape (catalogue entry): [views/09-products.md](./views/diagrams/09-produ | `type` | yes | string | `application` \| `integration` \| `platform` \| `data_store`. | | `domain` | no | string | Business domain. | | `capabilities` | no | list | `CAPABILITY-…` IDs supported. | +| `technical_capabilities` | no | list | Application-owned technical abilities and explicit business-capability mappings; field shape and containment boundary in [applications catalogue §6a](views/diagrams/10-applications.md#6a-technical-capabilities-and-business-coverage). | | `products` | no | list | `PRODUCT-…` IDs supported. | | `description` | recommended | string | Short description. | | `owner_role` | no | string | `ROLE-…`. **Time-varying** — sidecar ([CONTRACT.md](CONTRACT.md) §9), not inline. | diff --git a/notations/examples/capability-map/management-cycle.capability-map.transitrix.yaml b/notations/examples/capability-map/management-cycle.capability-map.transitrix.yaml new file mode 100644 index 00000000..3c7ef9f4 --- /dev/null +++ b/notations/examples/capability-map/management-cycle.capability-map.transitrix.yaml @@ -0,0 +1,82 @@ +# Complete canonical sources: tests/fixtures/capability-management-cycle/ +notation: capability-map +spec_version: '0.5' +name: Management cycle and system coverage +capability_map: + id: CAPABILITY_MAP-CYCLE-1 + name: Management cycle and system coverage + assessment_date: '2026-10-23' + capabilities: + - id: CAPABILITY-V1 + name: Order Management + type: domain + valid_from: '2026-01-01' + valid_to: null + children: + - id: CAPABILITY-V1.1 + name: Order strategy + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Strategic + - id: CAPABILITY-V1.2 + name: Order improvement learning + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Learning + - id: CAPABILITY-V1.3 + name: Fulfilment planning + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Planning + children: + - id: CAPABILITY-V1.3.1 + name: Pick and pack + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Execution + - id: CAPABILITY-H1 + name: Order assurance + type: supporting + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Assurance + tags: + - ESG + - MDM + - Support +view_config: + valid_at: null + scenario: null + management_cycle: + group: true + order: + - Strategic + - Learning + - Planning + - Execution + - Assurance + tags: + filter: + - ESG + - MDM + - Support + show: true + display: + depth: 3 + backlog: false + maturity: false + legend: true + systems: + labels: false + coverage: true + selected: + - APPLICATION-OMS-1 + - APPLICATION-GOVERNANCE-1 + highlight: null + shape: angled + rounded: false + overlap: true diff --git a/notations/schemas/README.md b/notations/schemas/README.md new file mode 100644 index 00000000..9bf22cc9 --- /dev/null +++ b/notations/schemas/README.md @@ -0,0 +1,22 @@ +# Capability-map additive schemas + +`capability-map-contract.schema.json` is JSON Schema draft-07. Validate a +canonical CAPABILITY fragment against `#/$defs/capabilityFields`, a canonical +APPLICATION (or applications catalogue entry) against `#/$defs/applicationFields`, +and a capability-map `view_config` against `#/$defs/viewConfig`. + +These are additive field contracts. Element admission, primitive lifecycle, +identity and the rest of each notation remain governed by their existing +specifications. Additional source element fields are accepted; unknown keys in +the newly defined view settings and technical inventory entries are rejected. +Schema `default` annotations describe render defaults and do not mutate sources. +Enable date format assertion in the schema consumer. + +The single-file `tools/lint.py` carries an equivalent embedded contract for +onboarding compatibility. Regression tests require exact parity with this +schema and the controlled vocabulary. The linter additionally checks local-key +uniqueness, canonical mapping references, view references and explicit lifetime +bounds. No external schema library is needed to run the reference linter. + +Use the complete synthetic model in `tests/fixtures/capability-management-cycle/` +for integration checks. See capability-map §16.4 for consumer compatibility. diff --git a/notations/schemas/capability-map-contract.schema.json b/notations/schemas/capability-map-contract.schema.json new file mode 100644 index 00000000..b9732dfc --- /dev/null +++ b/notations/schemas/capability-map-contract.schema.json @@ -0,0 +1,225 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Capability-map core additive field contracts", + "$comment": "Additive field contracts, not a complete model schema. Select a $defs entry. Referential integrity, local-key uniqueness and lifetime checks require tools/lint.py. Other fields retain their existing specifications.", + "$defs": { + "capabilityFields": { + "type": "object", + "properties": { + "management_cycle_stage": { + "enum": [ + null, + "Strategic", + "Learning", + "Planning", + "Execution", + "Assurance" + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "\\S" + } + } + }, + "additionalProperties": true + }, + "applicationFields": { + "type": "object", + "properties": { + "technical_capabilities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "pattern": "\\S" + }, + "name": { + "type": "string", + "minLength": 1, + "pattern": "\\S" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "\\S" + }, + "uniqueItems": true + }, + "valid_from": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "valid_to": { + "type": [ + "string", + "null" + ], + "format": "date" + } + }, + "additionalProperties": false, + "required": [ + "key", + "name" + ] + } + } + }, + "additionalProperties": true + }, + "viewConfig": { + "type": "object", + "properties": { + "valid_at": { + "type": [ + "string", + "null" + ], + "format": "date", + "default": null + }, + "scenario": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "default": null + }, + "management_cycle": { + "type": "object", + "properties": { + "group": { + "type": "boolean", + "default": false + }, + "order": { + "const": [ + "Strategic", + "Learning", + "Planning", + "Execution", + "Assurance" + ], + "default": [ + "Strategic", + "Learning", + "Planning", + "Execution", + "Assurance" + ] + } + }, + "additionalProperties": false + }, + "tags": { + "type": "object", + "properties": { + "filter": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "\\S" + }, + "uniqueItems": true, + "default": [] + }, + "show": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, + "display": { + "type": "object", + "properties": { + "depth": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "default": 3 + }, + "backlog": { + "type": "boolean", + "default": false + }, + "maturity": { + "type": "boolean", + "default": false + }, + "legend": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, + "systems": { + "type": "object", + "properties": { + "highlight": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "default": null + }, + "selected": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "\\S" + }, + "uniqueItems": true, + "default": [] + }, + "shape": { + "enum": [ + "angled", + "rectangular" + ], + "default": "angled" + }, + "labels": { + "type": "boolean", + "default": false + }, + "coverage": { + "type": "boolean", + "default": false + }, + "rounded": { + "type": "boolean", + "default": false + }, + "overlap": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } +} diff --git a/notations/views/diagrams/05-capability-map.md b/notations/views/diagrams/05-capability-map.md index 1540de3b..150e29ea 100644 --- a/notations/views/diagrams/05-capability-map.md +++ b/notations/views/diagrams/05-capability-map.md @@ -1,21 +1,21 @@ --- notation: "Capabilities Map" -version: "0.4" +version: "0.5" author: "Valerii Korobeinikov" -last_updated: "2026-09-26" +last_updated: "2026-10-23" status: "documented" file_extension: "*.capability-map.transitrix.yaml" -dsm_status: "implemented — Capabilities page, Editor (C), BCM tab" +dsm_status: "core grid implemented; management-cycle and coverage changes not verified in consumers" --- # Capabilities Map Notation — Reference -**Version:** 0.3 -**Date:** 2026-05-19 -**Status:** Implemented in Transitrix DSM +**Version:** 0.5 +**Date:** 2026-10-23 +**Status:** Core notation documented; management-cycle and coverage correction prepared for review (§16.4) **File extension:** `*.capability-map.transitrix.yaml` **Scope:** Capability hierarchy with CMMI V2.0 maturity assessment, addressing system, V/H orientation, lifecycle. Aligned to roles, processes, and applications. -**Renderer:** Transitrix DSM — Capabilities table, Editor (C), BCM tab; Transitrix Studio (planned) +**Renderer:** Existing core grid: Transitrix DSM — Capabilities table, Editor (C), BCM tab. The added stage, coverage and view-config contracts have no verified native consumer support; see §16.4. --- @@ -160,6 +160,59 @@ set_name . b . o . L1 . L2 . L3 --- +## 6a. Management-cycle stage and tags + +A capability MAY carry `management_cycle_stage`, an optional classification +independent of architectural `layer`, decomposition, V/H orientation, maturity +and application mappings. It belongs on the canonical capability element, not +inside view configuration or an `extensions` bag. Canonical `layer` remains +`business`. Omitted or null means not classified; it does not imply an absent +ability or an incomplete domain. + +The controlled values and display order are: + +| Value | PDCA + Learn correspondence | Capability contribution | +| --- | --- | --- | +| `Strategic` | Act and strategic Plan | Choose direction, strategic goals, priorities and significant organisational improvements; strategic planning | +| `Learning` | Learn, the explicit addition to PDCA | Research, reflection, knowledge capture and sharing, learning and competence development | +| `Planning` | Tactical and operational Plan | Budgets, capacity, resources, scheduling and executable operational plans | +| `Execution` | Do | Perform the domain's work, including operational master-data maintenance | +| `Assurance` | Check | Monitor outcomes, quality, deviations, controls and compliance | + +Strategic planning belongs to Strategic; tactical/operational planning belongs +to Planning. Classify by decision scope and represented ability, not the word +"planning" or a time horizon alone. The order is a layout convention, not a +process sequence. Local improvement can occur in every domain. A composite L1 +can remain unclassified while its children occupy several stages. + +Stages MUST NOT inherit from parents, tags, names, maturity or orientation. +Changing a stage requires an explicit source-model change. Dated views must not +apply a later classification retroactively when no historical source is supplied. +This field does not by itself introduce a new sidecar series. + +`tags` remains the common free-form classifier array. ESG, MDM and Support are +tag values, never management stages. ESG/MDM describe subject affiliation; +Support describes a supporting role and does not automatically change the +capability's `type`. A tag does not create, remove or imply an H capability or +intersection. Actual horizontal capabilities retain their identity, hierarchy, +maturity and explicit application mappings. Tags MUST NOT inherit. + +```yaml +# Fragment of a capability element; admission/lifecycle omitted. +notation: capability +id: CAPABILITY-H1.2.1 +name: Customer data quality control +layer: business +management_cycle_stage: Assurance +tags: [MDM, Support] +``` + +A layer or tag is a source fact; whether/how to show it is view configuration +(§16). Missing classification remains visible as Not specified when grouping; +it is not silently assigned a stage. Unknown authored stage values are invalid. + +--- + ## 7. Lifecycle states Each capability has **Start Date** and **End Date** attributes reflecting the period during which it is valid: @@ -264,6 +317,8 @@ capability_map: | `owner_role` | No | Reference to BusinessRole element ID. **Time-varying** — sidecar, not inline. | | `business_process` | No | Reference to BusinessProcess element ID. Stays inline in v1 (relations are Wave 3 territory). | | `applications` | No | List of ApplicationComponent element IDs. Stays inline in v1 (relations are Wave 3 territory). | +| `management_cycle_stage` | No | One of the five values in §6a, or null; canonical capability attribute, no inheritance. | +| `tags` | No | Common element tags; ESG/MDM/Support are independent classifier values, not stages (§6a). | | `children` | No | List of child capabilities. | --- @@ -357,3 +412,137 @@ Migration: adopters with existing inline values move each value into a single-en Schema shape/type failures follow [CONTRACT.md](../../CONTRACT.md) §18.6. Positive/rejecting cases and version compatibility are in the [diagnostic migration contract](../../../migrations/6.0-to-7.0/). + +--- + +## 16. System coverage, dated views and presentation defaults + +### 16.1 Explicit coverage + +An application's direct `capabilities` list and its technical inventory's +explicit capability lists ([applications catalogue §6a](10-applications.md#6a-technical-capabilities-and-business-coverage)) +are separate evidence paths to business capabilities. A projection MUST declare +its direct-link source and retain both paths in provenance. Conflicting inverse +lists require reconciliation, not union by assumption. Parent/child, tag and +stage classifications do not imply support. Missing mappings mean not specified, +not unsupported, absent or zero coverage. Several links from one application +count once when collecting that application's target set. + +A system polygon encloses its selected visible targets. Connecting empty space, +shape and area do not add mappings or express a coverage percentage. Colored +polygons may mask unrelated boxes. Shape is either an axis-aligned bounding +rectangle or an angled enclosing hull; rounding is independent. Small margins +must not create decorative overlap between neighboring domains. + +Red overlap outlines the boundary of the region inside **at least two selected +outer system polygons**: `R = union over i < j of (P_i intersection P_j)`. +Use the selected rounded/rectangular/angled shape. Ignore box cut-out masks for +this boundary; they must not introduce steps around nested capability boxes. +A triple overlap has no internal pairwise seams; disjoint components have +separate closed contours. Point/edge contact with zero area is not a region. +This is a geometric diagnostic, not proof of equivalent functions, unnecessary +duplication, measured adequacy or a recommendation to retire a system. + +### 16.2 State on a date + +Keep selected model-validity date, evidence/snapshot date, source revision and +observed-versus-planned intent distinct. Resolve capability/parent lifetimes and +current/target maturity from the canonical lifecycle, relation and sidecar +contracts; do not replace them with a renderer's date conventions. A target +state is planned composition, never an observed outcome because its date passed. + +A renderer using full snapshot inputs must identify them as projections and +preserve source revision and evidence date. Dated snapshot windows must have +explicit boundaries; overlapping alternatives require separate scenario +selection. Gaps mean unknown/no supplied state and must not retain a stale map +for export. Missing history cannot be reconstructed by interpolation or copying +current support into a future target. If a source has no dated coverage history, +disclose snapshot-based coverage instead of asserting historical accuracy. + +The standalone HTML adapter uses exclusive snapshot-period ends and inclusive +record lifetime ends. An adapter MUST state and test its conversion from the +source contract; no universal reinterpretation of canonical `valid_to` is made. +Future targets, snapshot projections and date gaps must remain visible in the +rendered context. A descriptive horizon is never parsed into a calendar date. + +### 16.3 view_config defaults + +These keys are declared by this notation under the shared view-config contract. +They contain presentation choices only; all stages, tags, mappings and dated +assessments stay in source elements/relations/sidecars. + +```yaml +view_config: + valid_at: null # use capability_map.assessment_date + scenario: null # optional explicit TARGET_STATE reference; null is no target selection + management_cycle: + group: false + order: [Strategic, Learning, Planning, Execution, Assurance] + tags: + filter: [] # all tags; nonempty values select direct matches (OR) + show: true + display: + depth: 3 # 1, 2 or 3 + backlog: false + maturity: false + legend: true + systems: + highlight: null # optional APPLICATION reference + labels: false + coverage: false + selected: [] # explicit APPLICATION references; empty means no polygons + shape: angled # angled or rectangular + rounded: false + overlap: false # effective only with coverage and two selected systems +``` + +Stage order is fixed to §6a. Tag filters retain direct matches and the muted +parent/domain/lane context required to read them; context does not inherit tags. +Grouping creates only occupied stage zones behind elements, plus missing-value +diagnostics. It must preserve L1/L2/L3 containment and explicit H/V intersections. +Visibility of a layer never depends on the existence of an H capability. + +The maturity legend uses samples matching actual rendered box styles and the +canonical 1–5 scale in CONTRACT §9.4. A legacy adapter may additionally show an +explicit 0 No capability absence diagnostic, clearly distinguished from the +canonical scale; missing maturity is not 0. Hiding Maturity or Legend removes +that key and its reserved space. A general legend explains only enabled modes. +SVG exports the exact selected view, its legend and provenance. Zoom and hiding +controls affect the HTML viewport, not the model or native SVG coordinates. +Display filters are not access controls or redaction. + +### 16.4 Compatibility and implementation status + +The §6a/§16 additions and APPLICATION technical inventory are a core-methodology +correction prepared for review, not an optional extension profile. Existing +architectural layer, common tags, lifecycle and maturity contracts are retained. +No new globally addressable technical-capability TYPE is introduced. + +The standalone renderer currently consumes adapter JSON, not canonical YAML. +It maps management_cycle_stage to its legacy JSON layer field. Its HTML pilot +selection and direct-SVG backlog/all-system defaults must be overridden when +projecting the canonical defaults above. Its bounded ESG/MDM/Support tag menu +is a subset of the common free-form tags, which must remain in the source. + +The machine-readable [additive field schema](../../schemas/capability-map-contract.schema.json) +exposes `capabilityFields`, `applicationFields` and `viewConfig` definitions; +it does not replace the full element/view envelope. `tools/lint.py` implements +these structural checks without a new runtime dependency, application-local key +uniqueness, technical mapping references/lifetimes and view selection references. +It checks canonical elements and native files under `views/`, preserving the +single-file onboarding installation. It does not resolve assessments/history, +render coverage geometry or prove evidence truth. Existing direct APPLICATION +mapping checks remain the consuming validator's responsibility; the new +technical inventory cannot use that legacy boundary to skip its own references. + +Released CLI, DSM and Studio adoption and a methodology release must still be +verified separately before claiming product support. The [compatibility check](../../../guides/capability-map-compatibility.md) records +which fields the released CLI actually checks; acceptance with skipped checks +is not evidence of adoption. Until that adoption, a +consumer must report unsupported fields/settings rather than silently dropping +them. Existing `extensions.management_cycle_stage` proposals migrate explicitly +to the defined field; conflicting values require reconciliation, and history is +preserved. Core view keys are no longer to be documented as arbitrary extension +keys. The methodology version pin changes only through the release procedure. + +--- diff --git a/notations/views/diagrams/10-applications.md b/notations/views/diagrams/10-applications.md index 73ba801b..4e183323 100644 --- a/notations/views/diagrams/10-applications.md +++ b/notations/views/diagrams/10-applications.md @@ -1,8 +1,8 @@ --- notation: "Applications Catalogue" -version: "0.2" +version: "0.3" author: "Valerii Korobeinikov" -last_updated: "2026-09-26" +last_updated: "2026-10-23" status: "draft" file_extension: "*.applications.transitrix.yaml" --- @@ -152,6 +152,7 @@ applications_catalogue: | `applications[].maturity` | No | CMM level ([CONTRACT.md](../../CONTRACT.md) §9.4 shared CMMI V2.0 scale). **Time-varying** — sidecar, not inline. | | `applications[].description` | No | Short description of the application's purpose. | | `applications[].capabilities` | No | List of capability IDs this application enables. Stays inline in v1 (relations are Wave 3 territory). | +| `applications[].technical_capabilities` | No | Application-owned technical inventory with explicit business-capability mappings (§6a). Canonical by containment; not an extensions entry or a new global TYPE. | | `applications[].products` | No | List of Product element IDs this application supports. Stays inline in v1 (relations are Wave 3 territory). | | `applications[].integrations` | No | List of outbound integration descriptors. Stays inline (a relation list scoped to the parent application). | | `integrations[].target` | No | Target application element ID | @@ -253,6 +254,67 @@ ArchiMate → formal model of application layer elements and rel --- +## 6a. Technical capabilities and business coverage + +An APPLICATION MAY contain `technical_capabilities`, the named technical +abilities it supplies to support business capabilities. The inventory belongs +on the canonical application element. Its evidence and admission are owned by +the application, not authored only inside a capability-map view. + +Each entry is canonical by containment, with an application-local identity. +It is not a new standalone TYPE or REL kind and must not masquerade as an +independently admitted global element. Identify an entry in its owning application projection by application ID +and local key together. This does not grant independent cross-model reference +semantics or bypass the common promotion rule. Separate technical systems do not share a local key's +identity, even if the text matches. + +| Entry field | Required | Meaning | +| --- | --- | --- | +| `key` | yes | Nonempty string unique within the application | +| `name` | yes | Source-evidenced technical ability | +| `capabilities` | no | Distinct canonical business CAPABILITY references; default empty means not specified | +| `valid_from`, `valid_to` | no | Optional explicit lifetime using the shared primitive lifecycle boundary semantics; omitted bounds are open within the owning application's lifecycle | + +```yaml +# Fragment of an APPLICATION element; common envelope omitted. +id: APPLICATION-CRM-1 +capabilities: [CAPABILITY-V2] +technical_capabilities: + - key: customer-data-validation + name: Customer data validation + capabilities: [CAPABILITY-H1.2.1] + valid_from: "2026-10-01" + valid_to: null +``` + +Direct application mappings and technical-entry mappings retain their separate +provenance. Their union is the application's business target set for a coverage +view; no parent/child inheritance is implied. All references must resolve before +view filtering. Duplicate keys, duplicate targets, missing required names/keys +and unknown targets are invalid. Missing inventory or an empty mapping does not +prove absent technical ability or unsupported business capability. + +An entry's effective lifetime is the intersection of its open/explicit bounds +and the application's lifecycle. Explicit bounds outside the owner lifetime or +an end before its start are invalid; a missing bound does not require inventing +an earlier history. + +A dated coverage view includes only explicitly active applications and inventory +entries. It does not recover mapping histories from today's lists. Source +snapshot/scenario intent and missing history must be disclosed as required by +[capability-map §16](05-capability-map.md#16-system-coverage-dated-views-and-presentation-defaults). +Changes to existing direct-list history remain subject to the existing relation +migration contract; this field does not silently create a new temporal REL kind. +The standalone JSON renderer maps each local key to technical entry `id`; source +`valid_from`/`valid_to` require an explicit endpoint conversion to its inclusive +`start_date`/`end_date`. The defined source field is not an `extensions` field. + +The additive schema and whole-model reference checks ship with the methodology +([capability-map §16.4](05-capability-map.md#164-compatibility-and-implementation-status)). +Released CLI/DSM/Studio adoption must still be verified independently. + +--- + ## 7. References - Application elements: `elements/03_application/*.yaml` diff --git a/notations/vocabulary.yaml b/notations/vocabulary.yaml index 8c2c8681..10f2220a 100644 --- a/notations/vocabulary.yaml +++ b/notations/vocabulary.yaml @@ -451,6 +451,11 @@ deprecated_rule_codes: # --------------------------------------------------------------------------- value_vocabularies: + CAPABILITY.management_cycle_stage: + values: [Strategic, Learning, Planning, Execution, Assurance] + spec: notations/views/diagrams/05-capability-map.md + rule: null + # REQUIREMENT — elements/15-requirement.md REQUIREMENT.origin: values: [legislative, process-product, project-product] diff --git a/publish-manifest.json b/publish-manifest.json index a3e5613a..8ea6f162 100644 --- a/publish-manifest.json +++ b/publish-manifest.json @@ -8,6 +8,7 @@ ".github/dependabot.yml", ".github/pull_request_template.md", ".github/workflows/adl-guard.yml", + ".github/workflows/capability-contract-test.yml", ".github/workflows/check-commit-authors.yml", ".github/workflows/decisions-cli-test.yml", ".github/workflows/dependabot-auto-merge.yml", @@ -50,6 +51,7 @@ "guides/adl-adopter-setup.md", "guides/adoption-health-profile.md", "guides/adr-applicability-in-pull-requests.md", + "guides/capability-map-compatibility.md", "guides/desktop-model-workplace.md", "guides/how-a-document-is-versioned.md", "guides/how-a-document-prints.md", @@ -240,6 +242,7 @@ "notations/examples/bpmn/xlarge-stress-test.bpmn.transitrix.yaml", "notations/examples/capability-map/README.md", "notations/examples/capability-map/business.capability-map.transitrix.yaml", + "notations/examples/capability-map/management-cycle.capability-map.transitrix.yaml", "notations/examples/capability-map/northbay-retail.capability-map.transitrix.yaml", "notations/examples/codex/standard/README.md", "notations/examples/codex/standard/codex/external/intl/STANDARD-iso-27001-1.yaml", @@ -461,6 +464,8 @@ "notations/packages/reqif.md", "notations/recurring-activities.md", "notations/requirement-language-advisories.md", + "notations/schemas/README.md", + "notations/schemas/capability-map-contract.schema.json", "notations/views/REPORT_VIEW_CONFIG.md", "notations/views/diagrams/01-bpmn.md", "notations/views/diagrams/02-dgca.md", @@ -671,6 +676,23 @@ "scripts/generate-plugin-manifests.mjs", "scripts/release-obligations.mjs", "scripts/release-obligations.test.mjs", + "tests/fixtures/capability-management-cycle/README.md", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.history.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.history.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.history.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.history.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.history.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.history.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-GOVERNANCE-1.yaml", + "tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-OMS-1.yaml", + "tests/fixtures/capability-management-cycle/views/coverage.applications.transitrix.yaml", + "tests/fixtures/capability-management-cycle/views/cycle.capability-map.transitrix.yaml", "tests/fixtures/comprehensive-notation-coverage/canon/elements/01_motivation/goals/GOAL-TEST-1.yaml", "tests/fixtures/comprehensive-notation-coverage/canon/elements/01_motivation/requirements/REQUIREMENT-TEST-1.yaml", "tests/fixtures/comprehensive-notation-coverage/canon/elements/05_implementation/actions/ACTION-TEST-1.yaml", @@ -684,6 +706,7 @@ "tools/knowledge_store_lint.py", "tools/lint.py", "tools/reporting.py", + "tools/tests/test_capability_contract.py", "tools/tests/test_knowledge_freshness.py", "tools/tests/test_knowledge_supersession.py", "tools/tests/test_lint_encoding.py", diff --git a/tests/fixtures/capability-management-cycle/README.md b/tests/fixtures/capability-management-cycle/README.md new file mode 100644 index 00000000..21be0322 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/README.md @@ -0,0 +1,21 @@ +# Capability management-cycle contract fixture + +A synthetic source model with five stages, a separate horizontal capability, +L1/L2/L3 containment and ESG/MDM/Support subject tags. Two applications share +coverage of the horizontal capability through independently owned technical +entries; identical local keys in different applications are legal. One technical +entry starts in 2027: its future lifecycle is source intent, not observed delivery. +Synthetic maturity assessments are kept in canonical history sidecars, never +duplicated inline. This fragment is a contract fixture, not a release-pinned +organisation catalogue. + +Run the reference validator from this directory: + +```sh +python ../../../tools/lint.py +``` + +The native view example in `notations/examples/capability-map/` repeats this +fixture's view and is checked for drift. Tests also verify legacy omission, +free-form tags, malformed settings, mapping references and application lifetimes. +This fixture tests contracts; it does not claim CLI/DSM/Studio renderer support. diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.history.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.history.yaml new file mode 100644 index 00000000..e58f842e --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.history.yaml @@ -0,0 +1,5 @@ +target: CAPABILITY-H1 +attribute_versions: + current_maturity: + - valid_from: '2026-01-01' + value: 3 diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.yaml new file mode 100644 index 00000000..82641881 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-H1.yaml @@ -0,0 +1,19 @@ +notation: capability +id: CAPABILITY-H1 +name: Order assurance +type: supporting +layer: business +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +management_cycle_stage: Assurance +tags: +- ESG +- MDM +- Support diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.history.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.history.yaml new file mode 100644 index 00000000..c6f103ab --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.history.yaml @@ -0,0 +1,5 @@ +target: CAPABILITY-V1.1 +attribute_versions: + current_maturity: + - valid_from: '2026-01-01' + value: 3 diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.yaml new file mode 100644 index 00000000..9bb60159 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.1.yaml @@ -0,0 +1,16 @@ +notation: capability +id: CAPABILITY-V1.1 +name: Order strategy +type: domain +layer: business +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +parent: CAPABILITY-V1 +management_cycle_stage: Strategic diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.history.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.history.yaml new file mode 100644 index 00000000..ad9b4c3d --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.history.yaml @@ -0,0 +1,5 @@ +target: CAPABILITY-V1.2 +attribute_versions: + current_maturity: + - valid_from: '2026-01-01' + value: 3 diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.yaml new file mode 100644 index 00000000..c38f52ba --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.2.yaml @@ -0,0 +1,16 @@ +notation: capability +id: CAPABILITY-V1.2 +name: Order improvement learning +type: domain +layer: business +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +parent: CAPABILITY-V1 +management_cycle_stage: Learning diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.history.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.history.yaml new file mode 100644 index 00000000..74767f9f --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.history.yaml @@ -0,0 +1,5 @@ +target: CAPABILITY-V1.3.1 +attribute_versions: + current_maturity: + - valid_from: '2026-01-01' + value: 3 diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.yaml new file mode 100644 index 00000000..8273d52c --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.1.yaml @@ -0,0 +1,16 @@ +notation: capability +id: CAPABILITY-V1.3.1 +name: Pick and pack +type: domain +layer: business +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +parent: CAPABILITY-V1.3 +management_cycle_stage: Execution diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.history.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.history.yaml new file mode 100644 index 00000000..85e5c312 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.history.yaml @@ -0,0 +1,5 @@ +target: CAPABILITY-V1.3 +attribute_versions: + current_maturity: + - valid_from: '2026-01-01' + value: 3 diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.yaml new file mode 100644 index 00000000..1b9ed2ec --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.3.yaml @@ -0,0 +1,16 @@ +notation: capability +id: CAPABILITY-V1.3 +name: Fulfilment planning +type: domain +layer: business +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +parent: CAPABILITY-V1 +management_cycle_stage: Planning diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.history.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.history.yaml new file mode 100644 index 00000000..435a0b06 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.history.yaml @@ -0,0 +1,5 @@ +target: CAPABILITY-V1 +attribute_versions: + current_maturity: + - valid_from: '2026-01-01' + value: 3 diff --git a/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.yaml b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.yaml new file mode 100644 index 00000000..46f75241 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/02_business/capabilities/CAPABILITY-V1.yaml @@ -0,0 +1,14 @@ +notation: capability +id: CAPABILITY-V1 +name: Order Management +type: domain +layer: business +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null diff --git a/tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-GOVERNANCE-1.yaml b/tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-GOVERNANCE-1.yaml new file mode 100644 index 00000000..b457a691 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-GOVERNANCE-1.yaml @@ -0,0 +1,28 @@ +notation: application +id: APPLICATION-GOVERNANCE-1 +name: Governance Portal +layer: application +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +capabilities: +- CAPABILITY-H1 +technical_capabilities: +- key: controls + name: Evidence and control workflow + capabilities: + - CAPABILITY-H1 + valid_from: '2026-01-01' + valid_to: null +- key: forecast + name: Planned forecasting + capabilities: + - CAPABILITY-V1.3 + valid_from: '2027-01-01' + valid_to: null diff --git a/tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-OMS-1.yaml b/tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-OMS-1.yaml new file mode 100644 index 00000000..8b1d87fb --- /dev/null +++ b/tests/fixtures/capability-management-cycle/canon/elements/03_application/applications/APPLICATION-OMS-1.yaml @@ -0,0 +1,29 @@ +notation: application +id: APPLICATION-OMS-1 +name: Order Management System +layer: application +zone: canon +admitted_at: '2026-10-23' +admitted_by: example-architect +gate_checks: + uniqueness: pass + consistency: pass + completeness: pass +valid_from: '2026-01-01' +valid_to: null +capabilities: +- CAPABILITY-V1.3.1 +technical_capabilities: +- key: controls + name: Evidence and control workflow + capabilities: + - CAPABILITY-V1.3.1 + - CAPABILITY-H1 + valid_from: '2026-01-01' + valid_to: null +- key: forecast + name: Planned forecasting + capabilities: + - CAPABILITY-V1.3 + valid_from: '2027-01-01' + valid_to: null diff --git a/tests/fixtures/capability-management-cycle/views/coverage.applications.transitrix.yaml b/tests/fixtures/capability-management-cycle/views/coverage.applications.transitrix.yaml new file mode 100644 index 00000000..3818b7e5 --- /dev/null +++ b/tests/fixtures/capability-management-cycle/views/coverage.applications.transitrix.yaml @@ -0,0 +1,47 @@ +notation: applications +spec_version: '0.3' +name: System coverage inventory +applications_catalogue: + id: APP-CAT-COVERAGE-1 + name: System coverage inventory + updated_at: '2026-10-23' + applications: + - app_id: APPLICATION-OMS-1 + name: Order Management System + type: application + status: Active + capabilities: + - CAPABILITY-V1.3.1 + technical_capabilities: + - key: controls + name: Evidence and control workflow + capabilities: + - CAPABILITY-V1.3.1 + - CAPABILITY-H1 + valid_from: '2026-01-01' + valid_to: null + - key: forecast + name: Planned forecasting + capabilities: + - CAPABILITY-V1.3 + valid_from: '2027-01-01' + valid_to: null + - app_id: APPLICATION-GOVERNANCE-1 + name: Governance Portal + type: application + status: Active + capabilities: + - CAPABILITY-H1 + technical_capabilities: + - key: controls + name: Evidence and control workflow + capabilities: + - CAPABILITY-H1 + valid_from: '2026-01-01' + valid_to: null + - key: forecast + name: Planned forecasting + capabilities: + - CAPABILITY-V1.3 + valid_from: '2027-01-01' + valid_to: null diff --git a/tests/fixtures/capability-management-cycle/views/cycle.capability-map.transitrix.yaml b/tests/fixtures/capability-management-cycle/views/cycle.capability-map.transitrix.yaml new file mode 100644 index 00000000..74e54eac --- /dev/null +++ b/tests/fixtures/capability-management-cycle/views/cycle.capability-map.transitrix.yaml @@ -0,0 +1,81 @@ +notation: capability-map +spec_version: '0.5' +name: Management cycle and system coverage +capability_map: + id: CAPABILITY_MAP-CYCLE-1 + name: Management cycle and system coverage + assessment_date: '2026-10-23' + capabilities: + - id: CAPABILITY-V1 + name: Order Management + type: domain + valid_from: '2026-01-01' + valid_to: null + children: + - id: CAPABILITY-V1.1 + name: Order strategy + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Strategic + - id: CAPABILITY-V1.2 + name: Order improvement learning + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Learning + - id: CAPABILITY-V1.3 + name: Fulfilment planning + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Planning + children: + - id: CAPABILITY-V1.3.1 + name: Pick and pack + type: domain + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Execution + - id: CAPABILITY-H1 + name: Order assurance + type: supporting + valid_from: '2026-01-01' + valid_to: null + management_cycle_stage: Assurance + tags: + - ESG + - MDM + - Support +view_config: + valid_at: null + scenario: null + management_cycle: + group: true + order: + - Strategic + - Learning + - Planning + - Execution + - Assurance + tags: + filter: + - ESG + - MDM + - Support + show: true + display: + depth: 3 + backlog: false + maturity: false + legend: true + systems: + labels: false + coverage: true + selected: + - APPLICATION-OMS-1 + - APPLICATION-GOVERNANCE-1 + highlight: null + shape: angled + rounded: false + overlap: true diff --git a/tools/lint.py b/tools/lint.py index 6a820474..9bfd3b0b 100644 --- a/tools/lint.py +++ b/tools/lint.py @@ -45,6 +45,168 @@ def _ensure_utf8_stdio() -> None: _ensure_utf8_stdio() + +# Additive core contracts. Kept in this single-file linter so existing onboarding +# installations can still vendor tools/lint.py without a new runtime dependency. +# The published JSON Schema is checked against this value by regression tests. +CAPABILITY_MAP_CONTRACT = {'$schema': 'http://json-schema.org/draft-07/schema#', + 'title': 'Capability-map core additive field contracts', + '$comment': 'Additive field contracts, not a complete model schema. Select a $defs entry. ' + 'Referential integrity, local-key uniqueness and lifetime checks require ' + 'tools/lint.py. Other fields retain their existing specifications.', + '$defs': {'capabilityFields': {'type': 'object', + 'properties': {'management_cycle_stage': {'enum': [None, + 'Strategic', + 'Learning', + 'Planning', + 'Execution', + 'Assurance']}, + 'tags': {'type': 'array', + 'items': {'type': 'string', + 'minLength': 1, + 'pattern': '\\S'}}}, + 'additionalProperties': True}, + 'applicationFields': {'type': 'object', + 'properties': {'technical_capabilities': {'type': 'array', + 'items': {'type': 'object', + 'properties': {'key': {'type': 'string', + 'minLength': 1, + 'pattern': '\\S'}, + 'name': {'type': 'string', + 'minLength': 1, + 'pattern': '\\S'}, + 'capabilities': {'type': 'array', + 'items': {'type': 'string', + 'minLength': 1, + 'pattern': '\\S'}, + 'uniqueItems': True}, + 'valid_from': {'type': ['string', + 'null'], + 'format': 'date'}, + 'valid_to': {'type': ['string', + 'null'], + 'format': 'date'}}, + 'additionalProperties': False, + 'required': ['key', + 'name']}}}, + 'additionalProperties': True}, + 'viewConfig': {'type': 'object', + 'properties': {'valid_at': {'type': ['string', 'null'], + 'format': 'date', + 'default': None}, + 'scenario': {'type': ['string', 'null'], + 'minLength': 1, + 'default': None}, + 'management_cycle': {'type': 'object', + 'properties': {'group': {'type': 'boolean', + 'default': False}, + 'order': {'const': ['Strategic', + 'Learning', + 'Planning', + 'Execution', + 'Assurance'], + 'default': ['Strategic', + 'Learning', + 'Planning', + 'Execution', + 'Assurance']}}, + 'additionalProperties': False}, + 'tags': {'type': 'object', + 'properties': {'filter': {'type': 'array', + 'items': {'type': 'string', + 'minLength': 1, + 'pattern': '\\S'}, + 'uniqueItems': True, + 'default': []}, + 'show': {'type': 'boolean', + 'default': True}}, + 'additionalProperties': False}, + 'display': {'type': 'object', + 'properties': {'depth': {'type': 'integer', + 'enum': [1, 2, 3], + 'default': 3}, + 'backlog': {'type': 'boolean', + 'default': False}, + 'maturity': {'type': 'boolean', + 'default': False}, + 'legend': {'type': 'boolean', + 'default': True}}, + 'additionalProperties': False}, + 'systems': {'type': 'object', + 'properties': {'highlight': {'type': ['string', + 'null'], + 'minLength': 1, + 'default': None}, + 'selected': {'type': 'array', + 'items': {'type': 'string', + 'minLength': 1, + 'pattern': '\\S'}, + 'uniqueItems': True, + 'default': []}, + 'shape': {'enum': ['angled', + 'rectangular'], + 'default': 'angled'}, + 'labels': {'type': 'boolean', + 'default': False}, + 'coverage': {'type': 'boolean', + 'default': False}, + 'rounded': {'type': 'boolean', + 'default': False}, + 'overlap': {'type': 'boolean', + 'default': False}}, + 'additionalProperties': False}}, + 'additionalProperties': False}}} + + +def _contract_errors(value, schema, path=""): + """Evaluate the bounded keywords used by CAPABILITY_MAP_CONTRACT. + + This is not a general JSON Schema engine. The published schema also works + with ordinary draft-07 validators; semantic references are checked below. + """ + from datetime import date + import re + errors = [] + types = {"object": dict, "array": list, "string": str, + "boolean": bool, "integer": int, "null": type(None)} + expected = schema.get("type") + if expected: + expected = [expected] if isinstance(expected, str) else expected + if not any(type(value) is types[t] for t in expected): + return [f"{path}: expected {' or '.join(expected)}"] + if "enum" in schema and not any(type(value) is type(v) and value == v for v in schema['enum']): + errors.append(f"{path}: unsupported value {value!r}") + if 'const' in schema and value != schema['const']: + errors.append(f"{path}: must equal {schema['const']!r}") + if isinstance(value, str): + if len(value) < schema.get('minLength', 0) or ('pattern' in schema and not re.search(schema['pattern'], value)): + errors.append(f"{path}: expected a nonempty string") + if schema.get('format') == 'date': + try: + if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value): + raise ValueError() + date.fromisoformat(value) + except ValueError: + errors.append(f"{path}: expected a quoted YYYY-MM-DD calendar date") + if isinstance(value, dict): + props = schema.get('properties', {}) + for key in schema.get('required', []): + if key not in value: + errors.append(f"{path}.{key}: required") + for key, item in value.items(): + if key in props: + errors.extend(_contract_errors(item, props[key], f"{path}.{key}")) + elif schema.get('additionalProperties') is False: + errors.append(f"{path}.{key}: unknown field") + if isinstance(value, list): + if schema.get('uniqueItems') and any(item in value[:i] for i, item in enumerate(value)): + errors.append(f"{path}: duplicate values") + if 'items' in schema: + for i, item in enumerate(value): + errors.extend(_contract_errors(item, schema['items'], f"{path}[{i}]")) + return errors + + @dataclass class LintError: file: str @@ -93,6 +255,9 @@ def run(self) -> bool: print("Phase 4: Validating referential integrity...") self._check_referential_integrity() + # Optional capability-map contracts and view selections. + self._check_capability_contracts() + # Phase 5: Policy compliance print("Phase 5: Checking compliance policies...") self._check_policies() @@ -254,6 +419,104 @@ def _check_referential_integrity(self): severity="error" )) + def _check_capability_contracts(self): + """Check native additive fields and capability-map view settings.""" + definitions = CAPABILITY_MAP_CONTRACT['$defs'] + + def report(file, messages): + for message in messages: + self.errors.append(LintError(str(file), 0, "Capability contract: " + message, "error")) + + def resolve(ref, prefix, path): + if not isinstance(ref, str): + return [] # structural diagnostic already emitted + if not ref.startswith(prefix + '-') or ref not in self.elements: + return [f"{path}: unresolved or wrong-type {prefix} reference {ref!r}"] + return [] + + def application(data, path): + errors = _contract_errors(data, definitions['applicationFields'], path) + inventory = data.get('technical_capabilities', []) + if not isinstance(inventory, list): + return errors + keys = [] + for i, ability in enumerate(inventory): + where = f"{path}.technical_capabilities[{i}]" + if not isinstance(ability, dict): + continue + key = ability.get('key') + if isinstance(key, str): + if key in keys: + errors.append(f"{where}.key: duplicate application-local key {key!r}") + keys.append(key) + targets = ability.get('capabilities', []) + if isinstance(targets, list): + for ref in targets: + errors.extend(resolve(ref, 'CAPABILITY', where + '.capabilities')) + start, end = ability.get('valid_from'), ability.get('valid_to') + # Structural errors above exclude malformed endpoints from comparisons. + bounds = {'type': ['string', 'null'], 'format': 'date'} + if not _contract_errors(start, bounds) and not _contract_errors(end, bounds): + if start and end and start > end: + errors.append(f"{where}: valid_to precedes valid_from") + owner_start, owner_end = data.get('valid_from'), data.get('valid_to') + for field, bound in [('valid_from', start), ('valid_to', end)]: + if bound is None: + continue + outside = (isinstance(owner_start, str) and bound < owner_start + or isinstance(owner_end, str) and bound > owner_end) + if outside: + errors.append(f"{where}.{field}: lifetime must be contained in APPLICATION lifetime") + return errors + + for element_id, data in self.elements.items(): + path = f"canon/elements/*/{element_id}.yaml" + if element_id.startswith('CAPABILITY-'): + report(path, _contract_errors(data, definitions['capabilityFields'], element_id)) + elif element_id.startswith('APPLICATION-'): + report(path, application(data, element_id)) + + def capability_tree(nodes, file): + if not isinstance(nodes, list): + return + for data in nodes: + if isinstance(data, dict): + report(file, _contract_errors(data, definitions['capabilityFields'], str(data.get('id', 'capability')))) + capability_tree(data.get('children', []), file) + + for file in sorted((self.repo_root / 'views').rglob('*.transitrix.yaml')): + try: + data = yaml.safe_load(file.read_text(encoding='utf-8')) + except (OSError, yaml.YAMLError) as error: + report(file, [f"cannot read YAML: {error}"]) + continue + if not isinstance(data, dict): + continue + if data.get('notation') == 'capability-map': + body = data.get('capability_map', {}) + if isinstance(body, dict): + capability_tree(body.get('capabilities', []), file) + if 'view_config' not in data: + continue + config = data['view_config'] + report(file, _contract_errors(config, definitions['viewConfig'], 'view_config')) + if not isinstance(config, dict): + continue + report(file, resolve(config.get('scenario'), 'TARGET_STATE', 'view_config.scenario')) + systems = config.get('systems', {}) + if isinstance(systems, dict): + report(file, resolve(systems.get('highlight'), 'APPLICATION', 'view_config.systems.highlight')) + selected = systems.get('selected', []) + if isinstance(selected, list): + for ref in selected: + report(file, resolve(ref, 'APPLICATION', 'view_config.systems.selected')) + elif data.get('notation') == 'applications': + body = data.get('applications_catalogue', {}) + if isinstance(body, dict) and isinstance(body.get('applications', []), list): + for app in body.get('applications', []): + if isinstance(app, dict): + report(file, application(app, str(app.get('app_id', 'application')))) + def _check_policies(self): """Check organizational policies (e.g., Active elements must have owner).""" for element_id, element_data in self.elements.items(): diff --git a/tools/tests/test_capability_contract.py b/tools/tests/test_capability_contract.py new file mode 100644 index 00000000..85046dd4 --- /dev/null +++ b/tools/tests/test_capability_contract.py @@ -0,0 +1,191 @@ +"""Native core contracts: compatibility, schema parity and refusal cases. + +Run: python -m unittest discover -s tools/tests -p test_capability_contract.py +""" +import contextlib +import copy +import importlib.util +import io +import json +from pathlib import Path +import shutil +import sys +import tempfile +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('model_lint', ROOT / 'tools/lint.py') +lint = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = lint +spec.loader.exec_module(lint) +FIXTURE = ROOT / 'tests/fixtures/capability-management-cycle' + + +class CapabilityContractTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + shutil.copytree(FIXTURE, self.root, dirs_exist_ok=True) + + def check(self, valid): + model = lint.TransitrixLinter(str(self.root)) + with contextlib.redirect_stdout(io.StringIO()): + try: + result = model.run() + except SystemExit as error: + result = error.code == 0 + self.assertEqual(result, valid, '\n'.join(e.message for e in model.errors)) + return '\n'.join(e.message for e in model.errors) + + def mutate(self, pattern, operation): + path = next(self.root.rglob(pattern)) + data = yaml.safe_load(path.read_text()) + operation(data) + path.write_text(yaml.safe_dump(data, sort_keys=False)) + + def test_complete_model(self): + self.check(True) + + def test_legacy_omission_and_existing_fixtures(self): + for file in self.root.rglob('*.yaml'): + data = yaml.safe_load(file.read_text()) + def strip(node): + if isinstance(node, dict): + for key in ('management_cycle_stage', 'technical_capabilities', 'view_config'): + node.pop(key, None) + for value in node.values(): + strip(value) + elif isinstance(node, list): + for value in node: + strip(value) + strip(data) + file.write_text(yaml.safe_dump(data)) + self.check(True) + # New checks alone must not reject any existing published example. + model = lint.TransitrixLinter(str(ROOT)) + model._check_capability_contracts() + self.assertEqual(model.errors, []) + for fixture in (ROOT / 'tests/fixtures').iterdir(): + if not fixture.is_dir() or fixture == FIXTURE: + continue + existing = lint.TransitrixLinter(str(fixture)) + existing._load_elements() + existing._check_capability_contracts() + self.assertEqual(existing.errors, [], str(fixture)) + for file in (ROOT / 'notations/examples').rglob('*.transitrix.yaml'): + if file.name.startswith('management-cycle.'): + continue + data = yaml.safe_load(file.read_text()) + if isinstance(data, dict) and data.get('notation') == 'capability-map': + def verify(nodes): + for node in nodes: + self.assertEqual(lint._contract_errors(node, lint.CAPABILITY_MAP_CONTRACT['$defs']['capabilityFields']), []) + verify(node.get('children', [])) + verify(data.get('capability_map', {}).get('capabilities', [])) + + def test_stage_values_and_no_inference(self): + for stage in [None, 'Strategic', 'Learning', 'Planning', 'Execution', 'Assurance']: + self.mutate('CAPABILITY-V1.yaml', lambda d: d.update(management_cycle_stage=stage, tags=['MDM', 'other-subject'])) + self.check(True) + for stage in ['Support', 'MDM', 'ESG', 'Plan', '', [], 1]: + with self.subTest(stage=stage): + self.mutate('CAPABILITY-V1.yaml', lambda d: d.update(management_cycle_stage=stage)) + self.assertIn('management_cycle_stage', self.check(False)) + + def test_inventory_refusals(self): + original = yaml.safe_load(next(self.root.rglob('APPLICATION-OMS-1.yaml')).read_text()) + cases = [ + lambda d: d.update(technical_capabilities={}), + lambda d: d['technical_capabilities'].append(copy.deepcopy(d['technical_capabilities'][0])), + lambda d: d['technical_capabilities'][0].pop('name'), + lambda d: d['technical_capabilities'][0].update(key=' '), + lambda d: d['technical_capabilities'][0].update(capabilities=['CAPABILITY-V99']), + lambda d: d['technical_capabilities'][0].update(capabilities=['APPLICATION-OMS-1']), + lambda d: d['technical_capabilities'][0].update(capabilities=['CAPABILITY-H1'] * 2), + lambda d: d['technical_capabilities'][0].update(capabilities=[{}]), + lambda d: d['technical_capabilities'][0].update(valid_from='2026-02-30'), + lambda d: d['technical_capabilities'][0].update(valid_from='2026-2-01'), + lambda d: d['technical_capabilities'][0].update(valid_from='2027-01-01', valid_to='2026-01-01'), + lambda d: d['technical_capabilities'][0].update(valid_from='2025-01-01'), + lambda d: d['technical_capabilities'][0].update(unknown=True), + ] + for index, change in enumerate(cases): + with self.subTest(case=index): + data = copy.deepcopy(original) + change(data) + next(self.root.rglob('APPLICATION-OMS-1.yaml')).write_text(yaml.safe_dump(data)) + self.assertIn('technical_capabilities', self.check(False)) + + def test_local_identity_and_open_lifetimes(self): + # Two applications may share a local key. Open ends are allowed for a live owner. + self.mutate('APPLICATION-OMS-1.yaml', lambda d: (d['technical_capabilities'][0].pop('valid_to'), d['technical_capabilities'][0].pop('valid_from'))) + self.check(True) + + def test_lifetime_end_and_inventory_shape(self): + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d.update(valid_to='2028-01-01')) + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d['technical_capabilities'][0].update(valid_to='2029-01-01')) + self.assertIn('lifetime must be contained', self.check(False)) + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d['technical_capabilities'][0].update(valid_to='2028-01-01')) + self.check(True) + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d.update(technical_capabilities=[None])) + self.assertIn('expected object', self.check(False)) + + def test_explicit_bounds_on_opposite_sides_of_owner_window(self): + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d.update(valid_to='2028-01-01')) + for bounds in [{'valid_from': '2029-01-01', 'valid_to': None}, + {'valid_from': None, 'valid_to': '2025-12-31'}]: + with self.subTest(bounds=bounds): + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d['technical_capabilities'][0].update(bounds)) + self.assertIn('lifetime must be contained', self.check(False)) + self.mutate('APPLICATION-OMS-1.yaml', lambda d: d['technical_capabilities'][0].update(valid_from=None, valid_to=None)) + self.check(True) + + def test_canonical_tag_type(self): + self.mutate('CAPABILITY-H1.yaml', lambda d: d.update(tags='Support')) + self.assertIn('tags', self.check(False)) + + def test_settings_refusals(self): + original = yaml.safe_load(next(self.root.rglob('cycle.capability-map.transitrix.yaml')).read_text()) + cases = [ + {'unknown': True}, {'display': {'unknown': True}}, {'display': {'depth': True}}, {'display': {'depth': 4}}, + {'systems': {'coverage': 'true'}}, {'systems': {'shape': 'convex'}}, + {'systems': {'selected': ['CAPABILITY-V1']}}, + {'systems': {'selected': ['APPLICATION-MISSING-1']}}, + {'systems': {'selected': ['APPLICATION-OMS-1'] * 2}}, + {'systems': {'highlight': 'APPLICATION-MISSING-1'}}, + {'scenario': 'CAPABILITY-V1'}, {'valid_at': '2026-02-30'}, + {'tags': {'filter': 'MDM'}}, + {'management_cycle': {'order': ['Execution', 'Planning']}}, + ] + for config in cases: + with self.subTest(config=config): + data = copy.deepcopy(original) + data['view_config'] = config + next(self.root.rglob('cycle.capability-map.transitrix.yaml')).write_text(yaml.safe_dump(data)) + self.assertIn('view_config', self.check(False)) + + def test_partial_settings_dates_and_inactive_options(self): + for config in [{}, {'valid_at': '2000-01-01'}, {'valid_at': '2050-01-01'}, + {'systems': {'coverage': False, 'selected': [], 'overlap': True}}, + {'systems': {'highlight': 'APPLICATION-OMS-1'}}, + {'tags': {'filter': ['ESG', 'custom-tag']}}]: + self.mutate('cycle.capability-map.transitrix.yaml', lambda d: d.update(view_config=config)) + self.check(True) + + def test_schema_and_vocabulary_parity(self): + schema = json.loads((ROOT / 'notations/schemas/capability-map-contract.schema.json').read_text()) + self.assertEqual(schema, lint.CAPABILITY_MAP_CONTRACT) + vocabulary = yaml.safe_load((ROOT / 'notations/vocabulary.yaml').read_text()) + enum = schema['$defs']['capabilityFields']['properties']['management_cycle_stage']['enum'][1:] + self.assertEqual(enum, vocabulary['value_vocabularies']['CAPABILITY.management_cycle_stage']['values']) + self.assertEqual(enum, schema['$defs']['viewConfig']['properties']['management_cycle']['properties']['order']['const']) + example = yaml.safe_load((ROOT / 'notations/examples/capability-map/management-cycle.capability-map.transitrix.yaml').read_text()) + fixture = yaml.safe_load((FIXTURE / 'views/cycle.capability-map.transitrix.yaml').read_text()) + self.assertEqual(example, fixture) + + +if __name__ == '__main__': + unittest.main()