diff --git a/.github/sig-floor.json b/.github/sig-floor.json new file mode 100644 index 000000000..aa4c7da02 --- /dev/null +++ b/.github/sig-floor.json @@ -0,0 +1,28 @@ +{ + "about": "SIG 4-star checks this app meets and must keep meeting (gate 123, sig-floor). Generated by check_sig_floor.py write; raised by the workflow, never lowered by hand.", + "schema": 1, + "profile": "four-star", + "tools": { + "lizard": "1.24.3" + }, + "measured_at": "dd80cc1c259151f94ad7fcc2abe0b9d617237aff", + "checks": [ + "javascript duplication", + "javascript unit_complexity >25", + "javascript unit_interfacing >2", + "javascript unit_interfacing >4", + "javascript unit_interfacing >6", + "php module_coupling >10", + "php unit_interfacing >4", + "php unit_interfacing >6", + "vue unit_complexity >10", + "vue unit_complexity >25", + "vue unit_complexity >5", + "vue unit_interfacing >2", + "vue unit_interfacing >4", + "vue unit_interfacing >6", + "vue unit_size >15", + "vue unit_size >30", + "vue unit_size >60" + ] +} diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 81c61d62d..ac80657ca 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -265,6 +265,11 @@ jobs: # here — the job reported `skipped`, which the Quality Report renders # identically to a pass. enable-hydra-gates: true + # Gate 122 (SIG unit ratchet) blocks: a unit this change adds or makes + # worse must stay at NLOC 60, McCabe 10 and at most 4 parameters, and a + # new duplicated fragment of 6+ lines fails. Units the change does not + # touch, or touches without making worse, are inherited and do not count. + hydra-gates-sig-blocking: true # No `hydra-gates-ref` here on purpose. The shared workflow defaults it # to @main, and this workflow is itself consumed at @main, so the two # sides move together and a gate fix reaches this repo without a commit diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c048e88..c6dc55506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and a contact person for its owner; a repeat import matches on APPID, so it updates instead of duplicating. See `docs/features/cmdb-import.md`. - CMDB import: on an OpenRegister with the migration-pack editor - (openregister#4602), the import runs the mapping packs kept in + (OpenRegister 2.1.41 or later), the import runs the mapping packs kept in OpenRegister's migration pack store, where an administrator edits them. The `SeedCmdbImportPacks` repair step stores them on install and upgrade; a pack nobody edited follows newer releases, an edited one is kept. diff --git a/appinfo/info.xml b/appinfo/info.xml index aa719115b..630d5de74 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -50,7 +50,7 @@ Vrij en open source onder de EUPL-licentie. **Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. Voor een Service Level Agreement (SLA), neem contact op via sales@conduction.nl. ]]> - 0.2.6-unstable.20261012120000 + 0.2.7-unstable.20261020120000 EUPL-1.2 Conduction Stackiq @@ -344,6 +344,14 @@ Vrij en open source onder de EUPL-licentie. shared `contractNumber`: a wrong cross-app link is worse than an empty one. --> OCA\Stackiq\Command\ReferencesAuditCommand + + OCA\Stackiq\Command\OrganisationLinkCommand + + OCA\Stackiq\Command\GemmaStandardLinksCommand diff --git a/appinfo/routes.php b/appinfo/routes.php index 5558d9de5..6a2aacf25 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -318,6 +318,10 @@ // Portfolio rationalization report (TIME quadrants + EOL + cloud + cost), JSON or CSV (?format=csv). // @spec openspec/changes/portfolio-rationalization-time/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation ['name' => 'portfolioReport#index', 'url' => '/api/portfolio-report', 'verb' => 'GET'], + // @spec openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md#requirement-req-cdq-003-a-data-quality-page-shows-how-complete-and-current-an-organisation-s-register-is + ['name' => 'dataQualityReport#index', 'url' => '/api/data-quality', 'verb' => 'GET'], + // @spec openspec/changes/architecture-reference-component-coverage/specs/reference-component-coverage/spec.md#requirement-req-rcc-001-stackiq-shall-give-each-reference-component-a-coverage-state-for-one-organisation + ['name' => 'referenceComponentCoverage#index', 'url' => '/api/reference-component-coverage', 'verb' => 'GET'], // SPA catch-all — serves the Vue app for any frontend route (history mode routing) // `postfix` keeps this SPA catch-all from colliding with the bare-root diff --git a/compare_archimate.py b/compare_archimate.py index 116cf265a..0cfa2d2a1 100644 --- a/compare_archimate.py +++ b/compare_archimate.py @@ -20,356 +20,302 @@ 2 = error (file not found, parse error, etc.) """ -import sys import json as json_module +import sys import xml.etree.ElementTree as ET -from collections import Counter +from collections import Counter, namedtuple NS = {"am": "http://www.opengroup.org/xsd/archimate/3.0/"} XSI = "http://www.w3.org/2001/XMLSchema-instance" +# Both parsed models with the outcome of compare(). +Comparison = namedtuple("Comparison", "original exported results all_pass") + + +def name_text(node): + """Stripped text of the node's child, or an empty string.""" + name_el = node.find("am:name", NS) + return (name_el.text or "").strip() if name_el is not None else "" + + +def xsi_type(node): + """The node's xsi:type attribute, or an empty string.""" + return node.get(f"{{{XSI}}}type", "") + + +def relationship_info(node): + """Type, source and target of a relationship node.""" + return { + "type": xsi_type(node), + "source": node.get("source", ""), + "target": node.get("target", ""), + } + + +# Section key -> (path of its nodes, value kept per identifier). +SECTION_NODES = { + "elements": (".//am:elements/am:element", xsi_type), + "relationships": (".//am:relationships/am:relationship", relationship_info), + "views": (".//am:views/am:diagrams/am:view", name_text), + "property_definitions": (".//am:propertyDefinitions/am:propertyDefinition", name_text), +} + + +def parse_section(root, key): + """Map identifier -> value for every node of a section that has an identifier.""" + path, value = SECTION_NODES[key] + return { + node.get("identifier", ""): value(node) + for node in root.findall(path, NS) + if node.get("identifier", "") + } + + +def parse_organizations(root): + """Count all organization s: total, element references and folders. + + ArchiMate organizations use with an optional identifierRef (an element + reference) or a