@transitrix/diagrams 1.0.0
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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/__tests__/edge-path.test.d.ts +1 -0
- package/dist/__tests__/edge-path.test.js +39 -0
- package/dist/__tests__/scope.test.d.ts +1 -0
- package/dist/__tests__/scope.test.js +23 -0
- package/dist/__tests__/yaml-normalize.test.d.ts +1 -0
- package/dist/__tests__/yaml-normalize.test.js +148 -0
- package/dist/activities/__tests__/cpm.test.d.ts +1 -0
- package/dist/activities/__tests__/cpm.test.js +146 -0
- package/dist/activities/__tests__/gantt.test.d.ts +1 -0
- package/dist/activities/__tests__/gantt.test.js +224 -0
- package/dist/activities/__tests__/layout.test.d.ts +1 -0
- package/dist/activities/__tests__/layout.test.js +98 -0
- package/dist/activities/__tests__/validate.test.d.ts +1 -0
- package/dist/activities/__tests__/validate.test.js +363 -0
- package/dist/activities/cpm.d.ts +2 -0
- package/dist/activities/cpm.js +86 -0
- package/dist/activities/gantt.d.ts +6 -0
- package/dist/activities/gantt.js +250 -0
- package/dist/activities/index.d.ts +5 -0
- package/dist/activities/index.js +4 -0
- package/dist/activities/layout.d.ts +2 -0
- package/dist/activities/layout.js +103 -0
- package/dist/activities/types.d.ts +125 -0
- package/dist/activities/types.js +1 -0
- package/dist/activities/validate.d.ts +2 -0
- package/dist/activities/validate.js +326 -0
- package/dist/activity-card/__tests__/example.test.d.ts +1 -0
- package/dist/activity-card/__tests__/example.test.js +56 -0
- package/dist/activity-card/__tests__/layout.test.d.ts +1 -0
- package/dist/activity-card/__tests__/layout.test.js +57 -0
- package/dist/activity-card/__tests__/resolver.test.d.ts +1 -0
- package/dist/activity-card/__tests__/resolver.test.js +146 -0
- package/dist/activity-card/__tests__/validate.test.d.ts +1 -0
- package/dist/activity-card/__tests__/validate.test.js +116 -0
- package/dist/activity-card/index.d.ts +5 -0
- package/dist/activity-card/index.js +3 -0
- package/dist/activity-card/layout.d.ts +7 -0
- package/dist/activity-card/layout.js +167 -0
- package/dist/activity-card/resolver.d.ts +7 -0
- package/dist/activity-card/resolver.js +310 -0
- package/dist/activity-card/types.d.ts +175 -0
- package/dist/activity-card/types.js +20 -0
- package/dist/activity-card/validate.d.ts +3 -0
- package/dist/activity-card/validate.js +145 -0
- package/dist/actor/__tests__/validate.test.d.ts +1 -0
- package/dist/actor/__tests__/validate.test.js +57 -0
- package/dist/actor/index.d.ts +3 -0
- package/dist/actor/index.js +2 -0
- package/dist/actor/types.d.ts +20 -0
- package/dist/actor/types.js +4 -0
- package/dist/actor/validate.d.ts +2 -0
- package/dist/actor/validate.js +34 -0
- package/dist/applications/__tests__/validate.test.d.ts +1 -0
- package/dist/applications/__tests__/validate.test.js +223 -0
- package/dist/applications/index.d.ts +2 -0
- package/dist/applications/index.js +1 -0
- package/dist/applications/types.d.ts +39 -0
- package/dist/applications/types.js +1 -0
- package/dist/applications/validate.d.ts +3 -0
- package/dist/applications/validate.js +109 -0
- package/dist/assertion/__tests__/example.test.d.ts +1 -0
- package/dist/assertion/__tests__/example.test.js +32 -0
- package/dist/assertion/__tests__/validate.test.d.ts +1 -0
- package/dist/assertion/__tests__/validate.test.js +130 -0
- package/dist/assertion/index.d.ts +4 -0
- package/dist/assertion/index.js +2 -0
- package/dist/assertion/types.d.ts +43 -0
- package/dist/assertion/types.js +7 -0
- package/dist/assertion/validate.d.ts +9 -0
- package/dist/assertion/validate.js +137 -0
- package/dist/blocks/__tests__/layout.test.d.ts +1 -0
- package/dist/blocks/__tests__/layout.test.js +153 -0
- package/dist/blocks/__tests__/validate.test.d.ts +1 -0
- package/dist/blocks/__tests__/validate.test.js +235 -0
- package/dist/blocks/index.d.ts +4 -0
- package/dist/blocks/index.js +2 -0
- package/dist/blocks/layout.d.ts +4 -0
- package/dist/blocks/layout.js +145 -0
- package/dist/blocks/types.d.ts +56 -0
- package/dist/blocks/types.js +1 -0
- package/dist/blocks/validate.d.ts +9 -0
- package/dist/blocks/validate.js +186 -0
- package/dist/capability-map/__tests__/validate.test.d.ts +1 -0
- package/dist/capability-map/__tests__/validate.test.js +212 -0
- package/dist/capability-map/index.d.ts +2 -0
- package/dist/capability-map/index.js +1 -0
- package/dist/capability-map/types.d.ts +26 -0
- package/dist/capability-map/types.js +1 -0
- package/dist/capability-map/validate.d.ts +3 -0
- package/dist/capability-map/validate.js +103 -0
- package/dist/change/__tests__/validate.test.d.ts +1 -0
- package/dist/change/__tests__/validate.test.js +73 -0
- package/dist/change/index.d.ts +2 -0
- package/dist/change/index.js +1 -0
- package/dist/change/types.d.ts +17 -0
- package/dist/change/types.js +3 -0
- package/dist/change/validate.d.ts +2 -0
- package/dist/change/validate.js +57 -0
- package/dist/compliance/__tests__/classify.test.d.ts +1 -0
- package/dist/compliance/__tests__/classify.test.js +24 -0
- package/dist/compliance/__tests__/confidence.test.d.ts +1 -0
- package/dist/compliance/__tests__/confidence.test.js +78 -0
- package/dist/compliance/__tests__/coverage-metric.test.d.ts +1 -0
- package/dist/compliance/__tests__/coverage-metric.test.js +164 -0
- package/dist/compliance/__tests__/demo-scenario.test.d.ts +1 -0
- package/dist/compliance/__tests__/demo-scenario.test.js +226 -0
- package/dist/compliance/__tests__/example.test.d.ts +1 -0
- package/dist/compliance/__tests__/example.test.js +51 -0
- package/dist/compliance/__tests__/gap-report.test.d.ts +1 -0
- package/dist/compliance/__tests__/gap-report.test.js +113 -0
- package/dist/compliance/__tests__/html.test.d.ts +1 -0
- package/dist/compliance/__tests__/html.test.js +122 -0
- package/dist/compliance/__tests__/impact.test.d.ts +1 -0
- package/dist/compliance/__tests__/impact.test.js +410 -0
- package/dist/compliance/__tests__/markdown.test.d.ts +1 -0
- package/dist/compliance/__tests__/markdown.test.js +85 -0
- package/dist/compliance/__tests__/views.test.d.ts +1 -0
- package/dist/compliance/__tests__/views.test.js +59 -0
- package/dist/compliance/classify.d.ts +27 -0
- package/dist/compliance/classify.js +60 -0
- package/dist/compliance/confidence.d.ts +27 -0
- package/dist/compliance/confidence.js +43 -0
- package/dist/compliance/coverage-metric.d.ts +74 -0
- package/dist/compliance/coverage-metric.js +181 -0
- package/dist/compliance/gap-report.d.ts +20 -0
- package/dist/compliance/gap-report.js +64 -0
- package/dist/compliance/html.d.ts +22 -0
- package/dist/compliance/html.js +256 -0
- package/dist/compliance/impact.d.ts +251 -0
- package/dist/compliance/impact.js +464 -0
- package/dist/compliance/index.d.ts +16 -0
- package/dist/compliance/index.js +9 -0
- package/dist/compliance/markdown.d.ts +18 -0
- package/dist/compliance/markdown.js +129 -0
- package/dist/compliance/reverse-index.d.ts +8 -0
- package/dist/compliance/reverse-index.js +30 -0
- package/dist/compliance/types.d.ts +98 -0
- package/dist/compliance/types.js +5 -0
- package/dist/compliance/views.d.ts +14 -0
- package/dist/compliance/views.js +35 -0
- package/dist/compliance-matrix/__tests__/build.test.d.ts +1 -0
- package/dist/compliance-matrix/__tests__/build.test.js +183 -0
- package/dist/compliance-matrix/__tests__/example.test.d.ts +1 -0
- package/dist/compliance-matrix/__tests__/example.test.js +49 -0
- package/dist/compliance-matrix/build.d.ts +18 -0
- package/dist/compliance-matrix/build.js +0 -0
- package/dist/compliance-matrix/index.d.ts +2 -0
- package/dist/compliance-matrix/index.js +1 -0
- package/dist/compliance-matrix/types.d.ts +86 -0
- package/dist/compliance-matrix/types.js +8 -0
- package/dist/confidence/__tests__/score.test.d.ts +1 -0
- package/dist/confidence/__tests__/score.test.js +188 -0
- package/dist/confidence/index.d.ts +2 -0
- package/dist/confidence/index.js +1 -0
- package/dist/confidence/score.d.ts +62 -0
- package/dist/confidence/score.js +215 -0
- package/dist/confidence/types.d.ts +79 -0
- package/dist/confidence/types.js +6 -0
- package/dist/edge-path.d.ts +18 -0
- package/dist/edge-path.js +35 -0
- package/dist/element-envelope.d.ts +15 -0
- package/dist/element-envelope.js +55 -0
- package/dist/factor/__tests__/validate.test.d.ts +1 -0
- package/dist/factor/__tests__/validate.test.js +83 -0
- package/dist/factor/index.d.ts +3 -0
- package/dist/factor/index.js +2 -0
- package/dist/factor/types.d.ts +23 -0
- package/dist/factor/types.js +6 -0
- package/dist/factor/validate.d.ts +2 -0
- package/dist/factor/validate.js +69 -0
- package/dist/fgca/__tests__/chain-table.test.d.ts +1 -0
- package/dist/fgca/__tests__/chain-table.test.js +158 -0
- package/dist/fgca/__tests__/layout.test.d.ts +1 -0
- package/dist/fgca/__tests__/layout.test.js +140 -0
- package/dist/fgca/__tests__/parse-canonical.test.d.ts +1 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +224 -0
- package/dist/fgca/__tests__/preview-layout.test.d.ts +1 -0
- package/dist/fgca/__tests__/preview-layout.test.js +106 -0
- package/dist/fgca/__tests__/validate.test.d.ts +1 -0
- package/dist/fgca/__tests__/validate.test.js +125 -0
- package/dist/fgca/chain-table.d.ts +36 -0
- package/dist/fgca/chain-table.js +177 -0
- package/dist/fgca/index.d.ts +16 -0
- package/dist/fgca/index.js +11 -0
- package/dist/fgca/layout.d.ts +27 -0
- package/dist/fgca/layout.js +156 -0
- package/dist/fgca/nodeTypes.d.ts +6 -0
- package/dist/fgca/nodeTypes.js +10 -0
- package/dist/fgca/nodes/FGCAActivityNode.d.ts +13 -0
- package/dist/fgca/nodes/FGCAActivityNode.js +35 -0
- package/dist/fgca/nodes/FGCAChangeNode.d.ts +12 -0
- package/dist/fgca/nodes/FGCAChangeNode.js +35 -0
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +12 -0
- package/dist/fgca/nodes/FGCAFactorNode.js +35 -0
- package/dist/fgca/nodes/FGCAGoalNode.d.ts +14 -0
- package/dist/fgca/nodes/FGCAGoalNode.js +40 -0
- package/dist/fgca/parse-canonical.d.ts +58 -0
- package/dist/fgca/parse-canonical.js +339 -0
- package/dist/fgca/preview-layout.d.ts +87 -0
- package/dist/fgca/preview-layout.js +122 -0
- package/dist/fgca/types.d.ts +39 -0
- package/dist/fgca/types.js +7 -0
- package/dist/fgca/validate.d.ts +14 -0
- package/dist/fgca/validate.js +108 -0
- package/dist/geometry.d.ts +7 -0
- package/dist/geometry.js +2 -0
- package/dist/goals/__tests__/layout.test.d.ts +1 -0
- package/dist/goals/__tests__/layout.test.js +153 -0
- package/dist/goals/__tests__/mutations.test.d.ts +1 -0
- package/dist/goals/__tests__/mutations.test.js +95 -0
- package/dist/goals/__tests__/parse-canonical.test.d.ts +1 -0
- package/dist/goals/__tests__/parse-canonical.test.js +129 -0
- package/dist/goals/__tests__/validate.test.d.ts +1 -0
- package/dist/goals/__tests__/validate.test.js +183 -0
- package/dist/goals/index.d.ts +6 -0
- package/dist/goals/index.js +4 -0
- package/dist/goals/layout.d.ts +11 -0
- package/dist/goals/layout.js +147 -0
- package/dist/goals/mutations.d.ts +6 -0
- package/dist/goals/mutations.js +116 -0
- package/dist/goals/parse-canonical.d.ts +29 -0
- package/dist/goals/parse-canonical.js +257 -0
- package/dist/goals/types.d.ts +71 -0
- package/dist/goals/types.js +1 -0
- package/dist/goals/validate.d.ts +2 -0
- package/dist/goals/validate.js +122 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/issues/__tests__/layout.test.d.ts +1 -0
- package/dist/issues/__tests__/layout.test.js +62 -0
- package/dist/issues/__tests__/validate.test.d.ts +1 -0
- package/dist/issues/__tests__/validate.test.js +82 -0
- package/dist/issues/index.d.ts +4 -0
- package/dist/issues/index.js +2 -0
- package/dist/issues/layout.d.ts +10 -0
- package/dist/issues/layout.js +82 -0
- package/dist/issues/types.d.ts +61 -0
- package/dist/issues/types.js +6 -0
- package/dist/issues/validate.d.ts +3 -0
- package/dist/issues/validate.js +143 -0
- package/dist/process-blueprint/__tests__/compliance-lane.test.d.ts +1 -0
- package/dist/process-blueprint/__tests__/compliance-lane.test.js +272 -0
- package/dist/process-blueprint/__tests__/layout.test.d.ts +1 -0
- package/dist/process-blueprint/__tests__/layout.test.js +159 -0
- package/dist/process-blueprint/__tests__/validate.test.d.ts +1 -0
- package/dist/process-blueprint/__tests__/validate.test.js +280 -0
- package/dist/process-blueprint/index.d.ts +4 -0
- package/dist/process-blueprint/index.js +2 -0
- package/dist/process-blueprint/layout.d.ts +2 -0
- package/dist/process-blueprint/layout.js +416 -0
- package/dist/process-blueprint/types.d.ts +220 -0
- package/dist/process-blueprint/types.js +1 -0
- package/dist/process-blueprint/validate.d.ts +3 -0
- package/dist/process-blueprint/validate.js +165 -0
- package/dist/process-map/__tests__/validate.test.d.ts +1 -0
- package/dist/process-map/__tests__/validate.test.js +199 -0
- package/dist/process-map/index.d.ts +2 -0
- package/dist/process-map/index.js +1 -0
- package/dist/process-map/types.d.ts +32 -0
- package/dist/process-map/types.js +1 -0
- package/dist/process-map/validate.d.ts +3 -0
- package/dist/process-map/validate.js +115 -0
- package/dist/products/__tests__/validate.test.d.ts +1 -0
- package/dist/products/__tests__/validate.test.js +210 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +1 -0
- package/dist/products/types.d.ts +28 -0
- package/dist/products/types.js +1 -0
- package/dist/products/validate.d.ts +3 -0
- package/dist/products/validate.js +93 -0
- package/dist/repo-validate/__tests__/validate-repo.test.d.ts +1 -0
- package/dist/repo-validate/__tests__/validate-repo.test.js +132 -0
- package/dist/repo-validate/index.d.ts +2 -0
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/types.d.ts +34 -0
- package/dist/repo-validate/types.js +14 -0
- package/dist/repo-validate/validate-repo.d.ts +8 -0
- package/dist/repo-validate/validate-repo.js +189 -0
- package/dist/requirement/__tests__/example.test.d.ts +1 -0
- package/dist/requirement/__tests__/example.test.js +21 -0
- package/dist/requirement/__tests__/validate.test.d.ts +1 -0
- package/dist/requirement/__tests__/validate.test.js +80 -0
- package/dist/requirement/index.d.ts +4 -0
- package/dist/requirement/index.js +2 -0
- package/dist/requirement/types.d.ts +25 -0
- package/dist/requirement/types.js +4 -0
- package/dist/requirement/validate.d.ts +7 -0
- package/dist/requirement/validate.js +81 -0
- package/dist/scenarios/__tests__/validate.test.d.ts +1 -0
- package/dist/scenarios/__tests__/validate.test.js +183 -0
- package/dist/scenarios/index.d.ts +2 -0
- package/dist/scenarios/index.js +1 -0
- package/dist/scenarios/types.d.ts +45 -0
- package/dist/scenarios/types.js +1 -0
- package/dist/scenarios/validate.d.ts +3 -0
- package/dist/scenarios/validate.js +110 -0
- package/dist/schema-version.d.ts +16 -0
- package/dist/schema-version.js +16 -0
- package/dist/scope.d.ts +23 -0
- package/dist/scope.js +38 -0
- package/dist/stakeholder/__tests__/validate.test.d.ts +1 -0
- package/dist/stakeholder/__tests__/validate.test.js +86 -0
- package/dist/stakeholder/index.d.ts +3 -0
- package/dist/stakeholder/index.js +2 -0
- package/dist/stakeholder/types.d.ts +24 -0
- package/dist/stakeholder/types.js +5 -0
- package/dist/stakeholder/validate.d.ts +2 -0
- package/dist/stakeholder/validate.js +58 -0
- package/dist/target-state/__tests__/validate.test.d.ts +1 -0
- package/dist/target-state/__tests__/validate.test.js +62 -0
- package/dist/target-state/index.d.ts +2 -0
- package/dist/target-state/index.js +1 -0
- package/dist/target-state/types.d.ts +18 -0
- package/dist/target-state/types.js +7 -0
- package/dist/target-state/validate.d.ts +2 -0
- package/dist/target-state/validate.js +61 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/themes.d.ts +16 -0
- package/dist/theme/themes.js +213 -0
- package/dist/theme/tokens.d.ts +158 -0
- package/dist/theme/tokens.js +105 -0
- package/dist/typed-id.d.ts +20 -0
- package/dist/typed-id.js +38 -0
- package/dist/validation-types.d.ts +34 -0
- package/dist/validation-types.js +1 -0
- package/dist/webview/__tests__/entry-notations.test.d.ts +1 -0
- package/dist/webview/__tests__/entry-notations.test.js +62 -0
- package/dist/webview/__tests__/entry.test.d.ts +1 -0
- package/dist/webview/__tests__/entry.test.js +56 -0
- package/dist/webview/__tests__/render-goals.test.d.ts +1 -0
- package/dist/webview/__tests__/render-goals.test.js +65 -0
- package/dist/webview/entry.d.ts +38 -0
- package/dist/webview/entry.js +182 -0
- package/dist/webview/render-activities.d.ts +24 -0
- package/dist/webview/render-activities.js +127 -0
- package/dist/webview/render-activity-card.d.ts +5 -0
- package/dist/webview/render-activity-card.js +168 -0
- package/dist/webview/render-applications.d.ts +11 -0
- package/dist/webview/render-applications.js +105 -0
- package/dist/webview/render-blocks.d.ts +5 -0
- package/dist/webview/render-blocks.js +71 -0
- package/dist/webview/render-capability-map.d.ts +7 -0
- package/dist/webview/render-capability-map.js +88 -0
- package/dist/webview/render-fgca.d.ts +8 -0
- package/dist/webview/render-fgca.js +94 -0
- package/dist/webview/render-goals.d.ts +6 -0
- package/dist/webview/render-goals.js +92 -0
- package/dist/webview/render-process-blueprint.d.ts +5 -0
- package/dist/webview/render-process-blueprint.js +135 -0
- package/dist/webview/render-process-map.d.ts +7 -0
- package/dist/webview/render-process-map.js +82 -0
- package/dist/webview/render-products.d.ts +7 -0
- package/dist/webview/render-products.js +81 -0
- package/dist/webview/render-scenarios.d.ts +7 -0
- package/dist/webview/render-scenarios.js +75 -0
- package/dist/webview/render-util.d.ts +16 -0
- package/dist/webview/render-util.js +31 -0
- package/dist/webview/transitrix-render.css +323 -0
- package/dist/webview/transitrix-render.js +6452 -0
- package/dist/webview/transitrix-render.js.map +7 -0
- package/dist/yaml-normalize.d.ts +20 -0
- package/dist/yaml-normalize.js +57 -0
- package/package.json +40 -0
- package/src/__tests__/edge-path.test.ts +46 -0
- package/src/__tests__/scope.test.ts +27 -0
- package/src/__tests__/yaml-normalize.test.ts +158 -0
- package/src/activities/__tests__/cpm.test.ts +164 -0
- package/src/activities/__tests__/gantt.test.ts +245 -0
- package/src/activities/__tests__/layout.test.ts +112 -0
- package/src/activities/__tests__/validate.test.ts +409 -0
- package/src/activities/cpm.ts +95 -0
- package/src/activities/gantt.ts +276 -0
- package/src/activities/index.ts +28 -0
- package/src/activities/layout.ts +112 -0
- package/src/activities/types.ts +148 -0
- package/src/activities/validate.ts +357 -0
- package/src/activity-card/__tests__/example.test.ts +66 -0
- package/src/activity-card/__tests__/layout.test.ts +63 -0
- package/src/activity-card/__tests__/resolver.test.ts +161 -0
- package/src/activity-card/__tests__/validate.test.ts +131 -0
- package/src/activity-card/index.ts +32 -0
- package/src/activity-card/layout.ts +194 -0
- package/src/activity-card/resolver.ts +337 -0
- package/src/activity-card/types.ts +237 -0
- package/src/activity-card/validate.ts +158 -0
- package/src/actor/__tests__/validate.test.ts +67 -0
- package/src/actor/index.ts +3 -0
- package/src/actor/types.ts +29 -0
- package/src/actor/validate.ts +38 -0
- package/src/applications/__tests__/validate.test.ts +256 -0
- package/src/applications/index.ts +11 -0
- package/src/applications/types.ts +43 -0
- package/src/applications/validate.ts +131 -0
- package/src/assertion/__tests__/example.test.ts +37 -0
- package/src/assertion/__tests__/validate.test.ts +142 -0
- package/src/assertion/index.ts +12 -0
- package/src/assertion/types.ts +58 -0
- package/src/assertion/validate.ts +145 -0
- package/src/blocks/__tests__/layout.test.ts +169 -0
- package/src/blocks/__tests__/validate.test.ts +260 -0
- package/src/blocks/index.ts +17 -0
- package/src/blocks/layout.ts +186 -0
- package/src/blocks/types.ts +62 -0
- package/src/blocks/validate.ts +206 -0
- package/src/capability-map/__tests__/validate.test.ts +238 -0
- package/src/capability-map/index.ts +8 -0
- package/src/capability-map/types.ts +29 -0
- package/src/capability-map/validate.ts +127 -0
- package/src/change/__tests__/validate.test.ts +88 -0
- package/src/change/index.ts +2 -0
- package/src/change/types.ts +23 -0
- package/src/change/validate.ts +61 -0
- package/src/compliance/__tests__/classify.test.ts +27 -0
- package/src/compliance/__tests__/confidence.test.ts +98 -0
- package/src/compliance/__tests__/coverage-metric.test.ts +188 -0
- package/src/compliance/__tests__/demo-scenario.test.ts +273 -0
- package/src/compliance/__tests__/example.test.ts +57 -0
- package/src/compliance/__tests__/gap-report.test.ts +129 -0
- package/src/compliance/__tests__/html.test.ts +142 -0
- package/src/compliance/__tests__/impact.test.ts +482 -0
- package/src/compliance/__tests__/markdown.test.ts +95 -0
- package/src/compliance/__tests__/views.test.ts +64 -0
- package/src/compliance/classify.ts +84 -0
- package/src/compliance/confidence.ts +69 -0
- package/src/compliance/coverage-metric.ts +263 -0
- package/src/compliance/gap-report.ts +94 -0
- package/src/compliance/html.ts +295 -0
- package/src/compliance/impact.ts +677 -0
- package/src/compliance/index.ts +56 -0
- package/src/compliance/markdown.ts +135 -0
- package/src/compliance/reverse-index.ts +33 -0
- package/src/compliance/types.ts +120 -0
- package/src/compliance/views.ts +46 -0
- package/src/compliance-matrix/__tests__/build.test.ts +200 -0
- package/src/compliance-matrix/__tests__/example.test.ts +57 -0
- package/src/compliance-matrix/build.ts +0 -0
- package/src/compliance-matrix/index.ts +11 -0
- package/src/compliance-matrix/types.ts +102 -0
- package/src/confidence/__tests__/score.test.ts +254 -0
- package/src/confidence/index.ts +21 -0
- package/src/confidence/score.ts +243 -0
- package/src/confidence/types.ts +96 -0
- package/src/edge-path.ts +44 -0
- package/src/element-envelope.ts +76 -0
- package/src/factor/__tests__/validate.test.ts +98 -0
- package/src/factor/index.ts +8 -0
- package/src/factor/types.ts +41 -0
- package/src/factor/validate.ts +74 -0
- package/src/fgca/__tests__/chain-table.test.ts +172 -0
- package/src/fgca/__tests__/layout.test.ts +179 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +248 -0
- package/src/fgca/__tests__/preview-layout.test.ts +124 -0
- package/src/fgca/__tests__/validate.test.ts +142 -0
- package/src/fgca/chain-table.ts +225 -0
- package/src/fgca/index.ts +45 -0
- package/src/fgca/layout.ts +204 -0
- package/src/fgca/nodeTypes.ts +11 -0
- package/src/fgca/nodes/FGCAActivityNode.tsx +65 -0
- package/src/fgca/nodes/FGCAChangeNode.tsx +64 -0
- package/src/fgca/nodes/FGCAFactorNode.tsx +64 -0
- package/src/fgca/nodes/FGCAGoalNode.tsx +72 -0
- package/src/fgca/parse-canonical.ts +400 -0
- package/src/fgca/preview-layout.ts +217 -0
- package/src/fgca/types.ts +50 -0
- package/src/fgca/validate.ts +126 -0
- package/src/geometry.ts +9 -0
- package/src/goals/__tests__/layout.test.ts +175 -0
- package/src/goals/__tests__/mutations.test.ts +111 -0
- package/src/goals/__tests__/parse-canonical.test.ts +145 -0
- package/src/goals/__tests__/validate.test.ts +202 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/layout.ts +172 -0
- package/src/goals/mutations.ts +123 -0
- package/src/goals/parse-canonical.ts +286 -0
- package/src/goals/types.ts +85 -0
- package/src/goals/validate.ts +136 -0
- package/src/index.ts +23 -0
- package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -0
- package/src/process-blueprint/__tests__/layout.test.ts +195 -0
- package/src/process-blueprint/__tests__/validate.test.ts +310 -0
- package/src/process-blueprint/index.ts +31 -0
- package/src/process-blueprint/layout.ts +531 -0
- package/src/process-blueprint/types.ts +242 -0
- package/src/process-blueprint/validate.ts +181 -0
- package/src/process-map/__tests__/validate.test.ts +223 -0
- package/src/process-map/index.ts +10 -0
- package/src/process-map/types.ts +36 -0
- package/src/process-map/validate.ts +135 -0
- package/src/products/__tests__/validate.test.ts +239 -0
- package/src/products/index.ts +9 -0
- package/src/products/types.ts +31 -0
- package/src/products/validate.ts +112 -0
- package/src/repo-validate/__tests__/validate-repo.test.ts +149 -0
- package/src/repo-validate/index.ts +2 -0
- package/src/repo-validate/types.ts +50 -0
- package/src/repo-validate/validate-repo.ts +196 -0
- package/src/requirement/__tests__/example.test.ts +24 -0
- package/src/requirement/__tests__/validate.test.ts +96 -0
- package/src/requirement/index.ts +4 -0
- package/src/requirement/types.ts +35 -0
- package/src/requirement/validate.ts +92 -0
- package/src/scenarios/__tests__/validate.test.ts +209 -0
- package/src/scenarios/index.ts +15 -0
- package/src/scenarios/types.ts +37 -0
- package/src/scenarios/validate.ts +122 -0
- package/src/schema-version.ts +16 -0
- package/src/scope.ts +46 -0
- package/src/stakeholder/__tests__/validate.test.ts +101 -0
- package/src/stakeholder/index.ts +3 -0
- package/src/stakeholder/types.ts +33 -0
- package/src/stakeholder/validate.ts +63 -0
- package/src/target-state/__tests__/validate.test.ts +74 -0
- package/src/target-state/index.ts +2 -0
- package/src/target-state/types.ts +28 -0
- package/src/target-state/validate.ts +67 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themes.ts +237 -0
- package/src/theme/tokens.ts +133 -0
- package/src/typed-id.ts +56 -0
- package/src/validation-types.ts +36 -0
- package/src/webview/__tests__/entry-notations.test.ts +77 -0
- package/src/webview/__tests__/entry.test.ts +64 -0
- package/src/webview/__tests__/render-goals.test.ts +72 -0
- package/src/webview/entry.ts +292 -0
- package/src/webview/render-activities.ts +156 -0
- package/src/webview/render-activity-card.ts +234 -0
- package/src/webview/render-applications.ts +123 -0
- package/src/webview/render-blocks.ts +91 -0
- package/src/webview/render-capability-map.ts +98 -0
- package/src/webview/render-fgca.ts +122 -0
- package/src/webview/render-goals.ts +110 -0
- package/src/webview/render-process-blueprint.ts +207 -0
- package/src/webview/render-process-map.ts +93 -0
- package/src/webview/render-products.ts +91 -0
- package/src/webview/render-scenarios.ts +90 -0
- package/src/webview/render-util.ts +34 -0
- package/src/webview/styles.css +323 -0
- package/src/yaml-normalize.ts +56 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { parseCanonicalFGCA, parseCanonicalFGA } from '../parse-canonical.js';
|
|
6
|
+
const VALID = {
|
|
7
|
+
notation: 'fgca',
|
|
8
|
+
spec_version: '0.1',
|
|
9
|
+
id: 'FGCA-SAMPLE-1',
|
|
10
|
+
name: 'Sample FGCA chain',
|
|
11
|
+
factors: [
|
|
12
|
+
{ id: 'FACTOR-1', name: 'Driver one', type: 'external' },
|
|
13
|
+
],
|
|
14
|
+
goals: [
|
|
15
|
+
{ id: 'GOAL-1', name: 'Outcome one', factors: ['FACTOR-1'] },
|
|
16
|
+
],
|
|
17
|
+
changes: [
|
|
18
|
+
{ id: 'CHANGE-1', name: 'Transformation', goals: ['GOAL-1'] },
|
|
19
|
+
],
|
|
20
|
+
activities: [
|
|
21
|
+
{ id: 'ACTIVITY-1', name: 'Workstream', changes: ['CHANGE-1'] },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
describe('parseCanonicalFGCA', () => {
|
|
25
|
+
it('accepts a valid canonical document', () => {
|
|
26
|
+
const r = parseCanonicalFGCA(VALID);
|
|
27
|
+
expect(r.valid).toBe(true);
|
|
28
|
+
expect(r.errors).toEqual([]);
|
|
29
|
+
expect(r.parsed).toBeDefined();
|
|
30
|
+
expect(r.parsed?.factors).toHaveLength(1);
|
|
31
|
+
expect(r.parsed?.goals).toHaveLength(1);
|
|
32
|
+
expect(r.parsed?.changes).toHaveLength(1);
|
|
33
|
+
expect(r.parsed?.activities).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
it('FGCA-001: rejects non-object input', () => {
|
|
36
|
+
expect(parseCanonicalFGCA(null).errors[0].code).toBe('FGCA-001');
|
|
37
|
+
expect(parseCanonicalFGCA('string').errors[0].code).toBe('FGCA-001');
|
|
38
|
+
});
|
|
39
|
+
it('FGCA-001: rejects wrong notation', () => {
|
|
40
|
+
const r = parseCanonicalFGCA({ ...VALID, notation: 'fga' });
|
|
41
|
+
expect(r.valid).toBe(false);
|
|
42
|
+
expect(r.errors.some((e) => e.code === 'FGCA-001')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('FGCA-002: rejects malformed doc id', () => {
|
|
45
|
+
const r = parseCanonicalFGCA({ ...VALID, id: 'fgca-lowercase-1' });
|
|
46
|
+
expect(r.valid).toBe(false);
|
|
47
|
+
expect(r.errors.some((e) => e.code === 'FGCA-002')).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('FGCA-003: rejects missing name', () => {
|
|
50
|
+
const { name: _, ...rest } = VALID;
|
|
51
|
+
const r = parseCanonicalFGCA(rest);
|
|
52
|
+
expect(r.valid).toBe(false);
|
|
53
|
+
expect(r.errors.some((e) => e.code === 'FGCA-003')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('FGCA-004: rejects missing factors array', () => {
|
|
56
|
+
const { factors: _, ...rest } = VALID;
|
|
57
|
+
const r = parseCanonicalFGCA(rest);
|
|
58
|
+
expect(r.valid).toBe(false);
|
|
59
|
+
expect(r.errors.some((e) => e.code === 'FGCA-004')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('FGCA-006: rejects duplicate IDs within a layer', () => {
|
|
62
|
+
const r = parseCanonicalFGCA({
|
|
63
|
+
...VALID,
|
|
64
|
+
factors: [
|
|
65
|
+
{ id: 'FACTOR-1', name: 'A' },
|
|
66
|
+
{ id: 'FACTOR-1', name: 'B' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
expect(r.valid).toBe(false);
|
|
70
|
+
expect(r.errors.some((e) => e.code === 'FGCA-006')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('FGCA-007: rejects malformed factor id', () => {
|
|
73
|
+
const r = parseCanonicalFGCA({
|
|
74
|
+
...VALID,
|
|
75
|
+
factors: [{ id: 'F-1', name: 'A' }],
|
|
76
|
+
});
|
|
77
|
+
expect(r.valid).toBe(false);
|
|
78
|
+
expect(r.errors.some((e) => e.code === 'FGCA-007')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('FGCA-008: rejects goal.factors[] referencing undefined factor', () => {
|
|
81
|
+
const r = parseCanonicalFGCA({
|
|
82
|
+
...VALID,
|
|
83
|
+
goals: [{ id: 'GOAL-1', name: 'A', factors: ['FACTOR-99'] }],
|
|
84
|
+
});
|
|
85
|
+
expect(r.valid).toBe(false);
|
|
86
|
+
expect(r.errors.some((e) => e.code === 'FGCA-008')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('FGCA-009: rejects change.goals[] referencing undefined goal', () => {
|
|
89
|
+
const r = parseCanonicalFGCA({
|
|
90
|
+
...VALID,
|
|
91
|
+
changes: [{ id: 'CHANGE-1', name: 'A', goals: ['GOAL-99'] }],
|
|
92
|
+
});
|
|
93
|
+
expect(r.valid).toBe(false);
|
|
94
|
+
expect(r.errors.some((e) => e.code === 'FGCA-009')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it('FGCA-010: rejects activity.changes[] referencing undefined change', () => {
|
|
97
|
+
const r = parseCanonicalFGCA({
|
|
98
|
+
...VALID,
|
|
99
|
+
activities: [{ id: 'ACTIVITY-1', name: 'A', changes: ['CHANGE-99'] }],
|
|
100
|
+
});
|
|
101
|
+
expect(r.valid).toBe(false);
|
|
102
|
+
expect(r.errors.some((e) => e.code === 'FGCA-010')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it('FGCA-015: rejects factor.references_constraint with malformed ID', () => {
|
|
105
|
+
const r = parseCanonicalFGCA({
|
|
106
|
+
...VALID,
|
|
107
|
+
factors: [{ id: 'FACTOR-1', name: 'A', references_constraint: ['BAD-1'] }],
|
|
108
|
+
});
|
|
109
|
+
expect(r.valid).toBe(false);
|
|
110
|
+
expect(r.errors.some((e) => e.code === 'FGCA-015')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('accepts factor.references_constraint with valid CONSTRAINT ID', () => {
|
|
113
|
+
const r = parseCanonicalFGCA({
|
|
114
|
+
...VALID,
|
|
115
|
+
factors: [{ id: 'FACTOR-1', name: 'A', references_constraint: ['CONSTRAINT-GDPR-1'] }],
|
|
116
|
+
});
|
|
117
|
+
expect(r.valid).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('populates internal change.activity_ids from canonical activity.changes (reversed direction)', () => {
|
|
120
|
+
const r = parseCanonicalFGCA({
|
|
121
|
+
...VALID,
|
|
122
|
+
changes: [{ id: 'CHANGE-1', name: 'C', goals: ['GOAL-1'] }],
|
|
123
|
+
activities: [
|
|
124
|
+
{ id: 'ACTIVITY-1', name: 'A1', changes: ['CHANGE-1'] },
|
|
125
|
+
{ id: 'ACTIVITY-2', name: 'A2', changes: ['CHANGE-1'] },
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
expect(r.valid).toBe(true);
|
|
129
|
+
expect(r.parsed?.changes[0].activity_ids).toHaveLength(2);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// The "FGCA preview blank / FGA no edges" regression (vkgeorgia/strategy#65,
|
|
133
|
+
// transitrix/methodology#65) was a data defect: the canonical flat shape was
|
|
134
|
+
// not being mapped to the internal cross-ref fields the renderer turns into
|
|
135
|
+
// edges. These assert the edge-driving fields are populated, so the renderer
|
|
136
|
+
// has something to draw — locking the fix.
|
|
137
|
+
describe('parseCanonicalFGCA — edge-driving fields populated', () => {
|
|
138
|
+
it('maps goal.factors → goal.factor[], change.goals → change.goal_id, activity.changes → change.activity_ids', () => {
|
|
139
|
+
const r = parseCanonicalFGCA(VALID);
|
|
140
|
+
expect(r.valid).toBe(true);
|
|
141
|
+
const parsed = r.parsed;
|
|
142
|
+
// factor → goal edge source
|
|
143
|
+
expect(parsed.goals[0].factor).toHaveLength(1);
|
|
144
|
+
// goal → change edge
|
|
145
|
+
expect(parsed.changes[0].goal_id).not.toBe(0);
|
|
146
|
+
// change → activity edge
|
|
147
|
+
expect(parsed.changes[0].activity_ids).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('parseCanonicalFGA', () => {
|
|
151
|
+
const VALID_FGA = {
|
|
152
|
+
notation: 'fga',
|
|
153
|
+
spec_version: '0.1',
|
|
154
|
+
id: 'FGA-SAMPLE-1',
|
|
155
|
+
name: 'Sample FGA chain',
|
|
156
|
+
factors: [{ id: 'FACTOR-1', name: 'Driver' }],
|
|
157
|
+
goals: [{ id: 'GOAL-1', name: 'Outcome', factors: ['FACTOR-1'] }],
|
|
158
|
+
activities: [{ id: 'ACTIVITY-1', name: 'Workstream', goals: ['GOAL-1'] }],
|
|
159
|
+
};
|
|
160
|
+
it('accepts a valid canonical FGA document (no changes layer)', () => {
|
|
161
|
+
const r = parseCanonicalFGA(VALID_FGA);
|
|
162
|
+
expect(r.valid).toBe(true);
|
|
163
|
+
expect(r.errors).toEqual([]);
|
|
164
|
+
expect(r.parsed?.factors).toHaveLength(1);
|
|
165
|
+
expect(r.parsed?.goals).toHaveLength(1);
|
|
166
|
+
expect(r.parsed?.activities).toHaveLength(1);
|
|
167
|
+
});
|
|
168
|
+
it('populates activity.goal_id from activity.goals[] (the FGA edge-driving field)', () => {
|
|
169
|
+
// The "FGA nodes render, no edges" bug: activities had no resolvable
|
|
170
|
+
// goal_id, so the renderer drew no goal → activity edges.
|
|
171
|
+
const r = parseCanonicalFGA(VALID_FGA);
|
|
172
|
+
expect(r.valid).toBe(true);
|
|
173
|
+
expect(r.parsed?.activities[0].goal_id).not.toBeNull();
|
|
174
|
+
expect(r.parsed?.activities[0].goal_id).not.toBe(0);
|
|
175
|
+
});
|
|
176
|
+
it('FGA-001: rejects wrong notation', () => {
|
|
177
|
+
const r = parseCanonicalFGA({ ...VALID_FGA, notation: 'fgca' });
|
|
178
|
+
expect(r.valid).toBe(false);
|
|
179
|
+
expect(r.errors.some((e) => e.code === 'FGA-001')).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
it('FGA-002: rejects malformed doc id', () => {
|
|
182
|
+
const r = parseCanonicalFGA({ ...VALID_FGA, id: 'fga-lower-1' });
|
|
183
|
+
expect(r.valid).toBe(false);
|
|
184
|
+
expect(r.errors.some((e) => e.code === 'FGA-002')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('remaps FGCA layer codes into the FGA registry (e.g. FGA-007 for a bad id)', () => {
|
|
187
|
+
const r = parseCanonicalFGA({ ...VALID_FGA, factors: [{ id: 'F-1', name: 'bad' }] });
|
|
188
|
+
expect(r.valid).toBe(false);
|
|
189
|
+
expect(r.errors.some((e) => e.code === 'FGA-007')).toBe(true);
|
|
190
|
+
// No raw FGCA-prefixed code should leak through the remap.
|
|
191
|
+
expect(r.errors.every((e) => !e.code.startsWith('FGCA-'))).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('parseCanonicalFGCA — example file regression', () => {
|
|
195
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fgca');
|
|
196
|
+
const files = fs.existsSync(EXAMPLES_DIR)
|
|
197
|
+
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
198
|
+
: [];
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
it(`accepts tests/fixtures/notation-corpus/fgca/${file}`, () => {
|
|
201
|
+
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
202
|
+
const parsedYaml = yaml.load(text);
|
|
203
|
+
const r = parseCanonicalFGCA(parsedYaml);
|
|
204
|
+
expect(r.errors).toEqual([]);
|
|
205
|
+
expect(r.valid).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
describe('parseCanonicalFGA — example file regression', () => {
|
|
210
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fga');
|
|
211
|
+
const files = fs.existsSync(EXAMPLES_DIR)
|
|
212
|
+
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
213
|
+
: [];
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
it(`accepts tests/fixtures/notation-corpus/fga/${file} and resolves every activity to a goal`, () => {
|
|
216
|
+
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
217
|
+
const r = parseCanonicalFGA(yaml.load(text));
|
|
218
|
+
expect(r.errors).toEqual([]);
|
|
219
|
+
expect(r.valid).toBe(true);
|
|
220
|
+
// Every activity must carry a goal_id, or the FGA preview draws no edges.
|
|
221
|
+
expect(r.parsed.activities.every((a) => a.goal_id != null && a.goal_id !== 0)).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { layoutFGCAPreview, FGCA_PAD, FGCA_NODE_W, } from '../preview-layout.js';
|
|
3
|
+
const doc = {
|
|
4
|
+
factors: [
|
|
5
|
+
{ id: 1, name: 'F1' },
|
|
6
|
+
{ id: 2, name: 'F2' },
|
|
7
|
+
],
|
|
8
|
+
goals: [
|
|
9
|
+
{ id: 10, name: 'G1', factor: [{ id: 1 }] },
|
|
10
|
+
{ id: 11, name: 'G2', factor: [{ id: 2 }] },
|
|
11
|
+
],
|
|
12
|
+
changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
|
|
13
|
+
activities: [
|
|
14
|
+
{ id: 30, name: 'A1', goal_id: 10 },
|
|
15
|
+
{ id: 31, name: 'A2', goal_id: 11 },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
describe('layoutFGCAPreview', () => {
|
|
19
|
+
it('lays out all four columns in order (FGCA)', () => {
|
|
20
|
+
const layout = layoutFGCAPreview(doc);
|
|
21
|
+
expect(layout.columns.map(c => c.col)).toEqual(['factor', 'goal', 'change', 'activity']);
|
|
22
|
+
// 2 factors + 2 goals + 1 change + 2 activities
|
|
23
|
+
expect(layout.nodes).toHaveLength(7);
|
|
24
|
+
expect(layout.width).toBeGreaterThan(0);
|
|
25
|
+
expect(layout.height).toBeGreaterThan(0);
|
|
26
|
+
});
|
|
27
|
+
it('produces the expected FGCA edge set', () => {
|
|
28
|
+
// F1→G1, F2→G2 (2), G1→C1 (1), C1→A1 (1), G2→A2 direct (1) = 5
|
|
29
|
+
const layout = layoutFGCAPreview(doc);
|
|
30
|
+
expect(layout.edges).toHaveLength(5);
|
|
31
|
+
});
|
|
32
|
+
it('hideChanges (FGA) drops the Changes column and links goals to activities', () => {
|
|
33
|
+
const layout = layoutFGCAPreview(doc, { hideChanges: true });
|
|
34
|
+
expect(layout.columns.map(c => c.col)).toEqual(['factor', 'goal', 'activity']);
|
|
35
|
+
// 2 factors + 2 goals + 2 activities
|
|
36
|
+
expect(layout.nodes).toHaveLength(6);
|
|
37
|
+
// F1→G1, F2→G2 (2), G1→A1, G2→A2 (2) = 4
|
|
38
|
+
expect(layout.edges).toHaveLength(4);
|
|
39
|
+
});
|
|
40
|
+
it('first column sits at the pad origin', () => {
|
|
41
|
+
const layout = layoutFGCAPreview(doc);
|
|
42
|
+
expect(layout.columns[0].x).toBe(FGCA_PAD);
|
|
43
|
+
});
|
|
44
|
+
it('empty options reproduce the default layout', () => {
|
|
45
|
+
const a = layoutFGCAPreview(doc);
|
|
46
|
+
const b = layoutFGCAPreview(doc, {});
|
|
47
|
+
expect(b.nodes.map(n => `${n.id}:${n.x},${n.y}`)).toEqual(a.nodes.map(n => `${n.id}:${n.x},${n.y}`));
|
|
48
|
+
});
|
|
49
|
+
it('larger colGap widens the column step', () => {
|
|
50
|
+
const stepOf = (colGap) => {
|
|
51
|
+
const cols = layoutFGCAPreview(doc, { colGap }).columns;
|
|
52
|
+
return cols[1].x - cols[0].x;
|
|
53
|
+
};
|
|
54
|
+
expect(stepOf(80)).toBe(FGCA_NODE_W + 80);
|
|
55
|
+
expect(stepOf(240)).toBe(FGCA_NODE_W + 240);
|
|
56
|
+
expect(stepOf(240)).toBeGreaterThan(stepOf(80));
|
|
57
|
+
});
|
|
58
|
+
it('larger rowGap increases the gap between stacked nodes', () => {
|
|
59
|
+
const gapOf = (rowGap) => {
|
|
60
|
+
const factors = layoutFGCAPreview(doc, { rowGap }).nodes.filter(n => n.col === 'factor');
|
|
61
|
+
return factors[1].y - factors[0].y;
|
|
62
|
+
};
|
|
63
|
+
expect(gapOf(120)).toBeGreaterThan(gapOf(20));
|
|
64
|
+
});
|
|
65
|
+
// vkgeorgia/strategy#77 — scope filtering. FGCA goals are flat, so 'root'
|
|
66
|
+
// selects the single matching goal plus the factors/changes/activities that
|
|
67
|
+
// touch it.
|
|
68
|
+
describe('scope', () => {
|
|
69
|
+
const idsOf = (layout) => new Set(layout.nodes.map(n => n.id));
|
|
70
|
+
it("mode 'root' keeps only the root goal and what connects to it", () => {
|
|
71
|
+
// root 10 → factor 1 (referenced by G10), change 20 (goal_id 10),
|
|
72
|
+
// activity 30 (via change 20). G11/F2/A31 are dropped.
|
|
73
|
+
const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '10' } });
|
|
74
|
+
expect(idsOf(layout)).toEqual(new Set(['factor_1', 'goal_10', 'change_20', 'activity_30']));
|
|
75
|
+
});
|
|
76
|
+
it("mode 'root' filters factors and activities to those touching the visible goal", () => {
|
|
77
|
+
// root 11 → factor 2, activity 31 (direct goal link); no change touches G11.
|
|
78
|
+
const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '11' } });
|
|
79
|
+
expect(idsOf(layout)).toEqual(new Set(['factor_2', 'goal_11', 'activity_31']));
|
|
80
|
+
// F1, A30, C20 (which only touch the hidden G10) are excluded.
|
|
81
|
+
expect(layout.nodes.some(n => n.id === 'factor_1')).toBe(false);
|
|
82
|
+
expect(layout.nodes.some(n => n.id === 'activity_30')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it("mode 'level' trims goals above the cap and their connections", () => {
|
|
85
|
+
const leveled = {
|
|
86
|
+
factors: [{ id: 1, name: 'F1' }, { id: 2, name: 'F2' }],
|
|
87
|
+
goals: [
|
|
88
|
+
{ id: 10, name: 'G1', level: 0, factor: [{ id: 1 }] },
|
|
89
|
+
{ id: 11, name: 'G2', level: 1, factor: [{ id: 2 }] },
|
|
90
|
+
],
|
|
91
|
+
changes: [],
|
|
92
|
+
activities: [
|
|
93
|
+
{ id: 30, name: 'A1', goal_id: 10 },
|
|
94
|
+
{ id: 31, name: 'A2', goal_id: 11 },
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
const layout = layoutFGCAPreview(leveled, { scope: { mode: 'level', maxLevel: 0 } });
|
|
98
|
+
expect(idsOf(layout)).toEqual(new Set(['factor_1', 'goal_10', 'activity_30']));
|
|
99
|
+
});
|
|
100
|
+
it("mode 'root' returns an empty layout when the root is absent", () => {
|
|
101
|
+
const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '999' } });
|
|
102
|
+
expect(layout.nodes).toHaveLength(0);
|
|
103
|
+
expect(layout.edges).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateFGCADoc } from '../validate.js';
|
|
3
|
+
const VALID_DOC = {
|
|
4
|
+
notation: 'fgca',
|
|
5
|
+
factors: [{ id: 1, name: 'Market pressure' }],
|
|
6
|
+
goals: [{ id: 1, name: 'Grow revenue', factor: [{ id: 1 }] }],
|
|
7
|
+
changes: [{ id: 1, name: 'Launch product', goal_id: 1, activity_ids: [1] }],
|
|
8
|
+
activities: [{ id: 1, name: 'Market research', goal_id: 1 }],
|
|
9
|
+
};
|
|
10
|
+
describe('validateFGCADoc', () => {
|
|
11
|
+
it('accepts a valid document', () => {
|
|
12
|
+
const result = validateFGCADoc(VALID_DOC);
|
|
13
|
+
expect(result.valid).toBe(true);
|
|
14
|
+
expect(result.errors).toHaveLength(0);
|
|
15
|
+
});
|
|
16
|
+
it('rejects non-object input', () => {
|
|
17
|
+
expect(validateFGCADoc('string').valid).toBe(false);
|
|
18
|
+
expect(validateFGCADoc(null).valid).toBe(false);
|
|
19
|
+
expect(validateFGCADoc(42).valid).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('errors on missing notation field', () => {
|
|
22
|
+
const { notation: _, ...without } = VALID_DOC;
|
|
23
|
+
const result = validateFGCADoc(without);
|
|
24
|
+
expect(result.valid).toBe(false);
|
|
25
|
+
expect(result.errors[0].code).toBe('MISSING_NOTATION');
|
|
26
|
+
});
|
|
27
|
+
it('errors on wrong notation value', () => {
|
|
28
|
+
const result = validateFGCADoc({ ...VALID_DOC, notation: 'goals' });
|
|
29
|
+
expect(result.valid).toBe(false);
|
|
30
|
+
expect(result.errors[0].code).toBe('WRONG_NOTATION');
|
|
31
|
+
});
|
|
32
|
+
it('errors when factors array is missing', () => {
|
|
33
|
+
const { factors: _, ...without } = VALID_DOC;
|
|
34
|
+
const result = validateFGCADoc({ ...without, notation: 'fgca' });
|
|
35
|
+
expect(result.valid).toBe(false);
|
|
36
|
+
expect(result.errors.some(e => e.path === 'factors')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('errors when goals array is missing', () => {
|
|
39
|
+
const { goals: _, ...without } = VALID_DOC;
|
|
40
|
+
const result = validateFGCADoc({ ...without, notation: 'fgca' });
|
|
41
|
+
expect(result.valid).toBe(false);
|
|
42
|
+
expect(result.errors.some(e => e.path === 'goals')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('errors when changes array is missing', () => {
|
|
45
|
+
const { changes: _, ...without } = VALID_DOC;
|
|
46
|
+
const result = validateFGCADoc({ ...without, notation: 'fgca' });
|
|
47
|
+
expect(result.valid).toBe(false);
|
|
48
|
+
expect(result.errors.some(e => e.path === 'changes')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('errors when activities array is missing', () => {
|
|
51
|
+
const { activities: _, ...without } = VALID_DOC;
|
|
52
|
+
const result = validateFGCADoc({ ...without, notation: 'fgca' });
|
|
53
|
+
expect(result.valid).toBe(false);
|
|
54
|
+
expect(result.errors.some(e => e.path === 'activities')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('errors on empty factor name', () => {
|
|
57
|
+
const result = validateFGCADoc({ ...VALID_DOC, factors: [{ id: 1, name: '' }] });
|
|
58
|
+
expect(result.valid).toBe(false);
|
|
59
|
+
expect(result.errors.some(e => e.code === 'EMPTY_NAME')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('errors on empty goal name', () => {
|
|
62
|
+
const result = validateFGCADoc({ ...VALID_DOC, goals: [{ id: 1, name: ' ', factor: [] }] });
|
|
63
|
+
expect(result.valid).toBe(false);
|
|
64
|
+
expect(result.errors.some(e => e.code === 'EMPTY_NAME')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it('warns on broken factor reference in goal', () => {
|
|
67
|
+
const result = validateFGCADoc({
|
|
68
|
+
...VALID_DOC,
|
|
69
|
+
goals: [{ id: 1, name: 'Goal', factor: [{ id: 99 }] }],
|
|
70
|
+
});
|
|
71
|
+
expect(result.valid).toBe(true);
|
|
72
|
+
expect(result.warnings.some(w => w.code === 'BROKEN_REF' && w.message.includes('factor 99'))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it('warns on broken goal reference in change', () => {
|
|
75
|
+
const result = validateFGCADoc({
|
|
76
|
+
...VALID_DOC,
|
|
77
|
+
changes: [{ id: 1, name: 'Change', goal_id: 99, activity_ids: [] }],
|
|
78
|
+
});
|
|
79
|
+
expect(result.valid).toBe(true);
|
|
80
|
+
expect(result.warnings.some(w => w.code === 'BROKEN_REF' && w.message.includes('goal 99'))).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('warns on broken activity reference in change', () => {
|
|
83
|
+
const result = validateFGCADoc({
|
|
84
|
+
...VALID_DOC,
|
|
85
|
+
changes: [{ id: 1, name: 'Change', goal_id: 1, activity_ids: [99] }],
|
|
86
|
+
});
|
|
87
|
+
expect(result.valid).toBe(true);
|
|
88
|
+
expect(result.warnings.some(w => w.code === 'BROKEN_REF' && w.message.includes('activity 99'))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('accepts a document with empty arrays', () => {
|
|
91
|
+
const result = validateFGCADoc({
|
|
92
|
+
notation: 'fgca',
|
|
93
|
+
factors: [],
|
|
94
|
+
goals: [],
|
|
95
|
+
changes: [],
|
|
96
|
+
activities: [],
|
|
97
|
+
});
|
|
98
|
+
expect(result.valid).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('accepts optional spec_version field', () => {
|
|
101
|
+
const result = validateFGCADoc({ ...VALID_DOC, spec_version: '0.1' });
|
|
102
|
+
expect(result.valid).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
// Pre-release blocker regression (orchestrator review 2026-05-21).
|
|
105
|
+
it('[blocker] tolerates a null element in goals[] without throwing', () => {
|
|
106
|
+
const r = validateFGCADoc({ ...VALID_DOC, goals: [null] });
|
|
107
|
+
expect(r.valid).toBe(false);
|
|
108
|
+
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it('[blocker] tolerates a string element in factors[] without throwing', () => {
|
|
111
|
+
const r = validateFGCADoc({ ...VALID_DOC, factors: ['x'] });
|
|
112
|
+
expect(r.valid).toBe(false);
|
|
113
|
+
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
it('[blocker] tolerates a null element in changes[] without throwing', () => {
|
|
116
|
+
const r = validateFGCADoc({ ...VALID_DOC, changes: [null] });
|
|
117
|
+
expect(r.valid).toBe(false);
|
|
118
|
+
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it('[blocker] tolerates a null element in activities[] without throwing', () => {
|
|
121
|
+
const r = validateFGCADoc({ ...VALID_DOC, activities: [null] });
|
|
122
|
+
expect(r.valid).toBe(false);
|
|
123
|
+
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FGCAPreviewDoc } from './preview-layout.js';
|
|
2
|
+
export type ChainColumn = 'factor' | 'goal' | 'change' | 'activity';
|
|
3
|
+
export interface ChainCell {
|
|
4
|
+
/** Column-qualified identity (e.g. "goal:3") — stable, unique across columns. */
|
|
5
|
+
key: string;
|
|
6
|
+
/** Display text — the element name, falling back to its id. */
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ChainTableCell {
|
|
10
|
+
/** The element to render, or null for a gap in the chain (empty cell). */
|
|
11
|
+
cell: ChainCell | null;
|
|
12
|
+
/** Vertical span — how many rows this cell covers. */
|
|
13
|
+
rowSpan: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ChainTable {
|
|
16
|
+
/** Columns left→right: FGA omits 'change'. */
|
|
17
|
+
columns: ChainColumn[];
|
|
18
|
+
/**
|
|
19
|
+
* Grid of rendered cells. `rows[r][c]` is the cell to emit at (r, c), or null
|
|
20
|
+
* when that position is covered by a `rowSpan` from a row above (emit no
|
|
21
|
+
* `<td>`). Each row corresponds to one path through the chain.
|
|
22
|
+
*/
|
|
23
|
+
rows: Array<Array<ChainTableCell | null>>;
|
|
24
|
+
}
|
|
25
|
+
export interface ChainTableOptions {
|
|
26
|
+
/** FGA hides the Change column (Goal links straight to Activity). */
|
|
27
|
+
hideChanges?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Flattens an FGCA/FGA doc into a chain table with rowspan-merged parent cells.
|
|
31
|
+
*
|
|
32
|
+
* Rows are enumerated in document order (deterministic). A parent that links to
|
|
33
|
+
* several children spans those children's rows; an element with no downstream
|
|
34
|
+
* link still appears, with empty cells to its right.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildChainTable(doc: FGCAPreviewDoc, options?: ChainTableOptions): ChainTable;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Chain-table model for the FGCA / FGA previews (vkgeorgia/strategy#137).
|
|
2
|
+
//
|
|
3
|
+
// The tree/chain preview (`layoutFGCAPreview`) shows Factor → Goal → Change →
|
|
4
|
+
// Activity as columns of nodes joined by edges. This module produces the
|
|
5
|
+
// *tabular* alternative: the same chain flattened into rows, with shared
|
|
6
|
+
// parents collapsed into vertically-merged (rowspan) cells.
|
|
7
|
+
//
|
|
8
|
+
// It is the pure, unit-testable half of the feature — no DOM, no vscode. The
|
|
9
|
+
// Studio preview renders the returned model as an HTML `<table>`.
|
|
10
|
+
//
|
|
11
|
+
// Link semantics mirror `layoutFGCAPreview` exactly (the authoritative source):
|
|
12
|
+
// - F → G via `goal.factor[]`
|
|
13
|
+
// - G → C via `change.goal_id` (FGCA only)
|
|
14
|
+
// - C → A via `change.activity_ids[]` (FGCA only)
|
|
15
|
+
// - G → A directly via `activity.goal_id` for activities not covered by any
|
|
16
|
+
// change (FGCA) / for every activity (FGA, no Change column)
|
|
17
|
+
//
|
|
18
|
+
// Broken references are validation *warnings*, not errors, so a rendered doc
|
|
19
|
+
// can contain a goal with a missing factor, a change with a missing goal, or an
|
|
20
|
+
// unlinked activity. Every element still appears in the table — rooted where it
|
|
21
|
+
// links, or as a trailing orphan row with empty cells to its left — so the
|
|
22
|
+
// table mirrors the document the same way the tree does.
|
|
23
|
+
function cellOf(col, id, name) {
|
|
24
|
+
const label = name && name.trim() ? name : String(id);
|
|
25
|
+
return { key: `${col}:${id}`, label };
|
|
26
|
+
}
|
|
27
|
+
/** Identity comparison for the rowspan grouping; both-null counts as equal. */
|
|
28
|
+
function sameCell(a, b) {
|
|
29
|
+
if (a === null || b === null)
|
|
30
|
+
return a === b;
|
|
31
|
+
return a.key === b.key;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Flattens an FGCA/FGA doc into a chain table with rowspan-merged parent cells.
|
|
35
|
+
*
|
|
36
|
+
* Rows are enumerated in document order (deterministic). A parent that links to
|
|
37
|
+
* several children spans those children's rows; an element with no downstream
|
|
38
|
+
* link still appears, with empty cells to its right.
|
|
39
|
+
*/
|
|
40
|
+
export function buildChainTable(doc, options = {}) {
|
|
41
|
+
const { hideChanges = false } = options;
|
|
42
|
+
const columns = hideChanges
|
|
43
|
+
? ['factor', 'goal', 'activity']
|
|
44
|
+
: ['factor', 'goal', 'change', 'activity'];
|
|
45
|
+
const changes = doc.changes ?? [];
|
|
46
|
+
const factorIds = new Set(doc.factors.map(f => f.id));
|
|
47
|
+
const activityById = new Map(doc.activities.map(a => [a.id, a]));
|
|
48
|
+
// An activity is "covered" by a change when any change lists it — those render
|
|
49
|
+
// under their change, not directly under the goal (mirrors the tree).
|
|
50
|
+
const coveredActivityIds = new Set(changes.flatMap(c => c.activity_ids));
|
|
51
|
+
const emittedGoals = new Set();
|
|
52
|
+
const emittedChanges = new Set();
|
|
53
|
+
const emittedActivities = new Set();
|
|
54
|
+
// Each path is an array (length = columns.length) of ChainCell | null.
|
|
55
|
+
const paths = [];
|
|
56
|
+
const pad = (cells) => {
|
|
57
|
+
// Defensive: ensure every path matches the column count.
|
|
58
|
+
while (cells.length < columns.length)
|
|
59
|
+
cells.push(null);
|
|
60
|
+
return cells;
|
|
61
|
+
};
|
|
62
|
+
const emitGoal = (factorCell, goal) => {
|
|
63
|
+
emittedGoals.add(goal.id);
|
|
64
|
+
const goalCell = cellOf('goal', goal.id, goal.name);
|
|
65
|
+
if (hideChanges) {
|
|
66
|
+
const acts = doc.activities.filter(a => a.goal_id != null && a.goal_id === goal.id);
|
|
67
|
+
if (acts.length === 0) {
|
|
68
|
+
paths.push(pad([factorCell, goalCell]));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const a of acts) {
|
|
72
|
+
emittedActivities.add(a.id);
|
|
73
|
+
paths.push([factorCell, goalCell, cellOf('activity', a.id, a.name)]);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const goalChanges = changes.filter(c => c.goal_id === goal.id);
|
|
78
|
+
const directActs = doc.activities.filter(a => a.goal_id != null && a.goal_id === goal.id && !coveredActivityIds.has(a.id));
|
|
79
|
+
if (goalChanges.length === 0 && directActs.length === 0) {
|
|
80
|
+
paths.push([factorCell, goalCell, null, null]);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const c of goalChanges) {
|
|
84
|
+
emittedChanges.add(c.id);
|
|
85
|
+
const changeCell = cellOf('change', c.id, c.name);
|
|
86
|
+
const acts = c.activity_ids.map(id => activityById.get(id)).filter((a) => a != null);
|
|
87
|
+
if (acts.length === 0) {
|
|
88
|
+
paths.push([factorCell, goalCell, changeCell, null]);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const a of acts) {
|
|
92
|
+
emittedActivities.add(a.id);
|
|
93
|
+
paths.push([factorCell, goalCell, changeCell, cellOf('activity', a.id, a.name)]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Change-less paths: activities bound straight to the goal (gap in Change).
|
|
97
|
+
for (const a of directActs) {
|
|
98
|
+
emittedActivities.add(a.id);
|
|
99
|
+
paths.push([factorCell, goalCell, null, cellOf('activity', a.id, a.name)]);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Phase 1 — factor-rooted paths.
|
|
103
|
+
for (const f of doc.factors) {
|
|
104
|
+
const factorCell = cellOf('factor', f.id, f.name);
|
|
105
|
+
const goalsOfFactor = doc.goals.filter(g => (g.factor ?? []).some(ref => ref.id === f.id));
|
|
106
|
+
if (goalsOfFactor.length === 0) {
|
|
107
|
+
paths.push(pad([factorCell]));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const g of goalsOfFactor)
|
|
111
|
+
emitGoal(factorCell, g);
|
|
112
|
+
}
|
|
113
|
+
// Phase 2 — goals with no existing factor (orphan or broken-ref): empty Factor.
|
|
114
|
+
for (const g of doc.goals) {
|
|
115
|
+
if (emittedGoals.has(g.id))
|
|
116
|
+
continue;
|
|
117
|
+
const hasExistingFactor = (g.factor ?? []).some(ref => factorIds.has(ref.id));
|
|
118
|
+
if (hasExistingFactor)
|
|
119
|
+
continue; // emitted under its factor in phase 1
|
|
120
|
+
emitGoal(null, g);
|
|
121
|
+
}
|
|
122
|
+
// Phase 3 — changes whose goal is missing (FGCA only): empty Factor + Goal.
|
|
123
|
+
if (!hideChanges) {
|
|
124
|
+
for (const c of changes) {
|
|
125
|
+
if (emittedChanges.has(c.id))
|
|
126
|
+
continue;
|
|
127
|
+
emittedChanges.add(c.id);
|
|
128
|
+
const changeCell = cellOf('change', c.id, c.name);
|
|
129
|
+
const acts = c.activity_ids.map(id => activityById.get(id)).filter((a) => a != null);
|
|
130
|
+
if (acts.length === 0) {
|
|
131
|
+
paths.push([null, null, changeCell, null]);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const a of acts) {
|
|
135
|
+
emittedActivities.add(a.id);
|
|
136
|
+
paths.push([null, null, changeCell, cellOf('activity', a.id, a.name)]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Phase 4 — activities linked to nothing: empty cells to their left.
|
|
141
|
+
for (const a of doc.activities) {
|
|
142
|
+
if (emittedActivities.has(a.id))
|
|
143
|
+
continue;
|
|
144
|
+
const row = columns.map(() => null);
|
|
145
|
+
row[columns.length - 1] = cellOf('activity', a.id, a.name);
|
|
146
|
+
paths.push(row);
|
|
147
|
+
}
|
|
148
|
+
return { columns, rows: computeRowSpans(paths, columns.length) };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Turns a flat list of equal-length paths into a render grid. For each column,
|
|
152
|
+
* a cell spans the run of consecutive rows that share identical values in every
|
|
153
|
+
* column from 0 through that column (so a child only merges within one parent).
|
|
154
|
+
*/
|
|
155
|
+
function computeRowSpans(paths, colCount) {
|
|
156
|
+
const n = paths.length;
|
|
157
|
+
const grid = paths.map(() => new Array(colCount).fill(null));
|
|
158
|
+
for (let c = 0; c < colCount; c++) {
|
|
159
|
+
let r = 0;
|
|
160
|
+
while (r < n) {
|
|
161
|
+
let end = r + 1;
|
|
162
|
+
while (end < n && prefixEqual(paths[end], paths[r], c))
|
|
163
|
+
end++;
|
|
164
|
+
grid[r][c] = { cell: paths[r][c], rowSpan: end - r };
|
|
165
|
+
r = end;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return grid;
|
|
169
|
+
}
|
|
170
|
+
/** True when two paths agree on every column 0..c (inclusive). */
|
|
171
|
+
function prefixEqual(a, b, c) {
|
|
172
|
+
for (let i = 0; i <= c; i++) {
|
|
173
|
+
if (!sameCell(a[i], b[i]))
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|