feat: enforce shared UI foundations and pin reference journey
This commit is contained in:
@@ -41,12 +41,15 @@ The first production-shaped slice is implemented:
|
|||||||
- the expanded left rail groups classified destinations while retaining
|
- the expanded left rail groups classified destinations while retaining
|
||||||
Dashboard and every authorized unclassified destination under More tools.
|
Dashboard and every authorized unclassified destination under More tools.
|
||||||
|
|
||||||
The remaining rollout is classification rather than a missing boundary: other
|
The baseline classification is now manifest-declared for every ordinary
|
||||||
user-facing modules must announce their product areas and future bounded tools,
|
user-facing module and enforced by the workspace manifest check. A separately
|
||||||
reference journeys need browser accessibility evidence, and richer active-object
|
versioned launch-context contract carries bounded active-object, acting,
|
||||||
context should be added only through a separately versioned launch-context
|
temporal, View and return references into full-page Quick Access fallbacks;
|
||||||
contract. Until classification is complete, authorized unclassified routes
|
Cases publishes the first active-object reference. The remaining rollout is to
|
||||||
remain visible rather than disappearing.
|
add useful bounded tools and active-object publishers only where a maintained
|
||||||
|
journey benefits, and to extend browser evidence to a pinned reference
|
||||||
|
composition. Authorized global and technical routes remain visible through
|
||||||
|
their dedicated shell entry or **All available tools**.
|
||||||
|
|
||||||
## Quick Access Boundary
|
## Quick Access Boundary
|
||||||
|
|
||||||
@@ -150,6 +153,25 @@ Familiar product nouns such as Calendar, Mail or Files may remain directly
|
|||||||
pinned. The objective is not to hide every module name; it is to prevent
|
pinned. The objective is not to hide every module name; it is to prevent
|
||||||
repository topology from determining a person's workflow.
|
repository topology from determining a person's workflow.
|
||||||
|
|
||||||
|
The initial module classification is deliberately outcome-oriented:
|
||||||
|
|
||||||
|
| Product area | Contributing user-facing modules |
|
||||||
|
| --- | --- |
|
||||||
|
| Work | Approvals, Projects, Tasks, Workflow |
|
||||||
|
| Services and Cases | Cases, Forms, Forms Runtime, Portal |
|
||||||
|
| Records and Documents | Files, Records, Templates |
|
||||||
|
| Communication | Campaigns, Distribution Lists, Mail, Notifications, Postbox |
|
||||||
|
| Meetings and Decisions | Calendar, Committee, Scheduling, Voting |
|
||||||
|
| Data and Assurance | Dataflow, Datasources, Reporting, Risk Compliance |
|
||||||
|
| People and Responsibility | Address Book, IDM, Organizations |
|
||||||
|
|
||||||
|
Dashboard, Search, Documentation and Quick Access remain global shell
|
||||||
|
affordances. Access, Administration, Audit, Encryption, Identity Trust,
|
||||||
|
Operations, Policy, Tenancy and Views remain administrative or platform
|
||||||
|
surfaces available through their dedicated entry point or **All available
|
||||||
|
tools**. The manifest-shape check enforces both this explicit exception set and
|
||||||
|
the shared label, icon, description and ordering of every canonical area.
|
||||||
|
|
||||||
## Full Access And Provenance
|
## Full Access And Provenance
|
||||||
|
|
||||||
The existing permission-derived module rail remains available as **All
|
The existing permission-derived module rail remains available as **All
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ identify the journey it improves, or provide security, operability, recovery,
|
|||||||
accessibility, or usability evidence that those journeys require. Work that
|
accessibility, or usability evidence that those journeys require. Work that
|
||||||
does neither stays in the backlog until a concrete consumer exists.
|
does neither stays in the backlog until a concrete consumer exists.
|
||||||
|
|
||||||
|
The maintained service-to-decision scenario is the German resident parking
|
||||||
|
permit (`Anwohnerparkausweis`), pinned by
|
||||||
|
`tests/fixtures/resident_parking_permit_journey.json`. It replaces generic
|
||||||
|
permit examples as acceptance evidence and fixes the service, exact Form
|
||||||
|
revision, digital and assisted intake, Case and Workflow handoff, formal
|
||||||
|
Decision, Postbox delivery, and Records target. Changing this flagship scenario
|
||||||
|
is a product decision; implementations may add further scenarios without
|
||||||
|
weakening or silently replacing its acceptance gates.
|
||||||
|
|
||||||
The Records vertical now supplies the journey's native file plan, immutable
|
The Records vertical now supplies the journey's native file plan, immutable
|
||||||
record and item revisions, chronology, close/reopen, retention calculation,
|
record and item revisions, chronology, close/reopen, retention calculation,
|
||||||
holds, appraisal, independent disposition approval, recovery-ledger evidence,
|
holds, appraisal, independent disposition approval, recovery-ledger evidence,
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ resolution -> approval/deliberation -> formal Decision -> observed delivery
|
|||||||
effect -> record and review references
|
effect -> record and review references
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The maintained concrete scenario is a German resident parking permit
|
||||||
|
(`Anwohnerparkausweis`). Its versioned fixture is
|
||||||
|
`tests/fixtures/resident_parking_permit_journey.json`. It pins the service,
|
||||||
|
exact Form revision, resident inputs, digital and assisted channels, Case type,
|
||||||
|
human review handoff, formal outcome, Postbox delivery channel, and Records
|
||||||
|
filing/retention target. Generic permit wording is no longer acceptance
|
||||||
|
evidence for this package.
|
||||||
|
|
||||||
An installed Forms and Forms Runtime pair adds an alternative governed entry
|
An installed Forms and Forms Runtime pair adds an alternative governed entry
|
||||||
path before case/workflow handoff:
|
path before case/workflow handoff:
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,11 @@
|
|||||||
"reference": "packages/product/service-to-decision/README.md",
|
"reference": "packages/product/service-to-decision/README.md",
|
||||||
"summary": "Defines the package boundary, authority path, recovery contract, and known operational limits."
|
"summary": "Defines the package boundary, authority path, recovery contract, and known operational limits."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "target_test",
|
||||||
|
"reference": "tests/fixtures/resident_parking_permit_journey.json",
|
||||||
|
"summary": "Pins the resident parking permit actors, channels, exact inputs, work item, formal outcome, filing target, and remaining manual acceptance gates."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "target_test",
|
"kind": "target_test",
|
||||||
"reference": "tests/test_institutional_governance_journey.py",
|
"reference": "tests/test_institutional_governance_journey.py",
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"id": "resident-parking-permit-berlin-style-reference",
|
||||||
|
"title": "Resident parking permit",
|
||||||
|
"title_de": "Anwohnerparkausweis",
|
||||||
|
"locale": "de-DE",
|
||||||
|
"service": {
|
||||||
|
"object_id": "resident-parking-permit",
|
||||||
|
"key": "resident_parking_permit.apply",
|
||||||
|
"version": "6",
|
||||||
|
"audience": "resident",
|
||||||
|
"required_evidence_types": [
|
||||||
|
"application",
|
||||||
|
"identity",
|
||||||
|
"primary_residence",
|
||||||
|
"vehicle_registration"
|
||||||
|
],
|
||||||
|
"channels": ["portal", "assisted"]
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"object_id": "resident-parking-permit-application",
|
||||||
|
"version": "3",
|
||||||
|
"fields": {
|
||||||
|
"applicant_name": "Ada Lovelace",
|
||||||
|
"residence_address": "Musterstrasse 17, 10115 Berlin",
|
||||||
|
"licence_plate": "B-AL 1843"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"case": {
|
||||||
|
"type_key": "resident-parking-permit-application",
|
||||||
|
"number": "RPP-2026-0001",
|
||||||
|
"initial_status": "intake",
|
||||||
|
"decided_status": "decided",
|
||||||
|
"deadline_days": 30
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"definition_name": "Resident parking permit decision",
|
||||||
|
"work_item_title": "Decide the resident parking permit application",
|
||||||
|
"instructions": "Review identity, primary residence, vehicle evidence, and the effective local rule before recording the decision."
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"type": "resident-parking-permit",
|
||||||
|
"operative_result": "Resident parking permit granted.",
|
||||||
|
"reasoning": "Identity, primary residence, vehicle registration, and the effective local rule were verified.",
|
||||||
|
"delivery_channel": "postbox",
|
||||||
|
"remedy": "review:administrative-court"
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"file_plan_key": "traffic.resident-parking-permits",
|
||||||
|
"retention_policy_ref": "records:resident-parking-permit"
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"automated": [
|
||||||
|
"The published service and exact form revision drive digital intake.",
|
||||||
|
"An idempotent replay returns the same persisted submission.",
|
||||||
|
"The human review handoff survives a database-session restart and remains visible in Tasks until completion.",
|
||||||
|
"The formal decision retains party, mandate, legal-basis, evidence, delivery, review, and exact revision references.",
|
||||||
|
"Forms Runtime, Cases, and Decisions can expose exact snapshots for explicit eAkte filing."
|
||||||
|
],
|
||||||
|
"manual_or_target": [
|
||||||
|
"Complete the digital journey with keyboard and screen reader at desktop and mobile widths.",
|
||||||
|
"Repeat intake through an assisted-channel operator without weakening identity, purpose, or provenance.",
|
||||||
|
"Verify the configured Postbox or external delivery provider, including unknown outcome and reconciliation.",
|
||||||
|
"Restore the pinned composition and reconstruct the exact form, case, decision, delivery evidence, and eAkte chronology.",
|
||||||
|
"Transfer through a named archive profile and retain independently signed target evidence."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -66,6 +68,11 @@ from govoplan_services.backend.service import SqlServiceDefinitionProvider, reco
|
|||||||
|
|
||||||
|
|
||||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||||
|
JOURNEY = json.loads(
|
||||||
|
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _reference(
|
def _reference(
|
||||||
@@ -177,31 +184,31 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
service = ServiceDefinition(
|
service = ServiceDefinition(
|
||||||
reference=_reference(
|
reference=_reference(
|
||||||
"service",
|
"service",
|
||||||
"permit-service",
|
JOURNEY["service"]["object_id"],
|
||||||
owner="services",
|
owner="services",
|
||||||
version="5",
|
version=JOURNEY["service"]["version"],
|
||||||
),
|
),
|
||||||
key="permit.apply",
|
key=JOURNEY["service"]["key"],
|
||||||
temporal=TemporalRevision(
|
temporal=TemporalRevision(
|
||||||
revision="5",
|
revision=JOURNEY["service"]["version"],
|
||||||
valid_from=NOW - timedelta(days=1),
|
valid_from=NOW - timedelta(days=1),
|
||||||
valid_to=NOW + timedelta(days=30),
|
valid_to=NOW + timedelta(days=30),
|
||||||
recorded_at=NOW - timedelta(days=2),
|
recorded_at=NOW - timedelta(days=2),
|
||||||
change_reason="Service published.",
|
change_reason="Service published.",
|
||||||
),
|
),
|
||||||
title="Apply for a permit",
|
title=JOURNEY["title"],
|
||||||
audience=("resident",),
|
audience=("resident",),
|
||||||
legal_bases=(legal_basis,),
|
legal_bases=(legal_basis,),
|
||||||
required_evidence_types=("application", "identity"),
|
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
|
||||||
channels=("portal", "postbox"),
|
channels=tuple(JOURNEY["service"]["channels"]) + ("postbox",),
|
||||||
responsible_organization_ref=organization,
|
responsible_organization_ref=organization,
|
||||||
responsible_function_ref=function,
|
responsible_function_ref=function,
|
||||||
mandate_ref=mandate_ref,
|
mandate_ref=mandate_ref,
|
||||||
jurisdiction_refs=(jurisdiction,),
|
jurisdiction_refs=(jurisdiction,),
|
||||||
bindings=(
|
bindings=(
|
||||||
ServiceBinding("case", "permit-application"),
|
ServiceBinding("case", JOURNEY["case"]["type_key"]),
|
||||||
ServiceBinding("workflow", "workflow:permit-review"),
|
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
|
||||||
ServiceBinding("result", "decision:permit"),
|
ServiceBinding("result", f"decision:{JOURNEY['decision']['type']}"),
|
||||||
),
|
),
|
||||||
remedy_refs=("review:administrative-court",),
|
remedy_refs=("review:administrative-court",),
|
||||||
publication_state="published",
|
publication_state="published",
|
||||||
@@ -230,37 +237,40 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
upsert_case_status(
|
upsert_case_status(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
status_key="intake",
|
status_key=JOURNEY["case"]["initial_status"],
|
||||||
label="Intake",
|
label="Intake",
|
||||||
)
|
)
|
||||||
upsert_case_status(
|
upsert_case_status(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
status_key="decided",
|
status_key=JOURNEY["case"]["decided_status"],
|
||||||
label="Decided",
|
label="Decided",
|
||||||
category="decided",
|
category="decided",
|
||||||
)
|
)
|
||||||
upsert_case_type(
|
upsert_case_type(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
type_key="permit-application",
|
type_key=JOURNEY["case"]["type_key"],
|
||||||
label="Permit application",
|
label=JOURNEY["title"],
|
||||||
initial_status_key="intake",
|
initial_status_key=JOURNEY["case"]["initial_status"],
|
||||||
allowed_status_keys=("intake", "decided"),
|
allowed_status_keys=(
|
||||||
|
JOURNEY["case"]["initial_status"],
|
||||||
|
JOURNEY["case"]["decided_status"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
case_record = create_case_from_intake(
|
case_record = create_case_from_intake(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
plan=intake,
|
plan=intake,
|
||||||
case_number="PERMIT-2026-0001",
|
case_number=JOURNEY["case"]["number"],
|
||||||
title="Permit application",
|
title=JOURNEY["title"],
|
||||||
status_key=None,
|
status_key=JOURNEY["case"]["initial_status"],
|
||||||
opened_at=NOW,
|
opened_at=NOW,
|
||||||
recorded_at=NOW,
|
recorded_at=NOW,
|
||||||
change_reason="Portal application received.",
|
change_reason="Portal application received.",
|
||||||
idempotency_key="journey-case-create",
|
idempotency_key="journey-case-create",
|
||||||
evidence_refs=(application_evidence,),
|
evidence_refs=(application_evidence,),
|
||||||
deadline_at=NOW + timedelta(days=30),
|
deadline_at=NOW + timedelta(days=JOURNEY["case"]["deadline_days"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
applicant = _reference("party", "applicant", owner="parties")
|
applicant = _reference("party", "applicant", owner="parties")
|
||||||
@@ -333,7 +343,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
change_reason="Permit authority delegated.",
|
change_reason="Permit authority delegated.",
|
||||||
),
|
),
|
||||||
task_types=("committee.formal_decision",),
|
task_types=("committee.formal_decision",),
|
||||||
authority_types=("permit",),
|
authority_types=(JOURNEY["decision"]["type"],),
|
||||||
organization_unit_refs=(organization,),
|
organization_unit_refs=(organization,),
|
||||||
function_refs=(function,),
|
function_refs=(function,),
|
||||||
jurisdiction_refs=(jurisdiction,),
|
jurisdiction_refs=(jurisdiction,),
|
||||||
@@ -380,7 +390,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
object_id="item-1",
|
object_id="item-1",
|
||||||
revision=1,
|
revision=1,
|
||||||
state="deliberating",
|
state="deliberating",
|
||||||
title="Permit application",
|
title=JOURNEY["title"],
|
||||||
parent_id=meeting.object_id,
|
parent_id=meeting.object_id,
|
||||||
recorded_at=NOW,
|
recorded_at=NOW,
|
||||||
change_reason="Agenda item entered deliberation.",
|
change_reason="Agenda item entered deliberation.",
|
||||||
@@ -398,7 +408,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
object_id="vote-1",
|
object_id="vote-1",
|
||||||
revision=1,
|
revision=1,
|
||||||
state="closed",
|
state="closed",
|
||||||
title="Vote on permit application",
|
title=f"Vote on {JOURNEY['title'].lower()}",
|
||||||
parent_id=agenda.object_id,
|
parent_id=agenda.object_id,
|
||||||
recorded_at=NOW,
|
recorded_at=NOW,
|
||||||
change_reason="Vote result accepted.",
|
change_reason="Vote result accepted.",
|
||||||
@@ -445,7 +455,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
effective_at=NOW,
|
effective_at=NOW,
|
||||||
meeting_ref="meeting-1",
|
meeting_ref="meeting-1",
|
||||||
agenda_item_ref="item-1",
|
agenda_item_ref="item-1",
|
||||||
decision_type="permit",
|
decision_type=JOURNEY["decision"]["type"],
|
||||||
subject_refs=(case_record.reference,),
|
subject_refs=(case_record.reference,),
|
||||||
organization_unit_ref=organization,
|
organization_unit_ref=organization,
|
||||||
function_ref=function,
|
function_ref=function,
|
||||||
@@ -461,8 +471,8 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
fact_evidence=(application_evidence, address_evidence),
|
fact_evidence=(application_evidence, address_evidence),
|
||||||
legal_bases=(legal_basis,),
|
legal_bases=(legal_basis,),
|
||||||
operative_result="Permit granted.",
|
operative_result=JOURNEY["decision"]["operative_result"],
|
||||||
reasoning="The application satisfies the effective rule.",
|
reasoning=JOURNEY["decision"]["reasoning"],
|
||||||
case_ref=case_record.reference,
|
case_ref=case_record.reference,
|
||||||
jurisdiction_refs=(jurisdiction,),
|
jurisdiction_refs=(jurisdiction,),
|
||||||
party_refs=(applicant, representative),
|
party_refs=(applicant, representative),
|
||||||
@@ -475,7 +485,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
classification="restricted",
|
classification="restricted",
|
||||||
purposes=("permit-decision", "party-delivery"),
|
purposes=("permit-decision", "party-delivery"),
|
||||||
legal_basis_refs=("permit-law:3@2026-01",),
|
legal_basis_refs=("permit-law:3@2026-01",),
|
||||||
retention_policy_ref="records:permit",
|
retention_policy_ref=JOURNEY["records"]["retention_policy_ref"],
|
||||||
disclosure_state="partly_disclosable",
|
disclosure_state="partly_disclosable",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -568,7 +578,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
get_case(session, principal, case_id="case-1"),
|
get_case(session, principal, case_id="case-1"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"decided",
|
JOURNEY["case"]["decided_status"],
|
||||||
get_workspace_object(
|
get_workspace_object(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
@@ -583,7 +593,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
|||||||
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
|
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
|
||||||
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
|
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
|
||||||
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
|
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
|
||||||
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"])
|
self.assertEqual(JOURNEY["decision"]["reasoning"], reconstruction["reasoning"])
|
||||||
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
|
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -82,6 +84,11 @@ from govoplan_workflow_engine.backend.work_items import WorkflowWorkItemProvider
|
|||||||
|
|
||||||
|
|
||||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||||
|
JOURNEY = json.loads(
|
||||||
|
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _service() -> ServiceDefinition:
|
def _service() -> ServiceDefinition:
|
||||||
@@ -89,24 +96,24 @@ def _service() -> ServiceDefinition:
|
|||||||
reference=InstitutionalReference(
|
reference=InstitutionalReference(
|
||||||
kind="service",
|
kind="service",
|
||||||
owner_module="portal",
|
owner_module="portal",
|
||||||
object_id="permit",
|
object_id=JOURNEY["service"]["object_id"],
|
||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
version="5",
|
version=JOURNEY["service"]["version"],
|
||||||
),
|
),
|
||||||
key="permit.apply",
|
key=JOURNEY["service"]["key"],
|
||||||
temporal=TemporalRevision(
|
temporal=TemporalRevision(
|
||||||
revision="5",
|
revision=JOURNEY["service"]["version"],
|
||||||
valid_from=NOW - timedelta(days=1),
|
valid_from=NOW - timedelta(days=1),
|
||||||
valid_to=NOW + timedelta(days=1),
|
valid_to=NOW + timedelta(days=1),
|
||||||
recorded_at=NOW - timedelta(days=2),
|
recorded_at=NOW - timedelta(days=2),
|
||||||
),
|
),
|
||||||
title="Apply for a permit",
|
title=JOURNEY["title"],
|
||||||
audience=("resident",),
|
audience=(JOURNEY["service"]["audience"],),
|
||||||
required_evidence_types=("application",),
|
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
|
||||||
bindings=(
|
bindings=(
|
||||||
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
|
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
|
||||||
ServiceBinding("case", "permit-application"),
|
ServiceBinding("case", JOURNEY["case"]["type_key"]),
|
||||||
ServiceBinding("workflow", "workflow:permit-review"),
|
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
|
||||||
),
|
),
|
||||||
publication_state="published",
|
publication_state="published",
|
||||||
)
|
)
|
||||||
@@ -230,8 +237,14 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
self.assertTrue(entries[0].available)
|
self.assertTrue(entries[0].available)
|
||||||
self.assertIs(definition, entries[0].definition)
|
self.assertIs(definition, entries[0].definition)
|
||||||
self.assertEqual(definition.reference, plan.service_ref)
|
self.assertEqual(definition.reference, plan.service_ref)
|
||||||
self.assertEqual("5", plan.context.service_ref.version)
|
self.assertEqual(JOURNEY["service"]["version"], plan.context.service_ref.version)
|
||||||
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
|
self.assertEqual("workflow:resident-parking-permit-review", plan.workflow_refs[0])
|
||||||
|
|
||||||
|
def test_reference_fixture_names_remaining_manual_target_evidence(self) -> None:
|
||||||
|
self.assertEqual("Anwohnerparkausweis", JOURNEY["title_de"])
|
||||||
|
self.assertEqual("de-DE", JOURNEY["locale"])
|
||||||
|
self.assertEqual(5, len(JOURNEY["acceptance"]["automated"]))
|
||||||
|
self.assertEqual(5, len(JOURNEY["acceptance"]["manual_or_target"]))
|
||||||
|
|
||||||
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
||||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||||
@@ -252,19 +265,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
reference=InstitutionalReference(
|
reference=InstitutionalReference(
|
||||||
kind="form",
|
kind="form",
|
||||||
owner_module="forms",
|
owner_module="forms",
|
||||||
object_id="permit-application",
|
object_id=JOURNEY["form"]["object_id"],
|
||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
version="3",
|
version="3",
|
||||||
),
|
),
|
||||||
key="permit-application",
|
key=JOURNEY["form"]["object_id"],
|
||||||
temporal=TemporalRevision(
|
temporal=TemporalRevision(
|
||||||
revision="3",
|
revision="3",
|
||||||
valid_from=NOW - timedelta(days=1),
|
valid_from=NOW - timedelta(days=1),
|
||||||
valid_to=NOW + timedelta(days=1),
|
valid_to=NOW + timedelta(days=1),
|
||||||
recorded_at=NOW - timedelta(days=2),
|
recorded_at=NOW - timedelta(days=2),
|
||||||
change_reason="Publish the permit application.",
|
change_reason="Publish the resident parking permit application.",
|
||||||
),
|
),
|
||||||
title="Permit application",
|
title=JOURNEY["title"],
|
||||||
fields=(
|
fields=(
|
||||||
FormFieldDefinition(
|
FormFieldDefinition(
|
||||||
key="applicant_name",
|
key="applicant_name",
|
||||||
@@ -272,6 +285,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
required=True,
|
required=True,
|
||||||
constraints={"min_length": 2},
|
constraints={"min_length": 2},
|
||||||
),
|
),
|
||||||
|
FormFieldDefinition(
|
||||||
|
key="residence_address",
|
||||||
|
label="Primary residence address",
|
||||||
|
required=True,
|
||||||
|
constraints={"min_length": 5},
|
||||||
|
),
|
||||||
|
FormFieldDefinition(
|
||||||
|
key="licence_plate",
|
||||||
|
label="Vehicle licence plate",
|
||||||
|
required=True,
|
||||||
|
constraints={"min_length": 3},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
publication_state="published",
|
publication_state="published",
|
||||||
allow_drafts=True,
|
allow_drafts=True,
|
||||||
@@ -286,18 +311,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
reference=InstitutionalReference(
|
reference=InstitutionalReference(
|
||||||
kind="service",
|
kind="service",
|
||||||
owner_module="services",
|
owner_module="services",
|
||||||
object_id="permit",
|
object_id=JOURNEY["service"]["object_id"],
|
||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
version="6",
|
version=JOURNEY["service"]["version"],
|
||||||
),
|
),
|
||||||
key="permit.apply",
|
key=JOURNEY["service"]["key"],
|
||||||
temporal=TemporalRevision(
|
temporal=TemporalRevision(
|
||||||
revision="6",
|
revision=JOURNEY["service"]["version"],
|
||||||
valid_from=NOW - timedelta(days=1),
|
valid_from=NOW - timedelta(days=1),
|
||||||
valid_to=NOW + timedelta(days=1),
|
valid_to=NOW + timedelta(days=1),
|
||||||
recorded_at=NOW - timedelta(days=2),
|
recorded_at=NOW - timedelta(days=2),
|
||||||
),
|
),
|
||||||
title="Apply for a permit",
|
title=JOURNEY["title"],
|
||||||
audience=("public",),
|
audience=("public",),
|
||||||
bindings=(binding,),
|
bindings=(binding,),
|
||||||
publication_state="published",
|
publication_state="published",
|
||||||
@@ -311,7 +336,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
reference=service.reference,
|
reference=service.reference,
|
||||||
requested_at=NOW,
|
requested_at=NOW,
|
||||||
idempotency_key="portal-form-launch-1",
|
idempotency_key="portal-form-launch-1",
|
||||||
parameters={"applicant_name": "Ada Lovelace"},
|
parameters=JOURNEY["form"]["fields"],
|
||||||
)
|
)
|
||||||
replay = directory.launch_service(
|
replay = directory.launch_service(
|
||||||
session,
|
session,
|
||||||
@@ -319,7 +344,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
reference=service.reference,
|
reference=service.reference,
|
||||||
requested_at=NOW,
|
requested_at=NOW,
|
||||||
idempotency_key="portal-form-launch-1",
|
idempotency_key="portal-form-launch-1",
|
||||||
parameters={"applicant_name": "Ada Lovelace"},
|
parameters=JOURNEY["form"]["fields"],
|
||||||
)
|
)
|
||||||
instance = FormRuntimeService(registry).get_instance(
|
instance = FormRuntimeService(registry).get_instance(
|
||||||
session,
|
session,
|
||||||
@@ -348,7 +373,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(NOW, instance.definition_ref.valid_at)
|
self.assertEqual(NOW, instance.definition_ref.valid_at)
|
||||||
self.assertEqual(service.reference, instance.service_ref)
|
self.assertEqual(service.reference, instance.service_ref)
|
||||||
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
|
self.assertEqual(JOURNEY["form"]["fields"], instance.values)
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
@@ -392,7 +417,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
actor_id="account-1",
|
actor_id="account-1",
|
||||||
payload=WorkflowDefinitionCreateRequest(
|
payload=WorkflowDefinitionCreateRequest(
|
||||||
name="Permit decision",
|
name=JOURNEY["workflow"]["definition_name"],
|
||||||
graph=WorkflowGraph(
|
graph=WorkflowGraph(
|
||||||
nodes=[
|
nodes=[
|
||||||
WorkflowNode(
|
WorkflowNode(
|
||||||
@@ -403,8 +428,8 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
id="review",
|
id="review",
|
||||||
type="workflow.activity",
|
type="workflow.activity",
|
||||||
config={
|
config={
|
||||||
"title": "Decide the permit application",
|
"title": JOURNEY["workflow"]["work_item_title"],
|
||||||
"instructions": "Review the filed evidence and record the decision.",
|
"instructions": JOURNEY["workflow"]["instructions"],
|
||||||
"assignee": "account:account-1",
|
"assignee": "account:account-1",
|
||||||
"due_after": "2d",
|
"due_after": "2d",
|
||||||
},
|
},
|
||||||
@@ -464,7 +489,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
self.assertEqual(1, work.total)
|
self.assertEqual(1, work.total)
|
||||||
self.assertEqual(step_id, work.items[0].id)
|
self.assertEqual(step_id, work.items[0].id)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"Decide the permit application",
|
JOURNEY["workflow"]["work_item_title"],
|
||||||
work.items[0].title,
|
work.items[0].title,
|
||||||
)
|
)
|
||||||
self.assertTrue(work.items[0].action_url.startswith("/workflow?"))
|
self.assertTrue(work.items[0].action_url.startswith("/workflow?"))
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ PY
|
|||||||
"$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
"$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-layouts.py"
|
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-layouts.py"
|
||||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-primitives.py"
|
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-primitives.py"
|
||||||
|
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-foundations.py"
|
||||||
"$PYTHON" -m unittest tests.test_module_system
|
"$PYTHON" -m unittest tests.test_module_system
|
||||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
|
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
|
||||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
||||||
@@ -121,6 +122,7 @@ cd "$ROOT/webui"
|
|||||||
"$NPM" run test:mail-components
|
"$NPM" run test:mail-components
|
||||||
"$NPM" run test:module-capabilities
|
"$NPM" run test:module-capabilities
|
||||||
"$NPM" run test:module-permutations
|
"$NPM" run test:module-permutations
|
||||||
|
"$NPM" run test:conformance
|
||||||
|
|
||||||
cd /mnt/DATA/git/govoplan-dataflow/webui
|
cd /mnt/DATA/git/govoplan-dataflow/webui
|
||||||
"$NPM" run test:structure
|
"$NPM" run test:structure
|
||||||
|
|||||||
@@ -17,6 +17,70 @@ MODULE_NAME_PATTERN = re.compile(
|
|||||||
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*"
|
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*"
|
||||||
)
|
)
|
||||||
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin", "user"})
|
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin", "user"})
|
||||||
|
CANONICAL_PRODUCT_AREAS = {
|
||||||
|
"work": (
|
||||||
|
"i18n:govoplan-core.product_area.work",
|
||||||
|
"list-checks",
|
||||||
|
"i18n:govoplan-core.product_area.work_description",
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
"services-cases": (
|
||||||
|
"i18n:govoplan-core.product_area.services_cases",
|
||||||
|
"landmark",
|
||||||
|
"i18n:govoplan-core.product_area.services_cases_description",
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
"records-documents": (
|
||||||
|
"i18n:govoplan-core.product_area.records_documents",
|
||||||
|
"folder",
|
||||||
|
"i18n:govoplan-core.product_area.records_documents_description",
|
||||||
|
30,
|
||||||
|
),
|
||||||
|
"communication": (
|
||||||
|
"i18n:govoplan-core.product_area.communication",
|
||||||
|
"mail",
|
||||||
|
"i18n:govoplan-core.product_area.communication_description",
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
"meetings-decisions": (
|
||||||
|
"i18n:govoplan-core.product_area.meetings_decisions",
|
||||||
|
"calendar",
|
||||||
|
"i18n:govoplan-core.product_area.meetings_decisions_description",
|
||||||
|
50,
|
||||||
|
),
|
||||||
|
"data-assurance": (
|
||||||
|
"i18n:govoplan-core.product_area.data_assurance",
|
||||||
|
"database-zap",
|
||||||
|
"i18n:govoplan-core.product_area.data_assurance_description",
|
||||||
|
60,
|
||||||
|
),
|
||||||
|
"people-responsibility": (
|
||||||
|
"i18n:govoplan-core.product_area.people_responsibility",
|
||||||
|
"users",
|
||||||
|
"i18n:govoplan-core.product_area.people_responsibility_description",
|
||||||
|
70,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
# These surfaces are intentionally global, administrative, security-policy, or
|
||||||
|
# shell infrastructure. They remain discoverable through their dedicated shell
|
||||||
|
# affordance or through "All available tools" instead of a business area.
|
||||||
|
PRODUCT_AREA_EXEMPT_MODULES = frozenset(
|
||||||
|
{
|
||||||
|
"access",
|
||||||
|
"admin",
|
||||||
|
"audit",
|
||||||
|
"dashboard",
|
||||||
|
"docs",
|
||||||
|
"encryption",
|
||||||
|
"identity_trust",
|
||||||
|
"ops",
|
||||||
|
"policy",
|
||||||
|
"quick_access",
|
||||||
|
"search",
|
||||||
|
"tenancy",
|
||||||
|
"views",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -113,6 +177,42 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
frontend = manifest.frontend
|
||||||
|
has_user_facing_surface = frontend is not None and bool(
|
||||||
|
frontend.routes
|
||||||
|
or frontend.public_routes
|
||||||
|
or frontend.nav_items
|
||||||
|
or frontend.settings_routes
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
has_user_facing_surface
|
||||||
|
and not frontend.product_areas
|
||||||
|
and manifest.id not in PRODUCT_AREA_EXEMPT_MODULES
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
f"{repository_name}: user-facing module {manifest.id!r} has no "
|
||||||
|
"ProductAreaContribution and is not an explicit global/technical exemption"
|
||||||
|
)
|
||||||
|
if frontend is not None:
|
||||||
|
for contribution in frontend.product_areas:
|
||||||
|
expected = CANONICAL_PRODUCT_AREAS.get(contribution.id)
|
||||||
|
actual = (
|
||||||
|
contribution.label,
|
||||||
|
contribution.icon,
|
||||||
|
contribution.description,
|
||||||
|
contribution.order,
|
||||||
|
)
|
||||||
|
if expected is None:
|
||||||
|
errors.append(
|
||||||
|
f"{repository_name}: module {manifest.id!r} uses unknown product "
|
||||||
|
f"area {contribution.id!r}"
|
||||||
|
)
|
||||||
|
elif actual != expected:
|
||||||
|
errors.append(
|
||||||
|
f"{repository_name}: module {manifest.id!r} redefines canonical "
|
||||||
|
f"product area {contribution.id!r}; expected {expected!r}, found {actual!r}"
|
||||||
|
)
|
||||||
|
|
||||||
repository_root = manifest_path.parents[3]
|
repository_root = manifest_path.parents[3]
|
||||||
if manifest.architecture is None:
|
if manifest.architecture is None:
|
||||||
if args.require_architecture:
|
if args.require_architecture:
|
||||||
@@ -142,6 +242,23 @@ def main() -> int:
|
|||||||
print("\n".join(errors), file=sys.stderr)
|
print("\n".join(errors), file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
contributed_product_areas = {
|
||||||
|
contribution.id
|
||||||
|
for manifest in manifests
|
||||||
|
if manifest.frontend is not None
|
||||||
|
for contribution in manifest.frontend.product_areas
|
||||||
|
}
|
||||||
|
missing_product_areas = sorted(
|
||||||
|
set(CANONICAL_PRODUCT_AREAS) - contributed_product_areas
|
||||||
|
)
|
||||||
|
if missing_product_areas:
|
||||||
|
print(
|
||||||
|
"Canonical product areas have no contributing module: "
|
||||||
|
+ ", ".join(missing_product_areas),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
registry = PlatformRegistry()
|
registry = PlatformRegistry()
|
||||||
try:
|
try:
|
||||||
for manifest in manifests:
|
for manifest in manifests:
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Enforce Core ownership of WebUI visual foundations."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
REPOS_ROOT = META_ROOT.parent
|
||||||
|
TOKENS_PATH = REPOS_ROOT / "govoplan-core/webui/src/styles/tokens.css"
|
||||||
|
|
||||||
|
RAW_HEX_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b")
|
||||||
|
RAW_COLOR_FUNCTION = re.compile(r"\b(?:rgb|rgba|hsl|hsla)\((?!\s*var\()", re.IGNORECASE)
|
||||||
|
RADIUS_DECLARATION = re.compile(r"border-radius\s*:\s*([^;}]+)")
|
||||||
|
MEDIA_MAX_WIDTH = re.compile(r"@media[^\n{]*\(max-width\s*:\s*(\d+)px\)")
|
||||||
|
|
||||||
|
RESPONSIVE_BANDS = {560, 600, 680, 760, 900, 1100, 1280}
|
||||||
|
REQUIRED_TOKENS = {
|
||||||
|
"--radius-hairline",
|
||||||
|
"--radius-tight",
|
||||||
|
"--radius-xs",
|
||||||
|
"--radius-sm",
|
||||||
|
"--radius-compact",
|
||||||
|
"--radius-md",
|
||||||
|
"--radius-lg",
|
||||||
|
"--radius-xl",
|
||||||
|
"--radius-round",
|
||||||
|
"--radius-pill",
|
||||||
|
"--shadow-drawer-side",
|
||||||
|
"--shadow-drawer-bottom",
|
||||||
|
"--action-primary-bg",
|
||||||
|
"--action-primary-border",
|
||||||
|
"--action-primary-text",
|
||||||
|
"--action-danger-bg",
|
||||||
|
"--action-danger-text",
|
||||||
|
"--badge-accent-text",
|
||||||
|
"--data-category-blue",
|
||||||
|
"--data-category-green",
|
||||||
|
"--data-category-amber",
|
||||||
|
"--data-category-purple",
|
||||||
|
"--data-category-rose",
|
||||||
|
*(f"--data-series-{index}" for index in range(1, 9)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def line_number(source: str, offset: int) -> int:
|
||||||
|
return source.count("\n", 0, offset) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def css_files() -> list[pathlib.Path]:
|
||||||
|
files: list[pathlib.Path] = []
|
||||||
|
for repository in sorted(REPOS_ROOT.glob("govoplan-*")):
|
||||||
|
styles = repository / "webui/src"
|
||||||
|
if styles.is_dir():
|
||||||
|
files.extend(sorted(styles.rglob("*.css")))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def display_path(path: pathlib.Path) -> str:
|
||||||
|
return str(path.relative_to(REPOS_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
errors: list[str] = []
|
||||||
|
tokens = TOKENS_PATH.read_text(encoding="utf-8")
|
||||||
|
for token in sorted(REQUIRED_TOKENS):
|
||||||
|
if f"{token}:" not in tokens:
|
||||||
|
errors.append(f"{display_path(TOKENS_PATH)}: missing required foundation token {token}")
|
||||||
|
|
||||||
|
files = css_files()
|
||||||
|
for path in files:
|
||||||
|
source = path.read_text(encoding="utf-8")
|
||||||
|
owns_literals = path == TOKENS_PATH
|
||||||
|
|
||||||
|
if not owns_literals:
|
||||||
|
for pattern, label in (
|
||||||
|
(RAW_HEX_COLOR, "raw color"),
|
||||||
|
(RAW_COLOR_FUNCTION, "raw color function"),
|
||||||
|
):
|
||||||
|
for match in pattern.finditer(source):
|
||||||
|
errors.append(
|
||||||
|
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||||
|
f"{label} must use a Core theme token"
|
||||||
|
)
|
||||||
|
|
||||||
|
for match in RADIUS_DECLARATION.finditer(source):
|
||||||
|
value = match.group(1).strip()
|
||||||
|
if "var(" not in value and value not in {"0", "inherit", "initial", "unset"}:
|
||||||
|
errors.append(
|
||||||
|
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||||
|
f"border radius {value!r} must use a Core radius token"
|
||||||
|
)
|
||||||
|
|
||||||
|
for match in MEDIA_MAX_WIDTH.finditer(source):
|
||||||
|
width = int(match.group(1))
|
||||||
|
if width not in RESPONSIVE_BANDS:
|
||||||
|
errors.append(
|
||||||
|
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||||
|
f"{width}px is not a shared responsive band; use one of "
|
||||||
|
f"{', '.join(f'{value}px' for value in sorted(RESPONSIVE_BANDS))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n".join(errors))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Shared WebUI foundation contract passed for "
|
||||||
|
f"{len(files)} stylesheets and {len(RESPONSIVE_BANDS)} responsive bands."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -34,7 +34,6 @@ govoplan-committee/webui/src/styles/committee.css|.committee-record-dialog
|
|||||||
govoplan-core/webui/src/styles/components.css|.dialog-panel.wysiwyg-editor-dialog
|
govoplan-core/webui/src/styles/components.css|.dialog-panel.wysiwyg-editor-dialog
|
||||||
govoplan-core/webui/src/styles/components.css|.guided-config-dialog
|
govoplan-core/webui/src/styles/components.css|.guided-config-dialog
|
||||||
govoplan-core/webui/src/styles/components.css|.password-generator-dialog
|
govoplan-core/webui/src/styles/components.css|.password-generator-dialog
|
||||||
govoplan-core/webui/src/styles/components.css|.unsaved-changes-dialog
|
|
||||||
govoplan-core/webui/src/styles/layout.css|.concurrency-conflict-dialog
|
govoplan-core/webui/src/styles/layout.css|.concurrency-conflict-dialog
|
||||||
govoplan-dashboard/webui/src/styles/dashboard.css|.dashboard-widget-config-dialog
|
govoplan-dashboard/webui/src/styles/dashboard.css|.dashboard-widget-config-dialog
|
||||||
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-decision-dialog
|
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-decision-dialog
|
||||||
@@ -64,4 +63,3 @@ govoplan-views/webui/src/styles/views.css|.views-assignment-dialog
|
|||||||
govoplan-workflow/webui/src/styles/workflow.css|.workflow-definition-dialog
|
govoplan-workflow/webui/src/styles/workflow.css|.workflow-definition-dialog
|
||||||
govoplan-workflow/webui/src/styles/workflow.css|.workflow-runs-dialog
|
govoplan-workflow/webui/src/styles/workflow.css|.workflow-runs-dialog
|
||||||
govoplan-workflow/webui/src/styles/workflow.css|.workflow-standard-comparison-dialog
|
govoplan-workflow/webui/src/styles/workflow.css|.workflow-standard-comparison-dialog
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user