@topogram/cli 0.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { getProjection, sharedUiProjectionForWeb, uiProjectionCandidates } from "./surfaces/shared.js";
|
|
2
|
+
import { buildComponentBehaviorRealizations } from "../component-behavior.js";
|
|
3
|
+
|
|
4
|
+
function byId(entries = []) {
|
|
5
|
+
return new Map(entries.map((entry) => [entry.id, entry]));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function stableUnique(values) {
|
|
9
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sourcePath(entry) {
|
|
13
|
+
return entry?.loc?.file || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function componentContract(component) {
|
|
17
|
+
return component?.componentContract || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function summarizeProjection(projection) {
|
|
21
|
+
return projection
|
|
22
|
+
? {
|
|
23
|
+
id: projection.id,
|
|
24
|
+
name: projection.name || projection.id,
|
|
25
|
+
platform: projection.platform || null,
|
|
26
|
+
status: projection.status || null,
|
|
27
|
+
source_path: sourcePath(projection)
|
|
28
|
+
}
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function summarizeComponent(component) {
|
|
33
|
+
return component
|
|
34
|
+
? {
|
|
35
|
+
id: component.id,
|
|
36
|
+
name: component.name || component.id,
|
|
37
|
+
category: component.category || null,
|
|
38
|
+
version: component.version || null,
|
|
39
|
+
status: component.status || null,
|
|
40
|
+
source_path: sourcePath(component)
|
|
41
|
+
}
|
|
42
|
+
: null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function summarizeComponentContract(component) {
|
|
46
|
+
const contract = componentContract(component);
|
|
47
|
+
if (!contract) return null;
|
|
48
|
+
return {
|
|
49
|
+
id: contract.id,
|
|
50
|
+
name: contract.name,
|
|
51
|
+
category: contract.category || null,
|
|
52
|
+
version: contract.version || null,
|
|
53
|
+
status: contract.status || null,
|
|
54
|
+
props: contract.props || [],
|
|
55
|
+
events: contract.events || [],
|
|
56
|
+
behaviors: contract.behaviors || [],
|
|
57
|
+
behavior: contract.behavior || [],
|
|
58
|
+
approvals: contract.approvals || [],
|
|
59
|
+
dependencies: contract.dependencies || [],
|
|
60
|
+
source_path: sourcePath(component)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function projectionRealizesIds(projection) {
|
|
65
|
+
return new Set((projection?.realizes || []).map((ref) => ref.id).filter(Boolean));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectionScreenMap(projection) {
|
|
69
|
+
return new Map((projection?.uiScreens || []).map((screen) => [screen.id, screen]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function projectionRegionKeys(projection) {
|
|
73
|
+
return new Set((projection?.uiScreenRegions || []).map((entry) => `${entry.screenId}:${entry.region}`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkRecord({
|
|
77
|
+
code,
|
|
78
|
+
severity,
|
|
79
|
+
message,
|
|
80
|
+
projection,
|
|
81
|
+
sourceProjection,
|
|
82
|
+
component,
|
|
83
|
+
usage,
|
|
84
|
+
prop = null,
|
|
85
|
+
event = null,
|
|
86
|
+
behavior = null,
|
|
87
|
+
suggestedFix
|
|
88
|
+
}) {
|
|
89
|
+
return {
|
|
90
|
+
code,
|
|
91
|
+
severity,
|
|
92
|
+
message,
|
|
93
|
+
projection: projection?.id || null,
|
|
94
|
+
source_projection: sourceProjection?.id || null,
|
|
95
|
+
component: component?.id || usage.component?.id || null,
|
|
96
|
+
screen: usage.screenId || null,
|
|
97
|
+
region: usage.region || null,
|
|
98
|
+
prop,
|
|
99
|
+
event,
|
|
100
|
+
behavior,
|
|
101
|
+
suggested_fix: suggestedFix
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function componentUsageKey(projection, sourceProjection, usage, index) {
|
|
106
|
+
return [
|
|
107
|
+
projection.id,
|
|
108
|
+
sourceProjection?.id || projection.id,
|
|
109
|
+
usage.screenId || "screen",
|
|
110
|
+
usage.region || "region",
|
|
111
|
+
usage.component?.id || "component",
|
|
112
|
+
String(index)
|
|
113
|
+
].join(":");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectUsageChecks({ graph, projection, sourceProjection, usage, component }) {
|
|
117
|
+
const checks = [];
|
|
118
|
+
const contract = componentContract(component);
|
|
119
|
+
const props = contract?.props || [];
|
|
120
|
+
const events = contract?.events || [];
|
|
121
|
+
const propNames = new Set(props.map((prop) => prop.name));
|
|
122
|
+
const eventNames = new Set(events.map((event) => event.id));
|
|
123
|
+
const boundProps = new Set((usage.dataBindings || []).map((binding) => binding.prop).filter(Boolean));
|
|
124
|
+
const statements = byId(graph.statements || []);
|
|
125
|
+
const screens = projectionScreenMap(sourceProjection);
|
|
126
|
+
const regionKeys = projectionRegionKeys(sourceProjection);
|
|
127
|
+
const realizedIds = projectionRealizesIds(sourceProjection);
|
|
128
|
+
|
|
129
|
+
if (!component) {
|
|
130
|
+
checks.push(checkRecord({
|
|
131
|
+
code: "component_missing",
|
|
132
|
+
severity: "error",
|
|
133
|
+
message: `Component '${usage.component?.id || "(missing)"}' could not be resolved.`,
|
|
134
|
+
projection,
|
|
135
|
+
sourceProjection,
|
|
136
|
+
component,
|
|
137
|
+
usage,
|
|
138
|
+
suggestedFix: "Create the component or update the projection ui_components binding."
|
|
139
|
+
}));
|
|
140
|
+
return checks;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (projection.status === "active" && component.status && component.status !== "active") {
|
|
144
|
+
checks.push(checkRecord({
|
|
145
|
+
code: "component_status_not_active",
|
|
146
|
+
severity: "warning",
|
|
147
|
+
message: `Active projection '${projection.id}' uses component '${component.id}' with status '${component.status}'.`,
|
|
148
|
+
projection,
|
|
149
|
+
sourceProjection,
|
|
150
|
+
component,
|
|
151
|
+
usage,
|
|
152
|
+
suggestedFix: "Promote the component to active or move the usage behind an explicit review boundary."
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!screens.has(usage.screenId)) {
|
|
157
|
+
checks.push(checkRecord({
|
|
158
|
+
code: "component_usage_screen_missing",
|
|
159
|
+
severity: "error",
|
|
160
|
+
message: `Component usage references missing screen '${usage.screenId}'.`,
|
|
161
|
+
projection,
|
|
162
|
+
sourceProjection,
|
|
163
|
+
component,
|
|
164
|
+
usage,
|
|
165
|
+
suggestedFix: "Add the screen to ui_screens or update the component usage screen id."
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!regionKeys.has(`${usage.screenId}:${usage.region}`)) {
|
|
170
|
+
checks.push(checkRecord({
|
|
171
|
+
code: "component_usage_region_missing",
|
|
172
|
+
severity: "error",
|
|
173
|
+
message: `Component usage references undeclared region '${usage.region}' on screen '${usage.screenId}'.`,
|
|
174
|
+
projection,
|
|
175
|
+
sourceProjection,
|
|
176
|
+
component,
|
|
177
|
+
usage,
|
|
178
|
+
suggestedFix: "Add the region to ui_screen_regions or update the component usage region."
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const prop of props.filter((entry) => entry.requiredness === "required")) {
|
|
183
|
+
if (!boundProps.has(prop.name)) {
|
|
184
|
+
checks.push(checkRecord({
|
|
185
|
+
code: "component_required_prop_missing",
|
|
186
|
+
severity: "error",
|
|
187
|
+
message: `Required prop '${prop.name}' is not bound for component '${component.id}'.`,
|
|
188
|
+
projection,
|
|
189
|
+
sourceProjection,
|
|
190
|
+
component,
|
|
191
|
+
usage,
|
|
192
|
+
prop: prop.name,
|
|
193
|
+
suggestedFix: `Add 'data ${prop.name} from <source>' to the projection ui_components entry.`
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const binding of usage.dataBindings || []) {
|
|
199
|
+
if (!propNames.has(binding.prop)) {
|
|
200
|
+
checks.push(checkRecord({
|
|
201
|
+
code: "component_prop_unknown",
|
|
202
|
+
severity: "error",
|
|
203
|
+
message: `Prop '${binding.prop}' is not declared by component '${component.id}'.`,
|
|
204
|
+
projection,
|
|
205
|
+
sourceProjection,
|
|
206
|
+
component,
|
|
207
|
+
usage,
|
|
208
|
+
prop: binding.prop || null,
|
|
209
|
+
suggestedFix: "Declare the prop on the component or update the projection binding."
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
if (!binding.source?.id || !statements.has(binding.source.id)) {
|
|
213
|
+
checks.push(checkRecord({
|
|
214
|
+
code: "component_data_source_missing",
|
|
215
|
+
severity: "error",
|
|
216
|
+
message: `Data binding for prop '${binding.prop}' references a missing source.`,
|
|
217
|
+
projection,
|
|
218
|
+
sourceProjection,
|
|
219
|
+
component,
|
|
220
|
+
usage,
|
|
221
|
+
prop: binding.prop || null,
|
|
222
|
+
suggestedFix: "Bind the prop to an existing capability, projection, shape, or entity."
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const binding of usage.eventBindings || []) {
|
|
228
|
+
if (!eventNames.has(binding.event)) {
|
|
229
|
+
checks.push(checkRecord({
|
|
230
|
+
code: "component_event_unknown",
|
|
231
|
+
severity: "error",
|
|
232
|
+
message: `Event '${binding.event}' is not declared by component '${component.id}'.`,
|
|
233
|
+
projection,
|
|
234
|
+
sourceProjection,
|
|
235
|
+
component,
|
|
236
|
+
usage,
|
|
237
|
+
event: binding.event || null,
|
|
238
|
+
suggestedFix: "Declare the event on the component or update the projection binding."
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
if (binding.action === "navigate") {
|
|
242
|
+
if (!screens.has(binding.target?.id)) {
|
|
243
|
+
checks.push(checkRecord({
|
|
244
|
+
code: "component_event_navigation_target_missing",
|
|
245
|
+
severity: "error",
|
|
246
|
+
message: `Event '${binding.event}' navigates to missing screen '${binding.target?.id || "(missing)"}'.`,
|
|
247
|
+
projection,
|
|
248
|
+
sourceProjection,
|
|
249
|
+
component,
|
|
250
|
+
usage,
|
|
251
|
+
event: binding.event || null,
|
|
252
|
+
suggestedFix: "Add the target screen or update the event navigation target."
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
} else if (binding.action === "action") {
|
|
256
|
+
const target = binding.target?.id ? statements.get(binding.target.id) : null;
|
|
257
|
+
if (!target || target.kind !== "capability") {
|
|
258
|
+
checks.push(checkRecord({
|
|
259
|
+
code: "component_event_action_missing",
|
|
260
|
+
severity: "error",
|
|
261
|
+
message: `Event '${binding.event}' targets missing capability '${binding.target?.id || "(missing)"}'.`,
|
|
262
|
+
projection,
|
|
263
|
+
sourceProjection,
|
|
264
|
+
component,
|
|
265
|
+
usage,
|
|
266
|
+
event: binding.event || null,
|
|
267
|
+
suggestedFix: "Bind the event to an existing capability."
|
|
268
|
+
}));
|
|
269
|
+
} else if (!realizedIds.has(target.id)) {
|
|
270
|
+
checks.push(checkRecord({
|
|
271
|
+
code: "component_event_action_not_in_projection",
|
|
272
|
+
severity: "error",
|
|
273
|
+
message: `Event '${binding.event}' targets capability '${target.id}', but projection '${sourceProjection.id}' does not realize it.`,
|
|
274
|
+
projection,
|
|
275
|
+
sourceProjection,
|
|
276
|
+
component,
|
|
277
|
+
usage,
|
|
278
|
+
event: binding.event || null,
|
|
279
|
+
suggestedFix: `Add '${target.id}' to projection '${sourceProjection.id}' realizes or choose a capability already in this projection context.`
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
checks.push(checkRecord({
|
|
284
|
+
code: "component_event_action_unsupported",
|
|
285
|
+
severity: "error",
|
|
286
|
+
message: `Event '${binding.event}' uses unsupported action '${binding.action}'.`,
|
|
287
|
+
projection,
|
|
288
|
+
sourceProjection,
|
|
289
|
+
component,
|
|
290
|
+
usage,
|
|
291
|
+
event: binding.event || null,
|
|
292
|
+
suggestedFix: "Use 'navigate' or 'action'."
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const behavior of contract?.behaviors || []) {
|
|
298
|
+
const stateProp = behavior.directives?.state;
|
|
299
|
+
if (stateProp && !propNames.has(stateProp)) {
|
|
300
|
+
checks.push(checkRecord({
|
|
301
|
+
code: "component_behavior_prop_missing",
|
|
302
|
+
severity: "error",
|
|
303
|
+
message: `Behavior '${behavior.kind}' references missing prop '${stateProp}'.`,
|
|
304
|
+
projection,
|
|
305
|
+
sourceProjection,
|
|
306
|
+
component,
|
|
307
|
+
usage,
|
|
308
|
+
prop: stateProp,
|
|
309
|
+
behavior: behavior.kind,
|
|
310
|
+
suggestedFix: "Update the behavior directive or declare the referenced prop."
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
const emits = Array.isArray(behavior.directives?.emits)
|
|
314
|
+
? behavior.directives.emits
|
|
315
|
+
: [behavior.directives?.emits].filter(Boolean);
|
|
316
|
+
for (const eventName of emits) {
|
|
317
|
+
if (!eventNames.has(eventName)) {
|
|
318
|
+
checks.push(checkRecord({
|
|
319
|
+
code: "component_behavior_event_missing",
|
|
320
|
+
severity: "error",
|
|
321
|
+
message: `Behavior '${behavior.kind}' references missing event '${eventName}'.`,
|
|
322
|
+
projection,
|
|
323
|
+
sourceProjection,
|
|
324
|
+
component,
|
|
325
|
+
usage,
|
|
326
|
+
event: eventName,
|
|
327
|
+
behavior: behavior.kind,
|
|
328
|
+
suggestedFix: "Update the behavior directive or declare the referenced event."
|
|
329
|
+
}));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (!(usage.eventBindings || []).some((binding) => binding.event === eventName)) {
|
|
333
|
+
checks.push(checkRecord({
|
|
334
|
+
code: "component_behavior_event_unbound",
|
|
335
|
+
severity: "warning",
|
|
336
|
+
message: `Behavior '${behavior.kind}' emits event '${eventName}', but this projection usage does not bind that event to navigation or an action.`,
|
|
337
|
+
projection,
|
|
338
|
+
sourceProjection,
|
|
339
|
+
component,
|
|
340
|
+
usage,
|
|
341
|
+
event: eventName,
|
|
342
|
+
behavior: behavior.kind,
|
|
343
|
+
suggestedFix: `Add 'event ${eventName} navigate <screen>' or 'event ${eventName} action <capability>' to the projection ui_components entry.`
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const declaredActions = [
|
|
348
|
+
...(Array.isArray(behavior.directives?.actions) ? behavior.directives.actions : [behavior.directives?.actions].filter(Boolean)),
|
|
349
|
+
...(Array.isArray(behavior.directives?.submit) ? behavior.directives.submit : [behavior.directives?.submit].filter(Boolean))
|
|
350
|
+
];
|
|
351
|
+
for (const actionTarget of declaredActions) {
|
|
352
|
+
if (eventNames.has(actionTarget)) {
|
|
353
|
+
if (!(usage.eventBindings || []).some((binding) => binding.event === actionTarget)) {
|
|
354
|
+
checks.push(checkRecord({
|
|
355
|
+
code: "component_behavior_action_unbound",
|
|
356
|
+
severity: "warning",
|
|
357
|
+
message: `Behavior '${behavior.kind}' declares action event '${actionTarget}', but this projection usage does not bind that event to navigation or an action.`,
|
|
358
|
+
projection,
|
|
359
|
+
sourceProjection,
|
|
360
|
+
component,
|
|
361
|
+
usage,
|
|
362
|
+
event: actionTarget,
|
|
363
|
+
behavior: behavior.kind,
|
|
364
|
+
suggestedFix: `Add 'event ${actionTarget} action <capability>' or 'event ${actionTarget} navigate <screen>' to the projection ui_components entry.`
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const target = statements.get(actionTarget);
|
|
371
|
+
if (!target || target.kind !== "capability") {
|
|
372
|
+
checks.push(checkRecord({
|
|
373
|
+
code: "component_behavior_action_missing",
|
|
374
|
+
severity: "error",
|
|
375
|
+
message: `Behavior '${behavior.kind}' references missing capability action '${actionTarget}'.`,
|
|
376
|
+
projection,
|
|
377
|
+
sourceProjection,
|
|
378
|
+
component,
|
|
379
|
+
usage,
|
|
380
|
+
behavior: behavior.kind,
|
|
381
|
+
suggestedFix: "Update the behavior directive or declare the referenced capability."
|
|
382
|
+
}));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (!realizedIds.has(actionTarget)) {
|
|
386
|
+
checks.push(checkRecord({
|
|
387
|
+
code: "component_behavior_action_not_in_projection",
|
|
388
|
+
severity: "error",
|
|
389
|
+
message: `Behavior '${behavior.kind}' references capability '${actionTarget}', but projection '${sourceProjection.id}' does not realize it.`,
|
|
390
|
+
projection,
|
|
391
|
+
sourceProjection,
|
|
392
|
+
component,
|
|
393
|
+
usage,
|
|
394
|
+
behavior: behavior.kind,
|
|
395
|
+
suggestedFix: `Add '${actionTarget}' to projection '${sourceProjection.id}' realizes or choose a capability already in this projection context.`
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
if (!(usage.eventBindings || []).some((binding) =>
|
|
399
|
+
binding.action === "action" &&
|
|
400
|
+
binding.target?.id === actionTarget &&
|
|
401
|
+
binding.target?.kind === "capability"
|
|
402
|
+
)) {
|
|
403
|
+
checks.push(checkRecord({
|
|
404
|
+
code: "component_behavior_action_unbound",
|
|
405
|
+
severity: "warning",
|
|
406
|
+
message: `Behavior '${behavior.kind}' declares capability action '${actionTarget}', but this projection usage does not bind any component event to that capability.`,
|
|
407
|
+
projection,
|
|
408
|
+
sourceProjection,
|
|
409
|
+
component,
|
|
410
|
+
usage,
|
|
411
|
+
behavior: behavior.kind,
|
|
412
|
+
suggestedFix: `Add 'event <component_event> action ${actionTarget}' to the projection ui_components entry.`
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return checks;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function projectionUsageEntries(graph, projection) {
|
|
422
|
+
const sharedProjection = sharedUiProjectionForWeb(graph, projection);
|
|
423
|
+
const entries = [];
|
|
424
|
+
if (sharedProjection) {
|
|
425
|
+
entries.push(...(sharedProjection.uiComponents || []).map((usage, index) => ({
|
|
426
|
+
projection,
|
|
427
|
+
sourceProjection: sharedProjection,
|
|
428
|
+
usage,
|
|
429
|
+
index
|
|
430
|
+
})));
|
|
431
|
+
}
|
|
432
|
+
entries.push(...(projection.uiComponents || []).map((usage, index) => ({
|
|
433
|
+
projection,
|
|
434
|
+
sourceProjection: projection,
|
|
435
|
+
usage,
|
|
436
|
+
index
|
|
437
|
+
})));
|
|
438
|
+
return entries;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function candidateProjections(graph, projectionId) {
|
|
442
|
+
if (projectionId) {
|
|
443
|
+
return [getProjection(graph, projectionId)];
|
|
444
|
+
}
|
|
445
|
+
const direct = uiProjectionCandidates(graph).filter((projection) => (projection.uiComponents || []).length > 0);
|
|
446
|
+
const inherited = (graph.byKind.projection || []).filter((projection) => {
|
|
447
|
+
if ((projection.uiComponents || []).length > 0) return false;
|
|
448
|
+
return Boolean(sharedUiProjectionForWeb(graph, projection)?.uiComponents?.length);
|
|
449
|
+
});
|
|
450
|
+
return [...direct, ...inherited].sort((a, b) => a.id.localeCompare(b.id));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function relatedVerificationFiles(graph, componentIds, projectionIds) {
|
|
454
|
+
const ids = new Set([...componentIds, ...projectionIds]);
|
|
455
|
+
return stableUnique((graph.byKind.verification || [])
|
|
456
|
+
.filter((verification) => (verification.validates || []).some((ref) => ids.has(ref.id)))
|
|
457
|
+
.map((verification) => sourcePath(verification)));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function generateComponentConformanceReport(graph, options = {}) {
|
|
461
|
+
const components = byId(graph.byKind.component || []);
|
|
462
|
+
if (options.componentId && !components.has(options.componentId)) {
|
|
463
|
+
throw new Error(`No component found with id '${options.componentId}'`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const projectionUsageRecords = [];
|
|
467
|
+
const checks = [];
|
|
468
|
+
const referencedComponentIds = new Set();
|
|
469
|
+
const affectedProjectionIds = new Set();
|
|
470
|
+
|
|
471
|
+
for (const projection of candidateProjections(graph, options.projectionId)) {
|
|
472
|
+
for (const entry of projectionUsageEntries(graph, projection)) {
|
|
473
|
+
const componentId = entry.usage.component?.id || null;
|
|
474
|
+
if (options.componentId && componentId !== options.componentId) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const component = componentId ? components.get(componentId) : null;
|
|
478
|
+
if (componentId) referencedComponentIds.add(componentId);
|
|
479
|
+
affectedProjectionIds.add(entry.projection.id);
|
|
480
|
+
if (entry.sourceProjection?.id) affectedProjectionIds.add(entry.sourceProjection.id);
|
|
481
|
+
const usageChecks = collectUsageChecks({ graph, projection: entry.projection, sourceProjection: entry.sourceProjection, usage: entry.usage, component });
|
|
482
|
+
checks.push(...usageChecks);
|
|
483
|
+
const outcome = usageChecks.some((check) => check.severity === "error")
|
|
484
|
+
? "error"
|
|
485
|
+
: usageChecks.some((check) => check.severity === "warning")
|
|
486
|
+
? "warning"
|
|
487
|
+
: "pass";
|
|
488
|
+
projectionUsageRecords.push({
|
|
489
|
+
key: componentUsageKey(entry.projection, entry.sourceProjection, entry.usage, entry.index),
|
|
490
|
+
projection: summarizeProjection(entry.projection),
|
|
491
|
+
source_projection: entry.sourceProjection.id === entry.projection.id ? null : summarizeProjection(entry.sourceProjection),
|
|
492
|
+
screen: {
|
|
493
|
+
id: entry.usage.screenId || null,
|
|
494
|
+
kind: projectionScreenMap(entry.sourceProjection).get(entry.usage.screenId)?.kind || null,
|
|
495
|
+
title: projectionScreenMap(entry.sourceProjection).get(entry.usage.screenId)?.title || null
|
|
496
|
+
},
|
|
497
|
+
region: entry.usage.region || null,
|
|
498
|
+
component: summarizeComponent(component) || { id: componentId, name: componentId, category: null, version: null, status: null, source_path: null },
|
|
499
|
+
data_bindings: entry.usage.dataBindings || [],
|
|
500
|
+
event_bindings: entry.usage.eventBindings || [],
|
|
501
|
+
behavior_realizations: buildComponentBehaviorRealizations(componentContract(component), entry.usage),
|
|
502
|
+
outcome,
|
|
503
|
+
check_codes: usageChecks.map((check) => check.code)
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const componentFiles = stableUnique([...referencedComponentIds].map((id) => sourcePath(components.get(id))));
|
|
509
|
+
const projectionFiles = stableUnique(
|
|
510
|
+
[...affectedProjectionIds].map((id) => sourcePath((graph.byKind.projection || []).find((projection) => projection.id === id)))
|
|
511
|
+
);
|
|
512
|
+
const verificationFiles = relatedVerificationFiles(graph, referencedComponentIds, affectedProjectionIds);
|
|
513
|
+
const errors = checks.filter((check) => check.severity === "error");
|
|
514
|
+
const warnings = checks.filter((check) => check.severity === "warning");
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
type: "component_conformance_report",
|
|
518
|
+
filters: {
|
|
519
|
+
projection: options.projectionId || null,
|
|
520
|
+
component: options.componentId || null
|
|
521
|
+
},
|
|
522
|
+
summary: {
|
|
523
|
+
total_usages: projectionUsageRecords.length,
|
|
524
|
+
passed_usages: projectionUsageRecords.filter((usage) => usage.outcome === "pass").length,
|
|
525
|
+
warning_usages: projectionUsageRecords.filter((usage) => usage.outcome === "warning").length,
|
|
526
|
+
error_usages: projectionUsageRecords.filter((usage) => usage.outcome === "error").length,
|
|
527
|
+
warnings: warnings.length,
|
|
528
|
+
errors: errors.length,
|
|
529
|
+
affected_projections: stableUnique([...affectedProjectionIds]),
|
|
530
|
+
affected_components: stableUnique([...referencedComponentIds])
|
|
531
|
+
},
|
|
532
|
+
projection_usages: projectionUsageRecords,
|
|
533
|
+
checks,
|
|
534
|
+
component_contracts: stableUnique([...referencedComponentIds])
|
|
535
|
+
.map((id) => summarizeComponentContract(components.get(id)))
|
|
536
|
+
.filter(Boolean),
|
|
537
|
+
write_scope: {
|
|
538
|
+
component_files: componentFiles,
|
|
539
|
+
projection_files: projectionFiles,
|
|
540
|
+
verification_files: verificationFiles,
|
|
541
|
+
paths: stableUnique([...componentFiles, ...projectionFiles, ...verificationFiles])
|
|
542
|
+
},
|
|
543
|
+
impact: {
|
|
544
|
+
projections: stableUnique([...affectedProjectionIds]),
|
|
545
|
+
components: stableUnique([...referencedComponentIds])
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function behaviorReportKey(usage, behavior, index) {
|
|
551
|
+
return [usage.key, behavior.kind || "behavior", String(index)].join(":");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function capabilityIdsFromBehavior(behavior) {
|
|
555
|
+
const ids = [];
|
|
556
|
+
for (const dependency of behavior.dataDependencies || []) {
|
|
557
|
+
if (dependency.source?.kind === "capability" && dependency.source.id) {
|
|
558
|
+
ids.push(dependency.source.id);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const action of behavior.actions || []) {
|
|
562
|
+
if (action.capability?.id) {
|
|
563
|
+
ids.push(action.capability.id);
|
|
564
|
+
}
|
|
565
|
+
for (const effect of action.effects || []) {
|
|
566
|
+
if (effect.capability?.id) {
|
|
567
|
+
ids.push(effect.capability.id);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const effect of behavior.effects || []) {
|
|
572
|
+
if (effect.capability?.id) {
|
|
573
|
+
ids.push(effect.capability.id);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return stableUnique(ids);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function effectTypesFromBehavior(behavior) {
|
|
580
|
+
const effects = behavior.effects || [];
|
|
581
|
+
if (effects.length === 0) {
|
|
582
|
+
return ["none"];
|
|
583
|
+
}
|
|
584
|
+
return stableUnique(effects.map((effect) => effect.type || "unknown"));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function checksForBehavior(conformanceReport, usage, behavior) {
|
|
588
|
+
return (conformanceReport.checks || [])
|
|
589
|
+
.filter((check) =>
|
|
590
|
+
check.code?.startsWith("component_behavior_") &&
|
|
591
|
+
check.projection === usage.projection?.id &&
|
|
592
|
+
check.component === usage.component?.id &&
|
|
593
|
+
check.screen === usage.screen?.id &&
|
|
594
|
+
check.region === usage.region &&
|
|
595
|
+
(!check.behavior || check.behavior === behavior.kind)
|
|
596
|
+
)
|
|
597
|
+
.map((check) => ({
|
|
598
|
+
code: check.code,
|
|
599
|
+
severity: check.severity,
|
|
600
|
+
message: check.message,
|
|
601
|
+
event: check.event || null,
|
|
602
|
+
behavior: check.behavior || behavior.kind || null,
|
|
603
|
+
suggested_fix: check.suggested_fix || null
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function behaviorHighlights(behaviorRows) {
|
|
608
|
+
const highlights = [];
|
|
609
|
+
for (const row of behaviorRows) {
|
|
610
|
+
if (row.behavior.status === "partial") {
|
|
611
|
+
highlights.push({
|
|
612
|
+
severity: "warning",
|
|
613
|
+
code: "component_behavior_partial",
|
|
614
|
+
message: `Behavior '${row.behavior.kind}' is partially realized for component '${row.component.id}' on screen '${row.screen.id}'.`,
|
|
615
|
+
projection: row.projection.id,
|
|
616
|
+
component: row.component.id,
|
|
617
|
+
screen: row.screen.id,
|
|
618
|
+
region: row.region,
|
|
619
|
+
behavior: row.behavior.kind,
|
|
620
|
+
suggested_fix: "Bind the required behavior data, events, or capability actions in the projection ui_components entry."
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
for (const emittedEvent of row.emits || []) {
|
|
624
|
+
if (!emittedEvent.bound) {
|
|
625
|
+
highlights.push({
|
|
626
|
+
severity: "warning",
|
|
627
|
+
code: "component_behavior_event_unbound",
|
|
628
|
+
message: `Behavior '${row.behavior.kind}' emits event '${emittedEvent.event}', but this component usage does not bind it.`,
|
|
629
|
+
projection: row.projection.id,
|
|
630
|
+
component: row.component.id,
|
|
631
|
+
screen: row.screen.id,
|
|
632
|
+
region: row.region,
|
|
633
|
+
event: emittedEvent.event || null,
|
|
634
|
+
behavior: row.behavior.kind,
|
|
635
|
+
suggested_fix: `Add 'event ${emittedEvent.event} navigate <screen>' or 'event ${emittedEvent.event} action <capability>' to the projection ui_components entry.`
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const action of row.actions || []) {
|
|
640
|
+
if (!action.bound) {
|
|
641
|
+
const target = action.capability?.id || action.event || "(unknown)";
|
|
642
|
+
highlights.push({
|
|
643
|
+
severity: "warning",
|
|
644
|
+
code: "component_behavior_action_unbound",
|
|
645
|
+
message: `Behavior '${row.behavior.kind}' declares action '${target}', but this component usage does not bind it.`,
|
|
646
|
+
projection: row.projection.id,
|
|
647
|
+
component: row.component.id,
|
|
648
|
+
screen: row.screen.id,
|
|
649
|
+
region: row.region,
|
|
650
|
+
event: action.event || null,
|
|
651
|
+
capability: action.capability?.id || null,
|
|
652
|
+
behavior: row.behavior.kind,
|
|
653
|
+
suggested_fix: action.capability?.id
|
|
654
|
+
? `Add 'event <component_event> action ${action.capability.id}' to the projection ui_components entry.`
|
|
655
|
+
: `Add 'event ${action.event} action <capability>' or 'event ${action.event} navigate <screen>' to the projection ui_components entry.`
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return highlights;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function groupBehaviorRows(rows, keyFn, itemFn = null) {
|
|
664
|
+
const groups = new Map();
|
|
665
|
+
for (const row of rows) {
|
|
666
|
+
for (const key of keyFn(row)) {
|
|
667
|
+
if (!groups.has(key)) {
|
|
668
|
+
groups.set(key, []);
|
|
669
|
+
}
|
|
670
|
+
groups.get(key).push(row);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return [...groups.entries()]
|
|
674
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
675
|
+
.map(([id, entries]) => ({
|
|
676
|
+
id,
|
|
677
|
+
total_behaviors: entries.length,
|
|
678
|
+
realized: entries.filter((entry) => entry.behavior.status === "realized").length,
|
|
679
|
+
partial: entries.filter((entry) => entry.behavior.status === "partial").length,
|
|
680
|
+
declared: entries.filter((entry) => entry.behavior.status === "declared").length,
|
|
681
|
+
behaviors: entries.map((entry) => itemFn ? itemFn(entry) : entry.key).sort()
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function generateComponentBehaviorReport(graph, options = {}) {
|
|
686
|
+
const conformanceReport = generateComponentConformanceReport(graph, options);
|
|
687
|
+
const behaviorRows = [];
|
|
688
|
+
|
|
689
|
+
for (const usage of conformanceReport.projection_usages || []) {
|
|
690
|
+
for (const [index, behavior] of (usage.behavior_realizations || []).entries()) {
|
|
691
|
+
const diagnostics = checksForBehavior(conformanceReport, usage, behavior);
|
|
692
|
+
behaviorRows.push({
|
|
693
|
+
key: behaviorReportKey(usage, behavior, index),
|
|
694
|
+
projection: usage.projection,
|
|
695
|
+
source_projection: usage.source_projection,
|
|
696
|
+
screen: usage.screen,
|
|
697
|
+
region: usage.region,
|
|
698
|
+
component: usage.component,
|
|
699
|
+
behavior: {
|
|
700
|
+
kind: behavior.kind || null,
|
|
701
|
+
source: behavior.source || null,
|
|
702
|
+
status: behavior.status || "declared",
|
|
703
|
+
directives: behavior.directives || {}
|
|
704
|
+
},
|
|
705
|
+
data_dependencies: behavior.dataDependencies || [],
|
|
706
|
+
emits: behavior.emits || [],
|
|
707
|
+
actions: behavior.actions || [],
|
|
708
|
+
effects: behavior.effects || [],
|
|
709
|
+
capabilities: capabilityIdsFromBehavior(behavior),
|
|
710
|
+
effect_types: effectTypesFromBehavior(behavior),
|
|
711
|
+
diagnostics,
|
|
712
|
+
check_codes: diagnostics.map((check) => check.code)
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const highlights = behaviorHighlights(behaviorRows);
|
|
718
|
+
const affectedCapabilities = stableUnique(behaviorRows.flatMap((row) => row.capabilities));
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
type: "component_behavior_report",
|
|
722
|
+
filters: conformanceReport.filters,
|
|
723
|
+
summary: {
|
|
724
|
+
total_usages: conformanceReport.summary.total_usages,
|
|
725
|
+
total_behaviors: behaviorRows.length,
|
|
726
|
+
realized: behaviorRows.filter((row) => row.behavior.status === "realized").length,
|
|
727
|
+
partial: behaviorRows.filter((row) => row.behavior.status === "partial").length,
|
|
728
|
+
declared: behaviorRows.filter((row) => row.behavior.status === "declared").length,
|
|
729
|
+
warnings: conformanceReport.summary.warnings,
|
|
730
|
+
errors: conformanceReport.summary.errors,
|
|
731
|
+
affected_components: conformanceReport.summary.affected_components,
|
|
732
|
+
affected_projections: conformanceReport.summary.affected_projections,
|
|
733
|
+
affected_capabilities: affectedCapabilities
|
|
734
|
+
},
|
|
735
|
+
groups: {
|
|
736
|
+
components: groupBehaviorRows(
|
|
737
|
+
behaviorRows,
|
|
738
|
+
(row) => [row.component.id].filter(Boolean),
|
|
739
|
+
(row) => row.key
|
|
740
|
+
),
|
|
741
|
+
screens: groupBehaviorRows(
|
|
742
|
+
behaviorRows,
|
|
743
|
+
(row) => [row.screen.id].filter(Boolean),
|
|
744
|
+
(row) => row.key
|
|
745
|
+
),
|
|
746
|
+
capabilities: groupBehaviorRows(
|
|
747
|
+
behaviorRows,
|
|
748
|
+
(row) => row.capabilities,
|
|
749
|
+
(row) => row.key
|
|
750
|
+
),
|
|
751
|
+
effects: groupBehaviorRows(
|
|
752
|
+
behaviorRows,
|
|
753
|
+
(row) => row.effect_types,
|
|
754
|
+
(row) => row.key
|
|
755
|
+
)
|
|
756
|
+
},
|
|
757
|
+
behaviors: behaviorRows,
|
|
758
|
+
highlights,
|
|
759
|
+
checks: conformanceReport.checks.filter((check) => check.code?.startsWith("component_behavior_")),
|
|
760
|
+
write_scope: conformanceReport.write_scope,
|
|
761
|
+
impact: {
|
|
762
|
+
projections: conformanceReport.impact.projections,
|
|
763
|
+
components: conformanceReport.impact.components,
|
|
764
|
+
capabilities: affectedCapabilities
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|