@atlashub/smartstack-cli 3.39.0 → 3.40.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/.documentation/apex.html +644 -644
- package/.documentation/css/styles.css +2320 -2320
- package/.documentation/init.html +1377 -1377
- package/.documentation/js/app.js +780 -780
- package/.documentation/prd-json-v2.0.0.md +396 -396
- package/.documentation/testing-ba-e2e.md +462 -462
- package/config/default-config.json +95 -95
- package/config/mcp-defaults.json +62 -62
- package/config/settings.json +53 -53
- package/config/settings.local.example.json +16 -16
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +6 -4
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +115 -115
- package/scripts/extract-api-endpoints.ts +325 -325
- package/scripts/extract-business-rules.ts +440 -440
- package/scripts/generate-doc-with-mock-ui.ts +804 -804
- package/scripts/health-check.sh +168 -168
- package/scripts/postinstall.js +18 -18
- package/templates/agents/action.md +37 -37
- package/templates/agents/ba-reader.md +378 -378
- package/templates/agents/ba-writer.md +861 -861
- package/templates/agents/code-reviewer.md +163 -163
- package/templates/agents/db-reader.md +149 -149
- package/templates/agents/docs-context-reader.md +143 -143
- package/templates/agents/docs-sync-checker.md +122 -122
- package/templates/agents/efcore/conflicts.md +84 -84
- package/templates/agents/efcore/db-deploy.md +74 -74
- package/templates/agents/efcore/db-reset.md +85 -85
- package/templates/agents/efcore/db-seed.md +61 -61
- package/templates/agents/efcore/db-status.md +86 -86
- package/templates/agents/efcore/migration.md +186 -186
- package/templates/agents/efcore/rebase-snapshot.md +108 -108
- package/templates/agents/efcore/scan.md +92 -92
- package/templates/agents/efcore/squash.md +161 -161
- package/templates/agents/explore-codebase.md +66 -66
- package/templates/agents/explore-docs.md +98 -98
- package/templates/agents/fix-grammar.md +50 -50
- package/templates/agents/gitflow/abort.md +45 -45
- package/templates/agents/gitflow/cleanup.md +96 -96
- package/templates/agents/gitflow/commit.md +236 -236
- package/templates/agents/gitflow/exec.md +48 -48
- package/templates/agents/gitflow/finish.md +146 -146
- package/templates/agents/gitflow/init-clone.md +199 -199
- package/templates/agents/gitflow/init-detect.md +137 -137
- package/templates/agents/gitflow/init-validate.md +225 -225
- package/templates/agents/gitflow/init.md +340 -340
- package/templates/agents/gitflow/merge.md +145 -145
- package/templates/agents/gitflow/plan.md +42 -42
- package/templates/agents/gitflow/pr.md +191 -191
- package/templates/agents/gitflow/review.md +49 -49
- package/templates/agents/gitflow/start.md +147 -147
- package/templates/agents/gitflow/status.md +95 -95
- package/templates/agents/mcp-healthcheck.md +163 -163
- package/templates/agents/snipper.md +37 -37
- package/templates/agents/websearch.md +46 -46
- package/templates/hooks/appsettings-guard.sh +76 -76
- package/templates/hooks/docs-drift-check.md +96 -96
- package/templates/hooks/ef-migration-check.md +139 -139
- package/templates/hooks/hooks.json +58 -58
- package/templates/hooks/mcp-check.md +64 -64
- package/templates/hooks/ralph-mcp-logger.sh +46 -46
- package/templates/hooks/ralph-session-end.sh +69 -69
- package/templates/hooks/stop-hook.sh +177 -177
- package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
- package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
- package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
- package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
- package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
- package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
- package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
- package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
- package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
- package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
- package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
- package/templates/project/DependencyInjection.Application.cs.template +25 -25
- package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
- package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
- package/templates/project/ExampleEntity.cs.template +116 -116
- package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
- package/templates/project/ExampleService.cs.template +146 -146
- package/templates/project/ExtensionsDbContext.cs.template +41 -41
- package/templates/project/IExtensionsDbContext.cs.template +22 -22
- package/templates/project/Program.cs.template +47 -47
- package/templates/project/README.md +79 -79
- package/templates/project/api.ts.template +12 -12
- package/templates/project/appsettings.json.template +170 -170
- package/templates/project/claude-settings.json.template +5 -5
- package/templates/project/test-frontend/msw/handlers.ts +58 -58
- package/templates/project/test-frontend/msw/server.ts +25 -25
- package/templates/project/test-frontend/setup.ts +16 -16
- package/templates/project/test-frontend/test-utils.tsx +59 -59
- package/templates/project/test-frontend/vitest.config.ts +31 -31
- package/templates/ralph/README.md +93 -93
- package/templates/ralph/ralph.config.yaml +113 -113
- package/templates/scripts/setup-ralph-loop.sh +173 -173
- package/templates/skills/_resources/config-safety.md +61 -61
- package/templates/skills/_resources/context-digest-template.md +53 -53
- package/templates/skills/_resources/doc-context-cache.md +60 -60
- package/templates/skills/_resources/docs-manifest-schema.md +155 -155
- package/templates/skills/_resources/formatting-guide.md +124 -124
- package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
- package/templates/skills/_shared.md +228 -228
- package/templates/skills/admin/SKILL.md +48 -48
- package/templates/skills/ai-prompt/SKILL.md +107 -107
- package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
- package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
- package/templates/skills/apex/SKILL.md +168 -168
- package/templates/skills/apex/_shared.md +141 -141
- package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
- package/templates/skills/apex/references/analysis-methods.md +141 -141
- package/templates/skills/apex/references/challenge-questions.md +145 -145
- package/templates/skills/apex/references/code-generation.md +412 -412
- package/templates/skills/apex/references/core-seed-data.md +1437 -1437
- package/templates/skills/apex/references/error-classification.md +144 -144
- package/templates/skills/apex/references/examine-build-validation.md +82 -82
- package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
- package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
- package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
- package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
- package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
- package/templates/skills/apex/references/post-checks.md +1584 -1584
- package/templates/skills/apex/references/smartstack-api.md +1053 -1053
- package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
- package/templates/skills/apex/references/smartstack-layers.md +402 -402
- package/templates/skills/apex/steps/step-00-init.md +307 -307
- package/templates/skills/apex/steps/step-01-analyze.md +165 -165
- package/templates/skills/apex/steps/step-02-plan.md +144 -144
- package/templates/skills/apex/steps/step-03-execute.md +328 -328
- package/templates/skills/apex/steps/step-04-examine.md +263 -263
- package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
- package/templates/skills/apex/steps/step-06-resolve.md +101 -101
- package/templates/skills/apex/steps/step-07-tests.md +238 -238
- package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
- package/templates/skills/application/SKILL.md +4 -4
- package/templates/skills/application/references/application-roles-template.md +227 -227
- package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
- package/templates/skills/application/references/backend-entity-seeding.md +72 -72
- package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
- package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
- package/templates/skills/application/references/backend-verification.md +88 -88
- package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
- package/templates/skills/application/references/frontend-route-naming.md +117 -117
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
- package/templates/skills/application/references/frontend-verification.md +156 -156
- package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
- package/templates/skills/application/references/provider-template.md +177 -177
- package/templates/skills/application/references/roles-client-project-handling.md +55 -55
- package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
- package/templates/skills/application/references/test-coverage-requirements.md +213 -213
- package/templates/skills/application/references/test-frontend.md +73 -73
- package/templates/skills/application/references/test-prerequisites.md +72 -72
- package/templates/skills/application/steps/step-05-frontend.md +176 -176
- package/templates/skills/application/steps/step-06-migration.md +193 -193
- package/templates/skills/application/steps/step-07-tests.md +356 -356
- package/templates/skills/application/steps/step-08-documentation.md +137 -137
- package/templates/skills/application/templates-backend.md +463 -463
- package/templates/skills/application/templates-frontend.md +685 -685
- package/templates/skills/application/templates-i18n.md +520 -520
- package/templates/skills/application/templates-seed.md +1096 -1096
- package/templates/skills/business-analyse/SKILL.md +327 -327
- package/templates/skills/business-analyse/_architecture.md +123 -123
- package/templates/skills/business-analyse/_elicitation.md +206 -206
- package/templates/skills/business-analyse/_module-loop.md +115 -115
- package/templates/skills/business-analyse/_shared.md +383 -383
- package/templates/skills/business-analyse/_suggestions.md +34 -34
- package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
- package/templates/skills/business-analyse/html/build-html.js +77 -77
- package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
- package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
- package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
- package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
- package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
- package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
- package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
- package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
- package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
- package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
- package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
- package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
- package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
- package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
- package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
- package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
- package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
- package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
- package/templates/skills/business-analyse/html/src/template.html +516 -516
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
- package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
- package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
- package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
- package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
- package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
- package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
- package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
- package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
- package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
- package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
- package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
- package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
- package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
- package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
- package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
- package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
- package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
- package/templates/skills/business-analyse/questionnaire.md +337 -337
- package/templates/skills/business-analyse/react/application-viewer.md +242 -242
- package/templates/skills/business-analyse/react/components.md +551 -551
- package/templates/skills/business-analyse/react/i18n-template.md +306 -306
- package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
- package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
- package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
- package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
- package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
- package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
- package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
- package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
- package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
- package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
- package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
- package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
- package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
- package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
- package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
- package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
- package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
- package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
- package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
- package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
- package/templates/skills/business-analyse/references/prd-generation.md +258 -258
- package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
- package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
- package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
- package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
- package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
- package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
- package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
- package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
- package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
- package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
- package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
- package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
- package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
- package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
- package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
- package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
- package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
- package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
- package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
- package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
- package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
- package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
- package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
- package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
- package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
- package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
- package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
- package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
- package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
- package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
- package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
- package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
- package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
- package/templates/skills/business-analyse/templates-frd.md +476 -476
- package/templates/skills/business-analyse/templates-react.md +574 -574
- package/templates/skills/cc-agent/SKILL.md +129 -129
- package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
- package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
- package/templates/skills/cc-agent/references/permission-modes.md +102 -102
- package/templates/skills/cc-agent/references/tools-reference.md +144 -144
- package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
- package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
- package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
- package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
- package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
- package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
- package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
- package/templates/skills/cc-audit/SKILL.md +108 -108
- package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
- package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
- package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
- package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
- package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
- package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
- package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
- package/templates/skills/cc-skill/SKILL.md +134 -134
- package/templates/skills/cc-skill/references/best-practices.md +167 -167
- package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
- package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
- package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
- package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
- package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
- package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
- package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
- package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
- package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
- package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
- package/templates/skills/cc-skill/templates/step-template.md +82 -82
- package/templates/skills/check-version/SKILL.md +196 -196
- package/templates/skills/controller/SKILL.md +162 -162
- package/templates/skills/controller/postman-templates.md +614 -614
- package/templates/skills/controller/references/controller-code-templates.md +159 -159
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
- package/templates/skills/controller/references/permission-sync-templates.md +149 -149
- package/templates/skills/controller/steps/step-00-init.md +193 -191
- package/templates/skills/controller/steps/step-01-analyze.md +146 -146
- package/templates/skills/controller/steps/step-02-plan.md +176 -176
- package/templates/skills/controller/steps/step-03-generate.md +189 -189
- package/templates/skills/controller/steps/step-04-perms.md +80 -80
- package/templates/skills/controller/steps/step-05-validate.md +107 -107
- package/templates/skills/controller/templates.md +1555 -1555
- package/templates/skills/debug/SKILL.md +70 -70
- package/templates/skills/debug/references/team-protocol.md +232 -232
- package/templates/skills/debug/steps/step-00-init.md +57 -57
- package/templates/skills/debug/steps/step-01-analyze.md +219 -219
- package/templates/skills/debug/steps/step-02-resolve.md +85 -85
- package/templates/skills/documentation/SKILL.md +132 -132
- package/templates/skills/documentation/data-schema.md +227 -227
- package/templates/skills/documentation/steps/step-00-init.md +70 -70
- package/templates/skills/documentation/steps/step-01-scan.md +113 -113
- package/templates/skills/documentation/steps/step-02-generate.md +231 -231
- package/templates/skills/documentation/steps/step-03-validate.md +251 -238
- package/templates/skills/documentation/templates.md +662 -663
- package/templates/skills/efcore/SKILL.md +167 -167
- package/templates/skills/efcore/references/both-contexts.md +32 -32
- package/templates/skills/efcore/references/database-operations.md +67 -67
- package/templates/skills/efcore/references/destructive-operations.md +38 -38
- package/templates/skills/efcore/references/reset-operations.md +81 -81
- package/templates/skills/efcore/references/seed-methods.md +86 -86
- package/templates/skills/efcore/references/shared-init-functions.md +250 -250
- package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
- package/templates/skills/efcore/references/troubleshooting.md +81 -81
- package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
- package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
- package/templates/skills/efcore/steps/db/step-reset.md +186 -186
- package/templates/skills/efcore/steps/db/step-seed.md +166 -166
- package/templates/skills/efcore/steps/db/step-status.md +173 -173
- package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
- package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
- package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
- package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
- package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
- package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
- package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
- package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
- package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
- package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
- package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
- package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
- package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
- package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
- package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
- package/templates/skills/explore/SKILL.md +98 -98
- package/templates/skills/feature-full/SKILL.md +111 -111
- package/templates/skills/feature-full/steps/step-00-init.md +57 -57
- package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
- package/templates/skills/gitflow/SKILL.md +377 -377
- package/templates/skills/gitflow/_shared.md +620 -620
- package/templates/skills/gitflow/phases/abort.md +189 -189
- package/templates/skills/gitflow/phases/cleanup.md +234 -234
- package/templates/skills/gitflow/phases/status.md +192 -192
- package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
- package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
- package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
- package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
- package/templates/skills/gitflow/references/init-config-template.md +135 -135
- package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
- package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
- package/templates/skills/gitflow/references/init-questions.md +185 -185
- package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
- package/templates/skills/gitflow/references/init-version-detection.md +21 -21
- package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
- package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
- package/templates/skills/gitflow/references/merge-execution.md +62 -62
- package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
- package/templates/skills/gitflow/references/plan-template.md +69 -69
- package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
- package/templates/skills/gitflow/references/pr-generation.md +58 -58
- package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
- package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
- package/templates/skills/gitflow/references/start-local-config.md +113 -113
- package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
- package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
- package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
- package/templates/skills/gitflow/steps/step-commit.md +199 -199
- package/templates/skills/gitflow/steps/step-finish.md +147 -147
- package/templates/skills/gitflow/steps/step-init.md +190 -190
- package/templates/skills/gitflow/steps/step-merge.md +85 -85
- package/templates/skills/gitflow/steps/step-plan.md +151 -151
- package/templates/skills/gitflow/steps/step-pr.md +199 -199
- package/templates/skills/gitflow/steps/step-start.md +195 -195
- package/templates/skills/gitflow/steps/step-sync.md +161 -161
- package/templates/skills/gitflow/templates/config.json +72 -72
- package/templates/skills/mcp/SKILL.md +62 -62
- package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
- package/templates/skills/mcp/steps/step-02-tools.md +73 -73
- package/templates/skills/notification/SKILL.md +173 -173
- package/templates/skills/quick-search/SKILL.md +99 -99
- package/templates/skills/ralph-loop/SKILL.md +234 -234
- package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
- package/templates/skills/ralph-loop/references/category-rules.md +96 -96
- package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
- package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
- package/templates/skills/ralph-loop/references/module-transition.md +151 -151
- package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
- package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
- package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
- package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
- package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
- package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
- package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
- package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
- package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
- package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
- package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
- package/templates/skills/refactor/SKILL.md +56 -56
- package/templates/skills/refactor/steps/step-01-discover.md +60 -60
- package/templates/skills/refactor/steps/step-02-execute.md +67 -67
- package/templates/skills/review-code/SKILL.md +94 -94
- package/templates/skills/review-code/references/clean-code-principles.md +292 -292
- package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
- package/templates/skills/review-code/references/feedback-patterns.md +149 -149
- package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
- package/templates/skills/review-code/references/security-checklist.md +212 -212
- package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
- package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
- package/templates/skills/review-code/steps/step-03-react.md +44 -44
- package/templates/skills/ui-components/SKILL.md +137 -137
- package/templates/skills/ui-components/accessibility.md +170 -170
- package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
- package/templates/skills/ui-components/patterns/data-table.md +39 -39
- package/templates/skills/ui-components/patterns/entity-card.md +77 -77
- package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
- package/templates/skills/ui-components/patterns/kanban.md +43 -43
- package/templates/skills/ui-components/responsive-guidelines.md +278 -278
- package/templates/skills/ui-components/style-guide.md +113 -113
- package/templates/skills/utils/SKILL.md +44 -44
- package/templates/skills/utils/subcommands/test-web-config.md +152 -152
- package/templates/skills/utils/subcommands/test-web.md +123 -123
- package/templates/skills/validate/SKILL.md +181 -181
- package/templates/skills/validate-feature/SKILL.md +101 -101
- package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
- package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
- package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
- package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
- package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
- package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
- package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
- package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
- package/templates/skills/workflow/SKILL.md +127 -127
- package/templates/skills/workflow/steps/step-00-init.md +57 -57
- package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
- package/templates/test-web/api-health.json +38 -38
- package/templates/test-web/minimal.json +19 -19
- package/templates/test-web/npm-package.json +46 -46
- package/templates/test-web/seo-check.json +54 -54
|
@@ -1,1584 +1,1584 @@
|
|
|
1
|
-
# BLOCKING POST-CHECKs
|
|
2
|
-
|
|
3
|
-
> **Referenced by:** step-04-examine.md (section 6b)
|
|
4
|
-
> These checks run on the actual generated files. Model-interpreted checks are unreliable.
|
|
5
|
-
|
|
6
|
-
### POST-CHECK 1: Navigation routes must be full paths starting with /
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
# Find all seed data files and check route values
|
|
10
|
-
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
11
|
-
if [ -n "$SEED_FILES" ]; then
|
|
12
|
-
# Check for short routes (no leading /) in Create() calls for navigation entities
|
|
13
|
-
BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
|
|
14
|
-
if [ -n "$BAD_ROUTES" ]; then
|
|
15
|
-
echo "BLOCKING: Navigation routes must be full paths starting with /"
|
|
16
|
-
echo "$BAD_ROUTES"
|
|
17
|
-
echo "Expected: \"/human-resources\" NOT \"humanresources\""
|
|
18
|
-
exit 1
|
|
19
|
-
fi
|
|
20
|
-
fi
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### POST-CHECK 2: All services must filter by TenantId (OWASP A01)
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# Find all service implementation files
|
|
27
|
-
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
28
|
-
if [ -n "$SERVICE_FILES" ]; then
|
|
29
|
-
# Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
|
|
30
|
-
for f in $SERVICE_FILES; do
|
|
31
|
-
# Accept either _currentTenant.TenantId (strict guard clause or nullable usage)
|
|
32
|
-
# or entities with IOptionalTenantEntity/IScopedTenantEntity (optional tenant pattern)
|
|
33
|
-
HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
|
|
34
|
-
HAS_OPTIONAL_ENTITY=false
|
|
35
|
-
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
|
|
36
|
-
HAS_OPTIONAL_ENTITY=true
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
|
|
40
|
-
echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
|
|
41
|
-
echo "Every service query MUST filter by _currentTenant.TenantId"
|
|
42
|
-
echo "OR work with entities that implement IOptionalTenantEntity/IScopedTenantEntity"
|
|
43
|
-
exit 1
|
|
44
|
-
fi
|
|
45
|
-
if grep -q "Guid.Empty" "$f"; then
|
|
46
|
-
echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
|
|
47
|
-
exit 1
|
|
48
|
-
fi
|
|
49
|
-
done
|
|
50
|
-
fi
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### POST-CHECK 3: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
# Find all controller files
|
|
57
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
58
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
59
|
-
for f in $CTRL_FILES; do
|
|
60
|
-
# Check controller has at least one RequirePermission attribute
|
|
61
|
-
if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
|
|
62
|
-
echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
|
|
63
|
-
echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
|
|
64
|
-
echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
|
|
65
|
-
exit 1
|
|
66
|
-
fi
|
|
67
|
-
done
|
|
68
|
-
fi
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### POST-CHECK 4: Seed data must not use Guid.NewGuid()
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
75
|
-
if [ -n "$SEED_FILES" ]; then
|
|
76
|
-
BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
|
|
77
|
-
if [ -n "$BAD_GUIDS" ]; then
|
|
78
|
-
echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
|
|
79
|
-
echo "$BAD_GUIDS"
|
|
80
|
-
exit 1
|
|
81
|
-
fi
|
|
82
|
-
fi
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### POST-CHECK 5: Services must inject ICurrentTenantService (tenant isolation)
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
89
|
-
if [ -n "$SERVICE_FILES" ]; then
|
|
90
|
-
for f in $SERVICE_FILES; do
|
|
91
|
-
# Accept either ICurrentTenantService or ICurrentUser (legacy) for tenant context
|
|
92
|
-
if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
|
|
93
|
-
echo "BLOCKING: Service missing tenant context injection: $f"
|
|
94
|
-
echo "All services MUST inject ICurrentTenantService for tenant isolation"
|
|
95
|
-
echo "Pattern: private readonly ICurrentTenantService _currentTenant;"
|
|
96
|
-
exit 1
|
|
97
|
-
fi
|
|
98
|
-
done
|
|
99
|
-
fi
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### POST-CHECK 6: Translation files must exist for all 4 languages (if frontend)
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
# Find all i18n namespaces used in tsx files
|
|
106
|
-
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
107
|
-
if [ -n "$TSX_FILES" ]; then
|
|
108
|
-
NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
|
|
109
|
-
for NS in $NAMESPACES; do
|
|
110
|
-
for LANG in fr en it de; do
|
|
111
|
-
if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
|
|
112
|
-
echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
|
|
113
|
-
exit 1
|
|
114
|
-
fi
|
|
115
|
-
done
|
|
116
|
-
done
|
|
117
|
-
fi
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### POST-CHECK 7: Pages must use lazy loading (no static page imports in routes)
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
|
|
124
|
-
if [ -n "$ROUTE_FILES" ]; then
|
|
125
|
-
STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
|
|
126
|
-
if [ -n "$STATIC_PAGE_IMPORTS" ]; then
|
|
127
|
-
echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
|
|
128
|
-
echo "$STATIC_PAGE_IMPORTS"
|
|
129
|
-
echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
|
|
130
|
-
exit 1
|
|
131
|
-
fi
|
|
132
|
-
fi
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
# Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
|
|
139
|
-
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
140
|
-
if [ -n "$PAGE_FILES" ]; then
|
|
141
|
-
FAIL=false
|
|
142
|
-
|
|
143
|
-
# 8a. Component imports (Modal, Dialog, Drawer, etc.)
|
|
144
|
-
MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
|
|
145
|
-
if [ -n "$MODAL_IMPORTS" ]; then
|
|
146
|
-
echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
|
|
147
|
-
echo "$MODAL_IMPORTS"
|
|
148
|
-
FAIL=true
|
|
149
|
-
fi
|
|
150
|
-
|
|
151
|
-
# 8b. Inline form state (catches drawers/slide-overs built without external components)
|
|
152
|
-
FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
|
|
153
|
-
if [ -n "$FORM_STATE" ]; then
|
|
154
|
-
echo "BLOCKING: Inline form state detected — forms embedded in ListPage as drawers/panels"
|
|
155
|
-
echo "Create/Edit forms MUST be separate page components with their own URL routes"
|
|
156
|
-
echo "$FORM_STATE"
|
|
157
|
-
FAIL=true
|
|
158
|
-
fi
|
|
159
|
-
|
|
160
|
-
if [ "$FAIL" = true ]; then
|
|
161
|
-
echo ""
|
|
162
|
-
echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
|
|
163
|
-
echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
|
|
164
|
-
exit 1
|
|
165
|
-
fi
|
|
166
|
-
fi
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### POST-CHECK 9: Create/Edit pages must exist as separate route pages (BLOCKING)
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
# For each module with a list page, verify create and edit pages exist
|
|
173
|
-
# If ListPage has navigate() calls to /create or /:id/edit, the target pages MUST exist
|
|
174
|
-
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
|
|
175
|
-
FAIL=false
|
|
176
|
-
if [ -n "$LIST_PAGES" ]; then
|
|
177
|
-
for LIST_PAGE in $LIST_PAGES; do
|
|
178
|
-
PAGE_DIR=$(dirname "$LIST_PAGE")
|
|
179
|
-
MODULE_NAME=$(basename "$PAGE_DIR")
|
|
180
|
-
|
|
181
|
-
# Detect if ListPage navigates to /create or /edit routes
|
|
182
|
-
HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
|
|
183
|
-
HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null)
|
|
184
|
-
|
|
185
|
-
# Check for create page
|
|
186
|
-
CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
|
|
187
|
-
if [ -z "$CREATE_PAGE" ]; then
|
|
188
|
-
if [ -n "$HAS_CREATE_NAV" ]; then
|
|
189
|
-
echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
|
|
190
|
-
echo " Dead link: $HAS_CREATE_NAV"
|
|
191
|
-
echo " Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
|
|
192
|
-
FAIL=true
|
|
193
|
-
else
|
|
194
|
-
echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
|
|
195
|
-
fi
|
|
196
|
-
fi
|
|
197
|
-
|
|
198
|
-
# Check for edit page
|
|
199
|
-
EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
|
|
200
|
-
if [ -z "$EDIT_PAGE" ]; then
|
|
201
|
-
if [ -n "$HAS_EDIT_NAV" ]; then
|
|
202
|
-
echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
|
|
203
|
-
echo " Dead link: $HAS_EDIT_NAV"
|
|
204
|
-
echo " Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
|
|
205
|
-
FAIL=true
|
|
206
|
-
else
|
|
207
|
-
echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
|
|
208
|
-
fi
|
|
209
|
-
fi
|
|
210
|
-
done
|
|
211
|
-
fi
|
|
212
|
-
|
|
213
|
-
if [ "$FAIL" = true ]; then
|
|
214
|
-
echo ""
|
|
215
|
-
echo "BLOCKING: Create/Edit pages are MISSING but ListPage buttons link to them."
|
|
216
|
-
echo "Users will see white screen / 404 when clicking Create or Edit buttons."
|
|
217
|
-
echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
|
|
218
|
-
exit 1
|
|
219
|
-
fi
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### POST-CHECK 10: Form pages must have companion test files
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
# Minimum requirement: if frontend pages exist, at least 1 test file must be present
|
|
226
|
-
PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
|
|
227
|
-
if [ -n "$PAGE_FILES" ]; then
|
|
228
|
-
ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
|
|
229
|
-
if [ -z "$ALL_TESTS" ]; then
|
|
230
|
-
echo "BLOCKING: No frontend test files found in src/pages/"
|
|
231
|
-
echo "Every form page MUST have a companion .test.tsx file"
|
|
232
|
-
exit 1
|
|
233
|
-
fi
|
|
234
|
-
fi
|
|
235
|
-
|
|
236
|
-
# Every CreatePage and EditPage must have a .test.tsx file
|
|
237
|
-
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
|
|
238
|
-
if [ -n "$FORM_PAGES" ]; then
|
|
239
|
-
for FORM_PAGE in $FORM_PAGES; do
|
|
240
|
-
TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
|
|
241
|
-
if [ ! -f "$TEST_FILE" ]; then
|
|
242
|
-
echo "BLOCKING: Form page missing test file: $FORM_PAGE"
|
|
243
|
-
echo "Expected: $TEST_FILE"
|
|
244
|
-
echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
|
|
245
|
-
exit 1
|
|
246
|
-
fi
|
|
247
|
-
done
|
|
248
|
-
fi
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### POST-CHECK 11: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (BLOCKING)
|
|
252
|
-
|
|
253
|
-
```bash
|
|
254
|
-
# Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
|
|
255
|
-
# Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
|
|
256
|
-
ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
257
|
-
if [ -n "$ALL_PAGES" ]; then
|
|
258
|
-
FAIL=false
|
|
259
|
-
|
|
260
|
-
# 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
|
|
261
|
-
FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
|
|
262
|
-
if [ -n "$FK_INPUTS" ]; then
|
|
263
|
-
echo "BLOCKING: FK fields rendered as <input> — MUST use EntityLookup component"
|
|
264
|
-
echo "$FK_INPUTS"
|
|
265
|
-
FAIL=true
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
|
-
# 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
|
|
269
|
-
FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
|
|
270
|
-
if [ -n "$FK_SELECTS" ]; then
|
|
271
|
-
echo "BLOCKING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
|
|
272
|
-
echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
|
|
273
|
-
echo "EntityLookup provides: debounced search, paginated results, display name resolution."
|
|
274
|
-
echo "$FK_SELECTS"
|
|
275
|
-
FAIL=true
|
|
276
|
-
fi
|
|
277
|
-
|
|
278
|
-
# 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
|
|
279
|
-
FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
|
|
280
|
-
if [ -n "$FK_SELECT_ONCHANGE" ]; then
|
|
281
|
-
echo "BLOCKING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
|
|
282
|
-
echo "$FK_SELECT_ONCHANGE"
|
|
283
|
-
FAIL=true
|
|
284
|
-
fi
|
|
285
|
-
|
|
286
|
-
# 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
|
|
287
|
-
FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*(?:[Ee]nter.*[Ii][Dd]|[Gg][Uu][Ii][Dd]|[Ss]elect.*[Ee]mployee|[Ss]elect.*[Dd]epartment|[Ss]elect.*[Pp]osition|[Ss]elect.*[Pp]roject|[Ss]elect.*[Cc]ategory)' $ALL_PAGES 2>/dev/null)
|
|
288
|
-
if [ -n "$FK_PLACEHOLDER" ]; then
|
|
289
|
-
echo "BLOCKING: Form has placeholder for FK field selection — use EntityLookup search instead"
|
|
290
|
-
echo "$FK_PLACEHOLDER"
|
|
291
|
-
FAIL=true
|
|
292
|
-
fi
|
|
293
|
-
|
|
294
|
-
# 5. Detect <option> elements with GUID-like values (sign of FK <select>)
|
|
295
|
-
FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
|
|
296
|
-
if [ -n "$FK_OPTIONS" ]; then
|
|
297
|
-
echo "BLOCKING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
|
|
298
|
-
echo "Replace the entire <select>/<option> block with <EntityLookup />"
|
|
299
|
-
echo "$FK_OPTIONS"
|
|
300
|
-
FAIL=true
|
|
301
|
-
fi
|
|
302
|
-
|
|
303
|
-
if [ "$FAIL" = true ]; then
|
|
304
|
-
echo ""
|
|
305
|
-
echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
|
|
306
|
-
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
307
|
-
echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
|
|
308
|
-
echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
|
|
309
|
-
exit 1
|
|
310
|
-
fi
|
|
311
|
-
fi
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### POST-CHECK 12: Backend APIs must support search parameter for EntityLookup
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
# Check that controller GetAll methods accept search parameter
|
|
318
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
319
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
320
|
-
for f in $CTRL_FILES; do
|
|
321
|
-
# Check if controller has GetAll but no search parameter
|
|
322
|
-
if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
|
|
323
|
-
if ! grep -q "search" "$f"; then
|
|
324
|
-
echo "WARNING: Controller missing search parameter on GetAll: $f"
|
|
325
|
-
echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
|
|
326
|
-
echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
|
|
327
|
-
fi
|
|
328
|
-
fi
|
|
329
|
-
done
|
|
330
|
-
fi
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### POST-CHECK 13: No hardcoded Tailwind colors in generated pages (BLOCKING)
|
|
334
|
-
|
|
335
|
-
```bash
|
|
336
|
-
# Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
|
|
337
|
-
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
338
|
-
if [ -n "$ALL_PAGES" ]; then
|
|
339
|
-
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
|
|
340
|
-
if [ -n "$HARDCODED" ]; then
|
|
341
|
-
echo "BLOCKING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
|
|
342
|
-
echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
|
|
343
|
-
echo ""
|
|
344
|
-
echo "Fix mapping:"
|
|
345
|
-
echo " bg-white → bg-[var(--bg-card)]"
|
|
346
|
-
echo " bg-gray-50 → bg-[var(--bg-primary)]"
|
|
347
|
-
echo " text-gray-900 → text-[var(--text-primary)]"
|
|
348
|
-
echo " text-gray-500 → text-[var(--text-secondary)]"
|
|
349
|
-
echo " border-gray-200 → border-[var(--border-color)]"
|
|
350
|
-
echo " bg-blue-600 → bg-[var(--color-accent-500)]"
|
|
351
|
-
echo " text-blue-600 → text-[var(--color-accent-500)]"
|
|
352
|
-
echo " text-red-500 → text-[var(--error-text)]"
|
|
353
|
-
echo " bg-green-500 → bg-[var(--success-bg)]"
|
|
354
|
-
echo ""
|
|
355
|
-
echo "See references/smartstack-frontend.md section 4 for full variable reference"
|
|
356
|
-
echo ""
|
|
357
|
-
echo "$HARDCODED"
|
|
358
|
-
exit 1
|
|
359
|
-
fi
|
|
360
|
-
fi
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### POST-CHECK 14: Routes seed data must match frontend
|
|
364
|
-
|
|
365
|
-
```bash
|
|
366
|
-
SEED_ROUTES=$(grep -Poh 'Route\s*=\s*"([^"]+)"' $(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null) 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
367
|
-
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
368
|
-
if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
|
|
369
|
-
FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
|
|
370
|
-
if [ -n "$FRONTEND_PATHS" ]; then
|
|
371
|
-
MISMATCH_FOUND=false
|
|
372
|
-
for SEED_ROUTE in $SEED_ROUTES; do
|
|
373
|
-
DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
|
|
374
|
-
if [ "$DEPTH" -lt 3 ]; then continue; fi
|
|
375
|
-
SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
|
|
376
|
-
SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
377
|
-
MATCH_FOUND=false
|
|
378
|
-
for FE_PATH in $FRONTEND_PATHS; do
|
|
379
|
-
# Flag FORBIDDEN /list suffix BEFORE normalization
|
|
380
|
-
if echo "$FE_PATH" | grep -qP '/list$'; then
|
|
381
|
-
echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
|
|
382
|
-
fi
|
|
383
|
-
FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
|
|
384
|
-
FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
385
|
-
if [ "$SEED_NORM" = "$FE_NORM" ]; then
|
|
386
|
-
MATCH_FOUND=true
|
|
387
|
-
break
|
|
388
|
-
fi
|
|
389
|
-
done
|
|
390
|
-
if [ "$MATCH_FOUND" = false ]; then
|
|
391
|
-
echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
|
|
392
|
-
MISMATCH_FOUND=true
|
|
393
|
-
fi
|
|
394
|
-
done
|
|
395
|
-
if [ "$MISMATCH_FOUND" = true ]; then
|
|
396
|
-
echo "Fix: Ensure every NavigationSeedData route has a corresponding
|
|
397
|
-
exit 1
|
|
398
|
-
fi
|
|
399
|
-
fi
|
|
400
|
-
fi
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### POST-CHECK 14b: Frontend routes must use kebab-case (BLOCKING)
|
|
404
|
-
|
|
405
|
-
```bash
|
|
406
|
-
# POST-CHECK 14 normalizes hyphens for existence check, but does NOT catch kebab-case mismatches.
|
|
407
|
-
# This supplementary check detects concatenated multi-word route segments without hyphens.
|
|
408
|
-
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
409
|
-
if [ -n "$APP_TSX" ]; then
|
|
410
|
-
# Extract route path strings from App.tsx
|
|
411
|
-
FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
|
|
412
|
-
for FE_PATH in $FE_PATHS; do
|
|
413
|
-
# Split path by / and check each segment
|
|
414
|
-
for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
|
|
415
|
-
# Skip dynamic segments (:id, :slug) and single words (< 10 chars likely single word)
|
|
416
|
-
echo "$SEG" | grep -qP '^:' && continue
|
|
417
|
-
# Detect multi-word segments without hyphens: 2+ consecutive lowercase sequences
|
|
418
|
-
# e.g., "humanresources" (human+resources), "timemanagement" (time+management)
|
|
419
|
-
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
420
|
-
# Potential concatenated multi-word — cross-check with seed data
|
|
421
|
-
SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG")
|
|
422
|
-
if [ -n "$SEED_MATCH" ]; then
|
|
423
|
-
echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
|
|
424
|
-
echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
|
|
425
|
-
echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
|
|
426
|
-
exit 1
|
|
427
|
-
fi
|
|
428
|
-
fi
|
|
429
|
-
done
|
|
430
|
-
done
|
|
431
|
-
fi
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### POST-CHECK 15: HasQueryFilter must not use Guid.Empty (OWASP A01)
|
|
435
|
-
|
|
436
|
-
```bash
|
|
437
|
-
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
438
|
-
if [ -n "$CONFIG_FILES" ]; then
|
|
439
|
-
BAD_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
|
|
440
|
-
if [ -n "$BAD_FILTERS" ]; then
|
|
441
|
-
echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
|
|
442
|
-
echo "$BAD_FILTERS"
|
|
443
|
-
echo ""
|
|
444
|
-
echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
|
|
445
|
-
echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
|
|
446
|
-
exit 1
|
|
447
|
-
fi
|
|
448
|
-
fi
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### POST-CHECK 16: GetAll methods must return PaginatedResult<T>
|
|
452
|
-
|
|
453
|
-
```bash
|
|
454
|
-
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
455
|
-
if [ -n "$SERVICE_FILES" ]; then
|
|
456
|
-
BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
|
|
457
|
-
if [ -n "$BAD_RETURNS" ]; then
|
|
458
|
-
echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
|
|
459
|
-
echo "$BAD_RETURNS"
|
|
460
|
-
echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
|
|
461
|
-
exit 1
|
|
462
|
-
fi
|
|
463
|
-
fi
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### POST-CHECK 17: i18n files must contain required structural keys
|
|
467
|
-
|
|
468
|
-
```bash
|
|
469
|
-
I18N_DIR="src/i18n/locales/fr"
|
|
470
|
-
if [ -d "$I18N_DIR" ]; then
|
|
471
|
-
REQUIRED_KEYS="actions columns empty errors form labels messages validation"
|
|
472
|
-
for JSON_FILE in "$I18N_DIR"/*.json; do
|
|
473
|
-
[ ! -f "$JSON_FILE" ] && continue
|
|
474
|
-
BASENAME=$(basename "$JSON_FILE")
|
|
475
|
-
case "$BASENAME" in common.json|navigation.json) continue;; esac
|
|
476
|
-
for KEY in $REQUIRED_KEYS; do
|
|
477
|
-
if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
|
|
478
|
-
echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
|
|
479
|
-
echo "Module i18n files MUST contain: $REQUIRED_KEYS"
|
|
480
|
-
exit 1
|
|
481
|
-
fi
|
|
482
|
-
done
|
|
483
|
-
done
|
|
484
|
-
fi
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
### POST-CHECK 18: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
|
|
488
|
-
|
|
489
|
-
```bash
|
|
490
|
-
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
|
|
491
|
-
if [ -n "$ENTITY_FILES" ]; then
|
|
492
|
-
for f in $ENTITY_FILES; do
|
|
493
|
-
if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
|
|
494
|
-
echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
|
|
495
|
-
echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
|
|
496
|
-
exit 1
|
|
497
|
-
fi
|
|
498
|
-
done
|
|
499
|
-
fi
|
|
500
|
-
CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
|
|
501
|
-
if [ -n "$CREATE_VALIDATORS" ]; then
|
|
502
|
-
for f in $CREATE_VALIDATORS; do
|
|
503
|
-
VALIDATOR_DIR=$(dirname "$f")
|
|
504
|
-
ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
|
|
505
|
-
if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
|
|
506
|
-
echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
|
|
507
|
-
echo " Found: $f"
|
|
508
|
-
echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
|
|
509
|
-
exit 1
|
|
510
|
-
fi
|
|
511
|
-
done
|
|
512
|
-
fi
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
### POST-CHECK 19: (REMOVED — Context level no longer exists in SmartStack navigation hierarchy)
|
|
516
|
-
|
|
517
|
-
### POST-CHECK 20: RolePermission seed data must NOT use deterministic role GUIDs
|
|
518
|
-
|
|
519
|
-
```bash
|
|
520
|
-
# System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
|
|
521
|
-
# RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
|
|
522
|
-
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
523
|
-
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
|
|
524
|
-
if [ -n "$SEED_ALL_FILES" ]; then
|
|
525
|
-
BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
|
|
526
|
-
if [ -n "$BAD_ROLE_GUID" ]; then
|
|
527
|
-
echo "BLOCKING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
|
|
528
|
-
echo "System roles are pre-seeded by SmartStack core with their own IDs"
|
|
529
|
-
echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
|
|
530
|
-
echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
|
|
531
|
-
echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
|
|
532
|
-
echo "$BAD_ROLE_GUID"
|
|
533
|
-
exit 1
|
|
534
|
-
fi
|
|
535
|
-
fi
|
|
536
|
-
# Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
|
|
537
|
-
ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
|
|
538
|
-
if [ -n "$ROLE_PERM_FILES" ]; then
|
|
539
|
-
BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
|
|
540
|
-
if [ -n "$BAD_ROLE_REF" ]; then
|
|
541
|
-
echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
|
|
542
|
-
echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
|
|
543
|
-
echo "$BAD_ROLE_REF"
|
|
544
|
-
fi
|
|
545
|
-
fi
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### POST-CHECK 21: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
|
|
549
|
-
|
|
550
|
-
```bash
|
|
551
|
-
# The !.Value pattern on Guid? throws InvalidOperationException (500) instead of clean 401
|
|
552
|
-
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
553
|
-
if [ -n "$SERVICE_FILES" ]; then
|
|
554
|
-
BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
|
|
555
|
-
if [ -n "$BAD_PATTERN" ]; then
|
|
556
|
-
echo "BLOCKING: Services use TenantId!.Value — causes 500 instead of 400 when tenant context is missing"
|
|
557
|
-
echo "$BAD_PATTERN"
|
|
558
|
-
echo ""
|
|
559
|
-
echo "Fix: Replace with guard clause at the start of every method:"
|
|
560
|
-
echo " var tenantId = _currentTenant.TenantId"
|
|
561
|
-
echo " ?? throw new TenantContextRequiredException();"
|
|
562
|
-
echo ""
|
|
563
|
-
echo "This produces a clean 400 Bad Request via GlobalExceptionHandlerMiddleware."
|
|
564
|
-
echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
|
|
565
|
-
exit 1
|
|
566
|
-
fi
|
|
567
|
-
fi
|
|
568
|
-
|
|
569
|
-
# POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
|
|
570
|
-
if [ -n "$SERVICE_FILES" ]; then
|
|
571
|
-
BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
|
|
572
|
-
if [ -n "$BAD_UNAUTH" ]; then
|
|
573
|
-
echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
|
|
574
|
-
echo "$BAD_UNAUTH"
|
|
575
|
-
echo ""
|
|
576
|
-
echo "Fix: Replace with:"
|
|
577
|
-
echo " var tenantId = _currentTenant.TenantId"
|
|
578
|
-
echo " ?? throw new TenantContextRequiredException();"
|
|
579
|
-
echo ""
|
|
580
|
-
echo "TenantContextRequiredException returns 400 Bad Request (does not clear token)."
|
|
581
|
-
echo "UnauthorizedAccessException returns 401 Unauthorized (clears token + redirects to login)."
|
|
582
|
-
exit 1
|
|
583
|
-
fi
|
|
584
|
-
fi
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### POST-CHECK 22: Cross-tenant entities must use Guid? TenantId
|
|
588
|
-
|
|
589
|
-
```bash
|
|
590
|
-
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
591
|
-
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
|
|
592
|
-
if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
|
|
593
|
-
echo "BLOCKING: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
|
|
594
|
-
exit 1
|
|
595
|
-
fi
|
|
596
|
-
fi
|
|
597
|
-
done
|
|
598
|
-
echo "POST-CHECK 22: OK"
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
### POST-CHECK 23: Scoped entities must have EntityScope property
|
|
602
|
-
|
|
603
|
-
```bash
|
|
604
|
-
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
605
|
-
if grep -q "IScopedTenantEntity" "$entity"; then
|
|
606
|
-
if ! grep -q "EntityScope\|Scope" "$entity"; then
|
|
607
|
-
echo "BLOCKING: Entity with IScopedTenantEntity must have EntityScope Scope property"
|
|
608
|
-
exit 1
|
|
609
|
-
fi
|
|
610
|
-
fi
|
|
611
|
-
done
|
|
612
|
-
echo "POST-CHECK 23: OK"
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
### POST-CHECK 24: Permissions.cs static constants must exist (BLOCKING)
|
|
616
|
-
|
|
617
|
-
```bash
|
|
618
|
-
# Every module with controllers MUST have a Permissions.cs with static constants
|
|
619
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
620
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
621
|
-
PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u)
|
|
622
|
-
for MODULE in $PERM_REFS; do
|
|
623
|
-
PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null)
|
|
624
|
-
if [ -z "$PERM_FILE" ]; then
|
|
625
|
-
echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
|
|
626
|
-
echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
|
|
627
|
-
exit 1
|
|
628
|
-
fi
|
|
629
|
-
done
|
|
630
|
-
fi
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
### POST-CHECK 25: ApplicationRolesSeedData.cs must exist (BLOCKING)
|
|
634
|
-
|
|
635
|
-
```bash
|
|
636
|
-
# If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
|
|
637
|
-
ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
|
|
638
|
-
if [ -n "$ROLE_SEED" ]; then
|
|
639
|
-
APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
|
|
640
|
-
if [ -z "$APP_ROLE_SEED" ]; then
|
|
641
|
-
echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
|
|
642
|
-
echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
|
|
643
|
-
echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
|
|
644
|
-
echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
|
|
645
|
-
exit 1
|
|
646
|
-
fi
|
|
647
|
-
fi
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
### POST-CHECK 25b: Section route completeness (NavigationSection → frontend route + permissions)
|
|
651
|
-
|
|
652
|
-
```bash
|
|
653
|
-
# Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
|
|
654
|
-
# and section-level permissions MUST exist for each section defined in seed data
|
|
655
|
-
SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
|
|
656
|
-
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
657
|
-
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
|
|
658
|
-
# Extract section routes from seed data
|
|
659
|
-
SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u)
|
|
660
|
-
for SECTION_ROUTE in $SECTION_ROUTES; do
|
|
661
|
-
# Extract the last segment (section-kebab) for frontend route matching
|
|
662
|
-
SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
|
|
663
|
-
if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
|
|
664
|
-
echo "BLOCKING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
|
|
665
|
-
echo "Expected path segment '$SECTION_SEG' in App.tsx
|
|
666
|
-
echo "Fix: Add section child routes to the module's children array in App.tsx"
|
|
667
|
-
fi
|
|
668
|
-
done
|
|
669
|
-
fi
|
|
670
|
-
|
|
671
|
-
# Controllers with section-level [NavRoute] (4 segments) must have matching [RequirePermission]
|
|
672
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
673
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
674
|
-
for f in $CTRL_FILES; do
|
|
675
|
-
# Match NavRoute with 4 dot-separated segments (section-level)
|
|
676
|
-
SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z]+\.[a-z]+\.[a-z]+\.[a-z]+"\)' "$f" 2>/dev/null)
|
|
677
|
-
if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
|
|
678
|
-
echo "BLOCKING: Section controller has [NavRoute] but no [RequirePermission]: $f"
|
|
679
|
-
echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
|
|
680
|
-
exit 1
|
|
681
|
-
fi
|
|
682
|
-
done
|
|
683
|
-
fi
|
|
684
|
-
|
|
685
|
-
# Section-level permissions must exist for each section in seed data
|
|
686
|
-
PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
|
|
687
|
-
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
|
|
688
|
-
SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
689
|
-
for CODE in $SECTION_CODES; do
|
|
690
|
-
PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
|
|
691
|
-
if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
|
|
692
|
-
echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
|
|
693
|
-
echo "Fix: Add section-level permissions via MCP generate_permissions with 4-segment navRoute"
|
|
694
|
-
fi
|
|
695
|
-
done
|
|
696
|
-
fi
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
### POST-CHECK 26: FORBIDDEN route patterns — /list and /detail/:id (BLOCKING)
|
|
700
|
-
|
|
701
|
-
```bash
|
|
702
|
-
# 1. Check seed data for FORBIDDEN suffixes
|
|
703
|
-
SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
|
|
704
|
-
if [ -n "$SEED_NAV_FILES" ]; then
|
|
705
|
-
BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
|
|
706
|
-
if [ -n "$BAD_ROUTES" ]; then
|
|
707
|
-
echo "BLOCKING: FORBIDDEN route pattern in seed data"
|
|
708
|
-
echo " - 'list' section route = module route (NO /list suffix)"
|
|
709
|
-
echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
|
|
710
|
-
echo "$BAD_ROUTES"
|
|
711
|
-
exit 1
|
|
712
|
-
fi
|
|
713
|
-
fi
|
|
714
|
-
|
|
715
|
-
# 2. Check frontend routes for FORBIDDEN path segments
|
|
716
|
-
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
717
|
-
if [ -n "$APP_TSX" ]; then
|
|
718
|
-
BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null)
|
|
719
|
-
if [ -n "$BAD_FE" ]; then
|
|
720
|
-
echo "BLOCKING: FORBIDDEN frontend route path"
|
|
721
|
-
echo " - list = index: true (no 'list' path segment)"
|
|
722
|
-
echo " - detail = ':id' (no 'detail' path segment)"
|
|
723
|
-
echo "$BAD_FE"
|
|
724
|
-
exit 1
|
|
725
|
-
fi
|
|
726
|
-
fi
|
|
727
|
-
echo "OK: No forbidden /list or /detail route patterns found"
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
### POST-CHECK 27: Permission path segment count (WARNING)
|
|
731
|
-
|
|
732
|
-
```bash
|
|
733
|
-
PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
|
|
734
|
-
if [ -n "$PERM_FILES" ]; then
|
|
735
|
-
while IFS= read -r line; do
|
|
736
|
-
PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"')
|
|
737
|
-
if [ -n "$PATH_VAL" ]; then
|
|
738
|
-
DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
|
|
739
|
-
# Module permissions: 2 dots (app.module.action = 3 segments = 2+1)
|
|
740
|
-
# Section permissions: 3 dots (app.module.section.action = 4 segments = 3+1)
|
|
741
|
-
# Wildcard: ends with .* (valid at any level)
|
|
742
|
-
if echo "$PATH_VAL" | grep -qP '\.\*$'; then
|
|
743
|
-
continue # Wildcards are valid
|
|
744
|
-
elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
|
|
745
|
-
echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
|
|
746
|
-
fi
|
|
747
|
-
fi
|
|
748
|
-
done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
|
|
749
|
-
fi
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
### POST-CHECK 28: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
|
|
753
|
-
|
|
754
|
-
```bash
|
|
755
|
-
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
756
|
-
if [ -n "$PROVIDER" ]; then
|
|
757
|
-
METHODS_FOUND=0
|
|
758
|
-
for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
|
|
759
|
-
if grep -q "$METHOD" "$PROVIDER"; then
|
|
760
|
-
METHODS_FOUND=$((METHODS_FOUND + 1))
|
|
761
|
-
else
|
|
762
|
-
echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
|
|
763
|
-
fi
|
|
764
|
-
done
|
|
765
|
-
if [ "$METHODS_FOUND" -lt 4 ]; then
|
|
766
|
-
echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
|
|
767
|
-
exit 1
|
|
768
|
-
fi
|
|
769
|
-
|
|
770
|
-
# Check DI registration
|
|
771
|
-
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
|
|
772
|
-
if [ -n "$DI_FILE" ]; then
|
|
773
|
-
if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
|
|
774
|
-
echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
|
|
775
|
-
echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
|
|
776
|
-
exit 1
|
|
777
|
-
fi
|
|
778
|
-
fi
|
|
779
|
-
fi
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
### POST-CHECK 29: i18n must use separate JSON files per language (not embedded in index.ts)
|
|
783
|
-
|
|
784
|
-
```bash
|
|
785
|
-
# Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
|
|
786
|
-
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
787
|
-
if [ -n "$TSX_FILES" ]; then
|
|
788
|
-
# Check if i18n locales directory exists
|
|
789
|
-
if [ ! -d "src/i18n/locales" ]; then
|
|
790
|
-
echo "BLOCKING: Missing src/i18n/locales/ directory"
|
|
791
|
-
echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
|
|
792
|
-
echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
|
|
793
|
-
exit 1
|
|
794
|
-
fi
|
|
795
|
-
|
|
796
|
-
# Check for embedded translations in index.ts (common anti-pattern)
|
|
797
|
-
I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
|
|
798
|
-
if [ -n "$I18N_INDEX" ]; then
|
|
799
|
-
EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
|
|
800
|
-
if [ -n "$EMBEDDED" ]; then
|
|
801
|
-
echo "BLOCKING: Translations embedded in i18n config file — must be in separate JSON files"
|
|
802
|
-
echo "Found embedded translations in:"
|
|
803
|
-
echo "$EMBEDDED"
|
|
804
|
-
echo ""
|
|
805
|
-
echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
|
|
806
|
-
echo "The i18n config should import from locales/ directory, not contain inline translations"
|
|
807
|
-
exit 1
|
|
808
|
-
fi
|
|
809
|
-
fi
|
|
810
|
-
|
|
811
|
-
# Verify all 4 language directories exist
|
|
812
|
-
for LANG in fr en it de; do
|
|
813
|
-
if [ ! -d "src/i18n/locales/$LANG" ]; then
|
|
814
|
-
echo "BLOCKING: Missing language directory: src/i18n/locales/$LANG/"
|
|
815
|
-
echo "SmartStack requires 4 languages: fr, en, it, de"
|
|
816
|
-
exit 1
|
|
817
|
-
fi
|
|
818
|
-
done
|
|
819
|
-
|
|
820
|
-
# Verify at least one JSON file exists per language
|
|
821
|
-
for LANG in fr en it de; do
|
|
822
|
-
JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
|
|
823
|
-
if [ "$JSON_COUNT" -eq 0 ]; then
|
|
824
|
-
echo "BLOCKING: No translation JSON files in src/i18n/locales/$LANG/"
|
|
825
|
-
echo "Each module must have a {module}.json file per language"
|
|
826
|
-
exit 1
|
|
827
|
-
fi
|
|
828
|
-
done
|
|
829
|
-
fi
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
### POST-CHECK 30: Pages must use useTranslation hook (no hardcoded user-facing strings)
|
|
833
|
-
|
|
834
|
-
```bash
|
|
835
|
-
# Verify that page components use i18n — detect hardcoded strings in JSX
|
|
836
|
-
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
837
|
-
if [ -n "$PAGE_FILES" ]; then
|
|
838
|
-
# Check that at least 80% of pages import useTranslation
|
|
839
|
-
TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
|
|
840
|
-
I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
|
|
841
|
-
if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
|
|
842
|
-
echo "BLOCKING: No page files use useTranslation — all user-facing text must be translated"
|
|
843
|
-
echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
|
|
844
|
-
echo ""
|
|
845
|
-
echo "Fix: Import and use useTranslation in every page component:"
|
|
846
|
-
echo " const { t } = useTranslation(['{module}']);"
|
|
847
|
-
echo " t('{module}:title', 'Fallback text')"
|
|
848
|
-
exit 1
|
|
849
|
-
fi
|
|
850
|
-
|
|
851
|
-
# Check for common hardcoded English strings in JSX (heuristic)
|
|
852
|
-
HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No data|Not found|Submit|Back|Actions|Name|Status|Description)\s*<' $PAGE_FILES 2>/dev/null | grep -v '{t(' | head -10)
|
|
853
|
-
if [ -n "$HARDCODED_TEXT" ]; then
|
|
854
|
-
echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
|
|
855
|
-
echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
|
|
856
|
-
echo "$HARDCODED_TEXT"
|
|
857
|
-
fi
|
|
858
|
-
fi
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
### POST-CHECK 31: List/Detail pages must include DocToggleButton (documentation panel)
|
|
862
|
-
|
|
863
|
-
```bash
|
|
864
|
-
# Every list and detail page MUST have DocToggleButton for inline documentation access
|
|
865
|
-
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
866
|
-
if [ -n "$LIST_PAGES" ]; then
|
|
867
|
-
MISSING_DOC=0
|
|
868
|
-
for PAGE in $LIST_PAGES; do
|
|
869
|
-
if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
|
|
870
|
-
echo "WARNING: Page missing DocToggleButton: $PAGE"
|
|
871
|
-
echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
|
|
872
|
-
echo " Place in header actions: <DocToggleButton />"
|
|
873
|
-
MISSING_DOC=$((MISSING_DOC + 1))
|
|
874
|
-
fi
|
|
875
|
-
done
|
|
876
|
-
if [ "$MISSING_DOC" -gt 0 ]; then
|
|
877
|
-
echo ""
|
|
878
|
-
echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
|
|
879
|
-
echo "See smartstack-frontend.md section 7 for placement pattern"
|
|
880
|
-
fi
|
|
881
|
-
fi
|
|
882
|
-
```
|
|
883
|
-
|
|
884
|
-
### POST-CHECK 32: Module documentation must be generated (doc-data.ts)
|
|
885
|
-
|
|
886
|
-
```bash
|
|
887
|
-
# After frontend pages exist, /documentation should have been called
|
|
888
|
-
TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
|
|
889
|
-
DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
|
|
890
|
-
if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
|
|
891
|
-
echo "WARNING: Frontend pages exist but no documentation generated"
|
|
892
|
-
echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
|
|
893
|
-
echo "The DocToggleButton in page headers will link to this documentation"
|
|
894
|
-
fi
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
### POST-CHECK 33: Pagination type must be PaginatedResult<T> — no aliases (BLOCKING)
|
|
898
|
-
|
|
899
|
-
```bash
|
|
900
|
-
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
901
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
902
|
-
ALL_FILES="$SERVICE_FILES $CTRL_FILES"
|
|
903
|
-
if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
|
|
904
|
-
BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
|
|
905
|
-
if [ -n "$BAD_NAMES" ]; then
|
|
906
|
-
echo "BLOCKING: Pagination type must be PaginatedResult<T> — found non-canonical names"
|
|
907
|
-
echo "$BAD_NAMES"
|
|
908
|
-
echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
|
|
909
|
-
echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
|
|
910
|
-
exit 1
|
|
911
|
-
fi
|
|
912
|
-
fi
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
### POST-CHECK 34: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
|
|
916
|
-
|
|
917
|
-
```bash
|
|
918
|
-
# If feature.json has entities with codePattern.strategy != "manual",
|
|
919
|
-
# verify that ICodeGenerator<Entity> is registered in DI
|
|
920
|
-
FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
|
|
921
|
-
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
|
|
922
|
-
if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
|
|
923
|
-
for FEATURE in $FEATURE_FILES; do
|
|
924
|
-
ENTITIES_WITH_CODE=$(python3 -c "
|
|
925
|
-
import json, sys
|
|
926
|
-
try:
|
|
927
|
-
with open('$FEATURE') as f:
|
|
928
|
-
data = json.load(f)
|
|
929
|
-
for e in data.get('analysis', {}).get('entities', []):
|
|
930
|
-
cp = e.get('codePattern', {})
|
|
931
|
-
if cp.get('strategy', 'manual') != 'manual':
|
|
932
|
-
print(e['name'])
|
|
933
|
-
except: pass
|
|
934
|
-
" 2>/dev/null)
|
|
935
|
-
for ENTITY in $ENTITIES_WITH_CODE; do
|
|
936
|
-
if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
|
|
937
|
-
echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
|
|
938
|
-
echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
|
|
939
|
-
exit 1
|
|
940
|
-
fi
|
|
941
|
-
done
|
|
942
|
-
done
|
|
943
|
-
fi
|
|
944
|
-
```
|
|
945
|
-
|
|
946
|
-
### POST-CHECK 35: Code regex must support hyphens (BLOCKING)
|
|
947
|
-
|
|
948
|
-
```bash
|
|
949
|
-
VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
|
|
950
|
-
if [ -n "$VALIDATOR_FILES" ]; then
|
|
951
|
-
OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
|
|
952
|
-
if [ -n "$OLD_REGEX" ]; then
|
|
953
|
-
echo "BLOCKING: Code validator uses old regex without hyphen support"
|
|
954
|
-
echo "$OLD_REGEX"
|
|
955
|
-
echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
|
|
956
|
-
exit 1
|
|
957
|
-
fi
|
|
958
|
-
fi
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
### POST-CHECK 36: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
|
|
962
|
-
|
|
963
|
-
```bash
|
|
964
|
-
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
965
|
-
if [ -n "$SERVICE_FILES" ]; then
|
|
966
|
-
for f in $SERVICE_FILES; do
|
|
967
|
-
if grep -q "ICodeGenerator" "$f"; then
|
|
968
|
-
ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
|
|
969
|
-
DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
|
|
970
|
-
if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
|
|
971
|
-
echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
|
|
972
|
-
echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
|
|
973
|
-
fi
|
|
974
|
-
fi
|
|
975
|
-
done
|
|
976
|
-
fi
|
|
977
|
-
```
|
|
978
|
-
|
|
979
|
-
### POST-CHECK 37: Translation seed data must have idempotency guard (BLOCKING)
|
|
980
|
-
|
|
981
|
-
```bash
|
|
982
|
-
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
983
|
-
if [ -n "$PROVIDER" ]; then
|
|
984
|
-
# Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
|
|
985
|
-
# Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
|
|
986
|
-
TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
|
|
987
|
-
TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)
|
|
988
|
-
|
|
989
|
-
if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
|
|
990
|
-
echo "BLOCKING: Translation seed data inserts without idempotency guard in $PROVIDER"
|
|
991
|
-
echo "Fix: Before each NavigationTranslations.Add block, check existence:"
|
|
992
|
-
echo " if (!await context.NavigationTranslations.AnyAsync("
|
|
993
|
-
echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
|
|
994
|
-
echo " && t.EntityType == NavigationEntityType.Module, ct))"
|
|
995
|
-
echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
|
|
996
|
-
echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
|
|
997
|
-
exit 1
|
|
998
|
-
fi
|
|
999
|
-
fi
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
### POST-CHECK 38: Resource seed data must use actual section IDs from DB (BLOCKING)
|
|
1003
|
-
|
|
1004
|
-
```bash
|
|
1005
|
-
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
1006
|
-
if [ -n "$PROVIDER" ]; then
|
|
1007
|
-
# Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (deterministic GUIDs)
|
|
1008
|
-
# instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
|
|
1009
|
-
if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
|
|
1010
|
-
echo "BLOCKING: Resource seed data uses deterministic GUID as SectionId in $PROVIDER"
|
|
1011
|
-
echo "NavigationSection.Create() generates its own ID — deterministic seed GUIDs do NOT exist in nav_Sections."
|
|
1012
|
-
echo "Fix: Query actual section from DB before creating resources:"
|
|
1013
|
-
echo " var actualSection = await context.NavigationSections"
|
|
1014
|
-
echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
|
|
1015
|
-
echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
|
|
1016
|
-
exit 1
|
|
1017
|
-
fi
|
|
1018
|
-
fi
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
### POST-CHECK 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)
|
|
1022
|
-
|
|
1023
|
-
```bash
|
|
1024
|
-
# [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
|
|
1025
|
-
# Having both is redundant and may cause route conflicts.
|
|
1026
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1027
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
1028
|
-
for f in $CTRL_FILES; do
|
|
1029
|
-
HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
|
|
1030
|
-
HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
|
|
1031
|
-
if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
|
|
1032
|
-
echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
|
|
1033
|
-
echo " Found [NavRoute]: $HAS_NAVROUTE"
|
|
1034
|
-
echo " Found [Route]: $HAS_ROUTE"
|
|
1035
|
-
echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
|
|
1036
|
-
exit 1
|
|
1037
|
-
fi
|
|
1038
|
-
done
|
|
1039
|
-
fi
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
### POST-CHECK 40: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
|
|
1043
|
-
|
|
1044
|
-
```bash
|
|
1045
|
-
# NavRoute segments are navigation entity Codes joined by dots.
|
|
1046
|
-
# Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
|
|
1047
|
-
# Verified from SmartStack.app: "support-client.my-tickets", "administration.access-requests"
|
|
1048
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1049
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
1050
|
-
for f in $CTRL_FILES; do
|
|
1051
|
-
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1052
|
-
if [ -n "$NAVROUTE_VAL" ]; then
|
|
1053
|
-
# Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
|
|
1054
|
-
for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
|
|
1055
|
-
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1056
|
-
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
1057
|
-
echo " Full NavRoute: $NAVROUTE_VAL"
|
|
1058
|
-
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1059
|
-
echo " SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
|
|
1060
|
-
exit 1
|
|
1061
|
-
fi
|
|
1062
|
-
done
|
|
1063
|
-
fi
|
|
1064
|
-
done
|
|
1065
|
-
fi
|
|
1066
|
-
|
|
1067
|
-
# Also check seed data Code values for navigation entities
|
|
1068
|
-
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
|
|
1069
|
-
if [ -n "$SEED_FILES" ]; then
|
|
1070
|
-
CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
1071
|
-
for CODE in $CODES; do
|
|
1072
|
-
if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
|
|
1073
|
-
echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
|
|
1074
|
-
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1075
|
-
exit 1
|
|
1076
|
-
fi
|
|
1077
|
-
done
|
|
1078
|
-
fi
|
|
1079
|
-
```
|
|
1080
|
-
|
|
1081
|
-
### POST-CHECK 41: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
|
|
1082
|
-
|
|
1083
|
-
```bash
|
|
1084
|
-
# Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
|
|
1085
|
-
# SmartStack.app convention: "support-client.my-tickets.read" (kebab-case everywhere)
|
|
1086
|
-
# FORBIDDEN: "humanresources.employees.read" — must be "human-resources.employees.read"
|
|
1087
|
-
|
|
1088
|
-
# Check [RequirePermission] attributes in controllers
|
|
1089
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1090
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
1091
|
-
for f in $CTRL_FILES; do
|
|
1092
|
-
PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1093
|
-
for PERM in $PERM_VALS; do
|
|
1094
|
-
# Check each segment (except the action suffix) for concatenated multi-word without hyphens
|
|
1095
|
-
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
|
|
1096
|
-
for SEG in $SEGMENTS; do
|
|
1097
|
-
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1098
|
-
echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
|
|
1099
|
-
echo " Full permission: $PERM"
|
|
1100
|
-
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1101
|
-
echo " SmartStack convention: 'support-client.my-tickets.read'"
|
|
1102
|
-
exit 1
|
|
1103
|
-
fi
|
|
1104
|
-
done
|
|
1105
|
-
done
|
|
1106
|
-
done
|
|
1107
|
-
fi
|
|
1108
|
-
|
|
1109
|
-
# Check Permissions.cs constants
|
|
1110
|
-
PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
|
|
1111
|
-
if [ -n "$PERM_FILES" ]; then
|
|
1112
|
-
for f in $PERM_FILES; do
|
|
1113
|
-
CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1114
|
-
for PERM in $CONST_VALS; do
|
|
1115
|
-
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
1116
|
-
for SEG in $SEGMENTS; do
|
|
1117
|
-
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1118
|
-
echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
|
|
1119
|
-
echo " Full permission: $PERM"
|
|
1120
|
-
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1121
|
-
exit 1
|
|
1122
|
-
fi
|
|
1123
|
-
done
|
|
1124
|
-
done
|
|
1125
|
-
done
|
|
1126
|
-
fi
|
|
1127
|
-
|
|
1128
|
-
# Check PermissionsSeedData.cs for mismatched paths
|
|
1129
|
-
SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
|
|
1130
|
-
if [ -n "$SEED_PERM_FILES" ]; then
|
|
1131
|
-
PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
|
|
1132
|
-
for PERM in $PATHS; do
|
|
1133
|
-
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
1134
|
-
for SEG in $SEGMENTS; do
|
|
1135
|
-
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1136
|
-
echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
|
|
1137
|
-
echo " Full permission path: $PERM"
|
|
1138
|
-
echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
|
|
1139
|
-
exit 1
|
|
1140
|
-
fi
|
|
1141
|
-
done
|
|
1142
|
-
done
|
|
1143
|
-
fi
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
### POST-CHECK 42: Frontend navigate() calls must have matching route definitions (BLOCKING)
|
|
1147
|
-
|
|
1148
|
-
```bash
|
|
1149
|
-
# Detect dead links: navigate() calls to paths that don't have corresponding page components.
|
|
1150
|
-
# Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
|
|
1151
|
-
PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
|
|
1152
|
-
if [ -n "$PAGE_FILES" ]; then
|
|
1153
|
-
# Extract navigate targets (relative paths like '../leave-types', './create', etc.)
|
|
1154
|
-
NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
|
|
1155
|
-
# Extract route paths from App.tsx or route config
|
|
1156
|
-
APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
|
|
1157
|
-
if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
|
|
1158
|
-
ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
|
|
1159
|
-
for TARGET in $NAV_TARGETS; do
|
|
1160
|
-
# Skip dynamic segments (:id), back navigation (-1), and absolute URLs
|
|
1161
|
-
if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
|
|
1162
|
-
# Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
|
|
1163
|
-
LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
|
|
1164
|
-
if [ -z "$LAST_SEG" ]; then continue; fi
|
|
1165
|
-
# Check if any route path contains this segment
|
|
1166
|
-
FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
|
|
1167
|
-
if [ -z "$FOUND" ]; then
|
|
1168
|
-
# Verify no page component exists for this path
|
|
1169
|
-
SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
|
|
1170
|
-
PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
|
|
1171
|
-
if [ -z "$PAGE_EXISTS" ]; then
|
|
1172
|
-
# Find which file has this navigate call
|
|
1173
|
-
SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
|
|
1174
|
-
echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
|
|
1175
|
-
echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
|
|
1176
|
-
echo " Fix: Either create the page component + route, or remove the navigate() button"
|
|
1177
|
-
exit 1
|
|
1178
|
-
fi
|
|
1179
|
-
fi
|
|
1180
|
-
done
|
|
1181
|
-
fi
|
|
1182
|
-
fi
|
|
1183
|
-
```
|
|
1184
|
-
|
|
1185
|
-
### POST-CHECK 43: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)
|
|
1186
|
-
|
|
1187
|
-
```bash
|
|
1188
|
-
# Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
|
|
1189
|
-
# Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
|
|
1190
|
-
# instead of rendering sub-resource content inline. Users lost detail page context.
|
|
1191
|
-
DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
1192
|
-
if [ -n "$DETAIL_PAGES" ]; then
|
|
1193
|
-
FAIL=false
|
|
1194
|
-
for DP in $DETAIL_PAGES; do
|
|
1195
|
-
# Check if the page has tabs (activeTab state)
|
|
1196
|
-
HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
|
|
1197
|
-
if [ -z "$HAS_TABS" ]; then continue; fi
|
|
1198
|
-
|
|
1199
|
-
# Check if any tab click handler calls navigate()
|
|
1200
|
-
# Pattern: function that both references setActiveTab AND navigate()
|
|
1201
|
-
# Look for navigate() calls inside handlers that also set tab state
|
|
1202
|
-
TAB_NAVIGATE=$(grep -Pn "navigate\(" "$DP" 2>/dev/null | grep -v "navigate\(\s*['\"]edit['\"]" | grep -v "navigate\(\s*-1\s*\)" | grep -v "navigate\(\s*['\`].*/:id/edit" | grep -v "//")
|
|
1203
|
-
if [ -n "$TAB_NAVIGATE" ]; then
|
|
1204
|
-
# Verify this navigate is in a tab handler context (near setActiveTab usage)
|
|
1205
|
-
# Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
|
|
1206
|
-
RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
|
|
1207
|
-
if [ -n "$RELATIVE_NAV" ]; then
|
|
1208
|
-
echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
|
|
1209
|
-
echo " Tab click handlers MUST only call setActiveTab() — render content inline"
|
|
1210
|
-
echo " Found navigate() calls (likely in tab handlers):"
|
|
1211
|
-
echo "$RELATIVE_NAV"
|
|
1212
|
-
echo ""
|
|
1213
|
-
echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
|
|
1214
|
-
echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
|
|
1215
|
-
echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
|
|
1216
|
-
FAIL=true
|
|
1217
|
-
fi
|
|
1218
|
-
fi
|
|
1219
|
-
done
|
|
1220
|
-
if [ "$FAIL" = true ]; then
|
|
1221
|
-
exit 1
|
|
1222
|
-
fi
|
|
1223
|
-
fi
|
|
1224
|
-
```
|
|
1225
|
-
|
|
1226
|
-
### POST-CHECK 44: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
|
|
1227
|
-
|
|
1228
|
-
```bash
|
|
1229
|
-
# Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
|
|
1230
|
-
# Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
|
|
1231
|
-
# are added in subsequent iterations without re-running migration.
|
|
1232
|
-
SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
|
|
1233
|
-
DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
|
|
1234
|
-
if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
|
|
1235
|
-
# Extract DbSet entity names from DbContext (DbSet<EntityName>)
|
|
1236
|
-
DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
|
|
1237
|
-
FAIL=false
|
|
1238
|
-
for ENTITY in $DBSET_ENTITIES; do
|
|
1239
|
-
# Skip base SmartStack entities (handled by core migrations)
|
|
1240
|
-
if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
|
|
1241
|
-
continue
|
|
1242
|
-
fi
|
|
1243
|
-
# Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
|
|
1244
|
-
if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
|
|
1245
|
-
echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
|
|
1246
|
-
echo " This means no migration was created for this entity — it will not exist in the database."
|
|
1247
|
-
echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
|
|
1248
|
-
FAIL=true
|
|
1249
|
-
fi
|
|
1250
|
-
done
|
|
1251
|
-
if [ "$FAIL" = true ]; then
|
|
1252
|
-
echo ""
|
|
1253
|
-
echo " Root cause: Migration was likely created once for the first batch of entities,"
|
|
1254
|
-
echo " but additional entities were added later without regenerating the migration."
|
|
1255
|
-
echo " Fix: Create a new migration that covers ALL missing entities."
|
|
1256
|
-
exit 1
|
|
1257
|
-
fi
|
|
1258
|
-
fi
|
|
1259
|
-
```
|
|
1260
|
-
|
|
1261
|
-
### POST-CHECK 45: I18n namespace files must be registered in i18n config (BLOCKING)
|
|
1262
|
-
|
|
1263
|
-
```bash
|
|
1264
|
-
# Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
|
|
1265
|
-
# registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
|
|
1266
|
-
# got empty translations at runtime.
|
|
1267
|
-
I18N_CONFIG=$(find src/ web/ -path "*/i18n/config.ts" -o -path "*/i18n/index.ts" -o -path "*/i18n/i18n.ts" 2>/dev/null | grep -v node_modules | head -1)
|
|
1268
|
-
if [ -n "$I18N_CONFIG" ]; then
|
|
1269
|
-
# Find all module JSON files in the primary language (fr)
|
|
1270
|
-
FR_FILES=$(find src/ web/ -path "*/i18n/locales/fr/*.json" 2>/dev/null | grep -v node_modules | grep -v common.json | grep -v navigation.json)
|
|
1271
|
-
if [ -n "$FR_FILES" ]; then
|
|
1272
|
-
FAIL=false
|
|
1273
|
-
for JSON_FILE in $FR_FILES; do
|
|
1274
|
-
NS=$(basename "$JSON_FILE" .json)
|
|
1275
|
-
# Check if namespace is referenced in config (import or resource key)
|
|
1276
|
-
if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
|
|
1277
|
-
echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
|
|
1278
|
-
echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
|
|
1279
|
-
echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
|
|
1280
|
-
FAIL=true
|
|
1281
|
-
fi
|
|
1282
|
-
done
|
|
1283
|
-
if [ "$FAIL" = true ]; then
|
|
1284
|
-
exit 1
|
|
1285
|
-
fi
|
|
1286
|
-
fi
|
|
1287
|
-
fi
|
|
1288
|
-
```
|
|
1289
|
-
|
|
1290
|
-
### POST-CHECK 46: FluentValidation validators must be registered via DI (BLOCKING)
|
|
1291
|
-
|
|
1292
|
-
```bash
|
|
1293
|
-
# Root cause (test-apex-007): Validators existed but were never registered in DI.
|
|
1294
|
-
# Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
|
|
1295
|
-
VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
|
|
1296
|
-
if [ -n "$VALIDATOR_FILES" ]; then
|
|
1297
|
-
# Check DI registration file exists
|
|
1298
|
-
DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
|
|
1299
|
-
if [ -z "$DI_FILE" ]; then
|
|
1300
|
-
echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
|
|
1301
|
-
exit 1
|
|
1302
|
-
fi
|
|
1303
|
-
# Check for AddValidatorsFromAssembly or individual validator registration
|
|
1304
|
-
HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
|
|
1305
|
-
if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
|
|
1306
|
-
# Check individual registrations as fallback
|
|
1307
|
-
VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
|
|
1308
|
-
REGISTERED_COUNT=0
|
|
1309
|
-
for VF in $VALIDATOR_FILES; do
|
|
1310
|
-
VN=$(basename "$VF" .cs)
|
|
1311
|
-
if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
|
|
1312
|
-
REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
|
|
1313
|
-
fi
|
|
1314
|
-
done
|
|
1315
|
-
if [ "$REGISTERED_COUNT" -eq 0 ]; then
|
|
1316
|
-
echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
|
|
1317
|
-
echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
|
|
1318
|
-
echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
|
|
1319
|
-
exit 1
|
|
1320
|
-
fi
|
|
1321
|
-
fi
|
|
1322
|
-
fi
|
|
1323
|
-
```
|
|
1324
|
-
|
|
1325
|
-
### POST-CHECK 47: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)
|
|
1326
|
-
|
|
1327
|
-
```bash
|
|
1328
|
-
# Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
|
|
1329
|
-
# This causes: invalid date parsing, no date validation, inconsistent formats across clients.
|
|
1330
|
-
DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
|
|
1331
|
-
if [ -n "$DTO_FILES" ]; then
|
|
1332
|
-
FAIL=false
|
|
1333
|
-
for f in $DTO_FILES; do
|
|
1334
|
-
# Find string properties whose name contains "Date" (case-insensitive)
|
|
1335
|
-
BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
|
|
1336
|
-
if [ -n "$BAD_DATES" ]; then
|
|
1337
|
-
echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
|
|
1338
|
-
echo "$BAD_DATES"
|
|
1339
|
-
echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
|
|
1340
|
-
echo " DateOnly is the correct .NET type for date-only values (no time component)"
|
|
1341
|
-
FAIL=true
|
|
1342
|
-
fi
|
|
1343
|
-
done
|
|
1344
|
-
if [ "$FAIL" = true ]; then
|
|
1345
|
-
exit 1
|
|
1346
|
-
fi
|
|
1347
|
-
fi
|
|
1348
|
-
```
|
|
1349
|
-
|
|
1350
|
-
### POST-CHECK 48: NavRoute attribute values must use kebab-case (BLOCKING)
|
|
1351
|
-
|
|
1352
|
-
```bash
|
|
1353
|
-
# Root cause (test-apex-007): Controllers had [NavRoute("humanresources.employees")]
|
|
1354
|
-
# instead of [NavRoute("human-resources.employees")]. This causes route mismatch with
|
|
1355
|
-
# seed data and permission codes, resulting in 404s at runtime.
|
|
1356
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1357
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
1358
|
-
FAIL=false
|
|
1359
|
-
for f in $CTRL_FILES; do
|
|
1360
|
-
NAVROUTE_VALS=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1361
|
-
for NR in $NAVROUTE_VALS; do
|
|
1362
|
-
# Check each segment for concatenated multi-word without hyphens
|
|
1363
|
-
SEGMENTS=$(echo "$NR" | tr '.' '\n')
|
|
1364
|
-
for SEG in $SEGMENTS; do
|
|
1365
|
-
# Detect segments that look like concatenated words (lowercase, 8+ chars, no hyphens)
|
|
1366
|
-
# Use a simpler heuristic: lowercase-only segment with known multi-word patterns
|
|
1367
|
-
if echo "$SEG" | grep -qP '^[a-z]{8,}$'; then
|
|
1368
|
-
# Additional check: does it contain a known multi-word pattern?
|
|
1369
|
-
if echo "$SEG" | grep -qP '(human|project|leave|client|support|email|time|work|resource)'; then
|
|
1370
|
-
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
1371
|
-
echo " Full NavRoute: $NR"
|
|
1372
|
-
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources', 'projectmanagement' → 'project-management'"
|
|
1373
|
-
FAIL=true
|
|
1374
|
-
fi
|
|
1375
|
-
fi
|
|
1376
|
-
done
|
|
1377
|
-
done
|
|
1378
|
-
done
|
|
1379
|
-
if [ "$FAIL" = true ]; then
|
|
1380
|
-
exit 1
|
|
1381
|
-
fi
|
|
1382
|
-
fi
|
|
1383
|
-
```
|
|
1384
|
-
|
|
1385
|
-
### POST-CHECK 49: Every module with entities must have a migration covering them (BLOCKING)
|
|
1386
|
-
|
|
1387
|
-
```bash
|
|
1388
|
-
# Complementary to POST-CHECK 44 — checks from the entity side.
|
|
1389
|
-
# Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
|
|
1390
|
-
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
|
|
1391
|
-
MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
|
|
1392
|
-
if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
|
|
1393
|
-
MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
|
|
1394
|
-
if [ -z "$MIGRATION_FILES" ]; then
|
|
1395
|
-
echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
|
|
1396
|
-
exit 1
|
|
1397
|
-
fi
|
|
1398
|
-
FAIL=false
|
|
1399
|
-
for EF in $ENTITY_FILES; do
|
|
1400
|
-
ENTITY_NAME=$(basename "$EF" .cs)
|
|
1401
|
-
# Skip abstract base classes and interfaces
|
|
1402
|
-
if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
|
|
1403
|
-
# Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
|
|
1404
|
-
FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
|
|
1405
|
-
if [ -z "$FOUND" ]; then
|
|
1406
|
-
echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
|
|
1407
|
-
echo " This entity will NOT have a database table."
|
|
1408
|
-
echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
|
|
1409
|
-
FAIL=true
|
|
1410
|
-
fi
|
|
1411
|
-
done
|
|
1412
|
-
if [ "$FAIL" = true ]; then
|
|
1413
|
-
exit 1
|
|
1414
|
-
fi
|
|
1415
|
-
fi
|
|
1416
|
-
```
|
|
1417
|
-
|
|
1418
|
-
### POST-CHECK 50: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
|
|
1419
|
-
|
|
1420
|
-
```bash
|
|
1421
|
-
# Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
|
|
1422
|
-
# In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
|
|
1423
|
-
# [Route] is standard ASP.NET Core static routing. When both exist:
|
|
1424
|
-
# - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
|
|
1425
|
-
# - [Route] may or may not take over depending on middleware order
|
|
1426
|
-
# - Result: 404 on ALL endpoints
|
|
1427
|
-
# The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
|
|
1428
|
-
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1429
|
-
if [ -n "$CTRL_FILES" ]; then
|
|
1430
|
-
FAIL=false
|
|
1431
|
-
for f in $CTRL_FILES; do
|
|
1432
|
-
HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
|
|
1433
|
-
HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
|
|
1434
|
-
if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
|
|
1435
|
-
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
1436
|
-
ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
1437
|
-
echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
|
|
1438
|
-
echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
|
|
1439
|
-
echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
|
|
1440
|
-
echo " Having [Route] alongside it causes route conflicts and 404s."
|
|
1441
|
-
echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
|
|
1442
|
-
FAIL=true
|
|
1443
|
-
fi
|
|
1444
|
-
done
|
|
1445
|
-
if [ "$FAIL" = true ]; then
|
|
1446
|
-
exit 1
|
|
1447
|
-
fi
|
|
1448
|
-
fi
|
|
1449
|
-
```
|
|
1450
|
-
|
|
1451
|
-
### POST-CHECK 51: RolesSeedData must map standard role-permission matrix (BLOCKING)
|
|
1452
|
-
|
|
1453
|
-
```bash
|
|
1454
|
-
# SmartStack standard role-permission matrix:
|
|
1455
|
-
# Admin = wildcard (*) — full access
|
|
1456
|
-
# Manager = CRU (read + create + update) — no delete
|
|
1457
|
-
# Contributor = CR (read + create) — no update, no delete
|
|
1458
|
-
# Viewer = R (read only)
|
|
1459
|
-
# If RolesSeedData deviates from this matrix, the RBAC model is broken.
|
|
1460
|
-
ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
|
|
1461
|
-
if [ -n "$ROLE_SEED_FILES" ]; then
|
|
1462
|
-
FAIL=false
|
|
1463
|
-
for f in $ROLE_SEED_FILES; do
|
|
1464
|
-
# Skip ApplicationRolesSeedData (defines roles, not mappings)
|
|
1465
|
-
BASENAME=$(basename "$f")
|
|
1466
|
-
if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi
|
|
1467
|
-
|
|
1468
|
-
# Check Admin has wildcard
|
|
1469
|
-
HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null)
|
|
1470
|
-
if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
|
|
1471
|
-
# Also accept .Access or wildcard pattern
|
|
1472
|
-
HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null)
|
|
1473
|
-
if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
|
|
1474
|
-
echo "BLOCKING: Admin role missing wildcard (*) permission in $f"
|
|
1475
|
-
echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
|
|
1476
|
-
FAIL=true
|
|
1477
|
-
fi
|
|
1478
|
-
fi
|
|
1479
|
-
|
|
1480
|
-
# Check Viewer has NO delete/create/update
|
|
1481
|
-
VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null)
|
|
1482
|
-
if [ "$VIEWER_WRITE" -gt 0 ]; then
|
|
1483
|
-
echo "BLOCKING: Viewer role has write permissions (create/update/delete) in $f"
|
|
1484
|
-
echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
|
|
1485
|
-
FAIL=true
|
|
1486
|
-
fi
|
|
1487
|
-
|
|
1488
|
-
# Check Manager has NO delete
|
|
1489
|
-
MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null)
|
|
1490
|
-
if [ "$MANAGER_DELETE" -gt 0 ]; then
|
|
1491
|
-
echo "WARNING: Manager role has delete permission in $f"
|
|
1492
|
-
echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
|
|
1493
|
-
fi
|
|
1494
|
-
done
|
|
1495
|
-
if [ "$FAIL" = true ]; then
|
|
1496
|
-
exit 1
|
|
1497
|
-
fi
|
|
1498
|
-
fi
|
|
1499
|
-
```
|
|
1500
|
-
|
|
1501
|
-
### POST-CHECK 52: PermissionAction enum must use valid typed values only (BLOCKING)
|
|
1502
|
-
|
|
1503
|
-
```bash
|
|
1504
|
-
# Valid PermissionAction enum values: Access(0), Read(1), Create(2), Update(3), Delete(4),
|
|
1505
|
-
# Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)
|
|
1506
|
-
# FORBIDDEN: Enum.Parse<PermissionAction>("...") — runtime crash if value doesn't exist
|
|
1507
|
-
# FORBIDDEN: (PermissionAction)99 or any cast beyond 0-10
|
|
1508
|
-
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
1509
|
-
if [ -n "$SEED_FILES" ]; then
|
|
1510
|
-
FAIL=false
|
|
1511
|
-
for f in $SEED_FILES; do
|
|
1512
|
-
# Check for Enum.Parse<PermissionAction> usage
|
|
1513
|
-
ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null)
|
|
1514
|
-
if [ -n "$ENUM_PARSE" ]; then
|
|
1515
|
-
echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
|
|
1516
|
-
echo "$ENUM_PARSE"
|
|
1517
|
-
echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
|
|
1518
|
-
FAIL=true
|
|
1519
|
-
fi
|
|
1520
|
-
|
|
1521
|
-
# Check for invalid cast values (PermissionAction)N where N > 10
|
|
1522
|
-
INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null)
|
|
1523
|
-
if [ -n "$INVALID_CAST" ]; then
|
|
1524
|
-
echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
|
|
1525
|
-
echo "$INVALID_CAST"
|
|
1526
|
-
echo "Valid values: Access(0), Read(1), Create(2), Update(3), Delete(4), Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)"
|
|
1527
|
-
FAIL=true
|
|
1528
|
-
fi
|
|
1529
|
-
done
|
|
1530
|
-
if [ "$FAIL" = true ]; then
|
|
1531
|
-
exit 1
|
|
1532
|
-
fi
|
|
1533
|
-
fi
|
|
1534
|
-
```
|
|
1535
|
-
|
|
1536
|
-
### POST-CHECK 53: Navigation translation completeness — 4 languages per level (BLOCKING)
|
|
1537
|
-
|
|
1538
|
-
```bash
|
|
1539
|
-
# Every navigation seed data file must provide translations for ALL 4 languages (fr, en, it, de).
|
|
1540
|
-
# If sections exist (GetSectionEntries), GetSectionTranslationEntries MUST also exist.
|
|
1541
|
-
# If resources exist (GetResourceEntries), resource translation entries MUST also exist.
|
|
1542
|
-
NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
|
|
1543
|
-
if [ -n "$NAV_SEED_FILES" ]; then
|
|
1544
|
-
FAIL=false
|
|
1545
|
-
for f in $NAV_SEED_FILES; do
|
|
1546
|
-
# Check module translations have all 4 languages
|
|
1547
|
-
LANG_COUNT=$(grep -c 'LanguageCode\s*=' "$f" 2>/dev/null)
|
|
1548
|
-
HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null)
|
|
1549
|
-
HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null)
|
|
1550
|
-
HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null)
|
|
1551
|
-
HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null)
|
|
1552
|
-
|
|
1553
|
-
if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
|
|
1554
|
-
echo "BLOCKING: Missing language(s) in navigation translations: $f"
|
|
1555
|
-
echo " fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
|
|
1556
|
-
echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
|
|
1557
|
-
FAIL=true
|
|
1558
|
-
fi
|
|
1559
|
-
|
|
1560
|
-
# If sections exist, section translations MUST exist
|
|
1561
|
-
HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null)
|
|
1562
|
-
HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null)
|
|
1563
|
-
if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
|
|
1564
|
-
echo "BLOCKING: Sections defined but GetSectionTranslationEntries() missing: $f"
|
|
1565
|
-
echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
|
|
1566
|
-
FAIL=true
|
|
1567
|
-
fi
|
|
1568
|
-
|
|
1569
|
-
# If resources exist, resource translations MUST exist
|
|
1570
|
-
HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null)
|
|
1571
|
-
HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null)
|
|
1572
|
-
if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
|
|
1573
|
-
echo "BLOCKING: Resources defined but resource translations missing: $f"
|
|
1574
|
-
echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
|
|
1575
|
-
FAIL=true
|
|
1576
|
-
fi
|
|
1577
|
-
done
|
|
1578
|
-
if [ "$FAIL" = true ]; then
|
|
1579
|
-
exit 1
|
|
1580
|
-
fi
|
|
1581
|
-
fi
|
|
1582
|
-
```
|
|
1583
|
-
|
|
1584
|
-
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|
|
1
|
+
# BLOCKING POST-CHECKs
|
|
2
|
+
|
|
3
|
+
> **Referenced by:** step-04-examine.md (section 6b)
|
|
4
|
+
> These checks run on the actual generated files. Model-interpreted checks are unreliable.
|
|
5
|
+
|
|
6
|
+
### POST-CHECK 1: Navigation routes must be full paths starting with /
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# Find all seed data files and check route values
|
|
10
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
11
|
+
if [ -n "$SEED_FILES" ]; then
|
|
12
|
+
# Check for short routes (no leading /) in Create() calls for navigation entities
|
|
13
|
+
BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
|
|
14
|
+
if [ -n "$BAD_ROUTES" ]; then
|
|
15
|
+
echo "BLOCKING: Navigation routes must be full paths starting with /"
|
|
16
|
+
echo "$BAD_ROUTES"
|
|
17
|
+
echo "Expected: \"/human-resources\" NOT \"humanresources\""
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### POST-CHECK 2: All services must filter by TenantId (OWASP A01)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Find all service implementation files
|
|
27
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
28
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
29
|
+
# Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
|
|
30
|
+
for f in $SERVICE_FILES; do
|
|
31
|
+
# Accept either _currentTenant.TenantId (strict guard clause or nullable usage)
|
|
32
|
+
# or entities with IOptionalTenantEntity/IScopedTenantEntity (optional tenant pattern)
|
|
33
|
+
HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
|
|
34
|
+
HAS_OPTIONAL_ENTITY=false
|
|
35
|
+
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
|
|
36
|
+
HAS_OPTIONAL_ENTITY=true
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
|
|
40
|
+
echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
|
|
41
|
+
echo "Every service query MUST filter by _currentTenant.TenantId"
|
|
42
|
+
echo "OR work with entities that implement IOptionalTenantEntity/IScopedTenantEntity"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
if grep -q "Guid.Empty" "$f"; then
|
|
46
|
+
echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
fi
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### POST-CHECK 3: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Find all controller files
|
|
57
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
58
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
59
|
+
for f in $CTRL_FILES; do
|
|
60
|
+
# Check controller has at least one RequirePermission attribute
|
|
61
|
+
if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
|
|
62
|
+
echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
|
|
63
|
+
echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
|
|
64
|
+
echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
fi
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### POST-CHECK 4: Seed data must not use Guid.NewGuid()
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
75
|
+
if [ -n "$SEED_FILES" ]; then
|
|
76
|
+
BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
|
|
77
|
+
if [ -n "$BAD_GUIDS" ]; then
|
|
78
|
+
echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
|
|
79
|
+
echo "$BAD_GUIDS"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### POST-CHECK 5: Services must inject ICurrentTenantService (tenant isolation)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
89
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
90
|
+
for f in $SERVICE_FILES; do
|
|
91
|
+
# Accept either ICurrentTenantService or ICurrentUser (legacy) for tenant context
|
|
92
|
+
if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
|
|
93
|
+
echo "BLOCKING: Service missing tenant context injection: $f"
|
|
94
|
+
echo "All services MUST inject ICurrentTenantService for tenant isolation"
|
|
95
|
+
echo "Pattern: private readonly ICurrentTenantService _currentTenant;"
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
fi
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### POST-CHECK 6: Translation files must exist for all 4 languages (if frontend)
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Find all i18n namespaces used in tsx files
|
|
106
|
+
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
107
|
+
if [ -n "$TSX_FILES" ]; then
|
|
108
|
+
NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
|
|
109
|
+
for NS in $NAMESPACES; do
|
|
110
|
+
for LANG in fr en it de; do
|
|
111
|
+
if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
|
|
112
|
+
echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
done
|
|
116
|
+
done
|
|
117
|
+
fi
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### POST-CHECK 7: Pages must use lazy loading (no static page imports in routes)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
|
|
124
|
+
if [ -n "$ROUTE_FILES" ]; then
|
|
125
|
+
STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
|
|
126
|
+
if [ -n "$STATIC_PAGE_IMPORTS" ]; then
|
|
127
|
+
echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
|
|
128
|
+
echo "$STATIC_PAGE_IMPORTS"
|
|
129
|
+
echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
|
|
139
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
140
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
141
|
+
FAIL=false
|
|
142
|
+
|
|
143
|
+
# 8a. Component imports (Modal, Dialog, Drawer, etc.)
|
|
144
|
+
MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
|
|
145
|
+
if [ -n "$MODAL_IMPORTS" ]; then
|
|
146
|
+
echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
|
|
147
|
+
echo "$MODAL_IMPORTS"
|
|
148
|
+
FAIL=true
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# 8b. Inline form state (catches drawers/slide-overs built without external components)
|
|
152
|
+
FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
|
|
153
|
+
if [ -n "$FORM_STATE" ]; then
|
|
154
|
+
echo "BLOCKING: Inline form state detected — forms embedded in ListPage as drawers/panels"
|
|
155
|
+
echo "Create/Edit forms MUST be separate page components with their own URL routes"
|
|
156
|
+
echo "$FORM_STATE"
|
|
157
|
+
FAIL=true
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if [ "$FAIL" = true ]; then
|
|
161
|
+
echo ""
|
|
162
|
+
echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
|
|
163
|
+
echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
|
|
164
|
+
exit 1
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### POST-CHECK 9: Create/Edit pages must exist as separate route pages (BLOCKING)
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# For each module with a list page, verify create and edit pages exist
|
|
173
|
+
# If ListPage has navigate() calls to /create or /:id/edit, the target pages MUST exist
|
|
174
|
+
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
|
|
175
|
+
FAIL=false
|
|
176
|
+
if [ -n "$LIST_PAGES" ]; then
|
|
177
|
+
for LIST_PAGE in $LIST_PAGES; do
|
|
178
|
+
PAGE_DIR=$(dirname "$LIST_PAGE")
|
|
179
|
+
MODULE_NAME=$(basename "$PAGE_DIR")
|
|
180
|
+
|
|
181
|
+
# Detect if ListPage navigates to /create or /edit routes
|
|
182
|
+
HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
|
|
183
|
+
HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null)
|
|
184
|
+
|
|
185
|
+
# Check for create page
|
|
186
|
+
CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
|
|
187
|
+
if [ -z "$CREATE_PAGE" ]; then
|
|
188
|
+
if [ -n "$HAS_CREATE_NAV" ]; then
|
|
189
|
+
echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
|
|
190
|
+
echo " Dead link: $HAS_CREATE_NAV"
|
|
191
|
+
echo " Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
|
|
192
|
+
FAIL=true
|
|
193
|
+
else
|
|
194
|
+
echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Check for edit page
|
|
199
|
+
EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
|
|
200
|
+
if [ -z "$EDIT_PAGE" ]; then
|
|
201
|
+
if [ -n "$HAS_EDIT_NAV" ]; then
|
|
202
|
+
echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
|
|
203
|
+
echo " Dead link: $HAS_EDIT_NAV"
|
|
204
|
+
echo " Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
|
|
205
|
+
FAIL=true
|
|
206
|
+
else
|
|
207
|
+
echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
done
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if [ "$FAIL" = true ]; then
|
|
214
|
+
echo ""
|
|
215
|
+
echo "BLOCKING: Create/Edit pages are MISSING but ListPage buttons link to them."
|
|
216
|
+
echo "Users will see white screen / 404 when clicking Create or Edit buttons."
|
|
217
|
+
echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### POST-CHECK 10: Form pages must have companion test files
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Minimum requirement: if frontend pages exist, at least 1 test file must be present
|
|
226
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
|
|
227
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
228
|
+
ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
|
|
229
|
+
if [ -z "$ALL_TESTS" ]; then
|
|
230
|
+
echo "BLOCKING: No frontend test files found in src/pages/"
|
|
231
|
+
echo "Every form page MUST have a companion .test.tsx file"
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# Every CreatePage and EditPage must have a .test.tsx file
|
|
237
|
+
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
|
|
238
|
+
if [ -n "$FORM_PAGES" ]; then
|
|
239
|
+
for FORM_PAGE in $FORM_PAGES; do
|
|
240
|
+
TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
|
|
241
|
+
if [ ! -f "$TEST_FILE" ]; then
|
|
242
|
+
echo "BLOCKING: Form page missing test file: $FORM_PAGE"
|
|
243
|
+
echo "Expected: $TEST_FILE"
|
|
244
|
+
echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
|
|
245
|
+
exit 1
|
|
246
|
+
fi
|
|
247
|
+
done
|
|
248
|
+
fi
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### POST-CHECK 11: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (BLOCKING)
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
|
|
255
|
+
# Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
|
|
256
|
+
ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
257
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
258
|
+
FAIL=false
|
|
259
|
+
|
|
260
|
+
# 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
|
|
261
|
+
FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
|
|
262
|
+
if [ -n "$FK_INPUTS" ]; then
|
|
263
|
+
echo "BLOCKING: FK fields rendered as <input> — MUST use EntityLookup component"
|
|
264
|
+
echo "$FK_INPUTS"
|
|
265
|
+
FAIL=true
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
|
|
269
|
+
FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
|
|
270
|
+
if [ -n "$FK_SELECTS" ]; then
|
|
271
|
+
echo "BLOCKING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
|
|
272
|
+
echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
|
|
273
|
+
echo "EntityLookup provides: debounced search, paginated results, display name resolution."
|
|
274
|
+
echo "$FK_SELECTS"
|
|
275
|
+
FAIL=true
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
# 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
|
|
279
|
+
FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
|
|
280
|
+
if [ -n "$FK_SELECT_ONCHANGE" ]; then
|
|
281
|
+
echo "BLOCKING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
|
|
282
|
+
echo "$FK_SELECT_ONCHANGE"
|
|
283
|
+
FAIL=true
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
|
|
287
|
+
FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*(?:[Ee]nter.*[Ii][Dd]|[Gg][Uu][Ii][Dd]|[Ss]elect.*[Ee]mployee|[Ss]elect.*[Dd]epartment|[Ss]elect.*[Pp]osition|[Ss]elect.*[Pp]roject|[Ss]elect.*[Cc]ategory)' $ALL_PAGES 2>/dev/null)
|
|
288
|
+
if [ -n "$FK_PLACEHOLDER" ]; then
|
|
289
|
+
echo "BLOCKING: Form has placeholder for FK field selection — use EntityLookup search instead"
|
|
290
|
+
echo "$FK_PLACEHOLDER"
|
|
291
|
+
FAIL=true
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# 5. Detect <option> elements with GUID-like values (sign of FK <select>)
|
|
295
|
+
FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
|
|
296
|
+
if [ -n "$FK_OPTIONS" ]; then
|
|
297
|
+
echo "BLOCKING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
|
|
298
|
+
echo "Replace the entire <select>/<option> block with <EntityLookup />"
|
|
299
|
+
echo "$FK_OPTIONS"
|
|
300
|
+
FAIL=true
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
if [ "$FAIL" = true ]; then
|
|
304
|
+
echo ""
|
|
305
|
+
echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
|
|
306
|
+
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
307
|
+
echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
|
|
308
|
+
echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
|
|
309
|
+
exit 1
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### POST-CHECK 12: Backend APIs must support search parameter for EntityLookup
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Check that controller GetAll methods accept search parameter
|
|
318
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
319
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
320
|
+
for f in $CTRL_FILES; do
|
|
321
|
+
# Check if controller has GetAll but no search parameter
|
|
322
|
+
if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
|
|
323
|
+
if ! grep -q "search" "$f"; then
|
|
324
|
+
echo "WARNING: Controller missing search parameter on GetAll: $f"
|
|
325
|
+
echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
|
|
326
|
+
echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
done
|
|
330
|
+
fi
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### POST-CHECK 13: No hardcoded Tailwind colors in generated pages (BLOCKING)
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
|
|
337
|
+
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
338
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
339
|
+
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
|
|
340
|
+
if [ -n "$HARDCODED" ]; then
|
|
341
|
+
echo "BLOCKING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
|
|
342
|
+
echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
|
|
343
|
+
echo ""
|
|
344
|
+
echo "Fix mapping:"
|
|
345
|
+
echo " bg-white → bg-[var(--bg-card)]"
|
|
346
|
+
echo " bg-gray-50 → bg-[var(--bg-primary)]"
|
|
347
|
+
echo " text-gray-900 → text-[var(--text-primary)]"
|
|
348
|
+
echo " text-gray-500 → text-[var(--text-secondary)]"
|
|
349
|
+
echo " border-gray-200 → border-[var(--border-color)]"
|
|
350
|
+
echo " bg-blue-600 → bg-[var(--color-accent-500)]"
|
|
351
|
+
echo " text-blue-600 → text-[var(--color-accent-500)]"
|
|
352
|
+
echo " text-red-500 → text-[var(--error-text)]"
|
|
353
|
+
echo " bg-green-500 → bg-[var(--success-bg)]"
|
|
354
|
+
echo ""
|
|
355
|
+
echo "See references/smartstack-frontend.md section 4 for full variable reference"
|
|
356
|
+
echo ""
|
|
357
|
+
echo "$HARDCODED"
|
|
358
|
+
exit 1
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### POST-CHECK 14: Routes seed data must match frontend application routes
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
SEED_ROUTES=$(grep -Poh 'Route\s*=\s*"([^"]+)"' $(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null) 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
367
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
368
|
+
if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
|
|
369
|
+
FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
|
|
370
|
+
if [ -n "$FRONTEND_PATHS" ]; then
|
|
371
|
+
MISMATCH_FOUND=false
|
|
372
|
+
for SEED_ROUTE in $SEED_ROUTES; do
|
|
373
|
+
DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
|
|
374
|
+
if [ "$DEPTH" -lt 3 ]; then continue; fi
|
|
375
|
+
SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
|
|
376
|
+
SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
377
|
+
MATCH_FOUND=false
|
|
378
|
+
for FE_PATH in $FRONTEND_PATHS; do
|
|
379
|
+
# Flag FORBIDDEN /list suffix BEFORE normalization
|
|
380
|
+
if echo "$FE_PATH" | grep -qP '/list$'; then
|
|
381
|
+
echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
|
|
382
|
+
fi
|
|
383
|
+
FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
|
|
384
|
+
FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
385
|
+
if [ "$SEED_NORM" = "$FE_NORM" ]; then
|
|
386
|
+
MATCH_FOUND=true
|
|
387
|
+
break
|
|
388
|
+
fi
|
|
389
|
+
done
|
|
390
|
+
if [ "$MATCH_FOUND" = false ]; then
|
|
391
|
+
echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
|
|
392
|
+
MISMATCH_FOUND=true
|
|
393
|
+
fi
|
|
394
|
+
done
|
|
395
|
+
if [ "$MISMATCH_FOUND" = true ]; then
|
|
396
|
+
echo "Fix: Ensure every NavigationSeedData route has a corresponding route entry in App.tsx (applicationRoutes or JSX Route wrappers)"
|
|
397
|
+
exit 1
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
fi
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### POST-CHECK 14b: Frontend routes must use kebab-case (BLOCKING)
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
# POST-CHECK 14 normalizes hyphens for existence check, but does NOT catch kebab-case mismatches.
|
|
407
|
+
# This supplementary check detects concatenated multi-word route segments without hyphens.
|
|
408
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
409
|
+
if [ -n "$APP_TSX" ]; then
|
|
410
|
+
# Extract route path strings from App.tsx
|
|
411
|
+
FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
|
|
412
|
+
for FE_PATH in $FE_PATHS; do
|
|
413
|
+
# Split path by / and check each segment
|
|
414
|
+
for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
|
|
415
|
+
# Skip dynamic segments (:id, :slug) and single words (< 10 chars likely single word)
|
|
416
|
+
echo "$SEG" | grep -qP '^:' && continue
|
|
417
|
+
# Detect multi-word segments without hyphens: 2+ consecutive lowercase sequences
|
|
418
|
+
# e.g., "humanresources" (human+resources), "timemanagement" (time+management)
|
|
419
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
420
|
+
# Potential concatenated multi-word — cross-check with seed data
|
|
421
|
+
SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG")
|
|
422
|
+
if [ -n "$SEED_MATCH" ]; then
|
|
423
|
+
echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
|
|
424
|
+
echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
|
|
425
|
+
echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
|
|
426
|
+
exit 1
|
|
427
|
+
fi
|
|
428
|
+
fi
|
|
429
|
+
done
|
|
430
|
+
done
|
|
431
|
+
fi
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### POST-CHECK 15: HasQueryFilter must not use Guid.Empty (OWASP A01)
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
438
|
+
if [ -n "$CONFIG_FILES" ]; then
|
|
439
|
+
BAD_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
|
|
440
|
+
if [ -n "$BAD_FILTERS" ]; then
|
|
441
|
+
echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
|
|
442
|
+
echo "$BAD_FILTERS"
|
|
443
|
+
echo ""
|
|
444
|
+
echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
|
|
445
|
+
echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
|
|
446
|
+
exit 1
|
|
447
|
+
fi
|
|
448
|
+
fi
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### POST-CHECK 16: GetAll methods must return PaginatedResult<T>
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
455
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
456
|
+
BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
|
|
457
|
+
if [ -n "$BAD_RETURNS" ]; then
|
|
458
|
+
echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
|
|
459
|
+
echo "$BAD_RETURNS"
|
|
460
|
+
echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
|
|
461
|
+
exit 1
|
|
462
|
+
fi
|
|
463
|
+
fi
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### POST-CHECK 17: i18n files must contain required structural keys
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
I18N_DIR="src/i18n/locales/fr"
|
|
470
|
+
if [ -d "$I18N_DIR" ]; then
|
|
471
|
+
REQUIRED_KEYS="actions columns empty errors form labels messages validation"
|
|
472
|
+
for JSON_FILE in "$I18N_DIR"/*.json; do
|
|
473
|
+
[ ! -f "$JSON_FILE" ] && continue
|
|
474
|
+
BASENAME=$(basename "$JSON_FILE")
|
|
475
|
+
case "$BASENAME" in common.json|navigation.json) continue;; esac
|
|
476
|
+
for KEY in $REQUIRED_KEYS; do
|
|
477
|
+
if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
|
|
478
|
+
echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
|
|
479
|
+
echo "Module i18n files MUST contain: $REQUIRED_KEYS"
|
|
480
|
+
exit 1
|
|
481
|
+
fi
|
|
482
|
+
done
|
|
483
|
+
done
|
|
484
|
+
fi
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### POST-CHECK 18: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
|
|
491
|
+
if [ -n "$ENTITY_FILES" ]; then
|
|
492
|
+
for f in $ENTITY_FILES; do
|
|
493
|
+
if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
|
|
494
|
+
echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
|
|
495
|
+
echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
|
|
496
|
+
exit 1
|
|
497
|
+
fi
|
|
498
|
+
done
|
|
499
|
+
fi
|
|
500
|
+
CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
|
|
501
|
+
if [ -n "$CREATE_VALIDATORS" ]; then
|
|
502
|
+
for f in $CREATE_VALIDATORS; do
|
|
503
|
+
VALIDATOR_DIR=$(dirname "$f")
|
|
504
|
+
ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
|
|
505
|
+
if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
|
|
506
|
+
echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
|
|
507
|
+
echo " Found: $f"
|
|
508
|
+
echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
|
|
509
|
+
exit 1
|
|
510
|
+
fi
|
|
511
|
+
done
|
|
512
|
+
fi
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### POST-CHECK 19: (REMOVED — Context level no longer exists in SmartStack navigation hierarchy)
|
|
516
|
+
|
|
517
|
+
### POST-CHECK 20: RolePermission seed data must NOT use deterministic role GUIDs
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
# System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
|
|
521
|
+
# RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
|
|
522
|
+
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
523
|
+
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
|
|
524
|
+
if [ -n "$SEED_ALL_FILES" ]; then
|
|
525
|
+
BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
|
|
526
|
+
if [ -n "$BAD_ROLE_GUID" ]; then
|
|
527
|
+
echo "BLOCKING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
|
|
528
|
+
echo "System roles are pre-seeded by SmartStack core with their own IDs"
|
|
529
|
+
echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
|
|
530
|
+
echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
|
|
531
|
+
echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
|
|
532
|
+
echo "$BAD_ROLE_GUID"
|
|
533
|
+
exit 1
|
|
534
|
+
fi
|
|
535
|
+
fi
|
|
536
|
+
# Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
|
|
537
|
+
ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
|
|
538
|
+
if [ -n "$ROLE_PERM_FILES" ]; then
|
|
539
|
+
BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
|
|
540
|
+
if [ -n "$BAD_ROLE_REF" ]; then
|
|
541
|
+
echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
|
|
542
|
+
echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
|
|
543
|
+
echo "$BAD_ROLE_REF"
|
|
544
|
+
fi
|
|
545
|
+
fi
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### POST-CHECK 21: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
# The !.Value pattern on Guid? throws InvalidOperationException (500) instead of clean 401
|
|
552
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
553
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
554
|
+
BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
|
|
555
|
+
if [ -n "$BAD_PATTERN" ]; then
|
|
556
|
+
echo "BLOCKING: Services use TenantId!.Value — causes 500 instead of 400 when tenant context is missing"
|
|
557
|
+
echo "$BAD_PATTERN"
|
|
558
|
+
echo ""
|
|
559
|
+
echo "Fix: Replace with guard clause at the start of every method:"
|
|
560
|
+
echo " var tenantId = _currentTenant.TenantId"
|
|
561
|
+
echo " ?? throw new TenantContextRequiredException();"
|
|
562
|
+
echo ""
|
|
563
|
+
echo "This produces a clean 400 Bad Request via GlobalExceptionHandlerMiddleware."
|
|
564
|
+
echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
|
|
565
|
+
exit 1
|
|
566
|
+
fi
|
|
567
|
+
fi
|
|
568
|
+
|
|
569
|
+
# POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
|
|
570
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
571
|
+
BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
|
|
572
|
+
if [ -n "$BAD_UNAUTH" ]; then
|
|
573
|
+
echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
|
|
574
|
+
echo "$BAD_UNAUTH"
|
|
575
|
+
echo ""
|
|
576
|
+
echo "Fix: Replace with:"
|
|
577
|
+
echo " var tenantId = _currentTenant.TenantId"
|
|
578
|
+
echo " ?? throw new TenantContextRequiredException();"
|
|
579
|
+
echo ""
|
|
580
|
+
echo "TenantContextRequiredException returns 400 Bad Request (does not clear token)."
|
|
581
|
+
echo "UnauthorizedAccessException returns 401 Unauthorized (clears token + redirects to login)."
|
|
582
|
+
exit 1
|
|
583
|
+
fi
|
|
584
|
+
fi
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### POST-CHECK 22: Cross-tenant entities must use Guid? TenantId
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
591
|
+
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
|
|
592
|
+
if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
|
|
593
|
+
echo "BLOCKING: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
|
|
594
|
+
exit 1
|
|
595
|
+
fi
|
|
596
|
+
fi
|
|
597
|
+
done
|
|
598
|
+
echo "POST-CHECK 22: OK"
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### POST-CHECK 23: Scoped entities must have EntityScope property
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
605
|
+
if grep -q "IScopedTenantEntity" "$entity"; then
|
|
606
|
+
if ! grep -q "EntityScope\|Scope" "$entity"; then
|
|
607
|
+
echo "BLOCKING: Entity with IScopedTenantEntity must have EntityScope Scope property"
|
|
608
|
+
exit 1
|
|
609
|
+
fi
|
|
610
|
+
fi
|
|
611
|
+
done
|
|
612
|
+
echo "POST-CHECK 23: OK"
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### POST-CHECK 24: Permissions.cs static constants must exist (BLOCKING)
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
# Every module with controllers MUST have a Permissions.cs with static constants
|
|
619
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
620
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
621
|
+
PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u)
|
|
622
|
+
for MODULE in $PERM_REFS; do
|
|
623
|
+
PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null)
|
|
624
|
+
if [ -z "$PERM_FILE" ]; then
|
|
625
|
+
echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
|
|
626
|
+
echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
|
|
627
|
+
exit 1
|
|
628
|
+
fi
|
|
629
|
+
done
|
|
630
|
+
fi
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### POST-CHECK 25: ApplicationRolesSeedData.cs must exist (BLOCKING)
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
# If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
|
|
637
|
+
ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
|
|
638
|
+
if [ -n "$ROLE_SEED" ]; then
|
|
639
|
+
APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
|
|
640
|
+
if [ -z "$APP_ROLE_SEED" ]; then
|
|
641
|
+
echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
|
|
642
|
+
echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
|
|
643
|
+
echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
|
|
644
|
+
echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
|
|
645
|
+
exit 1
|
|
646
|
+
fi
|
|
647
|
+
fi
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### POST-CHECK 25b: Section route completeness (NavigationSection → frontend route + permissions)
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
# Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
|
|
654
|
+
# and section-level permissions MUST exist for each section defined in seed data
|
|
655
|
+
SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
|
|
656
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
657
|
+
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
|
|
658
|
+
# Extract section routes from seed data
|
|
659
|
+
SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u)
|
|
660
|
+
for SECTION_ROUTE in $SECTION_ROUTES; do
|
|
661
|
+
# Extract the last segment (section-kebab) for frontend route matching
|
|
662
|
+
SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
|
|
663
|
+
if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
|
|
664
|
+
echo "BLOCKING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
|
|
665
|
+
echo "Expected path segment '$SECTION_SEG' in App.tsx application route block"
|
|
666
|
+
echo "Fix: Add section child routes to the module's children array in App.tsx"
|
|
667
|
+
fi
|
|
668
|
+
done
|
|
669
|
+
fi
|
|
670
|
+
|
|
671
|
+
# Controllers with section-level [NavRoute] (4 segments) must have matching [RequirePermission]
|
|
672
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
673
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
674
|
+
for f in $CTRL_FILES; do
|
|
675
|
+
# Match NavRoute with 4 dot-separated segments (section-level)
|
|
676
|
+
SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z]+\.[a-z]+\.[a-z]+\.[a-z]+"\)' "$f" 2>/dev/null)
|
|
677
|
+
if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
|
|
678
|
+
echo "BLOCKING: Section controller has [NavRoute] but no [RequirePermission]: $f"
|
|
679
|
+
echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
|
|
680
|
+
exit 1
|
|
681
|
+
fi
|
|
682
|
+
done
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
# Section-level permissions must exist for each section in seed data
|
|
686
|
+
PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
|
|
687
|
+
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
|
|
688
|
+
SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
689
|
+
for CODE in $SECTION_CODES; do
|
|
690
|
+
PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
|
|
691
|
+
if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
|
|
692
|
+
echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
|
|
693
|
+
echo "Fix: Add section-level permissions via MCP generate_permissions with 4-segment navRoute"
|
|
694
|
+
fi
|
|
695
|
+
done
|
|
696
|
+
fi
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### POST-CHECK 26: FORBIDDEN route patterns — /list and /detail/:id (BLOCKING)
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
# 1. Check seed data for FORBIDDEN suffixes
|
|
703
|
+
SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
|
|
704
|
+
if [ -n "$SEED_NAV_FILES" ]; then
|
|
705
|
+
BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
|
|
706
|
+
if [ -n "$BAD_ROUTES" ]; then
|
|
707
|
+
echo "BLOCKING: FORBIDDEN route pattern in seed data"
|
|
708
|
+
echo " - 'list' section route = module route (NO /list suffix)"
|
|
709
|
+
echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
|
|
710
|
+
echo "$BAD_ROUTES"
|
|
711
|
+
exit 1
|
|
712
|
+
fi
|
|
713
|
+
fi
|
|
714
|
+
|
|
715
|
+
# 2. Check frontend routes for FORBIDDEN path segments
|
|
716
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
717
|
+
if [ -n "$APP_TSX" ]; then
|
|
718
|
+
BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null)
|
|
719
|
+
if [ -n "$BAD_FE" ]; then
|
|
720
|
+
echo "BLOCKING: FORBIDDEN frontend route path"
|
|
721
|
+
echo " - list = index: true (no 'list' path segment)"
|
|
722
|
+
echo " - detail = ':id' (no 'detail' path segment)"
|
|
723
|
+
echo "$BAD_FE"
|
|
724
|
+
exit 1
|
|
725
|
+
fi
|
|
726
|
+
fi
|
|
727
|
+
echo "OK: No forbidden /list or /detail route patterns found"
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### POST-CHECK 27: Permission path segment count (WARNING)
|
|
731
|
+
|
|
732
|
+
```bash
|
|
733
|
+
PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
|
|
734
|
+
if [ -n "$PERM_FILES" ]; then
|
|
735
|
+
while IFS= read -r line; do
|
|
736
|
+
PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"')
|
|
737
|
+
if [ -n "$PATH_VAL" ]; then
|
|
738
|
+
DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
|
|
739
|
+
# Module permissions: 2 dots (app.module.action = 3 segments = 2+1)
|
|
740
|
+
# Section permissions: 3 dots (app.module.section.action = 4 segments = 3+1)
|
|
741
|
+
# Wildcard: ends with .* (valid at any level)
|
|
742
|
+
if echo "$PATH_VAL" | grep -qP '\.\*$'; then
|
|
743
|
+
continue # Wildcards are valid
|
|
744
|
+
elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
|
|
745
|
+
echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
|
|
746
|
+
fi
|
|
747
|
+
fi
|
|
748
|
+
done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
|
|
749
|
+
fi
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### POST-CHECK 28: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
|
|
753
|
+
|
|
754
|
+
```bash
|
|
755
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
756
|
+
if [ -n "$PROVIDER" ]; then
|
|
757
|
+
METHODS_FOUND=0
|
|
758
|
+
for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
|
|
759
|
+
if grep -q "$METHOD" "$PROVIDER"; then
|
|
760
|
+
METHODS_FOUND=$((METHODS_FOUND + 1))
|
|
761
|
+
else
|
|
762
|
+
echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
|
|
763
|
+
fi
|
|
764
|
+
done
|
|
765
|
+
if [ "$METHODS_FOUND" -lt 4 ]; then
|
|
766
|
+
echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
|
|
767
|
+
exit 1
|
|
768
|
+
fi
|
|
769
|
+
|
|
770
|
+
# Check DI registration
|
|
771
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
|
|
772
|
+
if [ -n "$DI_FILE" ]; then
|
|
773
|
+
if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
|
|
774
|
+
echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
|
|
775
|
+
echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
|
|
776
|
+
exit 1
|
|
777
|
+
fi
|
|
778
|
+
fi
|
|
779
|
+
fi
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### POST-CHECK 29: i18n must use separate JSON files per language (not embedded in index.ts)
|
|
783
|
+
|
|
784
|
+
```bash
|
|
785
|
+
# Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
|
|
786
|
+
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
787
|
+
if [ -n "$TSX_FILES" ]; then
|
|
788
|
+
# Check if i18n locales directory exists
|
|
789
|
+
if [ ! -d "src/i18n/locales" ]; then
|
|
790
|
+
echo "BLOCKING: Missing src/i18n/locales/ directory"
|
|
791
|
+
echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
|
|
792
|
+
echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
|
|
793
|
+
exit 1
|
|
794
|
+
fi
|
|
795
|
+
|
|
796
|
+
# Check for embedded translations in index.ts (common anti-pattern)
|
|
797
|
+
I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
|
|
798
|
+
if [ -n "$I18N_INDEX" ]; then
|
|
799
|
+
EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
|
|
800
|
+
if [ -n "$EMBEDDED" ]; then
|
|
801
|
+
echo "BLOCKING: Translations embedded in i18n config file — must be in separate JSON files"
|
|
802
|
+
echo "Found embedded translations in:"
|
|
803
|
+
echo "$EMBEDDED"
|
|
804
|
+
echo ""
|
|
805
|
+
echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
|
|
806
|
+
echo "The i18n config should import from locales/ directory, not contain inline translations"
|
|
807
|
+
exit 1
|
|
808
|
+
fi
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
# Verify all 4 language directories exist
|
|
812
|
+
for LANG in fr en it de; do
|
|
813
|
+
if [ ! -d "src/i18n/locales/$LANG" ]; then
|
|
814
|
+
echo "BLOCKING: Missing language directory: src/i18n/locales/$LANG/"
|
|
815
|
+
echo "SmartStack requires 4 languages: fr, en, it, de"
|
|
816
|
+
exit 1
|
|
817
|
+
fi
|
|
818
|
+
done
|
|
819
|
+
|
|
820
|
+
# Verify at least one JSON file exists per language
|
|
821
|
+
for LANG in fr en it de; do
|
|
822
|
+
JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
|
|
823
|
+
if [ "$JSON_COUNT" -eq 0 ]; then
|
|
824
|
+
echo "BLOCKING: No translation JSON files in src/i18n/locales/$LANG/"
|
|
825
|
+
echo "Each module must have a {module}.json file per language"
|
|
826
|
+
exit 1
|
|
827
|
+
fi
|
|
828
|
+
done
|
|
829
|
+
fi
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### POST-CHECK 30: Pages must use useTranslation hook (no hardcoded user-facing strings)
|
|
833
|
+
|
|
834
|
+
```bash
|
|
835
|
+
# Verify that page components use i18n — detect hardcoded strings in JSX
|
|
836
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
837
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
838
|
+
# Check that at least 80% of pages import useTranslation
|
|
839
|
+
TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
|
|
840
|
+
I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
|
|
841
|
+
if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
|
|
842
|
+
echo "BLOCKING: No page files use useTranslation — all user-facing text must be translated"
|
|
843
|
+
echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
|
|
844
|
+
echo ""
|
|
845
|
+
echo "Fix: Import and use useTranslation in every page component:"
|
|
846
|
+
echo " const { t } = useTranslation(['{module}']);"
|
|
847
|
+
echo " t('{module}:title', 'Fallback text')"
|
|
848
|
+
exit 1
|
|
849
|
+
fi
|
|
850
|
+
|
|
851
|
+
# Check for common hardcoded English strings in JSX (heuristic)
|
|
852
|
+
HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No data|Not found|Submit|Back|Actions|Name|Status|Description)\s*<' $PAGE_FILES 2>/dev/null | grep -v '{t(' | head -10)
|
|
853
|
+
if [ -n "$HARDCODED_TEXT" ]; then
|
|
854
|
+
echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
|
|
855
|
+
echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
|
|
856
|
+
echo "$HARDCODED_TEXT"
|
|
857
|
+
fi
|
|
858
|
+
fi
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### POST-CHECK 31: List/Detail pages must include DocToggleButton (documentation panel)
|
|
862
|
+
|
|
863
|
+
```bash
|
|
864
|
+
# Every list and detail page MUST have DocToggleButton for inline documentation access
|
|
865
|
+
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
866
|
+
if [ -n "$LIST_PAGES" ]; then
|
|
867
|
+
MISSING_DOC=0
|
|
868
|
+
for PAGE in $LIST_PAGES; do
|
|
869
|
+
if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
|
|
870
|
+
echo "WARNING: Page missing DocToggleButton: $PAGE"
|
|
871
|
+
echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
|
|
872
|
+
echo " Place in header actions: <DocToggleButton />"
|
|
873
|
+
MISSING_DOC=$((MISSING_DOC + 1))
|
|
874
|
+
fi
|
|
875
|
+
done
|
|
876
|
+
if [ "$MISSING_DOC" -gt 0 ]; then
|
|
877
|
+
echo ""
|
|
878
|
+
echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
|
|
879
|
+
echo "See smartstack-frontend.md section 7 for placement pattern"
|
|
880
|
+
fi
|
|
881
|
+
fi
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### POST-CHECK 32: Module documentation must be generated (doc-data.ts)
|
|
885
|
+
|
|
886
|
+
```bash
|
|
887
|
+
# After frontend pages exist, /documentation should have been called
|
|
888
|
+
TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
|
|
889
|
+
DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
|
|
890
|
+
if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
|
|
891
|
+
echo "WARNING: Frontend pages exist but no documentation generated"
|
|
892
|
+
echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
|
|
893
|
+
echo "The DocToggleButton in page headers will link to this documentation"
|
|
894
|
+
fi
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### POST-CHECK 33: Pagination type must be PaginatedResult<T> — no aliases (BLOCKING)
|
|
898
|
+
|
|
899
|
+
```bash
|
|
900
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
901
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
902
|
+
ALL_FILES="$SERVICE_FILES $CTRL_FILES"
|
|
903
|
+
if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
|
|
904
|
+
BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
|
|
905
|
+
if [ -n "$BAD_NAMES" ]; then
|
|
906
|
+
echo "BLOCKING: Pagination type must be PaginatedResult<T> — found non-canonical names"
|
|
907
|
+
echo "$BAD_NAMES"
|
|
908
|
+
echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
|
|
909
|
+
echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
|
|
910
|
+
exit 1
|
|
911
|
+
fi
|
|
912
|
+
fi
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### POST-CHECK 34: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
|
|
916
|
+
|
|
917
|
+
```bash
|
|
918
|
+
# If feature.json has entities with codePattern.strategy != "manual",
|
|
919
|
+
# verify that ICodeGenerator<Entity> is registered in DI
|
|
920
|
+
FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
|
|
921
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
|
|
922
|
+
if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
|
|
923
|
+
for FEATURE in $FEATURE_FILES; do
|
|
924
|
+
ENTITIES_WITH_CODE=$(python3 -c "
|
|
925
|
+
import json, sys
|
|
926
|
+
try:
|
|
927
|
+
with open('$FEATURE') as f:
|
|
928
|
+
data = json.load(f)
|
|
929
|
+
for e in data.get('analysis', {}).get('entities', []):
|
|
930
|
+
cp = e.get('codePattern', {})
|
|
931
|
+
if cp.get('strategy', 'manual') != 'manual':
|
|
932
|
+
print(e['name'])
|
|
933
|
+
except: pass
|
|
934
|
+
" 2>/dev/null)
|
|
935
|
+
for ENTITY in $ENTITIES_WITH_CODE; do
|
|
936
|
+
if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
|
|
937
|
+
echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
|
|
938
|
+
echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
|
|
939
|
+
exit 1
|
|
940
|
+
fi
|
|
941
|
+
done
|
|
942
|
+
done
|
|
943
|
+
fi
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### POST-CHECK 35: Code regex must support hyphens (BLOCKING)
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
|
|
950
|
+
if [ -n "$VALIDATOR_FILES" ]; then
|
|
951
|
+
OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
|
|
952
|
+
if [ -n "$OLD_REGEX" ]; then
|
|
953
|
+
echo "BLOCKING: Code validator uses old regex without hyphen support"
|
|
954
|
+
echo "$OLD_REGEX"
|
|
955
|
+
echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
|
|
956
|
+
exit 1
|
|
957
|
+
fi
|
|
958
|
+
fi
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
### POST-CHECK 36: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
|
|
962
|
+
|
|
963
|
+
```bash
|
|
964
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
965
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
966
|
+
for f in $SERVICE_FILES; do
|
|
967
|
+
if grep -q "ICodeGenerator" "$f"; then
|
|
968
|
+
ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
|
|
969
|
+
DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
|
|
970
|
+
if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
|
|
971
|
+
echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
|
|
972
|
+
echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
|
|
973
|
+
fi
|
|
974
|
+
fi
|
|
975
|
+
done
|
|
976
|
+
fi
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### POST-CHECK 37: Translation seed data must have idempotency guard (BLOCKING)
|
|
980
|
+
|
|
981
|
+
```bash
|
|
982
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
983
|
+
if [ -n "$PROVIDER" ]; then
|
|
984
|
+
# Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
|
|
985
|
+
# Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
|
|
986
|
+
TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
|
|
987
|
+
TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)
|
|
988
|
+
|
|
989
|
+
if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
|
|
990
|
+
echo "BLOCKING: Translation seed data inserts without idempotency guard in $PROVIDER"
|
|
991
|
+
echo "Fix: Before each NavigationTranslations.Add block, check existence:"
|
|
992
|
+
echo " if (!await context.NavigationTranslations.AnyAsync("
|
|
993
|
+
echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
|
|
994
|
+
echo " && t.EntityType == NavigationEntityType.Module, ct))"
|
|
995
|
+
echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
|
|
996
|
+
echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
|
|
997
|
+
exit 1
|
|
998
|
+
fi
|
|
999
|
+
fi
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### POST-CHECK 38: Resource seed data must use actual section IDs from DB (BLOCKING)
|
|
1003
|
+
|
|
1004
|
+
```bash
|
|
1005
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
1006
|
+
if [ -n "$PROVIDER" ]; then
|
|
1007
|
+
# Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (deterministic GUIDs)
|
|
1008
|
+
# instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
|
|
1009
|
+
if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
|
|
1010
|
+
echo "BLOCKING: Resource seed data uses deterministic GUID as SectionId in $PROVIDER"
|
|
1011
|
+
echo "NavigationSection.Create() generates its own ID — deterministic seed GUIDs do NOT exist in nav_Sections."
|
|
1012
|
+
echo "Fix: Query actual section from DB before creating resources:"
|
|
1013
|
+
echo " var actualSection = await context.NavigationSections"
|
|
1014
|
+
echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
|
|
1015
|
+
echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
|
|
1016
|
+
exit 1
|
|
1017
|
+
fi
|
|
1018
|
+
fi
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
### POST-CHECK 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)
|
|
1022
|
+
|
|
1023
|
+
```bash
|
|
1024
|
+
# [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
|
|
1025
|
+
# Having both is redundant and may cause route conflicts.
|
|
1026
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1027
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1028
|
+
for f in $CTRL_FILES; do
|
|
1029
|
+
HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
|
|
1030
|
+
HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
|
|
1031
|
+
if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
|
|
1032
|
+
echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
|
|
1033
|
+
echo " Found [NavRoute]: $HAS_NAVROUTE"
|
|
1034
|
+
echo " Found [Route]: $HAS_ROUTE"
|
|
1035
|
+
echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
|
|
1036
|
+
exit 1
|
|
1037
|
+
fi
|
|
1038
|
+
done
|
|
1039
|
+
fi
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### POST-CHECK 40: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
|
|
1043
|
+
|
|
1044
|
+
```bash
|
|
1045
|
+
# NavRoute segments are navigation entity Codes joined by dots.
|
|
1046
|
+
# Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
|
|
1047
|
+
# Verified from SmartStack.app: "support-client.my-tickets", "administration.access-requests"
|
|
1048
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1049
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1050
|
+
for f in $CTRL_FILES; do
|
|
1051
|
+
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1052
|
+
if [ -n "$NAVROUTE_VAL" ]; then
|
|
1053
|
+
# Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
|
|
1054
|
+
for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
|
|
1055
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1056
|
+
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
1057
|
+
echo " Full NavRoute: $NAVROUTE_VAL"
|
|
1058
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1059
|
+
echo " SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
|
|
1060
|
+
exit 1
|
|
1061
|
+
fi
|
|
1062
|
+
done
|
|
1063
|
+
fi
|
|
1064
|
+
done
|
|
1065
|
+
fi
|
|
1066
|
+
|
|
1067
|
+
# Also check seed data Code values for navigation entities
|
|
1068
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
|
|
1069
|
+
if [ -n "$SEED_FILES" ]; then
|
|
1070
|
+
CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
1071
|
+
for CODE in $CODES; do
|
|
1072
|
+
if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
|
|
1073
|
+
echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
|
|
1074
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1075
|
+
exit 1
|
|
1076
|
+
fi
|
|
1077
|
+
done
|
|
1078
|
+
fi
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
### POST-CHECK 41: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
|
|
1082
|
+
|
|
1083
|
+
```bash
|
|
1084
|
+
# Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
|
|
1085
|
+
# SmartStack.app convention: "support-client.my-tickets.read" (kebab-case everywhere)
|
|
1086
|
+
# FORBIDDEN: "humanresources.employees.read" — must be "human-resources.employees.read"
|
|
1087
|
+
|
|
1088
|
+
# Check [RequirePermission] attributes in controllers
|
|
1089
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1090
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1091
|
+
for f in $CTRL_FILES; do
|
|
1092
|
+
PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1093
|
+
for PERM in $PERM_VALS; do
|
|
1094
|
+
# Check each segment (except the action suffix) for concatenated multi-word without hyphens
|
|
1095
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
|
|
1096
|
+
for SEG in $SEGMENTS; do
|
|
1097
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1098
|
+
echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
|
|
1099
|
+
echo " Full permission: $PERM"
|
|
1100
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1101
|
+
echo " SmartStack convention: 'support-client.my-tickets.read'"
|
|
1102
|
+
exit 1
|
|
1103
|
+
fi
|
|
1104
|
+
done
|
|
1105
|
+
done
|
|
1106
|
+
done
|
|
1107
|
+
fi
|
|
1108
|
+
|
|
1109
|
+
# Check Permissions.cs constants
|
|
1110
|
+
PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
|
|
1111
|
+
if [ -n "$PERM_FILES" ]; then
|
|
1112
|
+
for f in $PERM_FILES; do
|
|
1113
|
+
CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1114
|
+
for PERM in $CONST_VALS; do
|
|
1115
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
1116
|
+
for SEG in $SEGMENTS; do
|
|
1117
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1118
|
+
echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
|
|
1119
|
+
echo " Full permission: $PERM"
|
|
1120
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1121
|
+
exit 1
|
|
1122
|
+
fi
|
|
1123
|
+
done
|
|
1124
|
+
done
|
|
1125
|
+
done
|
|
1126
|
+
fi
|
|
1127
|
+
|
|
1128
|
+
# Check PermissionsSeedData.cs for mismatched paths
|
|
1129
|
+
SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
|
|
1130
|
+
if [ -n "$SEED_PERM_FILES" ]; then
|
|
1131
|
+
PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
|
|
1132
|
+
for PERM in $PATHS; do
|
|
1133
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
1134
|
+
for SEG in $SEGMENTS; do
|
|
1135
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1136
|
+
echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
|
|
1137
|
+
echo " Full permission path: $PERM"
|
|
1138
|
+
echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
|
|
1139
|
+
exit 1
|
|
1140
|
+
fi
|
|
1141
|
+
done
|
|
1142
|
+
done
|
|
1143
|
+
fi
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
### POST-CHECK 42: Frontend navigate() calls must have matching route definitions (BLOCKING)
|
|
1147
|
+
|
|
1148
|
+
```bash
|
|
1149
|
+
# Detect dead links: navigate() calls to paths that don't have corresponding page components.
|
|
1150
|
+
# Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
|
|
1151
|
+
PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
|
|
1152
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
1153
|
+
# Extract navigate targets (relative paths like '../leave-types', './create', etc.)
|
|
1154
|
+
NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
|
|
1155
|
+
# Extract route paths from App.tsx or route config
|
|
1156
|
+
APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
|
|
1157
|
+
if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
|
|
1158
|
+
ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
|
|
1159
|
+
for TARGET in $NAV_TARGETS; do
|
|
1160
|
+
# Skip dynamic segments (:id), back navigation (-1), and absolute URLs
|
|
1161
|
+
if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
|
|
1162
|
+
# Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
|
|
1163
|
+
LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
|
|
1164
|
+
if [ -z "$LAST_SEG" ]; then continue; fi
|
|
1165
|
+
# Check if any route path contains this segment
|
|
1166
|
+
FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
|
|
1167
|
+
if [ -z "$FOUND" ]; then
|
|
1168
|
+
# Verify no page component exists for this path
|
|
1169
|
+
SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
|
|
1170
|
+
PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
|
|
1171
|
+
if [ -z "$PAGE_EXISTS" ]; then
|
|
1172
|
+
# Find which file has this navigate call
|
|
1173
|
+
SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
|
|
1174
|
+
echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
|
|
1175
|
+
echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
|
|
1176
|
+
echo " Fix: Either create the page component + route, or remove the navigate() button"
|
|
1177
|
+
exit 1
|
|
1178
|
+
fi
|
|
1179
|
+
fi
|
|
1180
|
+
done
|
|
1181
|
+
fi
|
|
1182
|
+
fi
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
### POST-CHECK 43: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)
|
|
1186
|
+
|
|
1187
|
+
```bash
|
|
1188
|
+
# Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
|
|
1189
|
+
# Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
|
|
1190
|
+
# instead of rendering sub-resource content inline. Users lost detail page context.
|
|
1191
|
+
DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
1192
|
+
if [ -n "$DETAIL_PAGES" ]; then
|
|
1193
|
+
FAIL=false
|
|
1194
|
+
for DP in $DETAIL_PAGES; do
|
|
1195
|
+
# Check if the page has tabs (activeTab state)
|
|
1196
|
+
HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
|
|
1197
|
+
if [ -z "$HAS_TABS" ]; then continue; fi
|
|
1198
|
+
|
|
1199
|
+
# Check if any tab click handler calls navigate()
|
|
1200
|
+
# Pattern: function that both references setActiveTab AND navigate()
|
|
1201
|
+
# Look for navigate() calls inside handlers that also set tab state
|
|
1202
|
+
TAB_NAVIGATE=$(grep -Pn "navigate\(" "$DP" 2>/dev/null | grep -v "navigate\(\s*['\"]edit['\"]" | grep -v "navigate\(\s*-1\s*\)" | grep -v "navigate\(\s*['\`].*/:id/edit" | grep -v "//")
|
|
1203
|
+
if [ -n "$TAB_NAVIGATE" ]; then
|
|
1204
|
+
# Verify this navigate is in a tab handler context (near setActiveTab usage)
|
|
1205
|
+
# Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
|
|
1206
|
+
RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
|
|
1207
|
+
if [ -n "$RELATIVE_NAV" ]; then
|
|
1208
|
+
echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
|
|
1209
|
+
echo " Tab click handlers MUST only call setActiveTab() — render content inline"
|
|
1210
|
+
echo " Found navigate() calls (likely in tab handlers):"
|
|
1211
|
+
echo "$RELATIVE_NAV"
|
|
1212
|
+
echo ""
|
|
1213
|
+
echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
|
|
1214
|
+
echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
|
|
1215
|
+
echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
|
|
1216
|
+
FAIL=true
|
|
1217
|
+
fi
|
|
1218
|
+
fi
|
|
1219
|
+
done
|
|
1220
|
+
if [ "$FAIL" = true ]; then
|
|
1221
|
+
exit 1
|
|
1222
|
+
fi
|
|
1223
|
+
fi
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### POST-CHECK 44: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
|
|
1227
|
+
|
|
1228
|
+
```bash
|
|
1229
|
+
# Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
|
|
1230
|
+
# Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
|
|
1231
|
+
# are added in subsequent iterations without re-running migration.
|
|
1232
|
+
SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
|
|
1233
|
+
DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
|
|
1234
|
+
if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
|
|
1235
|
+
# Extract DbSet entity names from DbContext (DbSet<EntityName>)
|
|
1236
|
+
DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
|
|
1237
|
+
FAIL=false
|
|
1238
|
+
for ENTITY in $DBSET_ENTITIES; do
|
|
1239
|
+
# Skip base SmartStack entities (handled by core migrations)
|
|
1240
|
+
if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
|
|
1241
|
+
continue
|
|
1242
|
+
fi
|
|
1243
|
+
# Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
|
|
1244
|
+
if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
|
|
1245
|
+
echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
|
|
1246
|
+
echo " This means no migration was created for this entity — it will not exist in the database."
|
|
1247
|
+
echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
|
|
1248
|
+
FAIL=true
|
|
1249
|
+
fi
|
|
1250
|
+
done
|
|
1251
|
+
if [ "$FAIL" = true ]; then
|
|
1252
|
+
echo ""
|
|
1253
|
+
echo " Root cause: Migration was likely created once for the first batch of entities,"
|
|
1254
|
+
echo " but additional entities were added later without regenerating the migration."
|
|
1255
|
+
echo " Fix: Create a new migration that covers ALL missing entities."
|
|
1256
|
+
exit 1
|
|
1257
|
+
fi
|
|
1258
|
+
fi
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
### POST-CHECK 45: I18n namespace files must be registered in i18n config (BLOCKING)
|
|
1262
|
+
|
|
1263
|
+
```bash
|
|
1264
|
+
# Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
|
|
1265
|
+
# registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
|
|
1266
|
+
# got empty translations at runtime.
|
|
1267
|
+
I18N_CONFIG=$(find src/ web/ -path "*/i18n/config.ts" -o -path "*/i18n/index.ts" -o -path "*/i18n/i18n.ts" 2>/dev/null | grep -v node_modules | head -1)
|
|
1268
|
+
if [ -n "$I18N_CONFIG" ]; then
|
|
1269
|
+
# Find all module JSON files in the primary language (fr)
|
|
1270
|
+
FR_FILES=$(find src/ web/ -path "*/i18n/locales/fr/*.json" 2>/dev/null | grep -v node_modules | grep -v common.json | grep -v navigation.json)
|
|
1271
|
+
if [ -n "$FR_FILES" ]; then
|
|
1272
|
+
FAIL=false
|
|
1273
|
+
for JSON_FILE in $FR_FILES; do
|
|
1274
|
+
NS=$(basename "$JSON_FILE" .json)
|
|
1275
|
+
# Check if namespace is referenced in config (import or resource key)
|
|
1276
|
+
if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
|
|
1277
|
+
echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
|
|
1278
|
+
echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
|
|
1279
|
+
echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
|
|
1280
|
+
FAIL=true
|
|
1281
|
+
fi
|
|
1282
|
+
done
|
|
1283
|
+
if [ "$FAIL" = true ]; then
|
|
1284
|
+
exit 1
|
|
1285
|
+
fi
|
|
1286
|
+
fi
|
|
1287
|
+
fi
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
### POST-CHECK 46: FluentValidation validators must be registered via DI (BLOCKING)
|
|
1291
|
+
|
|
1292
|
+
```bash
|
|
1293
|
+
# Root cause (test-apex-007): Validators existed but were never registered in DI.
|
|
1294
|
+
# Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
|
|
1295
|
+
VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
|
|
1296
|
+
if [ -n "$VALIDATOR_FILES" ]; then
|
|
1297
|
+
# Check DI registration file exists
|
|
1298
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
|
|
1299
|
+
if [ -z "$DI_FILE" ]; then
|
|
1300
|
+
echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
|
|
1301
|
+
exit 1
|
|
1302
|
+
fi
|
|
1303
|
+
# Check for AddValidatorsFromAssembly or individual validator registration
|
|
1304
|
+
HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
|
|
1305
|
+
if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
|
|
1306
|
+
# Check individual registrations as fallback
|
|
1307
|
+
VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
|
|
1308
|
+
REGISTERED_COUNT=0
|
|
1309
|
+
for VF in $VALIDATOR_FILES; do
|
|
1310
|
+
VN=$(basename "$VF" .cs)
|
|
1311
|
+
if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
|
|
1312
|
+
REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
|
|
1313
|
+
fi
|
|
1314
|
+
done
|
|
1315
|
+
if [ "$REGISTERED_COUNT" -eq 0 ]; then
|
|
1316
|
+
echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
|
|
1317
|
+
echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
|
|
1318
|
+
echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
|
|
1319
|
+
exit 1
|
|
1320
|
+
fi
|
|
1321
|
+
fi
|
|
1322
|
+
fi
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### POST-CHECK 47: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)
|
|
1326
|
+
|
|
1327
|
+
```bash
|
|
1328
|
+
# Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
|
|
1329
|
+
# This causes: invalid date parsing, no date validation, inconsistent formats across clients.
|
|
1330
|
+
DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
|
|
1331
|
+
if [ -n "$DTO_FILES" ]; then
|
|
1332
|
+
FAIL=false
|
|
1333
|
+
for f in $DTO_FILES; do
|
|
1334
|
+
# Find string properties whose name contains "Date" (case-insensitive)
|
|
1335
|
+
BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
|
|
1336
|
+
if [ -n "$BAD_DATES" ]; then
|
|
1337
|
+
echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
|
|
1338
|
+
echo "$BAD_DATES"
|
|
1339
|
+
echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
|
|
1340
|
+
echo " DateOnly is the correct .NET type for date-only values (no time component)"
|
|
1341
|
+
FAIL=true
|
|
1342
|
+
fi
|
|
1343
|
+
done
|
|
1344
|
+
if [ "$FAIL" = true ]; then
|
|
1345
|
+
exit 1
|
|
1346
|
+
fi
|
|
1347
|
+
fi
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### POST-CHECK 48: NavRoute attribute values must use kebab-case (BLOCKING)
|
|
1351
|
+
|
|
1352
|
+
```bash
|
|
1353
|
+
# Root cause (test-apex-007): Controllers had [NavRoute("humanresources.employees")]
|
|
1354
|
+
# instead of [NavRoute("human-resources.employees")]. This causes route mismatch with
|
|
1355
|
+
# seed data and permission codes, resulting in 404s at runtime.
|
|
1356
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1357
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1358
|
+
FAIL=false
|
|
1359
|
+
for f in $CTRL_FILES; do
|
|
1360
|
+
NAVROUTE_VALS=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1361
|
+
for NR in $NAVROUTE_VALS; do
|
|
1362
|
+
# Check each segment for concatenated multi-word without hyphens
|
|
1363
|
+
SEGMENTS=$(echo "$NR" | tr '.' '\n')
|
|
1364
|
+
for SEG in $SEGMENTS; do
|
|
1365
|
+
# Detect segments that look like concatenated words (lowercase, 8+ chars, no hyphens)
|
|
1366
|
+
# Use a simpler heuristic: lowercase-only segment with known multi-word patterns
|
|
1367
|
+
if echo "$SEG" | grep -qP '^[a-z]{8,}$'; then
|
|
1368
|
+
# Additional check: does it contain a known multi-word pattern?
|
|
1369
|
+
if echo "$SEG" | grep -qP '(human|project|leave|client|support|email|time|work|resource)'; then
|
|
1370
|
+
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
1371
|
+
echo " Full NavRoute: $NR"
|
|
1372
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources', 'projectmanagement' → 'project-management'"
|
|
1373
|
+
FAIL=true
|
|
1374
|
+
fi
|
|
1375
|
+
fi
|
|
1376
|
+
done
|
|
1377
|
+
done
|
|
1378
|
+
done
|
|
1379
|
+
if [ "$FAIL" = true ]; then
|
|
1380
|
+
exit 1
|
|
1381
|
+
fi
|
|
1382
|
+
fi
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
### POST-CHECK 49: Every module with entities must have a migration covering them (BLOCKING)
|
|
1386
|
+
|
|
1387
|
+
```bash
|
|
1388
|
+
# Complementary to POST-CHECK 44 — checks from the entity side.
|
|
1389
|
+
# Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
|
|
1390
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
|
|
1391
|
+
MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
|
|
1392
|
+
if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
|
|
1393
|
+
MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
|
|
1394
|
+
if [ -z "$MIGRATION_FILES" ]; then
|
|
1395
|
+
echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
|
|
1396
|
+
exit 1
|
|
1397
|
+
fi
|
|
1398
|
+
FAIL=false
|
|
1399
|
+
for EF in $ENTITY_FILES; do
|
|
1400
|
+
ENTITY_NAME=$(basename "$EF" .cs)
|
|
1401
|
+
# Skip abstract base classes and interfaces
|
|
1402
|
+
if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
|
|
1403
|
+
# Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
|
|
1404
|
+
FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
|
|
1405
|
+
if [ -z "$FOUND" ]; then
|
|
1406
|
+
echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
|
|
1407
|
+
echo " This entity will NOT have a database table."
|
|
1408
|
+
echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
|
|
1409
|
+
FAIL=true
|
|
1410
|
+
fi
|
|
1411
|
+
done
|
|
1412
|
+
if [ "$FAIL" = true ]; then
|
|
1413
|
+
exit 1
|
|
1414
|
+
fi
|
|
1415
|
+
fi
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
### POST-CHECK 50: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
|
|
1419
|
+
|
|
1420
|
+
```bash
|
|
1421
|
+
# Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
|
|
1422
|
+
# In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
|
|
1423
|
+
# [Route] is standard ASP.NET Core static routing. When both exist:
|
|
1424
|
+
# - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
|
|
1425
|
+
# - [Route] may or may not take over depending on middleware order
|
|
1426
|
+
# - Result: 404 on ALL endpoints
|
|
1427
|
+
# The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
|
|
1428
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1429
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1430
|
+
FAIL=false
|
|
1431
|
+
for f in $CTRL_FILES; do
|
|
1432
|
+
HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
|
|
1433
|
+
HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
|
|
1434
|
+
if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
|
|
1435
|
+
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
1436
|
+
ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
1437
|
+
echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
|
|
1438
|
+
echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
|
|
1439
|
+
echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
|
|
1440
|
+
echo " Having [Route] alongside it causes route conflicts and 404s."
|
|
1441
|
+
echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
|
|
1442
|
+
FAIL=true
|
|
1443
|
+
fi
|
|
1444
|
+
done
|
|
1445
|
+
if [ "$FAIL" = true ]; then
|
|
1446
|
+
exit 1
|
|
1447
|
+
fi
|
|
1448
|
+
fi
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
### POST-CHECK 51: RolesSeedData must map standard role-permission matrix (BLOCKING)
|
|
1452
|
+
|
|
1453
|
+
```bash
|
|
1454
|
+
# SmartStack standard role-permission matrix:
|
|
1455
|
+
# Admin = wildcard (*) — full access
|
|
1456
|
+
# Manager = CRU (read + create + update) — no delete
|
|
1457
|
+
# Contributor = CR (read + create) — no update, no delete
|
|
1458
|
+
# Viewer = R (read only)
|
|
1459
|
+
# If RolesSeedData deviates from this matrix, the RBAC model is broken.
|
|
1460
|
+
ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
|
|
1461
|
+
if [ -n "$ROLE_SEED_FILES" ]; then
|
|
1462
|
+
FAIL=false
|
|
1463
|
+
for f in $ROLE_SEED_FILES; do
|
|
1464
|
+
# Skip ApplicationRolesSeedData (defines roles, not mappings)
|
|
1465
|
+
BASENAME=$(basename "$f")
|
|
1466
|
+
if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi
|
|
1467
|
+
|
|
1468
|
+
# Check Admin has wildcard
|
|
1469
|
+
HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null)
|
|
1470
|
+
if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
|
|
1471
|
+
# Also accept .Access or wildcard pattern
|
|
1472
|
+
HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null)
|
|
1473
|
+
if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
|
|
1474
|
+
echo "BLOCKING: Admin role missing wildcard (*) permission in $f"
|
|
1475
|
+
echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
|
|
1476
|
+
FAIL=true
|
|
1477
|
+
fi
|
|
1478
|
+
fi
|
|
1479
|
+
|
|
1480
|
+
# Check Viewer has NO delete/create/update
|
|
1481
|
+
VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null)
|
|
1482
|
+
if [ "$VIEWER_WRITE" -gt 0 ]; then
|
|
1483
|
+
echo "BLOCKING: Viewer role has write permissions (create/update/delete) in $f"
|
|
1484
|
+
echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
|
|
1485
|
+
FAIL=true
|
|
1486
|
+
fi
|
|
1487
|
+
|
|
1488
|
+
# Check Manager has NO delete
|
|
1489
|
+
MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null)
|
|
1490
|
+
if [ "$MANAGER_DELETE" -gt 0 ]; then
|
|
1491
|
+
echo "WARNING: Manager role has delete permission in $f"
|
|
1492
|
+
echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
|
|
1493
|
+
fi
|
|
1494
|
+
done
|
|
1495
|
+
if [ "$FAIL" = true ]; then
|
|
1496
|
+
exit 1
|
|
1497
|
+
fi
|
|
1498
|
+
fi
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
### POST-CHECK 52: PermissionAction enum must use valid typed values only (BLOCKING)
|
|
1502
|
+
|
|
1503
|
+
```bash
|
|
1504
|
+
# Valid PermissionAction enum values: Access(0), Read(1), Create(2), Update(3), Delete(4),
|
|
1505
|
+
# Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)
|
|
1506
|
+
# FORBIDDEN: Enum.Parse<PermissionAction>("...") — runtime crash if value doesn't exist
|
|
1507
|
+
# FORBIDDEN: (PermissionAction)99 or any cast beyond 0-10
|
|
1508
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
1509
|
+
if [ -n "$SEED_FILES" ]; then
|
|
1510
|
+
FAIL=false
|
|
1511
|
+
for f in $SEED_FILES; do
|
|
1512
|
+
# Check for Enum.Parse<PermissionAction> usage
|
|
1513
|
+
ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null)
|
|
1514
|
+
if [ -n "$ENUM_PARSE" ]; then
|
|
1515
|
+
echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
|
|
1516
|
+
echo "$ENUM_PARSE"
|
|
1517
|
+
echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
|
|
1518
|
+
FAIL=true
|
|
1519
|
+
fi
|
|
1520
|
+
|
|
1521
|
+
# Check for invalid cast values (PermissionAction)N where N > 10
|
|
1522
|
+
INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null)
|
|
1523
|
+
if [ -n "$INVALID_CAST" ]; then
|
|
1524
|
+
echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
|
|
1525
|
+
echo "$INVALID_CAST"
|
|
1526
|
+
echo "Valid values: Access(0), Read(1), Create(2), Update(3), Delete(4), Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)"
|
|
1527
|
+
FAIL=true
|
|
1528
|
+
fi
|
|
1529
|
+
done
|
|
1530
|
+
if [ "$FAIL" = true ]; then
|
|
1531
|
+
exit 1
|
|
1532
|
+
fi
|
|
1533
|
+
fi
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
### POST-CHECK 53: Navigation translation completeness — 4 languages per level (BLOCKING)
|
|
1537
|
+
|
|
1538
|
+
```bash
|
|
1539
|
+
# Every navigation seed data file must provide translations for ALL 4 languages (fr, en, it, de).
|
|
1540
|
+
# If sections exist (GetSectionEntries), GetSectionTranslationEntries MUST also exist.
|
|
1541
|
+
# If resources exist (GetResourceEntries), resource translation entries MUST also exist.
|
|
1542
|
+
NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
|
|
1543
|
+
if [ -n "$NAV_SEED_FILES" ]; then
|
|
1544
|
+
FAIL=false
|
|
1545
|
+
for f in $NAV_SEED_FILES; do
|
|
1546
|
+
# Check module translations have all 4 languages
|
|
1547
|
+
LANG_COUNT=$(grep -c 'LanguageCode\s*=' "$f" 2>/dev/null)
|
|
1548
|
+
HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null)
|
|
1549
|
+
HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null)
|
|
1550
|
+
HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null)
|
|
1551
|
+
HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null)
|
|
1552
|
+
|
|
1553
|
+
if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
|
|
1554
|
+
echo "BLOCKING: Missing language(s) in navigation translations: $f"
|
|
1555
|
+
echo " fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
|
|
1556
|
+
echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
|
|
1557
|
+
FAIL=true
|
|
1558
|
+
fi
|
|
1559
|
+
|
|
1560
|
+
# If sections exist, section translations MUST exist
|
|
1561
|
+
HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null)
|
|
1562
|
+
HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null)
|
|
1563
|
+
if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
|
|
1564
|
+
echo "BLOCKING: Sections defined but GetSectionTranslationEntries() missing: $f"
|
|
1565
|
+
echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
|
|
1566
|
+
FAIL=true
|
|
1567
|
+
fi
|
|
1568
|
+
|
|
1569
|
+
# If resources exist, resource translations MUST exist
|
|
1570
|
+
HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null)
|
|
1571
|
+
HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null)
|
|
1572
|
+
if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
|
|
1573
|
+
echo "BLOCKING: Resources defined but resource translations missing: $f"
|
|
1574
|
+
echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
|
|
1575
|
+
FAIL=true
|
|
1576
|
+
fi
|
|
1577
|
+
done
|
|
1578
|
+
if [ "$FAIL" = true ]; then
|
|
1579
|
+
exit 1
|
|
1580
|
+
fi
|
|
1581
|
+
fi
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|