@flextudio/scenario 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +260 -0
- package/bin/flex-scenario.mjs +895 -0
- package/commands/flex-spec.md +28 -0
- package/contract-data/README.md +267 -0
- package/contract-data/examples/README.md +147 -0
- package/contract-data/examples/basic/basic-list-detail.json +2521 -0
- package/contract-data/examples/basic/delivery-address-popup.json +2175 -0
- package/contract-data/examples/basic/fixedtop-floating-form.json +1524 -0
- package/contract-data/examples/basic/search-period-popup.json +1524 -0
- package/contract-data/examples/design/booking-wizard-3step.json +1335 -0
- package/contract-data/examples/design/list-detail-form-3step.json +1516 -0
- package/contract-data/examples/design/list-filter-2step.json +1365 -0
- package/contract-data/examples/design/list-filter-detail-3step.json +1570 -0
- package/contract-data/examples/design/result-entry-wizard-4step.json +1861 -0
- package/contract-data/examples/intent/mock-employee-list.json +54 -0
- package/contract-data/examples/intent/script-cart-flow.json +96 -0
- package/contract-data/examples/intent/script-dialog-confirm.json +61 -0
- package/contract-data/runtime/v1/collection/README.md +119 -0
- package/contract-data/runtime/v1/collection/_c.md +205 -0
- package/contract-data/runtime/v1/collection/category.md +165 -0
- package/contract-data/runtime/v1/collection/collection.md +135 -0
- package/contract-data/runtime/v1/collection/lifecycle.md +140 -0
- package/contract-data/runtime/v1/collection/naming.md +174 -0
- package/contract-data/runtime/v1/collection/sector.md +176 -0
- package/contract-data/runtime/v1/collection/status.md +114 -0
- package/contract-data/runtime/v1/intent-system/INTENT.md +552 -0
- package/contract-data/runtime/v1/intent-system/README.md +118 -0
- package/contract-data/runtime/v1/intent-system/intent.schema.json +164 -0
- package/contract-data/runtime/v1/intent-system/kinds/_base.mjs +137 -0
- package/contract-data/runtime/v1/intent-system/kinds/clearCategory.mjs +35 -0
- package/contract-data/runtime/v1/intent-system/kinds/copyStepValues.mjs +100 -0
- package/contract-data/runtime/v1/intent-system/kinds/deleteActive.mjs +45 -0
- package/contract-data/runtime/v1/intent-system/kinds/dialog.mjs +127 -0
- package/contract-data/runtime/v1/intent-system/kinds/reloadGroup.mjs +28 -0
- package/contract-data/runtime/v1/intent-system/kinds/updateActive.mjs +74 -0
- package/contract-data/runtime/v1/intent-system/materialize.mjs +495 -0
- package/contract-data/runtime/v1/script/README.md +37 -0
- package/contract-data/runtime/v1/script/current_step.md +112 -0
- package/contract-data/runtime/v1/script/f_collection.md +142 -0
- package/contract-data/runtime/v1/script/f_content.md +133 -0
- package/contract-data/runtime/v1/script/f_messagebox.md +132 -0
- package/contract-data/runtime/v1/script/scenario-gen-rules.md +264 -0
- package/contract-data/schema/v1/SKELETON-SHELL.md +120 -0
- package/contract-data/schema/v1/bottom-buttons.md +155 -0
- package/contract-data/schema/v1/bottom-buttons.schema.json +134 -0
- package/contract-data/schema/v1/control/_base.schema.json +1296 -0
- package/contract-data/schema/v1/control/button.md +147 -0
- package/contract-data/schema/v1/control/button.schema.json +55 -0
- package/contract-data/schema/v1/control/calendar-navigator.md +143 -0
- package/contract-data/schema/v1/control/calendar-navigator.schema.json +164 -0
- package/contract-data/schema/v1/control/calendar.md +128 -0
- package/contract-data/schema/v1/control/calendar.schema.json +18 -0
- package/contract-data/schema/v1/control/check-box.md +117 -0
- package/contract-data/schema/v1/control/check-box.schema.json +41 -0
- package/contract-data/schema/v1/control/combo.md +293 -0
- package/contract-data/schema/v1/control/combo.schema.json +23 -0
- package/contract-data/schema/v1/control/embed.md +211 -0
- package/contract-data/schema/v1/control/embed.schema.json +30 -0
- package/contract-data/schema/v1/control/image-box.md +237 -0
- package/contract-data/schema/v1/control/image-box.schema.json +49 -0
- package/contract-data/schema/v1/control/input-date.md +175 -0
- package/contract-data/schema/v1/control/input-date.schema.json +27 -0
- package/contract-data/schema/v1/control/input-file.md +192 -0
- package/contract-data/schema/v1/control/input-file.schema.json +139 -0
- package/contract-data/schema/v1/control/input-mask.md +89 -0
- package/contract-data/schema/v1/control/input-mask.schema.json +45 -0
- package/contract-data/schema/v1/control/input-number.md +55 -0
- package/contract-data/schema/v1/control/input-number.schema.json +24 -0
- package/contract-data/schema/v1/control/input-text.md +73 -0
- package/contract-data/schema/v1/control/input-text.schema.json +24 -0
- package/contract-data/schema/v1/control/label.md +127 -0
- package/contract-data/schema/v1/control/label.schema.json +113 -0
- package/contract-data/schema/v1/control/multi-input-box.md +51 -0
- package/contract-data/schema/v1/control/multi-input-box.schema.json +19 -0
- package/contract-data/schema/v1/control/other.schema.json +24 -0
- package/contract-data/schema/v1/control/radio-box.md +150 -0
- package/contract-data/schema/v1/control/radio-box.schema.json +87 -0
- package/contract-data/schema/v1/control/search.md +163 -0
- package/contract-data/schema/v1/control/search.schema.json +23 -0
- package/contract-data/schema/v1/control/tab.md +240 -0
- package/contract-data/schema/v1/control/tab.schema.json +112 -0
- package/contract-data/schema/v1/control/tree.md +278 -0
- package/contract-data/schema/v1/control/tree.schema.json +83 -0
- package/contract-data/schema/v1/control.md +264 -0
- package/contract-data/schema/v1/control.schema.json +30 -0
- package/contract-data/schema/v1/data-objects.md +278 -0
- package/contract-data/schema/v1/data-objects.schema.json +161 -0
- package/contract-data/schema/v1/data-sources.md +215 -0
- package/contract-data/schema/v1/data-sources.schema.json +184 -0
- package/contract-data/schema/v1/design-system/DESIGN.md +282 -0
- package/contract-data/schema/v1/design-system/README.md +235 -0
- package/contract-data/schema/v1/design-system/design.schema.json +496 -0
- package/contract-data/schema/v1/design-system/error-mapping.mjs +132 -0
- package/contract-data/schema/v1/design-system/materialize.mjs +1435 -0
- package/contract-data/schema/v1/design-system/tests/_base.additional-meta.test.mjs +98 -0
- package/contract-data/schema/v1/design-system/tests/_base.materialize.test.mjs +596 -0
- package/contract-data/schema/v1/design-system/tests/checklist.test.mjs +135 -0
- package/contract-data/schema/v1/design-system/tests/design.schema.test.mjs +487 -0
- package/contract-data/schema/v1/design-system/tests/error-mapping.test.mjs +244 -0
- package/contract-data/schema/v1/design-system/tests/materialize.test.mjs +1093 -0
- package/contract-data/schema/v1/design-system/tests/move-step.test.mjs +119 -0
- package/contract-data/schema/v1/design-system/tests/override.integration.test.mjs +116 -0
- package/contract-data/schema/v1/design-system/tokens.json +76 -0
- package/contract-data/schema/v1/events.md +467 -0
- package/contract-data/schema/v1/events.schema.json +272 -0
- package/contract-data/schema/v1/group.md +146 -0
- package/contract-data/schema/v1/group.schema.json +133 -0
- package/contract-data/schema/v1/mockup-data.md +186 -0
- package/contract-data/schema/v1/naming-objects.schema.json +133 -0
- package/contract-data/schema/v1/node.md +32 -0
- package/contract-data/schema/v1/node.schema.json +24 -0
- package/contract-data/schema/v1/patterns/_base.additional-meta.mjs +135 -0
- package/contract-data/schema/v1/patterns/_base.controls.mjs +759 -0
- package/contract-data/schema/v1/patterns/_base.icon.mjs +61 -0
- package/contract-data/schema/v1/patterns/_base.id.mjs +15 -0
- package/contract-data/schema/v1/patterns/_base.materialize.mjs +1181 -0
- package/contract-data/schema/v1/patterns/_base.style.mjs +312 -0
- package/contract-data/schema/v1/patterns/atom/badge.materialize.mjs +68 -0
- package/contract-data/schema/v1/patterns/atom/badge.schema.json +44 -0
- package/contract-data/schema/v1/patterns/atom/button.materialize.mjs +91 -0
- package/contract-data/schema/v1/patterns/atom/button.schema.json +70 -0
- package/contract-data/schema/v1/patterns/atom/checkbox.materialize.mjs +40 -0
- package/contract-data/schema/v1/patterns/atom/checkbox.schema.json +39 -0
- package/contract-data/schema/v1/patterns/atom/segmented-radio.materialize.mjs +67 -0
- package/contract-data/schema/v1/patterns/atom/segmented-radio.schema.json +49 -0
- package/contract-data/schema/v1/patterns/atom/switch.materialize.mjs +58 -0
- package/contract-data/schema/v1/patterns/atom/switch.schema.json +27 -0
- package/contract-data/schema/v1/patterns/molecule/checklist.materialize.mjs +130 -0
- package/contract-data/schema/v1/patterns/molecule/checklist.schema.json +63 -0
- package/contract-data/schema/v1/patterns/molecule/input-action.materialize.mjs +356 -0
- package/contract-data/schema/v1/patterns/molecule/input-action.schema.json +251 -0
- package/contract-data/schema/v1/patterns/molecule/input-button.materialize.mjs +183 -0
- package/contract-data/schema/v1/patterns/molecule/input-button.schema.json +141 -0
- package/contract-data/schema/v1/patterns/molecule/input-pair.materialize.mjs +254 -0
- package/contract-data/schema/v1/patterns/molecule/input-pair.schema.json +210 -0
- package/contract-data/schema/v1/scenario.md +112 -0
- package/contract-data/schema/v1/scenario.schema.json +161 -0
- package/contract-data/schema/v1/service/CustomSystem.md +139 -0
- package/contract-data/schema/v1/service/CustomSystem.schema.json +41 -0
- package/contract-data/schema/v1/service/Firestore.md +125 -0
- package/contract-data/schema/v1/service/Firestore.schema.json +28 -0
- package/contract-data/schema/v1/service/FlexAutoQuery.md +128 -0
- package/contract-data/schema/v1/service/FlexAutoQuery.schema.json +35 -0
- package/contract-data/schema/v1/service/FlexSQL.md +96 -0
- package/contract-data/schema/v1/service/FlexSQL.schema.json +33 -0
- package/contract-data/schema/v1/service/Flextudio API.md +161 -0
- package/contract-data/schema/v1/service/Flextudio API.schema.json +115 -0
- package/contract-data/schema/v1/service/Flextudio.md +87 -0
- package/contract-data/schema/v1/service/Flextudio.schema.json +26 -0
- package/contract-data/schema/v1/service/GoogleSheet.md +71 -0
- package/contract-data/schema/v1/service/GoogleSheet.schema.json +31 -0
- package/contract-data/schema/v1/service/README.md +127 -0
- package/contract-data/schema/v1/service/_base.schema.json +31 -0
- package/contract-data/schema/v1/service/ksystem.md +76 -0
- package/contract-data/schema/v1/service/ksystem.schema.json +36 -0
- package/contract-data/schema/v1/service-binding.md +228 -0
- package/contract-data/schema/v1/service-binding.schema.json +51 -0
- package/contract-data/schema/v1/step.md +546 -0
- package/contract-data/schema/v1/step.schema.json +258 -0
- package/contract-data/schema/v1/style-objects.md +183 -0
- package/contract-data/schema/v1/style-objects.schema.json +155 -0
- package/contract-data/spec/catalog.json +406 -0
- package/contract-data/spec/rules.md +400 -0
- package/node_modules/@flextudio/contract-core/contract.mjs +367 -0
- package/node_modules/@flextudio/contract-core/index.mjs +16 -0
- package/node_modules/@flextudio/contract-core/package.json +21 -0
- package/node_modules/@flextudio/contract-core/paths.mjs +72 -0
- package/node_modules/@flextudio/contract-core/schemas.mjs +474 -0
- package/node_modules/@flextudio/contract-core/validators.mjs +2471 -0
- package/node_modules/ajv/.runkit_example.js +23 -0
- package/node_modules/ajv/LICENSE +22 -0
- package/node_modules/ajv/README.md +207 -0
- package/node_modules/ajv/dist/2019.d.ts +19 -0
- package/node_modules/ajv/dist/2019.js +61 -0
- package/node_modules/ajv/dist/2019.js.map +1 -0
- package/node_modules/ajv/dist/2020.d.ts +19 -0
- package/node_modules/ajv/dist/2020.js +55 -0
- package/node_modules/ajv/dist/2020.js.map +1 -0
- package/node_modules/ajv/dist/ajv.d.ts +18 -0
- package/node_modules/ajv/dist/ajv.js +50 -0
- package/node_modules/ajv/dist/ajv.js.map +1 -0
- package/node_modules/ajv/dist/compile/codegen/code.d.ts +40 -0
- package/node_modules/ajv/dist/compile/codegen/code.js +156 -0
- package/node_modules/ajv/dist/compile/codegen/code.js.map +1 -0
- package/node_modules/ajv/dist/compile/codegen/index.d.ts +79 -0
- package/node_modules/ajv/dist/compile/codegen/index.js +697 -0
- package/node_modules/ajv/dist/compile/codegen/index.js.map +1 -0
- package/node_modules/ajv/dist/compile/codegen/scope.d.ts +79 -0
- package/node_modules/ajv/dist/compile/codegen/scope.js +143 -0
- package/node_modules/ajv/dist/compile/codegen/scope.js.map +1 -0
- package/node_modules/ajv/dist/compile/errors.d.ts +13 -0
- package/node_modules/ajv/dist/compile/errors.js +123 -0
- package/node_modules/ajv/dist/compile/errors.js.map +1 -0
- package/node_modules/ajv/dist/compile/index.d.ts +80 -0
- package/node_modules/ajv/dist/compile/index.js +242 -0
- package/node_modules/ajv/dist/compile/index.js.map +1 -0
- package/node_modules/ajv/dist/compile/jtd/parse.d.ts +4 -0
- package/node_modules/ajv/dist/compile/jtd/parse.js +350 -0
- package/node_modules/ajv/dist/compile/jtd/parse.js.map +1 -0
- package/node_modules/ajv/dist/compile/jtd/serialize.d.ts +4 -0
- package/node_modules/ajv/dist/compile/jtd/serialize.js +236 -0
- package/node_modules/ajv/dist/compile/jtd/serialize.js.map +1 -0
- package/node_modules/ajv/dist/compile/jtd/types.d.ts +6 -0
- package/node_modules/ajv/dist/compile/jtd/types.js +14 -0
- package/node_modules/ajv/dist/compile/jtd/types.js.map +1 -0
- package/node_modules/ajv/dist/compile/names.d.ts +20 -0
- package/node_modules/ajv/dist/compile/names.js +28 -0
- package/node_modules/ajv/dist/compile/names.js.map +1 -0
- package/node_modules/ajv/dist/compile/ref_error.d.ts +6 -0
- package/node_modules/ajv/dist/compile/ref_error.js +12 -0
- package/node_modules/ajv/dist/compile/ref_error.js.map +1 -0
- package/node_modules/ajv/dist/compile/resolve.d.ts +12 -0
- package/node_modules/ajv/dist/compile/resolve.js +155 -0
- package/node_modules/ajv/dist/compile/resolve.js.map +1 -0
- package/node_modules/ajv/dist/compile/rules.d.ts +28 -0
- package/node_modules/ajv/dist/compile/rules.js +26 -0
- package/node_modules/ajv/dist/compile/rules.js.map +1 -0
- package/node_modules/ajv/dist/compile/util.d.ts +40 -0
- package/node_modules/ajv/dist/compile/util.js +178 -0
- package/node_modules/ajv/dist/compile/util.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/applicability.d.ts +6 -0
- package/node_modules/ajv/dist/compile/validate/applicability.js +19 -0
- package/node_modules/ajv/dist/compile/validate/applicability.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/boolSchema.d.ts +4 -0
- package/node_modules/ajv/dist/compile/validate/boolSchema.js +50 -0
- package/node_modules/ajv/dist/compile/validate/boolSchema.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/dataType.d.ts +17 -0
- package/node_modules/ajv/dist/compile/validate/dataType.js +203 -0
- package/node_modules/ajv/dist/compile/validate/dataType.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/defaults.d.ts +2 -0
- package/node_modules/ajv/dist/compile/validate/defaults.js +35 -0
- package/node_modules/ajv/dist/compile/validate/defaults.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/index.d.ts +42 -0
- package/node_modules/ajv/dist/compile/validate/index.js +520 -0
- package/node_modules/ajv/dist/compile/validate/index.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/keyword.d.ts +8 -0
- package/node_modules/ajv/dist/compile/validate/keyword.js +124 -0
- package/node_modules/ajv/dist/compile/validate/keyword.js.map +1 -0
- package/node_modules/ajv/dist/compile/validate/subschema.d.ts +47 -0
- package/node_modules/ajv/dist/compile/validate/subschema.js +81 -0
- package/node_modules/ajv/dist/compile/validate/subschema.js.map +1 -0
- package/node_modules/ajv/dist/core.d.ts +174 -0
- package/node_modules/ajv/dist/core.js +618 -0
- package/node_modules/ajv/dist/core.js.map +1 -0
- package/node_modules/ajv/dist/jtd.d.ts +47 -0
- package/node_modules/ajv/dist/jtd.js +72 -0
- package/node_modules/ajv/dist/jtd.js.map +1 -0
- package/node_modules/ajv/dist/refs/data.json +13 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/index.d.ts +2 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/index.js +28 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/index.js.map +1 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/meta/applicator.json +53 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/meta/content.json +17 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/meta/core.json +57 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/meta/format.json +14 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/meta/meta-data.json +37 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/meta/validation.json +90 -0
- package/node_modules/ajv/dist/refs/json-schema-2019-09/schema.json +39 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/index.d.ts +2 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/index.js +30 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/index.js.map +1 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/applicator.json +48 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/content.json +17 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/core.json +51 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/format-annotation.json +14 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/meta-data.json +37 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/unevaluated.json +15 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.json +90 -0
- package/node_modules/ajv/dist/refs/json-schema-2020-12/schema.json +55 -0
- package/node_modules/ajv/dist/refs/json-schema-draft-06.json +137 -0
- package/node_modules/ajv/dist/refs/json-schema-draft-07.json +151 -0
- package/node_modules/ajv/dist/refs/json-schema-secure.json +88 -0
- package/node_modules/ajv/dist/refs/jtd-schema.d.ts +3 -0
- package/node_modules/ajv/dist/refs/jtd-schema.js +118 -0
- package/node_modules/ajv/dist/refs/jtd-schema.js.map +1 -0
- package/node_modules/ajv/dist/runtime/equal.d.ts +6 -0
- package/node_modules/ajv/dist/runtime/equal.js +7 -0
- package/node_modules/ajv/dist/runtime/equal.js.map +1 -0
- package/node_modules/ajv/dist/runtime/parseJson.d.ts +18 -0
- package/node_modules/ajv/dist/runtime/parseJson.js +185 -0
- package/node_modules/ajv/dist/runtime/parseJson.js.map +1 -0
- package/node_modules/ajv/dist/runtime/quote.d.ts +5 -0
- package/node_modules/ajv/dist/runtime/quote.js +30 -0
- package/node_modules/ajv/dist/runtime/quote.js.map +1 -0
- package/node_modules/ajv/dist/runtime/re2.d.ts +6 -0
- package/node_modules/ajv/dist/runtime/re2.js +6 -0
- package/node_modules/ajv/dist/runtime/re2.js.map +1 -0
- package/node_modules/ajv/dist/runtime/timestamp.d.ts +5 -0
- package/node_modules/ajv/dist/runtime/timestamp.js +42 -0
- package/node_modules/ajv/dist/runtime/timestamp.js.map +1 -0
- package/node_modules/ajv/dist/runtime/ucs2length.d.ts +5 -0
- package/node_modules/ajv/dist/runtime/ucs2length.js +24 -0
- package/node_modules/ajv/dist/runtime/ucs2length.js.map +1 -0
- package/node_modules/ajv/dist/runtime/uri.d.ts +6 -0
- package/node_modules/ajv/dist/runtime/uri.js +6 -0
- package/node_modules/ajv/dist/runtime/uri.js.map +1 -0
- package/node_modules/ajv/dist/runtime/validation_error.d.ts +7 -0
- package/node_modules/ajv/dist/runtime/validation_error.js +11 -0
- package/node_modules/ajv/dist/runtime/validation_error.js.map +1 -0
- package/node_modules/ajv/dist/standalone/index.d.ts +6 -0
- package/node_modules/ajv/dist/standalone/index.js +90 -0
- package/node_modules/ajv/dist/standalone/index.js.map +1 -0
- package/node_modules/ajv/dist/standalone/instance.d.ts +12 -0
- package/node_modules/ajv/dist/standalone/instance.js +35 -0
- package/node_modules/ajv/dist/standalone/instance.js.map +1 -0
- package/node_modules/ajv/dist/types/index.d.ts +183 -0
- package/node_modules/ajv/dist/types/index.js +3 -0
- package/node_modules/ajv/dist/types/index.js.map +1 -0
- package/node_modules/ajv/dist/types/json-schema.d.ts +125 -0
- package/node_modules/ajv/dist/types/json-schema.js +3 -0
- package/node_modules/ajv/dist/types/json-schema.js.map +1 -0
- package/node_modules/ajv/dist/types/jtd-schema.d.ts +174 -0
- package/node_modules/ajv/dist/types/jtd-schema.js +3 -0
- package/node_modules/ajv/dist/types/jtd-schema.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/additionalItems.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/applicator/additionalItems.js +49 -0
- package/node_modules/ajv/dist/vocabularies/applicator/additionalItems.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/additionalProperties.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js +106 -0
- package/node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/allOf.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/applicator/allOf.js +23 -0
- package/node_modules/ajv/dist/vocabularies/applicator/allOf.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/anyOf.d.ts +4 -0
- package/node_modules/ajv/dist/vocabularies/applicator/anyOf.js +12 -0
- package/node_modules/ajv/dist/vocabularies/applicator/anyOf.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/contains.d.ts +7 -0
- package/node_modules/ajv/dist/vocabularies/applicator/contains.js +95 -0
- package/node_modules/ajv/dist/vocabularies/applicator/contains.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/dependencies.d.ts +21 -0
- package/node_modules/ajv/dist/vocabularies/applicator/dependencies.js +85 -0
- package/node_modules/ajv/dist/vocabularies/applicator/dependencies.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/dependentSchemas.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/applicator/dependentSchemas.js +11 -0
- package/node_modules/ajv/dist/vocabularies/applicator/dependentSchemas.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/if.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/applicator/if.js +66 -0
- package/node_modules/ajv/dist/vocabularies/applicator/if.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/index.d.ts +13 -0
- package/node_modules/ajv/dist/vocabularies/applicator/index.js +44 -0
- package/node_modules/ajv/dist/vocabularies/applicator/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/items.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/applicator/items.js +52 -0
- package/node_modules/ajv/dist/vocabularies/applicator/items.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/items2020.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/applicator/items2020.js +30 -0
- package/node_modules/ajv/dist/vocabularies/applicator/items2020.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/not.d.ts +4 -0
- package/node_modules/ajv/dist/vocabularies/applicator/not.js +26 -0
- package/node_modules/ajv/dist/vocabularies/applicator/not.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/oneOf.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/applicator/oneOf.js +60 -0
- package/node_modules/ajv/dist/vocabularies/applicator/oneOf.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/patternProperties.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/applicator/patternProperties.js +75 -0
- package/node_modules/ajv/dist/vocabularies/applicator/patternProperties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/prefixItems.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/applicator/prefixItems.js +12 -0
- package/node_modules/ajv/dist/vocabularies/applicator/prefixItems.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/properties.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/applicator/properties.js +54 -0
- package/node_modules/ajv/dist/vocabularies/applicator/properties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/propertyNames.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/applicator/propertyNames.js +38 -0
- package/node_modules/ajv/dist/vocabularies/applicator/propertyNames.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/applicator/thenElse.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/applicator/thenElse.js +13 -0
- package/node_modules/ajv/dist/vocabularies/applicator/thenElse.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/code.d.ts +17 -0
- package/node_modules/ajv/dist/vocabularies/code.js +131 -0
- package/node_modules/ajv/dist/vocabularies/code.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/core/id.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/core/id.js +10 -0
- package/node_modules/ajv/dist/vocabularies/core/id.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/core/index.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/core/index.js +16 -0
- package/node_modules/ajv/dist/vocabularies/core/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/core/ref.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/core/ref.js +122 -0
- package/node_modules/ajv/dist/vocabularies/core/ref.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/discriminator/index.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/discriminator/index.js +104 -0
- package/node_modules/ajv/dist/vocabularies/discriminator/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/discriminator/types.d.ts +10 -0
- package/node_modules/ajv/dist/vocabularies/discriminator/types.js +9 -0
- package/node_modules/ajv/dist/vocabularies/discriminator/types.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/draft2020.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/draft2020.js +23 -0
- package/node_modules/ajv/dist/vocabularies/draft2020.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/draft7.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/draft7.js +17 -0
- package/node_modules/ajv/dist/vocabularies/draft7.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/dynamicAnchor.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/dynamicAnchor.js +30 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/dynamicAnchor.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/dynamicRef.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/dynamicRef.js +51 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/dynamicRef.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/index.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/index.js +9 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/recursiveAnchor.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/recursiveAnchor.js +16 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/recursiveAnchor.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/recursiveRef.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/recursiveRef.js +10 -0
- package/node_modules/ajv/dist/vocabularies/dynamic/recursiveRef.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/errors.d.ts +9 -0
- package/node_modules/ajv/dist/vocabularies/errors.js +3 -0
- package/node_modules/ajv/dist/vocabularies/errors.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/format/format.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/format/format.js +92 -0
- package/node_modules/ajv/dist/vocabularies/format/format.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/format/index.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/format/index.js +6 -0
- package/node_modules/ajv/dist/vocabularies/format/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/discriminator.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/jtd/discriminator.js +71 -0
- package/node_modules/ajv/dist/vocabularies/jtd/discriminator.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/elements.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/jtd/elements.js +24 -0
- package/node_modules/ajv/dist/vocabularies/jtd/elements.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/enum.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/jtd/enum.js +43 -0
- package/node_modules/ajv/dist/vocabularies/jtd/enum.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/error.d.ts +9 -0
- package/node_modules/ajv/dist/vocabularies/jtd/error.js +20 -0
- package/node_modules/ajv/dist/vocabularies/jtd/error.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/index.d.ts +10 -0
- package/node_modules/ajv/dist/vocabularies/jtd/index.js +29 -0
- package/node_modules/ajv/dist/vocabularies/jtd/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/metadata.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/jtd/metadata.js +25 -0
- package/node_modules/ajv/dist/vocabularies/jtd/metadata.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/nullable.d.ts +4 -0
- package/node_modules/ajv/dist/vocabularies/jtd/nullable.js +22 -0
- package/node_modules/ajv/dist/vocabularies/jtd/nullable.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/optionalProperties.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/jtd/optionalProperties.js +15 -0
- package/node_modules/ajv/dist/vocabularies/jtd/optionalProperties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/properties.d.ts +22 -0
- package/node_modules/ajv/dist/vocabularies/jtd/properties.js +149 -0
- package/node_modules/ajv/dist/vocabularies/jtd/properties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/ref.d.ts +4 -0
- package/node_modules/ajv/dist/vocabularies/jtd/ref.js +67 -0
- package/node_modules/ajv/dist/vocabularies/jtd/ref.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/type.d.ts +10 -0
- package/node_modules/ajv/dist/vocabularies/jtd/type.js +69 -0
- package/node_modules/ajv/dist/vocabularies/jtd/type.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/union.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/jtd/union.js +12 -0
- package/node_modules/ajv/dist/vocabularies/jtd/union.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/jtd/values.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/jtd/values.js +51 -0
- package/node_modules/ajv/dist/vocabularies/jtd/values.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/metadata.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/metadata.js +18 -0
- package/node_modules/ajv/dist/vocabularies/metadata.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/next.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/next.js +8 -0
- package/node_modules/ajv/dist/vocabularies/next.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/index.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/index.js +7 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedItems.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedItems.js +40 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedItems.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.js +65 -0
- package/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/const.d.ts +6 -0
- package/node_modules/ajv/dist/vocabularies/validation/const.js +25 -0
- package/node_modules/ajv/dist/vocabularies/validation/const.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/dependentRequired.d.ts +5 -0
- package/node_modules/ajv/dist/vocabularies/validation/dependentRequired.js +12 -0
- package/node_modules/ajv/dist/vocabularies/validation/dependentRequired.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/enum.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/validation/enum.js +48 -0
- package/node_modules/ajv/dist/vocabularies/validation/enum.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/index.d.ts +16 -0
- package/node_modules/ajv/dist/vocabularies/validation/index.js +33 -0
- package/node_modules/ajv/dist/vocabularies/validation/index.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitContains.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitContains.js +15 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitContains.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitItems.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitItems.js +24 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitItems.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitLength.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitLength.js +27 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitLength.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitNumber.d.ts +11 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitNumber.js +27 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitNumber.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitProperties.d.ts +3 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitProperties.js +24 -0
- package/node_modules/ajv/dist/vocabularies/validation/limitProperties.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/multipleOf.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/validation/multipleOf.js +26 -0
- package/node_modules/ajv/dist/vocabularies/validation/multipleOf.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/pattern.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/validation/pattern.js +33 -0
- package/node_modules/ajv/dist/vocabularies/validation/pattern.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/required.d.ts +8 -0
- package/node_modules/ajv/dist/vocabularies/validation/required.js +79 -0
- package/node_modules/ajv/dist/vocabularies/validation/required.js.map +1 -0
- package/node_modules/ajv/dist/vocabularies/validation/uniqueItems.d.ts +9 -0
- package/node_modules/ajv/dist/vocabularies/validation/uniqueItems.js +64 -0
- package/node_modules/ajv/dist/vocabularies/validation/uniqueItems.js.map +1 -0
- package/node_modules/ajv/lib/2019.ts +81 -0
- package/node_modules/ajv/lib/2020.ts +75 -0
- package/node_modules/ajv/lib/ajv.ts +70 -0
- package/node_modules/ajv/lib/compile/codegen/code.ts +169 -0
- package/node_modules/ajv/lib/compile/codegen/index.ts +852 -0
- package/node_modules/ajv/lib/compile/codegen/scope.ts +215 -0
- package/node_modules/ajv/lib/compile/errors.ts +184 -0
- package/node_modules/ajv/lib/compile/index.ts +324 -0
- package/node_modules/ajv/lib/compile/jtd/parse.ts +411 -0
- package/node_modules/ajv/lib/compile/jtd/serialize.ts +277 -0
- package/node_modules/ajv/lib/compile/jtd/types.ts +16 -0
- package/node_modules/ajv/lib/compile/names.ts +27 -0
- package/node_modules/ajv/lib/compile/ref_error.ts +13 -0
- package/node_modules/ajv/lib/compile/resolve.ts +149 -0
- package/node_modules/ajv/lib/compile/rules.ts +50 -0
- package/node_modules/ajv/lib/compile/util.ts +213 -0
- package/node_modules/ajv/lib/compile/validate/applicability.ts +22 -0
- package/node_modules/ajv/lib/compile/validate/boolSchema.ts +47 -0
- package/node_modules/ajv/lib/compile/validate/dataType.ts +230 -0
- package/node_modules/ajv/lib/compile/validate/defaults.ts +32 -0
- package/node_modules/ajv/lib/compile/validate/index.ts +582 -0
- package/node_modules/ajv/lib/compile/validate/keyword.ts +171 -0
- package/node_modules/ajv/lib/compile/validate/subschema.ts +135 -0
- package/node_modules/ajv/lib/core.ts +892 -0
- package/node_modules/ajv/lib/jtd.ts +132 -0
- package/node_modules/ajv/lib/refs/data.json +13 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/index.ts +28 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/meta/applicator.json +53 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/meta/content.json +17 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/meta/core.json +57 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/meta/format.json +14 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/meta/meta-data.json +37 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/meta/validation.json +90 -0
- package/node_modules/ajv/lib/refs/json-schema-2019-09/schema.json +39 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/index.ts +30 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/applicator.json +48 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/content.json +17 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/core.json +51 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/format-annotation.json +14 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/meta-data.json +37 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/unevaluated.json +15 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/meta/validation.json +90 -0
- package/node_modules/ajv/lib/refs/json-schema-2020-12/schema.json +55 -0
- package/node_modules/ajv/lib/refs/json-schema-draft-06.json +137 -0
- package/node_modules/ajv/lib/refs/json-schema-draft-07.json +151 -0
- package/node_modules/ajv/lib/refs/json-schema-secure.json +88 -0
- package/node_modules/ajv/lib/refs/jtd-schema.ts +130 -0
- package/node_modules/ajv/lib/runtime/equal.ts +7 -0
- package/node_modules/ajv/lib/runtime/parseJson.ts +177 -0
- package/node_modules/ajv/lib/runtime/quote.ts +31 -0
- package/node_modules/ajv/lib/runtime/re2.ts +6 -0
- package/node_modules/ajv/lib/runtime/timestamp.ts +46 -0
- package/node_modules/ajv/lib/runtime/ucs2length.ts +20 -0
- package/node_modules/ajv/lib/runtime/uri.ts +6 -0
- package/node_modules/ajv/lib/runtime/validation_error.ts +13 -0
- package/node_modules/ajv/lib/standalone/index.ts +100 -0
- package/node_modules/ajv/lib/standalone/instance.ts +36 -0
- package/node_modules/ajv/lib/types/index.ts +244 -0
- package/node_modules/ajv/lib/types/json-schema.ts +187 -0
- package/node_modules/ajv/lib/types/jtd-schema.ts +273 -0
- package/node_modules/ajv/lib/vocabularies/applicator/additionalItems.ts +56 -0
- package/node_modules/ajv/lib/vocabularies/applicator/additionalProperties.ts +118 -0
- package/node_modules/ajv/lib/vocabularies/applicator/allOf.ts +22 -0
- package/node_modules/ajv/lib/vocabularies/applicator/anyOf.ts +14 -0
- package/node_modules/ajv/lib/vocabularies/applicator/contains.ts +109 -0
- package/node_modules/ajv/lib/vocabularies/applicator/dependencies.ts +112 -0
- package/node_modules/ajv/lib/vocabularies/applicator/dependentSchemas.ts +11 -0
- package/node_modules/ajv/lib/vocabularies/applicator/if.ts +80 -0
- package/node_modules/ajv/lib/vocabularies/applicator/index.ts +53 -0
- package/node_modules/ajv/lib/vocabularies/applicator/items.ts +59 -0
- package/node_modules/ajv/lib/vocabularies/applicator/items2020.ts +36 -0
- package/node_modules/ajv/lib/vocabularies/applicator/not.ts +38 -0
- package/node_modules/ajv/lib/vocabularies/applicator/oneOf.ts +82 -0
- package/node_modules/ajv/lib/vocabularies/applicator/patternProperties.ts +91 -0
- package/node_modules/ajv/lib/vocabularies/applicator/prefixItems.ts +12 -0
- package/node_modules/ajv/lib/vocabularies/applicator/properties.ts +57 -0
- package/node_modules/ajv/lib/vocabularies/applicator/propertyNames.ts +50 -0
- package/node_modules/ajv/lib/vocabularies/applicator/thenElse.ts +13 -0
- package/node_modules/ajv/lib/vocabularies/code.ts +168 -0
- package/node_modules/ajv/lib/vocabularies/core/id.ts +10 -0
- package/node_modules/ajv/lib/vocabularies/core/index.ts +16 -0
- package/node_modules/ajv/lib/vocabularies/core/ref.ts +129 -0
- package/node_modules/ajv/lib/vocabularies/discriminator/index.ts +113 -0
- package/node_modules/ajv/lib/vocabularies/discriminator/types.ts +12 -0
- package/node_modules/ajv/lib/vocabularies/draft2020.ts +23 -0
- package/node_modules/ajv/lib/vocabularies/draft7.ts +17 -0
- package/node_modules/ajv/lib/vocabularies/dynamic/dynamicAnchor.ts +31 -0
- package/node_modules/ajv/lib/vocabularies/dynamic/dynamicRef.ts +51 -0
- package/node_modules/ajv/lib/vocabularies/dynamic/index.ts +9 -0
- package/node_modules/ajv/lib/vocabularies/dynamic/recursiveAnchor.ts +14 -0
- package/node_modules/ajv/lib/vocabularies/dynamic/recursiveRef.ts +10 -0
- package/node_modules/ajv/lib/vocabularies/errors.ts +18 -0
- package/node_modules/ajv/lib/vocabularies/format/format.ts +120 -0
- package/node_modules/ajv/lib/vocabularies/format/index.ts +6 -0
- package/node_modules/ajv/lib/vocabularies/jtd/discriminator.ts +89 -0
- package/node_modules/ajv/lib/vocabularies/jtd/elements.ts +32 -0
- package/node_modules/ajv/lib/vocabularies/jtd/enum.ts +45 -0
- package/node_modules/ajv/lib/vocabularies/jtd/error.ts +23 -0
- package/node_modules/ajv/lib/vocabularies/jtd/index.ts +37 -0
- package/node_modules/ajv/lib/vocabularies/jtd/metadata.ts +24 -0
- package/node_modules/ajv/lib/vocabularies/jtd/nullable.ts +21 -0
- package/node_modules/ajv/lib/vocabularies/jtd/optionalProperties.ts +15 -0
- package/node_modules/ajv/lib/vocabularies/jtd/properties.ts +184 -0
- package/node_modules/ajv/lib/vocabularies/jtd/ref.ts +76 -0
- package/node_modules/ajv/lib/vocabularies/jtd/type.ts +75 -0
- package/node_modules/ajv/lib/vocabularies/jtd/union.ts +12 -0
- package/node_modules/ajv/lib/vocabularies/jtd/values.ts +58 -0
- package/node_modules/ajv/lib/vocabularies/metadata.ts +17 -0
- package/node_modules/ajv/lib/vocabularies/next.ts +8 -0
- package/node_modules/ajv/lib/vocabularies/unevaluated/index.ts +7 -0
- package/node_modules/ajv/lib/vocabularies/unevaluated/unevaluatedItems.ts +47 -0
- package/node_modules/ajv/lib/vocabularies/unevaluated/unevaluatedProperties.ts +85 -0
- package/node_modules/ajv/lib/vocabularies/validation/const.ts +28 -0
- package/node_modules/ajv/lib/vocabularies/validation/dependentRequired.ts +23 -0
- package/node_modules/ajv/lib/vocabularies/validation/enum.ts +54 -0
- package/node_modules/ajv/lib/vocabularies/validation/index.ts +49 -0
- package/node_modules/ajv/lib/vocabularies/validation/limitContains.ts +16 -0
- package/node_modules/ajv/lib/vocabularies/validation/limitItems.ts +26 -0
- package/node_modules/ajv/lib/vocabularies/validation/limitLength.ts +30 -0
- package/node_modules/ajv/lib/vocabularies/validation/limitNumber.ts +42 -0
- package/node_modules/ajv/lib/vocabularies/validation/limitProperties.ts +26 -0
- package/node_modules/ajv/lib/vocabularies/validation/multipleOf.ts +34 -0
- package/node_modules/ajv/lib/vocabularies/validation/pattern.ts +39 -0
- package/node_modules/ajv/lib/vocabularies/validation/required.ts +98 -0
- package/node_modules/ajv/lib/vocabularies/validation/uniqueItems.ts +79 -0
- package/node_modules/ajv/package.json +126 -0
- package/node_modules/fast-deep-equal/LICENSE +21 -0
- package/node_modules/fast-deep-equal/README.md +96 -0
- package/node_modules/fast-deep-equal/es6/index.d.ts +2 -0
- package/node_modules/fast-deep-equal/es6/index.js +72 -0
- package/node_modules/fast-deep-equal/es6/react.d.ts +2 -0
- package/node_modules/fast-deep-equal/es6/react.js +79 -0
- package/node_modules/fast-deep-equal/index.d.ts +4 -0
- package/node_modules/fast-deep-equal/index.js +46 -0
- package/node_modules/fast-deep-equal/package.json +61 -0
- package/node_modules/fast-deep-equal/react.d.ts +2 -0
- package/node_modules/fast-deep-equal/react.js +53 -0
- package/node_modules/fast-uri/.gitattributes +2 -0
- package/node_modules/fast-uri/.github/dependabot.yml +13 -0
- package/node_modules/fast-uri/.github/workflows/ci.yml +106 -0
- package/node_modules/fast-uri/.github/workflows/lock-threads.yml +19 -0
- package/node_modules/fast-uri/.github/workflows/package-manager-ci.yml +24 -0
- package/node_modules/fast-uri/LICENSE +30 -0
- package/node_modules/fast-uri/README.md +152 -0
- package/node_modules/fast-uri/benchmark/benchmark.mjs +159 -0
- package/node_modules/fast-uri/benchmark/equal.mjs +51 -0
- package/node_modules/fast-uri/benchmark/non-simple-domain.mjs +22 -0
- package/node_modules/fast-uri/benchmark/package.json +17 -0
- package/node_modules/fast-uri/benchmark/string-array-to-hex-stripped.mjs +24 -0
- package/node_modules/fast-uri/benchmark/ws-is-secure.mjs +65 -0
- package/node_modules/fast-uri/eslint.config.js +6 -0
- package/node_modules/fast-uri/index.js +406 -0
- package/node_modules/fast-uri/lib/schemes.js +267 -0
- package/node_modules/fast-uri/lib/utils.js +443 -0
- package/node_modules/fast-uri/package.json +68 -0
- package/node_modules/fast-uri/test/ajv.test.js +43 -0
- package/node_modules/fast-uri/test/equal.test.js +117 -0
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +501 -0
- package/node_modules/fast-uri/test/fixtures/uri-js-serialize.json +120 -0
- package/node_modules/fast-uri/test/parse.test.js +323 -0
- package/node_modules/fast-uri/test/resolve.test.js +87 -0
- package/node_modules/fast-uri/test/rfc-3986.test.js +90 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +39 -0
- package/node_modules/fast-uri/test/security.test.js +133 -0
- package/node_modules/fast-uri/test/serialize.test.js +151 -0
- package/node_modules/fast-uri/test/uri-js-compatibility.test.js +33 -0
- package/node_modules/fast-uri/test/uri-js.test.js +912 -0
- package/node_modules/fast-uri/test/util.test.js +38 -0
- package/node_modules/fast-uri/tsconfig.json +9 -0
- package/node_modules/fast-uri/types/index.d.ts +60 -0
- package/node_modules/fast-uri/types/index.test-d.ts +17 -0
- package/node_modules/json-schema-traverse/.eslintrc.yml +27 -0
- package/node_modules/json-schema-traverse/.github/FUNDING.yml +2 -0
- package/node_modules/json-schema-traverse/.github/workflows/build.yml +28 -0
- package/node_modules/json-schema-traverse/.github/workflows/publish.yml +27 -0
- package/node_modules/json-schema-traverse/LICENSE +21 -0
- package/node_modules/json-schema-traverse/README.md +95 -0
- package/node_modules/json-schema-traverse/index.d.ts +40 -0
- package/node_modules/json-schema-traverse/index.js +93 -0
- package/node_modules/json-schema-traverse/package.json +43 -0
- package/node_modules/json-schema-traverse/spec/.eslintrc.yml +6 -0
- package/node_modules/json-schema-traverse/spec/fixtures/schema.js +125 -0
- package/node_modules/json-schema-traverse/spec/index.spec.js +171 -0
- package/node_modules/require-from-string/index.js +34 -0
- package/node_modules/require-from-string/license +21 -0
- package/node_modules/require-from-string/package.json +28 -0
- package/node_modules/require-from-string/readme.md +56 -0
- package/package.json +51 -0
- package/scripts/api-call.mjs +59 -0
- package/scripts/auth.mjs +262 -0
- package/scripts/build-skill-payload.mjs +338 -0
- package/scripts/cleanup.mjs +166 -0
- package/scripts/install-paths.mjs +146 -0
- package/scripts/postinstall.mjs +218 -0
- package/scripts/postpack.mjs +43 -0
- package/scripts/prepack.mjs +222 -0
- package/scripts/tty-menu.mjs +100 -0
- package/skill/README.md +55 -0
- package/skill/SKILL.md +138 -0
- package/skill/catalog.json +406 -0
- package/skill/design-rules.md +282 -0
- package/skill/design-system.md +235 -0
- package/skill/docs/SKELETON-SHELL.md +120 -0
- package/skill/docs/bottom-buttons.md +155 -0
- package/skill/docs/control/button.md +147 -0
- package/skill/docs/control/calendar-navigator.md +143 -0
- package/skill/docs/control/calendar.md +128 -0
- package/skill/docs/control/check-box.md +117 -0
- package/skill/docs/control/combo.md +293 -0
- package/skill/docs/control/embed.md +211 -0
- package/skill/docs/control/image-box.md +237 -0
- package/skill/docs/control/input-date.md +175 -0
- package/skill/docs/control/input-file.md +192 -0
- package/skill/docs/control/input-mask.md +89 -0
- package/skill/docs/control/input-number.md +55 -0
- package/skill/docs/control/input-text.md +73 -0
- package/skill/docs/control/label.md +127 -0
- package/skill/docs/control/multi-input-box.md +51 -0
- package/skill/docs/control/radio-box.md +150 -0
- package/skill/docs/control/search.md +163 -0
- package/skill/docs/control/tab.md +240 -0
- package/skill/docs/control/tree.md +278 -0
- package/skill/docs/control.md +264 -0
- package/skill/docs/data-objects.md +278 -0
- package/skill/docs/data-sources.md +215 -0
- package/skill/docs/events.md +467 -0
- package/skill/docs/group.md +146 -0
- package/skill/docs/index.json +125 -0
- package/skill/docs/mockup-data.md +186 -0
- package/skill/docs/node.md +32 -0
- package/skill/docs/scenario.md +112 -0
- package/skill/docs/service/CustomSystem.md +139 -0
- package/skill/docs/service/Firestore.md +125 -0
- package/skill/docs/service/FlexAutoQuery.md +128 -0
- package/skill/docs/service/FlexSQL.md +96 -0
- package/skill/docs/service/Flextudio API.md +161 -0
- package/skill/docs/service/Flextudio.md +87 -0
- package/skill/docs/service/GoogleSheet.md +71 -0
- package/skill/docs/service/README.md +127 -0
- package/skill/docs/service/ksystem.md +76 -0
- package/skill/docs/service-binding.md +228 -0
- package/skill/docs/step.md +546 -0
- package/skill/docs/style-objects.md +183 -0
- package/skill/examples/README.md +147 -0
- package/skill/examples/basic/basic-list-detail.json +2521 -0
- package/skill/examples/basic/delivery-address-popup.json +2175 -0
- package/skill/examples/basic/fixedtop-floating-form.json +1524 -0
- package/skill/examples/basic/search-period-popup.json +1524 -0
- package/skill/examples/design/booking-wizard-3step.json +1335 -0
- package/skill/examples/design/list-detail-form-3step.json +1516 -0
- package/skill/examples/design/list-filter-2step.json +1365 -0
- package/skill/examples/design/list-filter-detail-3step.json +1570 -0
- package/skill/examples/design/result-entry-wizard-4step.json +1861 -0
- package/skill/examples/index.json +150 -0
- package/skill/examples/intent/mock-employee-list.json +54 -0
- package/skill/examples/intent/script-cart-flow.json +96 -0
- package/skill/examples/intent/script-dialog-confirm.json +61 -0
- package/skill/intent-rules.md +552 -0
- package/skill/intent-system.md +118 -0
- package/skill/patterns/atom/badge.json +103 -0
- package/skill/patterns/atom/button.json +111 -0
- package/skill/patterns/atom/checkbox.json +70 -0
- package/skill/patterns/atom/segmented-radio.json +92 -0
- package/skill/patterns/atom/switch.json +57 -0
- package/skill/patterns/index.json +471 -0
- package/skill/patterns/molecule/checklist.json +107 -0
- package/skill/patterns/molecule/input-action.json +355 -0
- package/skill/patterns/molecule/input-button.json +212 -0
- package/skill/patterns/molecule/input-pair.json +284 -0
- package/skill/references/build-sequence.md +55 -0
- package/skill/references/patterns-cheatsheet.md +87 -0
- package/skill/references/spec-evolve/README.md +43 -0
- package/skill/references/spec-evolve/conventions.md +92 -0
- package/skill/references/spec-evolve/data-connection.md +102 -0
- package/skill/references/spec-evolve/intake.md +41 -0
- package/skill/references/spec-evolve/key-roles.md +44 -0
- package/skill/references/spec-evolve/lint.md +27 -0
- package/skill/references/spec-evolve/mockup.md +50 -0
- package/skill/references/spec-evolve/query-screens.md +66 -0
- package/skill/references/spec-evolve/routing.md +43 -0
- package/skill/references/spec-evolve/spec-format.md +76 -0
- package/skill/references/spec-evolve/templates/master-detail.spec.md +97 -0
- package/skill/references/spec-evolve/templates/query-screen.spec.md +96 -0
- package/skill/references/spec-evolve/templates/static-prototype.spec.md +54 -0
- package/skill/references/spec-evolve/templates-index.json +26 -0
- package/skill/rules.md +400 -0
- package/skill/schemas/bottom-buttons.schema.json +134 -0
- package/skill/schemas/control/_base.schema.json +1296 -0
- package/skill/schemas/control/button.schema.json +55 -0
- package/skill/schemas/control/calendar-navigator.schema.json +164 -0
- package/skill/schemas/control/calendar.schema.json +18 -0
- package/skill/schemas/control/check-box.schema.json +41 -0
- package/skill/schemas/control/combo.schema.json +23 -0
- package/skill/schemas/control/embed.schema.json +30 -0
- package/skill/schemas/control/image-box.schema.json +49 -0
- package/skill/schemas/control/input-date.schema.json +27 -0
- package/skill/schemas/control/input-file.schema.json +139 -0
- package/skill/schemas/control/input-mask.schema.json +45 -0
- package/skill/schemas/control/input-number.schema.json +24 -0
- package/skill/schemas/control/input-text.schema.json +24 -0
- package/skill/schemas/control/label.schema.json +113 -0
- package/skill/schemas/control/multi-input-box.schema.json +19 -0
- package/skill/schemas/control/other.schema.json +24 -0
- package/skill/schemas/control/radio-box.schema.json +87 -0
- package/skill/schemas/control/search.schema.json +23 -0
- package/skill/schemas/control/tab.schema.json +112 -0
- package/skill/schemas/control/tree.schema.json +83 -0
- package/skill/schemas/control.schema.json +30 -0
- package/skill/schemas/data-objects.schema.json +161 -0
- package/skill/schemas/data-sources.schema.json +184 -0
- package/skill/schemas/design-system/design.schema.json +496 -0
- package/skill/schemas/events.schema.json +272 -0
- package/skill/schemas/group.schema.json +133 -0
- package/skill/schemas/index.json +362 -0
- package/skill/schemas/naming-objects.schema.json +133 -0
- package/skill/schemas/node.schema.json +24 -0
- package/skill/schemas/patterns/atom/badge.schema.json +44 -0
- package/skill/schemas/patterns/atom/button.schema.json +70 -0
- package/skill/schemas/patterns/atom/checkbox.schema.json +39 -0
- package/skill/schemas/patterns/atom/segmented-radio.schema.json +49 -0
- package/skill/schemas/patterns/atom/switch.schema.json +27 -0
- package/skill/schemas/patterns/molecule/checklist.schema.json +63 -0
- package/skill/schemas/patterns/molecule/input-action.schema.json +251 -0
- package/skill/schemas/patterns/molecule/input-button.schema.json +141 -0
- package/skill/schemas/patterns/molecule/input-pair.schema.json +210 -0
- package/skill/schemas/scenario.schema.json +161 -0
- package/skill/schemas/service/CustomSystem.schema.json +41 -0
- package/skill/schemas/service/Firestore.schema.json +28 -0
- package/skill/schemas/service/FlexAutoQuery.schema.json +35 -0
- package/skill/schemas/service/FlexSQL.schema.json +33 -0
- package/skill/schemas/service/Flextudio API.schema.json +115 -0
- package/skill/schemas/service/Flextudio.schema.json +26 -0
- package/skill/schemas/service/GoogleSheet.schema.json +31 -0
- package/skill/schemas/service/_base.schema.json +31 -0
- package/skill/schemas/service/ksystem.schema.json +36 -0
- package/skill/schemas/service-binding.schema.json +51 -0
- package/skill/schemas/step.schema.json +258 -0
- package/skill/schemas/style-objects.schema.json +155 -0
- package/skill/skill-guides/README.md +50 -0
- package/skill/skill-guides/control-options.md +126 -0
- package/skill/skill-guides/events.md +61 -0
- package/skill/skill-guides/failure-discipline.md +95 -0
- package/skill/skill-guides/index.json +40 -0
- package/skill/skill-guides/prompt-interview.md +92 -0
- package/skill/skill-guides/scripts.md +169 -0
- package/skill/skill-guides/step-navigation.md +98 -0
|
@@ -0,0 +1,2471 @@
|
|
|
1
|
+
// validate.mjs 의 cross-check 11종을 모듈로 포팅.
|
|
2
|
+
// 시나리오 전체 컨텍스트를 가정하므로 fragment 검증 시에는 호출하지 않는다.
|
|
3
|
+
|
|
4
|
+
const CKEY_BOUND = new Set([
|
|
5
|
+
'InputText', 'InputNumber', 'InputMask', 'InputDate', 'MultiInputBox',
|
|
6
|
+
'Combo', 'Search', 'CheckBox', 'RadioBox',
|
|
7
|
+
// Tree 는 Ckeys 로 컬렉션 컬럼을 바인딩하는 컨트롤 — 단일선택 시 래퍼 Group
|
|
8
|
+
// UseDataConnection:true 필수(tree.md). 누락 시 dc-empty 가 Tree 의 Ckeys 를
|
|
9
|
+
// 못 세어 오탐(DC 그룹 자식에 Tree 만 있는 트리/마스터-디테일 케이스).
|
|
10
|
+
'Tree',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const DIALOG_HOST = new Set(['Combo', 'Search', 'InputDate']);
|
|
14
|
+
|
|
15
|
+
const DIALOG_INNER_OF = { Combo: 'ComboList', Search: 'List' };
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------------ */
|
|
18
|
+
/* [reach] Step reachability */
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
export function checkStepReachability(scenario) {
|
|
21
|
+
const steps = scenario?.Steps ?? {};
|
|
22
|
+
const stepIds = Object.keys(steps);
|
|
23
|
+
const startSteps = Array.isArray(scenario?.StartSteps) ? scenario.StartSteps : [];
|
|
24
|
+
|
|
25
|
+
const occurrences = new Map();
|
|
26
|
+
const add = (id, from, via) => {
|
|
27
|
+
if (!occurrences.has(id)) occurrences.set(id, []);
|
|
28
|
+
occurrences.get(id).push({ from, via });
|
|
29
|
+
};
|
|
30
|
+
for (const id of startSteps) add(id, 'StartSteps', null);
|
|
31
|
+
for (const sid of stepIds) {
|
|
32
|
+
const next = steps[sid]?.Next;
|
|
33
|
+
if (Array.isArray(next)) for (const target of next) add(target, 'Step.Next', sid);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const errors = [];
|
|
37
|
+
for (const id of startSteps) {
|
|
38
|
+
if (!stepIds.includes(id)) errors.push(`StartSteps references unknown Step '${id}'`);
|
|
39
|
+
}
|
|
40
|
+
for (const sid of stepIds) {
|
|
41
|
+
const next = steps[sid]?.Next;
|
|
42
|
+
if (Array.isArray(next)) {
|
|
43
|
+
for (const target of next) {
|
|
44
|
+
if (!stepIds.includes(target)) {
|
|
45
|
+
errors.push(`Step '${sid}'.Next references unknown Step '${target}'`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const sid of stepIds) {
|
|
51
|
+
const occ = occurrences.get(sid) ?? [];
|
|
52
|
+
if (occ.length === 0) {
|
|
53
|
+
errors.push(`Step '${sid}' is unreachable: not in StartSteps and not in any Step.Next`);
|
|
54
|
+
} else if (occ.length > 1) {
|
|
55
|
+
const where = occ.map(o => o.via ? `${o.from}('${o.via}')` : o.from).join(', ');
|
|
56
|
+
errors.push(`Step '${sid}' is referenced ${occ.length} times (must be exactly 1; cycle or multi-entry detected): ${where}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const sid of stepIds) {
|
|
60
|
+
const bbNext = steps[sid]?.BackButton?.Next;
|
|
61
|
+
if (bbNext === undefined || bbNext === 'None') continue;
|
|
62
|
+
if (typeof bbNext !== 'string') continue;
|
|
63
|
+
if (bbNext === sid) {
|
|
64
|
+
errors.push(`Step '${sid}'.BackButton.Next references itself ('${sid}') — must be 'None' or another StepId`);
|
|
65
|
+
} else if (!stepIds.includes(bbNext)) {
|
|
66
|
+
errors.push(`Step '${sid}'.BackButton.Next references unknown Step '${bbNext}'`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/* [id] Id 유일성 */
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
export function checkIdUniqueness(scenario) {
|
|
76
|
+
const found = [];
|
|
77
|
+
const visit = (node, path) => {
|
|
78
|
+
if (!node || typeof node !== 'object') return;
|
|
79
|
+
if (typeof node.Id === 'string') found.push({ id: node.Id, path });
|
|
80
|
+
if (Array.isArray(node.Contents)) {
|
|
81
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
82
|
+
}
|
|
83
|
+
const layouts = node?.Dialog?.Layouts;
|
|
84
|
+
if (Array.isArray(layouts)) {
|
|
85
|
+
layouts.forEach((layout, li) => {
|
|
86
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
87
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const steps = scenario?.Steps ?? {};
|
|
94
|
+
for (const sid of Object.keys(steps)) {
|
|
95
|
+
const step = steps[sid];
|
|
96
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
97
|
+
(step?.[ch] ?? []).forEach((g, i) => visit(g, `Steps/${sid}/${ch}/${i}`));
|
|
98
|
+
}
|
|
99
|
+
(step?.BottomButtons ?? []).forEach((b, i) => {
|
|
100
|
+
if (b && typeof b.Id === 'string') {
|
|
101
|
+
found.push({ id: b.Id, path: `Steps/${sid}/BottomButtons/${i}` });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const groups = new Map();
|
|
107
|
+
for (const { id, path } of found) {
|
|
108
|
+
if (!groups.has(id)) groups.set(id, []);
|
|
109
|
+
groups.get(id).push(path);
|
|
110
|
+
}
|
|
111
|
+
const errors = [];
|
|
112
|
+
for (const [id, paths] of groups.entries()) {
|
|
113
|
+
if (paths.length > 1) {
|
|
114
|
+
errors.push(`Id '${id}' duplicated ${paths.length} times: ${paths.join(' | ')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return errors;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ------------------------------------------------------------------ */
|
|
121
|
+
/* [date] InputDate ↔ Calendar 정합 */
|
|
122
|
+
/* ------------------------------------------------------------------ */
|
|
123
|
+
export function checkInputDateConsistency(scenario) {
|
|
124
|
+
const errors = [];
|
|
125
|
+
const isFilledArr = (v) => Array.isArray(v) && v.length > 0;
|
|
126
|
+
// 페어(SingleDate/StartDate/EndDate) 객체 누락은 항상 에러. 빈 Ckeys 는 조상 Group 중
|
|
127
|
+
// UseDataConnection:true 인 컨텍스트에서만 에러 — 비-연결 캘린더 placeholder 는 빈 배열 허용.
|
|
128
|
+
const visit = (node, path, hasActiveDC) => {
|
|
129
|
+
if (!node || typeof node !== 'object') return;
|
|
130
|
+
const dcCtx = hasActiveDC || (node.ContentsType === 'Group' && node.UseDataConnection === true);
|
|
131
|
+
|
|
132
|
+
if (node.ControlType === 'InputDate') {
|
|
133
|
+
const df = node.DateFormat;
|
|
134
|
+
const layouts = node?.Dialog?.Layouts;
|
|
135
|
+
const calendars = [];
|
|
136
|
+
if (Array.isArray(layouts)) {
|
|
137
|
+
layouts.forEach((layout, li) => {
|
|
138
|
+
(layout?.Controls ?? []).forEach((c, ci) => {
|
|
139
|
+
if (c && c.ControlType === 'Calendar') {
|
|
140
|
+
calendars.push({ ctrl: c, p: `${path}/Dialog/Layouts/${li}/Controls/${ci}` });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const { ctrl, p } of calendars) {
|
|
146
|
+
const ct = ctrl.CalendarType;
|
|
147
|
+
if (df === 'YYYY-MM' && ct && ct !== 'monthCalendar') {
|
|
148
|
+
errors.push(`${path}.DateFormat='YYYY-MM' requires inner Calendar.CalendarType='monthCalendar' (got '${ct}' at ${p})`);
|
|
149
|
+
}
|
|
150
|
+
if (ct === 'monthCalendar' && df && !['', 'YYYY-MM'].includes(df)) {
|
|
151
|
+
errors.push(`${path}.DateFormat='${df}' is incompatible with Calendar.CalendarType='monthCalendar' (use '' or 'YYYY-MM')`);
|
|
152
|
+
}
|
|
153
|
+
const st = ctrl.SelectType;
|
|
154
|
+
if (st === 'SingleDate') {
|
|
155
|
+
if (!ctrl.SingleDate) {
|
|
156
|
+
errors.push(`${p} (Calendar) SelectType='SingleDate' 인데 SingleDate 객체 누락 — pair 룰. 런타임이 dlgJson.SingleDate.Ckeys 로 직접 접근.`);
|
|
157
|
+
} else if (dcCtx && !isFilledArr(ctrl.SingleDate.Ckeys)) {
|
|
158
|
+
errors.push(`${p} (Calendar) SelectType='SingleDate' + 조상 Group UseDataConnection:true 인데 SingleDate.Ckeys 가 비어있음 — 데이터 연결 활성 컨텍스트에서는 1개 이상 ckey 필요.`);
|
|
159
|
+
}
|
|
160
|
+
} else if (st === 'FromTo') {
|
|
161
|
+
if (!ctrl.StartDate) {
|
|
162
|
+
errors.push(`${p} (Calendar) SelectType='FromTo' 인데 StartDate 객체 누락 — pair 룰.`);
|
|
163
|
+
} else if (dcCtx && !isFilledArr(ctrl.StartDate.Ckeys)) {
|
|
164
|
+
errors.push(`${p} (Calendar) SelectType='FromTo' + 조상 Group UseDataConnection:true 인데 StartDate.Ckeys 가 비어있음 — 데이터 연결 활성 컨텍스트에서는 1개 이상 ckey 필요.`);
|
|
165
|
+
}
|
|
166
|
+
if (!ctrl.EndDate) {
|
|
167
|
+
errors.push(`${p} (Calendar) SelectType='FromTo' 인데 EndDate 객체 누락 — pair 룰.`);
|
|
168
|
+
} else if (dcCtx && !isFilledArr(ctrl.EndDate.Ckeys)) {
|
|
169
|
+
errors.push(`${p} (Calendar) SelectType='FromTo' + 조상 Group UseDataConnection:true 인데 EndDate.Ckeys 가 비어있음 — 데이터 연결 활성 컨텍스트에서는 1개 이상 ckey 필요.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(node.Contents)) {
|
|
175
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, dcCtx));
|
|
176
|
+
}
|
|
177
|
+
const layouts = node?.Dialog?.Layouts;
|
|
178
|
+
if (Array.isArray(layouts)) {
|
|
179
|
+
layouts.forEach((layout, li) => {
|
|
180
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
181
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, dcCtx)
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
forEachStepRoot(scenario, (root, p) => visit(root, p, false));
|
|
187
|
+
return errors;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ------------------------------------------------------------------ */
|
|
191
|
+
/* [event] Combo/Search/InputDate 본체 이벤트 위치 */
|
|
192
|
+
/* ------------------------------------------------------------------ */
|
|
193
|
+
export function checkDialogEventPlacement(scenario) {
|
|
194
|
+
const errors = [];
|
|
195
|
+
const visit = (node, path) => {
|
|
196
|
+
if (!node || typeof node !== 'object') return;
|
|
197
|
+
if (DIALOG_HOST.has(node.ControlType)) {
|
|
198
|
+
if (node.UseEvents === true || node.Events !== undefined) {
|
|
199
|
+
errors.push(`${path} (${node.ControlType}) has UseEvents/Events on root — must be inside Dialog.Layouts[0].Controls[0] (system-prompt-v2 §7.7)`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(node.Contents)) {
|
|
203
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
204
|
+
}
|
|
205
|
+
const layouts = node?.Dialog?.Layouts;
|
|
206
|
+
if (Array.isArray(layouts)) {
|
|
207
|
+
layouts.forEach((layout, li) => {
|
|
208
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
209
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
forEachStepRoot(scenario, visit);
|
|
215
|
+
return errors;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ------------------------------------------------------------------ */
|
|
219
|
+
/* [dialog-host] Combo/Search/InputDate Dialog 구조 필수 */
|
|
220
|
+
/* - 런타임/스튜디오가 `controlMeta.Dialog.Layouts[0].Controls[0]` 직 */
|
|
221
|
+
/* 접 접근 후 `.UseMultiData` 등을 읽는다. Dialog 가 빠지면 */
|
|
222
|
+
/* "Cannot read properties of undefined (reading 'UseMultiData')" */
|
|
223
|
+
/* TypeError 발생 → 시나리오 자체가 깨진 화면이 된다. */
|
|
224
|
+
/* - 따라서 본체에 Dialog.Layouts[0].Controls[0] 와 짝 내부 컨트롤이 */
|
|
225
|
+
/* 반드시 존재해야 한다 (makeInputDate / makeCombo / makeSearch */
|
|
226
|
+
/* helper 가 항상 채움 — 직접 작성한 raw JSON 에서 누락되기 쉬움). */
|
|
227
|
+
/* ------------------------------------------------------------------ */
|
|
228
|
+
const DIALOG_HOST_INNER = {
|
|
229
|
+
Combo: 'ComboList',
|
|
230
|
+
Search: 'List',
|
|
231
|
+
InputDate: 'Calendar',
|
|
232
|
+
};
|
|
233
|
+
export function checkDialogHostStructure(scenario) {
|
|
234
|
+
const errors = [];
|
|
235
|
+
const visit = (node, path) => {
|
|
236
|
+
if (!node || typeof node !== 'object') return;
|
|
237
|
+
if (DIALOG_HOST.has(node.ControlType)) {
|
|
238
|
+
const expected = DIALOG_HOST_INNER[node.ControlType];
|
|
239
|
+
const dlg = node.Dialog;
|
|
240
|
+
const layouts = dlg && dlg.Layouts;
|
|
241
|
+
const layout0 = Array.isArray(layouts) ? layouts[0] : undefined;
|
|
242
|
+
const controls = layout0 && layout0.Controls;
|
|
243
|
+
const inner = Array.isArray(controls) ? controls[0] : undefined;
|
|
244
|
+
const where = `${path} (${node.ControlType})`;
|
|
245
|
+
const helperHint =
|
|
246
|
+
node.ControlType === 'InputDate' ? 'makeInputDate({ inner: makeInnerCalendar({...}) })'
|
|
247
|
+
: node.ControlType === 'Combo' ? 'makeCombo({ inner: makeInnerComboList({...}) })'
|
|
248
|
+
: 'makeSearch({ inner: makeInnerSearchList({...}) })';
|
|
249
|
+
if (!dlg || typeof dlg !== 'object') {
|
|
250
|
+
errors.push(`${where} Dialog 누락 — 런타임이 controlMeta.Dialog.Layouts[0].Controls[0] 를 직접 읽으므로 TypeError. ${helperHint} 로 내부 ${expected} 까지 함께 생성할 것.`);
|
|
251
|
+
} else if (!Array.isArray(layouts) || layouts.length < 1) {
|
|
252
|
+
errors.push(`${where} Dialog.Layouts 가 비어있음 — Layouts[0].Controls[0] 에 ${expected} 가 있어야 한다.`);
|
|
253
|
+
} else if (!Array.isArray(controls) || controls.length < 1 || !inner) {
|
|
254
|
+
errors.push(`${where} Dialog.Layouts[0].Controls[0] 누락 — ${expected} 내부 컨트롤을 채울 것.`);
|
|
255
|
+
} else if (inner.ControlType !== expected) {
|
|
256
|
+
errors.push(`${where} Dialog.Layouts[0].Controls[0].ControlType='${inner.ControlType}' — ${expected} 여야 한다 (${node.ControlType} 의 짝 내부 컨트롤).`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(node.Contents)) {
|
|
260
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
261
|
+
}
|
|
262
|
+
const layouts = node?.Dialog?.Layouts;
|
|
263
|
+
if (Array.isArray(layouts)) {
|
|
264
|
+
layouts.forEach((layout, li) => {
|
|
265
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
266
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
forEachStepRoot(scenario, visit);
|
|
272
|
+
return errors;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ------------------------------------------------------------------ */
|
|
276
|
+
/* [sample] AI 생성 시 sample-safe DataSource 만 허용 (Fixed/Grid) */
|
|
277
|
+
/* ------------------------------------------------------------------ */
|
|
278
|
+
export function checkSampleDataSources(scenario, strictSample = true) {
|
|
279
|
+
if (!strictSample) return [];
|
|
280
|
+
const errors = [];
|
|
281
|
+
const ds = scenario?.DataSources;
|
|
282
|
+
if (!ds || typeof ds !== 'object') return [];
|
|
283
|
+
const allowed = new Set(['Fixed', 'Grid']);
|
|
284
|
+
for (const [name, defn] of Object.entries(ds)) {
|
|
285
|
+
if (!defn || typeof defn !== 'object') continue;
|
|
286
|
+
const t = defn.DataSourceType;
|
|
287
|
+
if (t && !allowed.has(t)) {
|
|
288
|
+
errors.push(`DataSources.${name}.DataSourceType='${t}' is not sample-safe — AI generation must use 'Fixed' or 'Grid' (system-prompt-v2 §17). Set strict_sample=false to allow operational types.`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return errors;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ------------------------------------------------------------------ */
|
|
295
|
+
/* [dsref] DataSourceName 참조 → scenario.DataSources 정의 존재 검증 */
|
|
296
|
+
/* ------------------------------------------------------------------ */
|
|
297
|
+
export function checkDataSourceReferences(scenario) {
|
|
298
|
+
const errors = [];
|
|
299
|
+
const ds = scenario?.DataSources;
|
|
300
|
+
const known = new Set(ds && typeof ds === 'object' ? Object.keys(ds) : []);
|
|
301
|
+
const refs = [];
|
|
302
|
+
const visit = (node, path) => {
|
|
303
|
+
if (!node || typeof node !== 'object') return;
|
|
304
|
+
if (typeof node.DataSourceName === 'string' && node.DataSourceName) {
|
|
305
|
+
refs.push({ name: node.DataSourceName, path });
|
|
306
|
+
}
|
|
307
|
+
if (Array.isArray(node.Contents)) {
|
|
308
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
309
|
+
}
|
|
310
|
+
const layouts = node?.Dialog?.Layouts;
|
|
311
|
+
if (Array.isArray(layouts)) {
|
|
312
|
+
layouts.forEach((layout, li) => {
|
|
313
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
314
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
forEachStepRoot(scenario, visit);
|
|
320
|
+
for (const { name, path } of refs) {
|
|
321
|
+
if (!known.has(name)) {
|
|
322
|
+
errors.push(`${path}.DataSourceName='${name}' is not defined in scenario.DataSources (known: ${[...known].join(', ') || '(empty)'})`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return errors;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ------------------------------------------------------------------ */
|
|
329
|
+
/* [bind] Ckey 바인딩 ↔ 조상 Group DataConnection */
|
|
330
|
+
/* ------------------------------------------------------------------ */
|
|
331
|
+
export function checkCkeyBinding(scenario) {
|
|
332
|
+
const errors = [];
|
|
333
|
+
const visit = (node, path, hasDC) => {
|
|
334
|
+
if (!node || typeof node !== 'object') return;
|
|
335
|
+
|
|
336
|
+
let nextHasDC = hasDC;
|
|
337
|
+
if (node.ContentsType === 'Group' && node.UseDataConnection === true && node.DataConnection) {
|
|
338
|
+
nextHasDC = true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const ct = node.ControlType;
|
|
342
|
+
if (ct && CKEY_BOUND.has(ct)) {
|
|
343
|
+
const hasCkeys = Array.isArray(node.Ckeys) && node.Ckeys.length > 0;
|
|
344
|
+
const hasSaveValueKey = typeof node.SaveValueKey === 'string' && node.SaveValueKey.length > 0;
|
|
345
|
+
if ((hasCkeys || hasSaveValueKey) && !hasDC) {
|
|
346
|
+
const why = hasCkeys ? 'Ckeys' : 'SaveValueKey';
|
|
347
|
+
errors.push(`${path} (${ct}) uses ${why} but no ancestor Group has UseDataConnection:true + DataConnection`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (ct === 'Label' && node.LabelType === 'LabelCKey' && typeof node.LabelCKey === 'string' && node.LabelCKey.length > 0 && !hasDC) {
|
|
351
|
+
errors.push(`${path} (Label LabelCKey='${node.LabelCKey}') requires ancestor Group with UseDataConnection:true + DataConnection`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (Array.isArray(node.Contents)) {
|
|
355
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, nextHasDC));
|
|
356
|
+
}
|
|
357
|
+
const layouts = node?.Dialog?.Layouts;
|
|
358
|
+
if (Array.isArray(layouts)) {
|
|
359
|
+
layouts.forEach((layout, li) => {
|
|
360
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
361
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, nextHasDC)
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
forEachStepRoot(scenario, (node, path) => visit(node, path, false));
|
|
367
|
+
return errors;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* ------------------------------------------------------------------ */
|
|
371
|
+
/* [move] MoveSteps 타깃 Step 존재 검증 */
|
|
372
|
+
/* ------------------------------------------------------------------ */
|
|
373
|
+
export function checkMoveStepsTargets(scenario) {
|
|
374
|
+
const errors = [];
|
|
375
|
+
const stepIds = new Set(Object.keys(scenario?.Steps ?? {}));
|
|
376
|
+
const visit = (node, path) => {
|
|
377
|
+
if (!node || typeof node !== 'object') return;
|
|
378
|
+
if (node.UseMove === true && node.MoveSteps) {
|
|
379
|
+
const ms = node.MoveSteps;
|
|
380
|
+
const order = ms.MoveStepOrder;
|
|
381
|
+
if (Array.isArray(order)) {
|
|
382
|
+
for (const target of order) {
|
|
383
|
+
if (typeof target !== 'string' || !target) continue;
|
|
384
|
+
if (!stepIds.has(target)) {
|
|
385
|
+
errors.push(`${path}.MoveSteps.MoveStepOrder references unknown Step '${target}'`);
|
|
386
|
+
}
|
|
387
|
+
// [move-steps-shape] property-data.js createMoveStepArea 가 moveSteps[stepId]
|
|
388
|
+
// 로 옵션 객체에 접근. 누락 시 valueObj[K.MOVETO] 에서 TypeError 로 프로퍼티
|
|
389
|
+
// 패널 렌더 실패. MoveStepOrder 의 모든 StepId 가 동일 레벨에 옵션 객체 키로
|
|
390
|
+
// 존재해야 한다 (schema 의 patternProperties 만으로는 강제 불가).
|
|
391
|
+
const opt = ms[target];
|
|
392
|
+
if (!opt || typeof opt !== 'object' || Array.isArray(opt)) {
|
|
393
|
+
errors.push(
|
|
394
|
+
`${path}.MoveSteps.${target} 옵션 객체 누락 — { Condition: '', MoveTo: 'Next' } 형태로 ` +
|
|
395
|
+
`같은 레벨에 추가 필요. property-data.js 가 moveSteps[stepId] 로 접근, 누락 시 ` +
|
|
396
|
+
`프로퍼티 패널 렌더 TypeError (moveTypeCombo.value(valueObj[K.MOVETO])).`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (Array.isArray(node.Contents)) {
|
|
403
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
404
|
+
}
|
|
405
|
+
const layouts = node?.Dialog?.Layouts;
|
|
406
|
+
if (Array.isArray(layouts)) {
|
|
407
|
+
layouts.forEach((layout, li) => {
|
|
408
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
409
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const steps = scenario?.Steps ?? {};
|
|
415
|
+
for (const sid of Object.keys(steps)) {
|
|
416
|
+
const step = steps[sid];
|
|
417
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
418
|
+
(step?.[ch] ?? []).forEach((g, i) => visit(g, `Steps/${sid}/${ch}/${i}`));
|
|
419
|
+
}
|
|
420
|
+
(step?.BottomButtons ?? []).forEach((b, i) => visit(b, `Steps/${sid}/BottomButtons/${i}`));
|
|
421
|
+
}
|
|
422
|
+
return errors;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ------------------------------------------------------------------ */
|
|
426
|
+
/* [ds-key] Combo/Search 본체 ↔ 자식 ComboList/List Ckeys 일치 */
|
|
427
|
+
/* ------------------------------------------------------------------ */
|
|
428
|
+
export function checkComboSearchInnerKeys(scenario) {
|
|
429
|
+
const errors = [];
|
|
430
|
+
const visit = (node, path) => {
|
|
431
|
+
if (!node || typeof node !== 'object') return;
|
|
432
|
+
const ct = node.ControlType;
|
|
433
|
+
if (DIALOG_INNER_OF[ct]) {
|
|
434
|
+
const innerType = DIALOG_INNER_OF[ct];
|
|
435
|
+
const layouts = node?.Dialog?.Layouts;
|
|
436
|
+
if (Array.isArray(layouts)) {
|
|
437
|
+
layouts.forEach((layout, li) => {
|
|
438
|
+
(layout?.Controls ?? []).forEach((inner, ci) => {
|
|
439
|
+
if (!inner || inner.ControlType !== innerType) return;
|
|
440
|
+
const ipath = `${path}/Dialog/Layouts/${li}/Controls/${ci}`;
|
|
441
|
+
// 본체-자식 Ckeys 정합 (본체 Ckeys 는 [combo-body-ckeys] 가 별도 차단 — 있을 때만 비교)
|
|
442
|
+
if (Array.isArray(node.Ckeys) && Array.isArray(inner.Ckeys)) {
|
|
443
|
+
const a = node.Ckeys, b = inner.Ckeys;
|
|
444
|
+
const eq = a.length === b.length && a.every((k, idx) => k === b[idx]);
|
|
445
|
+
if (!eq) {
|
|
446
|
+
errors.push(`${path}.Ckeys=[${a.join(',')}] does not match Dialog/Layouts/${li}/Controls/${ci} (${innerType}).Ckeys=[${b.join(',')}]`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Title — 리스트 표시 키 (ComboList/List). DataSource 모드에서 누락 시 선택지에 명칭이 안 보임.
|
|
450
|
+
if (innerType === 'ComboList' || innerType === 'List') {
|
|
451
|
+
const dsMode = inner.UseDataSource !== false &&
|
|
452
|
+
typeof inner.DataSourceName === 'string' && inner.DataSourceName.length > 0;
|
|
453
|
+
const title = inner.Title;
|
|
454
|
+
if (dsMode && (typeof title !== 'string' || title.length === 0)) {
|
|
455
|
+
errors.push(
|
|
456
|
+
`${ipath} (${innerType}, DataSource '${inner.DataSourceName}') 에 Title 이 없음 — ` +
|
|
457
|
+
`Title 에 DataSource 키 중 화면에 보여줄 키를 연결해야 리스트에 명칭이 보인다 ` +
|
|
458
|
+
`(Fixed DataSource 면 FixedItems 의 키, 예: "ItemName"/"ItemValue"). schema/v1/control/combo.md §ComboList.Title.`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
// Title 이 실제 원본 데이터 필드인지 (정적으로 알 수 있는 Fixed DataSource / 인라인 Items 만)
|
|
462
|
+
if (typeof title === 'string' && title.length > 0) {
|
|
463
|
+
let objs = null, src = '';
|
|
464
|
+
if (dsMode) {
|
|
465
|
+
const ds = scenario?.DataSources?.[inner.DataSourceName];
|
|
466
|
+
if (ds && ds.DataSourceType === 'Fixed' && Array.isArray(ds.FixedItems)) {
|
|
467
|
+
objs = ds.FixedItems.filter((o) => o && typeof o === 'object' && !Array.isArray(o));
|
|
468
|
+
src = `DataSource '${inner.DataSourceName}'.FixedItems`;
|
|
469
|
+
}
|
|
470
|
+
} else if (Array.isArray(inner.Items)) {
|
|
471
|
+
objs = inner.Items.filter((o) => o && typeof o === 'object' && !Array.isArray(o));
|
|
472
|
+
src = 'Items';
|
|
473
|
+
}
|
|
474
|
+
if (objs && objs.length > 0 &&
|
|
475
|
+
!objs.every((o) => Object.prototype.hasOwnProperty.call(o, title))) {
|
|
476
|
+
errors.push(
|
|
477
|
+
`${ipath} (${innerType}) 의 Title='${title}' 이 ${src} 의 필드에 없음 — ` +
|
|
478
|
+
`선택지가 빈 칸으로 표시된다. 실제 항목 객체의 키 중 표시할 키로 연결할 것.`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (Array.isArray(node.Contents)) {
|
|
488
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
489
|
+
}
|
|
490
|
+
const layouts = node?.Dialog?.Layouts;
|
|
491
|
+
if (Array.isArray(layouts)) {
|
|
492
|
+
layouts.forEach((layout, li) => {
|
|
493
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
494
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
forEachStepRoot(scenario, visit);
|
|
500
|
+
return errors;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ------------------------------------------------------------------ */
|
|
504
|
+
/* [combo-body-ckeys] Combo/Search 본체(Dialog 밖)에는 Ckeys 금지 */
|
|
505
|
+
/* - 스튜디오 속성 패널은 본체에 Ckeys 입력이 없다 (property-data.js */
|
|
506
|
+
/* createDataSection 은 'ComboList' 만 등록). Ckeys 는 자식 */
|
|
507
|
+
/* ComboList(Combo)/List(Search) 의 속성이다. 운영 메타 대다수도 */
|
|
508
|
+
/* 본체 Ckeys 가 없다(있는 건 런타임이 무시하는 레거시 잔재). */
|
|
509
|
+
/* - 본체 선택값 표시는 DisplayValue:"{=표시Ckey}" 또는 CtrlDisplayCkey. */
|
|
510
|
+
/* - 자식 Ckeys 는 그대로 둔다(여기선 본체만 검사). */
|
|
511
|
+
/* - opts.strictComboBodyCkeys=false 로 비활성. */
|
|
512
|
+
/* ------------------------------------------------------------------ */
|
|
513
|
+
export function checkComboBodyNoCkeys(scenario, strictComboBodyCkeys = true) {
|
|
514
|
+
if (!strictComboBodyCkeys) return [];
|
|
515
|
+
const errors = [];
|
|
516
|
+
const visit = (node, path) => {
|
|
517
|
+
if (!node || typeof node !== 'object') return;
|
|
518
|
+
const ct = node.ControlType;
|
|
519
|
+
if (DIALOG_INNER_OF[ct] && Array.isArray(node.Ckeys) && node.Ckeys.length > 0) {
|
|
520
|
+
const inner = DIALOG_INNER_OF[ct];
|
|
521
|
+
errors.push(
|
|
522
|
+
`${path} (${ct} 본체) 에 Ckeys=[${node.Ckeys.join(', ')}] 가 있음 — ` +
|
|
523
|
+
`Combo/Search 본체(Dialog 밖)에는 Ckeys 를 두지 않는다. ` +
|
|
524
|
+
`Ckeys 는 자식 ${inner}(Dialog.Layouts[*].Controls[*]) 에만 두고, ` +
|
|
525
|
+
`본체 선택값 표시는 DisplayValue:"{=표시Ckey}" 또는 CtrlDisplayCkey 로 한다. (strictComboBodyCkeys=false 로 비활성)`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (Array.isArray(node.Contents)) {
|
|
529
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
530
|
+
}
|
|
531
|
+
const layouts = node?.Dialog?.Layouts;
|
|
532
|
+
if (Array.isArray(layouts)) {
|
|
533
|
+
layouts.forEach((layout, li) =>
|
|
534
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
535
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
forEachStepRoot(scenario, visit);
|
|
539
|
+
return errors;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ------------------------------------------------------------------ */
|
|
543
|
+
/* [dc-parent-key] DCParentKey/DCChildKey 는 DataConnection 최상위 금지 */
|
|
544
|
+
/* - 부모-자식 조인 키는 TargetSector.Filters 의 Parent 필터 안에 둔다: */
|
|
545
|
+
/* { FilterType:"Parent", DCParentKey, DCChildKey } (운영 메타·schema */
|
|
546
|
+
/* sectorFilter Parent 분기 정본). new 부모키 매핑이면 DCLinkCkey. */
|
|
547
|
+
/* DCLinkItems[] 안. DataConnection 최상위 flat 은 어느 경우도 아니다. */
|
|
548
|
+
/* - 모델이 new(DCLinkItems) 와 query(Parent 필터)를 혼동해 flat 으로 두는 */
|
|
549
|
+
/* 케이스. materialize(canonicalizeDataConnection)가 query/Parent 는 */
|
|
550
|
+
/* 자동 이전하므로, 여기 걸리는 건 그 외(이전 불가) 잔여 케이스. */
|
|
551
|
+
/* - opts.strictDcParentKey=false 로 비활성. */
|
|
552
|
+
/* ------------------------------------------------------------------ */
|
|
553
|
+
export function checkDataConnectionParentKeys(scenario, strictDcParentKey = true) {
|
|
554
|
+
if (!strictDcParentKey) return [];
|
|
555
|
+
const errors = [];
|
|
556
|
+
const visit = (node, path) => {
|
|
557
|
+
if (!node || typeof node !== 'object') return;
|
|
558
|
+
const dc = node.DataConnection;
|
|
559
|
+
if (dc && typeof dc === 'object' && !Array.isArray(dc)) {
|
|
560
|
+
const hasP = typeof dc.DCParentKey === 'string' && dc.DCParentKey !== '';
|
|
561
|
+
const hasC = typeof dc.DCChildKey === 'string' && dc.DCChildKey !== '';
|
|
562
|
+
if (hasP || hasC) {
|
|
563
|
+
const which = [hasP ? 'DCParentKey' : null, hasC ? 'DCChildKey' : null].filter(Boolean).join('/');
|
|
564
|
+
errors.push(
|
|
565
|
+
`${path}/DataConnection 에 flat ${which} 가 있음 — 부모-자식 조인 키는 DataConnection 최상위가 아니라 ` +
|
|
566
|
+
`TargetSector.Filters 의 Parent 필터 안에 둔다: ` +
|
|
567
|
+
`{ "FilterType":"Parent", "DCParentKey":"…", "DCChildKey":"…" }. ` +
|
|
568
|
+
`(new 부모키 매핑이면 DCLinkCkey.DCLinkItems[] 안. strictDcParentKey=false 로 비활성)`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (Array.isArray(node.Contents)) {
|
|
573
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
574
|
+
}
|
|
575
|
+
const layouts = node?.Dialog?.Layouts;
|
|
576
|
+
if (Array.isArray(layouts)) {
|
|
577
|
+
layouts.forEach((layout, li) =>
|
|
578
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
579
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
forEachStepRoot(scenario, visit);
|
|
583
|
+
return errors;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* ------------------------------------------------------------------ */
|
|
587
|
+
/* [dc-parent-ctx] 상위 데이터연결 없는 Parent 필터 금지 */
|
|
588
|
+
/* - TargetSector.Filters 의 { FilterType:"Parent" } 는 조상 Group 의 */
|
|
589
|
+
/* DataConnection 섹터(부모 행)와 DCParentKey/DCChildKey 를 매칭하는 */
|
|
590
|
+
/* 필터다. 조상 체인에 DC 그룹이 하나도 없으면 매칭할 부모 섹터 자체가 */
|
|
591
|
+
/* 없어 빈 목록/오동작 — 모델이 1:N 예제를 흉내내며 독립 목록에도 */
|
|
592
|
+
/* Parent 필터를 붙이는 케이스. */
|
|
593
|
+
/* - 독립 목록이면 Parent 필터를 제거(일반 query), 부모-자식 구조면 */
|
|
594
|
+
/* 부모 Group 에 UseDataConnection:true + DataConnection 을 먼저 둔다. */
|
|
595
|
+
/* - opts.strictDcParentCtx=false 로 비활성. */
|
|
596
|
+
/* ------------------------------------------------------------------ */
|
|
597
|
+
export function checkParentFilterAncestorDc(scenario, strictDcParentCtx = true) {
|
|
598
|
+
if (!strictDcParentCtx) return [];
|
|
599
|
+
const errors = [];
|
|
600
|
+
const visit = (node, path, hasAncestorDc) => {
|
|
601
|
+
if (!node || typeof node !== 'object') return;
|
|
602
|
+
const dc = node.DataConnection;
|
|
603
|
+
const filters = dc?.TargetSector?.Filters;
|
|
604
|
+
if (Array.isArray(filters) && !hasAncestorDc) {
|
|
605
|
+
filters.forEach((f, fi) => {
|
|
606
|
+
if (f && f.FilterType === 'Parent') {
|
|
607
|
+
errors.push(
|
|
608
|
+
`${path}/DataConnection/TargetSector/Filters/${fi} 에 Parent 필터가 있는데 상위(조상)에 데이터연결이 없음 — ` +
|
|
609
|
+
`Parent 필터는 조상 Group 의 DataConnection 섹터(부모 행)와 DCParentKey/DCChildKey 를 매칭한다. ` +
|
|
610
|
+
`독립 목록이면 Parent 필터를 제거하고 일반 query 로 두고, 부모-자식 1:N 이면 ` +
|
|
611
|
+
`부모 Group 에 UseDataConnection:true + DataConnection 을 먼저 둘 것. (strictDcParentCtx=false 로 비활성)`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const childHasDc = hasAncestorDc || (node.UseDataConnection === true && !!dc);
|
|
617
|
+
if (Array.isArray(node.Contents)) {
|
|
618
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, childHasDc));
|
|
619
|
+
}
|
|
620
|
+
const layouts = node?.Dialog?.Layouts;
|
|
621
|
+
if (Array.isArray(layouts)) {
|
|
622
|
+
layouts.forEach((layout, li) =>
|
|
623
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
624
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, childHasDc)));
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
forEachStepRoot(scenario, (g, p) => visit(g, p, false));
|
|
628
|
+
return errors;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* ------------------------------------------------------------------ */
|
|
632
|
+
/* [combo-fixed] Fixed 모드 ComboList 의 SaveNameKey/SaveValueKey 정합 */
|
|
633
|
+
/* - UseDataSource:false + Items (인라인 선택지) 일 때, SaveNameKey(표시) */
|
|
634
|
+
/* / SaveValueKey(저장 주값) 가 실제 Items 객체 필드로 resolve 돼야 함. */
|
|
635
|
+
/* - resolve: CollectionMapper[key] 있으면 그 값, 없으면 key 자체. */
|
|
636
|
+
/* resolved 필드가 모든 Items 객체에 있어야 선택 후 표시/저장됨 — */
|
|
637
|
+
/* 없으면 런타임에 빈 콤보 (기본은 SaveNameKey='ItemName'/'ItemValue'). */
|
|
638
|
+
/* - 미지정(SaveNameKey 없음)은 별도 규칙(선택 후 텍스트 미표시) — 여기선 */
|
|
639
|
+
/* "지정했는데 Items 필드와 불일치" 만 잡는다. */
|
|
640
|
+
/* - opts.strictComboFixed=false (runAllCrossChecks) 로 비활성. */
|
|
641
|
+
/* ------------------------------------------------------------------ */
|
|
642
|
+
export function checkComboFixedKeys(scenario, strictComboFixed = true) {
|
|
643
|
+
if (!strictComboFixed) return [];
|
|
644
|
+
const errors = [];
|
|
645
|
+
const visit = (node, path) => {
|
|
646
|
+
if (!node || typeof node !== 'object') return;
|
|
647
|
+
if (
|
|
648
|
+
node.ControlType === 'ComboList' &&
|
|
649
|
+
node.UseDataSource === false &&
|
|
650
|
+
Array.isArray(node.Items) &&
|
|
651
|
+
node.Items.length > 0
|
|
652
|
+
) {
|
|
653
|
+
const mapper =
|
|
654
|
+
node.CollectionMapper && typeof node.CollectionMapper === 'object' && !Array.isArray(node.CollectionMapper)
|
|
655
|
+
? node.CollectionMapper
|
|
656
|
+
: {};
|
|
657
|
+
const itemObjs = node.Items.filter((it) => it && typeof it === 'object' && !Array.isArray(it));
|
|
658
|
+
const fieldInAllItems = (field) =>
|
|
659
|
+
itemObjs.length > 0 && itemObjs.every((it) => Object.prototype.hasOwnProperty.call(it, field));
|
|
660
|
+
const itemFields = itemObjs.length > 0 ? Object.keys(itemObjs[0]) : [];
|
|
661
|
+
const ckeys = Array.isArray(node.Ckeys) ? node.Ckeys : [];
|
|
662
|
+
for (const slot of ['SaveNameKey', 'SaveValueKey']) {
|
|
663
|
+
const key = node[slot];
|
|
664
|
+
if (typeof key !== 'string' || key === '') continue; // 미지정은 별도 규칙
|
|
665
|
+
// Fixed 모드 의미론 (studio-init.js: SaveValueKey='값에 해당하는 컬렉션 키',
|
|
666
|
+
// SaveNameKey='이름에 해당하는 컬렉션 키'):
|
|
667
|
+
// - SaveValueKey/SaveNameKey 는 "저장될 컬렉션키"(=Ckeys 멤버)이지 Items 필드명이 아니다.
|
|
668
|
+
// - Fixed Items 는 고정 필드 ItemValue(값)/ItemName(표시)를 가지며,
|
|
669
|
+
// 런타임이 Item.ItemValue → 컬렉션[SaveValueKey], Item.ItemName → 컬렉션[SaveNameKey] 로 반영.
|
|
670
|
+
// - CollectionMapper 는 UseDataSource:true(DataSource 모드) 전용 — Fixed 에선 쓰지 않는다.
|
|
671
|
+
// (레거시로 매핑이 있으면 그 값을 원본 필드로 존중하되, 기본은 ItemValue/ItemName.)
|
|
672
|
+
const fixedField = slot === 'SaveValueKey' ? 'ItemValue' : 'ItemName';
|
|
673
|
+
const resolved = typeof mapper[key] === 'string' && mapper[key] ? mapper[key] : fixedField;
|
|
674
|
+
if (!fieldInAllItems(resolved)) {
|
|
675
|
+
errors.push(
|
|
676
|
+
`${path} (Fixed ComboList) ${slot}='${key}' 의 표시/저장 원본 필드 '${resolved}'${mapper[key] ? ` (CollectionMapper→)` : ''} 가 Items 에 없음 — ` +
|
|
677
|
+
`Fixed 콤보 Items 는 {ItemValue(값), ItemName(표시)} 고정 필드를 가져야 선택 후 표시/저장된다. ` +
|
|
678
|
+
`현재 Items 필드: [${itemFields.join(', ') || '(없음)'}]. (strictComboFixed=false 로 비활성)`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
// [combo-fixed/ckeys] 컬렉션에 바인딩된(Ckeys 비어있지 않은) Fixed 콤보면
|
|
682
|
+
// SaveValueKey/SaveNameKey 는 "저장 대상 컬렉션키"(Ckeys 중 하나)여야 한다.
|
|
683
|
+
// - SaveKey 에 Items 필드명('ItemValue'/'ItemName')을 직접 박으면 Ckeys(예: fLeaveType)와
|
|
684
|
+
// 어긋나 선택값이 엉뚱한 키로 저장되고 본체(CtrlDisplayCkey)가 빈 칸이 된다.
|
|
685
|
+
// - 값/표시 두 컬럼이면 Ckeys:[값키,표시키] + SaveValueKey=값키 / SaveNameKey=표시키
|
|
686
|
+
// (Items 는 ItemValue/ItemName 고정, CollectionMapper 불필요).
|
|
687
|
+
// - Ckeys 가 비면(컬렉션 미바인딩 순수 드롭다운) 저장 대상이 없어 검사 제외.
|
|
688
|
+
if (ckeys.length > 0 && !ckeys.includes(key)) {
|
|
689
|
+
errors.push(
|
|
690
|
+
`${path} (Fixed ComboList) ${slot}='${key}' 가 Ckeys=[${ckeys.join(', ')}] 의 멤버가 아님 — ` +
|
|
691
|
+
`SaveValueKey/SaveNameKey 는 저장 대상 컬렉션키(Ckeys 중 하나)여야 한다. ` +
|
|
692
|
+
`고정 Items 의 'ItemValue'/'ItemName' 를 SaveKey 로 직접 쓰지 말 것 — ` +
|
|
693
|
+
`Ckeys:[값키, 표시키] 로 선언하고 SaveValueKey=값키 / SaveNameKey=표시키 로 두면 된다 ` +
|
|
694
|
+
`(Items 는 ItemValue/ItemName 고정, CollectionMapper 불필요). ` +
|
|
695
|
+
`(현재대로면 선택값이 '${key}' 키로 저장돼 컬렉션키와 어긋나 런타임에 빈 콤보/미저장 — strictComboFixed=false 로 비활성)`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (Array.isArray(node.Contents)) {
|
|
701
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
702
|
+
}
|
|
703
|
+
const layouts = node?.Dialog?.Layouts;
|
|
704
|
+
if (Array.isArray(layouts)) {
|
|
705
|
+
layouts.forEach((layout, li) => {
|
|
706
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
707
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
forEachStepRoot(scenario, visit);
|
|
713
|
+
return errors;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* ------------------------------------------------------------------ */
|
|
717
|
+
/* [display-key] DataSource 기반 Combo/Search 본체 표시 키 필수 */
|
|
718
|
+
/* - Search 는 항상 DataSource 기반 — 본체에 CtrlDisplayCkey 또는 */
|
|
719
|
+
/* DisplayValue (비어있지 않은 값) 가 있어야 선택 후 표시 가능. */
|
|
720
|
+
/* - Combo 는 자식 ComboList.UseDataSource 분기로 판단: */
|
|
721
|
+
/* · UseDataSource !== false (true 또는 undefined → 런타임 default) */
|
|
722
|
+
/* → 본체에 CtrlDisplayCkey 또는 DisplayValue (비어있지 않은 값) 필수.*/
|
|
723
|
+
/* · UseDataSource === false (Fixed Items) → DisplayValue:"" 컨벤션 */
|
|
724
|
+
/* 허용 — 검사 대상 외 (런타임이 ComboList.SaveNameKey 로 fallback). */
|
|
725
|
+
/* - DisplayType 명시 시 그에 맞는 짝 키만 채워야 함 (상호배타). */
|
|
726
|
+
/* - 환경변수 DISPLAY_KEY_STRICT=0 또는 opts.strictDisplayKey=false 비활성. */
|
|
727
|
+
/* ------------------------------------------------------------------ */
|
|
728
|
+
export function checkDisplayCkey(scenario, strictDisplayKey = true) {
|
|
729
|
+
if (!strictDisplayKey) return [];
|
|
730
|
+
const errors = [];
|
|
731
|
+
const isFilledString = (v) => typeof v === 'string' && v.length > 0;
|
|
732
|
+
const visit = (node, path) => {
|
|
733
|
+
if (!node || typeof node !== 'object') return;
|
|
734
|
+
const ct = node.ControlType;
|
|
735
|
+
if (ct === 'Search' || ct === 'Combo') {
|
|
736
|
+
let needsDisplay = false;
|
|
737
|
+
let reason = '';
|
|
738
|
+
if (ct === 'Search') {
|
|
739
|
+
needsDisplay = true;
|
|
740
|
+
reason = 'Search 본체';
|
|
741
|
+
} else {
|
|
742
|
+
const layouts = node?.Dialog?.Layouts;
|
|
743
|
+
if (Array.isArray(layouts)) {
|
|
744
|
+
for (const layout of layouts) {
|
|
745
|
+
for (const inner of (layout?.Controls ?? [])) {
|
|
746
|
+
if (inner && inner.ControlType === 'ComboList') {
|
|
747
|
+
if (inner.UseDataSource !== false) {
|
|
748
|
+
needsDisplay = true;
|
|
749
|
+
reason = 'Combo 본체 (DataSource 모드 ComboList)';
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (needsDisplay) {
|
|
757
|
+
const dt = node.DisplayType;
|
|
758
|
+
const ccKey = node.CtrlDisplayCkey;
|
|
759
|
+
const dv = node.DisplayValue;
|
|
760
|
+
if (dt === 'CtrlDisplayCkey') {
|
|
761
|
+
if (!isFilledString(ccKey)) {
|
|
762
|
+
errors.push(`${path} (${reason}) DisplayType='CtrlDisplayCkey' 인데 CtrlDisplayCkey 가 비어있음 — 데이터소스에서 표시할 컬렉션 필드명을 채워야 한다.`);
|
|
763
|
+
}
|
|
764
|
+
} else if (dt === 'DisplayValue') {
|
|
765
|
+
if (!isFilledString(dv)) {
|
|
766
|
+
errors.push(`${path} (${reason}) DisplayType='DisplayValue' 인데 DisplayValue 가 비어있음 — '{=Field}' 또는 '{% JS %}' 표현식을 채워야 한다.`);
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
if (!isFilledString(ccKey) && !isFilledString(dv)) {
|
|
770
|
+
errors.push(`${path} (${reason}) DataSource 기반인데 CtrlDisplayCkey 와 DisplayValue 가 모두 비어있음 — Ckeys 중 본체에 표시할 필드명을 'CtrlDisplayCkey' 로 등록할 것 (또는 DisplayType='DisplayValue' + DisplayValue 표현식).`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (Array.isArray(node.Contents)) {
|
|
776
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
777
|
+
}
|
|
778
|
+
const layouts = node?.Dialog?.Layouts;
|
|
779
|
+
if (Array.isArray(layouts)) {
|
|
780
|
+
layouts.forEach((layout, li) => {
|
|
781
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
782
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
783
|
+
);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
forEachStepRoot(scenario, visit);
|
|
788
|
+
return errors;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/* ------------------------------------------------------------------ */
|
|
792
|
+
/* [category-name] 카테고리명 규칙 — 'Ctg' 접두 + PascalCase + JS 식별자 */
|
|
793
|
+
/* - 출처: runtime/v1/collection/naming.md §2-1 (Ctg + PascalCase), */
|
|
794
|
+
/* system-prompt-v2.md ("Ctg prefix + descriptive PascalCase name") */
|
|
795
|
+
/* - 안티패턴: Ctg(New|Edit|Add|Insert)* — 별도 카테고리 대신 */
|
|
796
|
+
/* DataUsage:'new'/'update' 사용 (naming.md §2-2). */
|
|
797
|
+
/* - 카테고리 식별은 DataConnection.CategoryName 만 사용한다. */
|
|
798
|
+
/* TargetSector.CategoryName 은 잘못된 형태 — 별도 에러로 보고. */
|
|
799
|
+
/* ------------------------------------------------------------------ */
|
|
800
|
+
const CATEGORY_NAME_RE = /^Ctg[A-Z][A-Za-z0-9]*$/;
|
|
801
|
+
const CATEGORY_ANTIPATTERN_RE = /^Ctg(New|Edit|Add|Insert)/i;
|
|
802
|
+
|
|
803
|
+
export function checkCategoryNaming(scenario, strictCategoryName = true) {
|
|
804
|
+
if (!strictCategoryName) return [];
|
|
805
|
+
const errors = [];
|
|
806
|
+
const seen = new Map(); // name → first path
|
|
807
|
+
const report = (name, path) => {
|
|
808
|
+
if (typeof name !== 'string' || !name) return;
|
|
809
|
+
if (!seen.has(name)) {
|
|
810
|
+
if (!CATEGORY_NAME_RE.test(name)) {
|
|
811
|
+
errors.push(
|
|
812
|
+
`${path} CategoryName='${name}' 명명 규칙 위반 — 'Ctg' 접두 + PascalCase 필수 ` +
|
|
813
|
+
`(예: CtgItemList, CtgFilter, CtgUserList). JS 식별자 규칙 (영문/숫자/언더스코어, 첫 글자 영문) 도 준수. ` +
|
|
814
|
+
`(naming.md §2-1)`
|
|
815
|
+
);
|
|
816
|
+
} else if (CATEGORY_ANTIPATTERN_RE.test(name)) {
|
|
817
|
+
errors.push(
|
|
818
|
+
`${path} CategoryName='${name}' 안티패턴 — 'Ctg(New|Edit|Add|Insert)*' 는 별도 카테고리 만드는 안티패턴. ` +
|
|
819
|
+
`메인 리스트 카테고리 + DataUsage:'new'(추가) / 'update'(수정) 사용 권장. ` +
|
|
820
|
+
`(naming.md §2-2; 단 저장 전 별도 검증/변환 로직이 필요한 케이스만 정당화)`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
seen.set(name, path);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const reportInvalidLocation = (cn, path) => {
|
|
827
|
+
if (typeof cn !== 'string' || !cn) return;
|
|
828
|
+
errors.push(
|
|
829
|
+
`${path}.CategoryName='${cn}' 는 잘못된 위치 — 카테고리 식별은 DataConnection.CategoryName 만 사용. ` +
|
|
830
|
+
`TargetSector.CategoryName 은 운영 메타에 잔존하는 레거시 형태로 마이그레이션 대상.`
|
|
831
|
+
);
|
|
832
|
+
};
|
|
833
|
+
const visit = (node, path) => {
|
|
834
|
+
if (!node || typeof node !== 'object') return;
|
|
835
|
+
if (Array.isArray(node)) { node.forEach((v, i) => visit(v, `${path}[${i}]`)); return; }
|
|
836
|
+
const dc = node.DataConnection;
|
|
837
|
+
if (dc && typeof dc === 'object') {
|
|
838
|
+
report(dc.CategoryName, `${path}.DataConnection`);
|
|
839
|
+
const ts = dc.TargetSector;
|
|
840
|
+
if (ts && typeof ts === 'object' && ts.CategoryName) {
|
|
841
|
+
reportInvalidLocation(ts.CategoryName, `${path}.DataConnection.TargetSector`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const ts2 = node.TargetSector;
|
|
845
|
+
if (ts2 && typeof ts2 === 'object' && ts2.CategoryName) {
|
|
846
|
+
reportInvalidLocation(ts2.CategoryName, `${path}.TargetSector`);
|
|
847
|
+
}
|
|
848
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
849
|
+
const layouts = node?.Dialog?.Layouts;
|
|
850
|
+
if (Array.isArray(layouts)) {
|
|
851
|
+
layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
852
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
forEachStepRoot(scenario, visit);
|
|
856
|
+
return errors;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* ------------------------------------------------------------------ */
|
|
860
|
+
/* [v1-only] v1 컨트랙트 명시 컨트롤만 사용 — MCP 생성 시 기본 strict */
|
|
861
|
+
/* - 본체 ControlType 은 아래 17종 허용 (catalog.controlTypes 와 일치) */
|
|
862
|
+
/* - ComboList / List 는 Dialog 자식 (Combo.Dialog / Search.Dialog 안) */
|
|
863
|
+
/* 으로만 허용 — 본체에 단독으로 두지 말 것 */
|
|
864
|
+
/* - Calendar 는 InputDate.Dialog 자식으로도, 본체 단독으로도 허용 */
|
|
865
|
+
/* - CalendarNavigator 는 본체/TimeTable 내부 모두 허용 */
|
|
866
|
+
/* - design-system asset: policy.v1-controls-only */
|
|
867
|
+
/* ------------------------------------------------------------------ */
|
|
868
|
+
const V1_CONTROLS_BODY = new Set([
|
|
869
|
+
'InputText', 'InputNumber', 'InputMask', 'InputDate', 'MultiInputBox',
|
|
870
|
+
'Combo', 'Search', 'CheckBox', 'RadioBox', 'Label',
|
|
871
|
+
'ImageBox', 'InputFile', 'Tree', 'Tab', 'Embed',
|
|
872
|
+
'Calendar', 'CalendarNavigator',
|
|
873
|
+
]);
|
|
874
|
+
const V1_CONTROLS_DIALOG_ONLY = new Set(['ComboList', 'List']);
|
|
875
|
+
|
|
876
|
+
export function checkV1ControlsOnly(scenario, strictV1Controls = true) {
|
|
877
|
+
if (!strictV1Controls) return [];
|
|
878
|
+
const errors = [];
|
|
879
|
+
|
|
880
|
+
const visit = (node, path, inDialog) => {
|
|
881
|
+
if (!node || typeof node !== 'object') return;
|
|
882
|
+
if (Array.isArray(node)) { node.forEach((v, i) => visit(v, `${path}[${i}]`, inDialog)); return; }
|
|
883
|
+
|
|
884
|
+
const ct = node.ControlType;
|
|
885
|
+
if (typeof ct === 'string') {
|
|
886
|
+
if (V1_CONTROLS_BODY.has(ct)) {
|
|
887
|
+
// OK
|
|
888
|
+
} else if (V1_CONTROLS_DIALOG_ONLY.has(ct)) {
|
|
889
|
+
if (!inDialog) {
|
|
890
|
+
errors.push(`${path} ControlType:'${ct}' 는 Dialog.Layouts[*].Controls 안에서만 사용 가능 — 본체에 직접 두지 말 것 (policy.v1-controls-only).`);
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
errors.push(
|
|
894
|
+
`${path} ControlType:'${ct}' 는 v1 컨트랙트 외 — v1 명시 컨트롤만 허용: ` +
|
|
895
|
+
`[${[...V1_CONTROLS_BODY].join(', ')}] (본체) / ` +
|
|
896
|
+
`[${[...V1_CONTROLS_DIALOG_ONLY].join(', ')}] (Dialog 자식 전용). ` +
|
|
897
|
+
`Button → BottomButton(Step 채널) 또는 Label + UseClickEvent. ` +
|
|
898
|
+
`다른 v1 외 컨트롤은 v2 컨트랙트 후속. (policy.v1-controls-only / strict_v1_controls=false 로 비활성)`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (Array.isArray(node.Contents)) {
|
|
904
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, inDialog));
|
|
905
|
+
}
|
|
906
|
+
const layouts = node?.Dialog?.Layouts;
|
|
907
|
+
if (Array.isArray(layouts)) {
|
|
908
|
+
layouts.forEach((layout, li) => {
|
|
909
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
910
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, true)
|
|
911
|
+
);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
forEachStepRoot(scenario, (node, path) => visit(node, path, false));
|
|
916
|
+
return errors;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* ------------------------------------------------------------------ */
|
|
920
|
+
/* [proto] 프로토타이핑 모드 — 진짜 정적 목업일 때만 DC/Ckeys/LabelCKey 차단 */
|
|
921
|
+
/* - 표준 흐름 ([mock-init] 정책): 시작 Step.Init 에서 f.Collection. */
|
|
922
|
+
/* addSector 로 목업 카테고리 데이터를 N건 박고, 그룹은 UseDataConnection*/
|
|
923
|
+
/* + DataConnection 으로 그 카테고리에 연결되어 같은 컨트롤이 N번 반복 */
|
|
924
|
+
/* 렌더링된다. 이 흐름에서는 DataSources 는 비어있어도 OK — 데이터는 */
|
|
925
|
+
/* Init mock 으로 채워진다. */
|
|
926
|
+
/* - 따라서 [proto] 는 (a) DataSources 비어있고 (b) Init 에 addSector mock*/
|
|
927
|
+
/* 패턴도 없을 때만 발동. 즉 데이터 흐름이 전혀 없는 정적 UI 목업 한정. */
|
|
928
|
+
/* - 그 경우 Ckeys / SaveValueKey / Label.LabelCKey / UseDataConnection */
|
|
929
|
+
/* 사용 금지. 정적 텍스트는 Label.labeltext + LabelType:'labeltext' 로 */
|
|
930
|
+
/* - design-system asset: policy.prototype-data-binding */
|
|
931
|
+
/* ------------------------------------------------------------------ */
|
|
932
|
+
export function checkPrototypeBinding(scenario, strictProto = true) {
|
|
933
|
+
if (!strictProto) return [];
|
|
934
|
+
const errors = [];
|
|
935
|
+
const ds = scenario?.DataSources;
|
|
936
|
+
const hasDataSource = ds && typeof ds === 'object' && Object.keys(ds).length > 0;
|
|
937
|
+
if (hasDataSource) return [];
|
|
938
|
+
|
|
939
|
+
// 시작 Step.Init 에서 addSector mock 이 한 번이라도 호출되면 mock-init + DC
|
|
940
|
+
// 반복 렌더링 흐름 — Ckeys/DataConnection 사용은 정상. [proto] 발동하지 않음.
|
|
941
|
+
// Init 이 named 핸들러(문자열)든 intent.mock 인라인(배열)이든 동일하게 인식.
|
|
942
|
+
if (categoriesAddedInScripts(gatherStartStepInitScripts(scenario)).size > 0) return [];
|
|
943
|
+
|
|
944
|
+
const visit = (node, path) => {
|
|
945
|
+
if (!node || typeof node !== 'object') return;
|
|
946
|
+
|
|
947
|
+
if (node.ContentsType === 'Group') {
|
|
948
|
+
if (node.UseDataConnection === true) {
|
|
949
|
+
errors.push(`${path} (Group) UseDataConnection:true but scenario.DataSources is empty — 프로토타이핑 모드. DataSource(Fixed/Grid) 없이 DataConnection 사용 금지 (policy.prototype-data-binding).`);
|
|
950
|
+
}
|
|
951
|
+
if (node.DataConnection && Object.keys(node.DataConnection).length > 0) {
|
|
952
|
+
errors.push(`${path} (Group) has DataConnection but scenario.DataSources is empty — DataConnection 박지 말 것.`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const ct = node.ControlType;
|
|
957
|
+
if (ct) {
|
|
958
|
+
const hasCkeys = Array.isArray(node.Ckeys) && node.Ckeys.length > 0;
|
|
959
|
+
const hasSaveValueKey = typeof node.SaveValueKey === 'string' && node.SaveValueKey.length > 0;
|
|
960
|
+
if (hasCkeys) {
|
|
961
|
+
errors.push(`${path} (${ct}) uses Ckeys=[${node.Ckeys.join(',')}] but scenario.DataSources is empty — 프로토타이핑이면 Ckeys 박지 않음. 정적 표시는 Label.labeltext.`);
|
|
962
|
+
}
|
|
963
|
+
if (hasSaveValueKey) {
|
|
964
|
+
// ComboList(Fixed 모드) 의 SaveValueKey/SaveNameKey 는 inline Items 의
|
|
965
|
+
// 객체 필드 셀렉터 (예: "ItemValue", "ItemName") — scenario.DataSources
|
|
966
|
+
// 와 무관하므로 [proto] 발동 대상에서 제외. (UseDataSource:false + Items[])
|
|
967
|
+
const isFixedComboList =
|
|
968
|
+
ct === 'ComboList' &&
|
|
969
|
+
node.UseDataSource === false &&
|
|
970
|
+
Array.isArray(node.Items);
|
|
971
|
+
if (!isFixedComboList) {
|
|
972
|
+
errors.push(`${path} (${ct}) uses SaveValueKey='${node.SaveValueKey}' but scenario.DataSources is empty.`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (ct === 'Label' && node.LabelType === 'LabelCKey' && typeof node.LabelCKey === 'string' && node.LabelCKey.length > 0) {
|
|
976
|
+
errors.push(`${path} (Label) LabelType:'LabelCKey' + LabelCKey:'${node.LabelCKey}' but scenario.DataSources is empty — 정적 텍스트는 LabelType:'labeltext' + labeltext:'예시값' 으로.`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (Array.isArray(node.Contents)) {
|
|
981
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
982
|
+
}
|
|
983
|
+
const layouts = node?.Dialog?.Layouts;
|
|
984
|
+
if (Array.isArray(layouts)) {
|
|
985
|
+
layouts.forEach((layout, li) => {
|
|
986
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
987
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
forEachStepRoot(scenario, visit);
|
|
993
|
+
return errors;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/* ------------------------------------------------------------------ */
|
|
997
|
+
/* [image] ImageBox.ViewGroup.{ImgPath,BucketUrl} 비어있어야 함 */
|
|
998
|
+
/* ------------------------------------------------------------------ */
|
|
999
|
+
export function checkImagePathEmpty(scenario, strictImage = true) {
|
|
1000
|
+
if (!strictImage) return [];
|
|
1001
|
+
const errors = [];
|
|
1002
|
+
const groupKeys = ['ViewGroup', 'DefaultViewGroup'];
|
|
1003
|
+
const fieldKeys = ['ImgPath', 'BucketUrl'];
|
|
1004
|
+
|
|
1005
|
+
const visit = (node, path) => {
|
|
1006
|
+
if (!node || typeof node !== 'object') return;
|
|
1007
|
+
if (node.ControlType === 'ImageBox') {
|
|
1008
|
+
for (const gk of groupKeys) {
|
|
1009
|
+
const g = node[gk];
|
|
1010
|
+
if (!g || typeof g !== 'object') continue;
|
|
1011
|
+
for (const fk of fieldKeys) {
|
|
1012
|
+
const v = g[fk];
|
|
1013
|
+
if (v != null && v !== '') {
|
|
1014
|
+
errors.push(
|
|
1015
|
+
`${path} (ImageBox).${gk}.${fk} must be empty (got ${JSON.stringify(v)}) ` +
|
|
1016
|
+
`— 샘플링/생성 시 비워둠 (운영 시 채워짐). strict_image=false 로 비활성화 가능.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (Array.isArray(node.Contents)) {
|
|
1023
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
1024
|
+
}
|
|
1025
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1026
|
+
if (Array.isArray(layouts)) {
|
|
1027
|
+
layouts.forEach((layout, li) => {
|
|
1028
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
1029
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
forEachStepRoot(scenario, visit);
|
|
1035
|
+
return errors;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/* ------------------------------------------------------------------ */
|
|
1039
|
+
/* [inner] UseInnerBlock 그룹의 조상 DataConnection 검증 */
|
|
1040
|
+
/* ------------------------------------------------------------------ */
|
|
1041
|
+
export function checkInnerBlockContext(scenario) {
|
|
1042
|
+
const errors = [];
|
|
1043
|
+
const visit = (node, path, ancestorHasDC) => {
|
|
1044
|
+
if (!node || typeof node !== 'object') return;
|
|
1045
|
+
if (node.ContentsType === 'Group' && node.UseInnerBlock === true) {
|
|
1046
|
+
if (!ancestorHasDC) {
|
|
1047
|
+
errors.push(`${path} has UseInnerBlock:true but no ancestor Group has UseDataConnection:true + DataConnection (sector context required for InnerBlock iteration)`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
let nextHasDC = ancestorHasDC;
|
|
1051
|
+
if (node.ContentsType === 'Group' && node.UseDataConnection === true && node.DataConnection) {
|
|
1052
|
+
nextHasDC = true;
|
|
1053
|
+
}
|
|
1054
|
+
if (Array.isArray(node.Contents)) {
|
|
1055
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, nextHasDC));
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
forEachStepRoot(scenario, (node, path) => visit(node, path, false));
|
|
1059
|
+
return errors;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* ------------------------------------------------------------------ */
|
|
1063
|
+
/* [no-service] 서비스 구성 금지 + 이벤트 액션 제한 (MCP 생성 strict) */
|
|
1064
|
+
/* - scenario.ServiceBinding 은 비어있는 객체 {} 여야 한다 */
|
|
1065
|
+
/* - scenario.Events 안의 Action 은 Script | LinkedEvent 만 허용 */
|
|
1066
|
+
/* - DataSources 의 DataSourceType:'Service'|'API' 도 함께 거부 (보강) */
|
|
1067
|
+
/* ------------------------------------------------------------------ */
|
|
1068
|
+
const ALLOWED_EVENT_ACTIONS = new Set(['Script', 'LinkedEvent']);
|
|
1069
|
+
const FORBIDDEN_DS_TYPES_GEN = new Set(['Service', 'API']);
|
|
1070
|
+
|
|
1071
|
+
export function checkNoService(scenario, strictNoService = true) {
|
|
1072
|
+
if (!strictNoService) return [];
|
|
1073
|
+
const errors = [];
|
|
1074
|
+
|
|
1075
|
+
const sb = scenario?.ServiceBinding;
|
|
1076
|
+
if (sb && typeof sb === 'object' && !Array.isArray(sb) && Object.keys(sb).length > 0) {
|
|
1077
|
+
errors.push(
|
|
1078
|
+
`scenario.ServiceBinding 에 ${Object.keys(sb).length} 개 항목이 있음 — MCP 생성에서는 ` +
|
|
1079
|
+
`ServiceBinding 비어있는 객체 {} 만 허용. 서비스 호출은 운영 단계에서 구성. ` +
|
|
1080
|
+
`(strict_no_service=false 로 비활성)`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const events = scenario?.Events;
|
|
1085
|
+
if (events && typeof events === 'object' && !Array.isArray(events)) {
|
|
1086
|
+
for (const [name, arr] of Object.entries(events)) {
|
|
1087
|
+
if (!Array.isArray(arr)) continue;
|
|
1088
|
+
arr.forEach((action, i) => {
|
|
1089
|
+
if (!action || typeof action !== 'object') return;
|
|
1090
|
+
const a = action.Action;
|
|
1091
|
+
if (typeof a !== 'string') return;
|
|
1092
|
+
if (!ALLOWED_EVENT_ACTIONS.has(a)) {
|
|
1093
|
+
errors.push(
|
|
1094
|
+
`scenario.Events['${name}'][${i}].Action='${a}' 는 MCP 생성 금지 — ` +
|
|
1095
|
+
`Script / LinkedEvent 만 허용. Service / API 액션은 운영 단계 구성. ` +
|
|
1096
|
+
`(strict_no_service=false 로 비활성)`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const ds = scenario?.DataSources;
|
|
1104
|
+
if (ds && typeof ds === 'object') {
|
|
1105
|
+
for (const [name, defn] of Object.entries(ds)) {
|
|
1106
|
+
const t = defn?.DataSourceType;
|
|
1107
|
+
if (typeof t === 'string' && FORBIDDEN_DS_TYPES_GEN.has(t)) {
|
|
1108
|
+
errors.push(
|
|
1109
|
+
`scenario.DataSources['${name}'].DataSourceType='${t}' 는 MCP 생성 금지 — ` +
|
|
1110
|
+
`Fixed / Grid 만 사용. (strict_no_service=false 로 비활성; [sample] 과 별개로 ` +
|
|
1111
|
+
`이벤트-서비스 정합성 차원에서도 차단)`
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return errors;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/* ------------------------------------------------------------------ */
|
|
1120
|
+
/* [event-ref] 이벤트 핸들러 이름 참조 정합 */
|
|
1121
|
+
/* - Step.Events / BottomButton.Events / Group.Events / Control.Events */
|
|
1122
|
+
/* 의 핸들러 값(이벤트 이름) 은 scenario.Events 의 키 중 하나여야 함. */
|
|
1123
|
+
/* - LinkedEvent.EventName 도 동일. */
|
|
1124
|
+
/* ------------------------------------------------------------------ */
|
|
1125
|
+
const EVENT_ORDER_KEYS = new Set([
|
|
1126
|
+
'StepEventOrder', 'InputEventOrder', 'ComboOrder', 'SearchOrder',
|
|
1127
|
+
'ImageEventOrder', 'CalendarNaviEventsOrder', 'DateOrder',
|
|
1128
|
+
'CheckBoxOrder', 'ContentEvent', 'InputFileOrder', 'TreeOrder',
|
|
1129
|
+
]);
|
|
1130
|
+
|
|
1131
|
+
export function checkEventReferences(scenario) {
|
|
1132
|
+
const errors = [];
|
|
1133
|
+
const definedEvents = new Set(
|
|
1134
|
+
scenario?.Events && typeof scenario.Events === 'object' && !Array.isArray(scenario.Events)
|
|
1135
|
+
? Object.keys(scenario.Events) : []
|
|
1136
|
+
);
|
|
1137
|
+
const refs = []; // { name, path }
|
|
1138
|
+
|
|
1139
|
+
const collectFromEventsObj = (eventsObj, path) => {
|
|
1140
|
+
if (!eventsObj || typeof eventsObj !== 'object' || Array.isArray(eventsObj)) return;
|
|
1141
|
+
for (const [k, v] of Object.entries(eventsObj)) {
|
|
1142
|
+
if (EVENT_ORDER_KEYS.has(k)) continue;
|
|
1143
|
+
if (typeof v === 'string' && v) refs.push({ name: v, path: `${path}/Events/${k}` });
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const collectLinkedFromActions = (eventsObj, path) => {
|
|
1148
|
+
if (!eventsObj || typeof eventsObj !== 'object' || Array.isArray(eventsObj)) return;
|
|
1149
|
+
for (const [name, arr] of Object.entries(eventsObj)) {
|
|
1150
|
+
if (!Array.isArray(arr)) continue;
|
|
1151
|
+
arr.forEach((a, i) => {
|
|
1152
|
+
if (a && typeof a === 'object' && a.Action === 'LinkedEvent' && typeof a.EventName === 'string' && a.EventName) {
|
|
1153
|
+
refs.push({ name: a.EventName, path: `${path}['${name}'][${i}].EventName` });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
collectLinkedFromActions(scenario?.Events, 'scenario.Events');
|
|
1160
|
+
|
|
1161
|
+
const visit = (node, path) => {
|
|
1162
|
+
if (!node || typeof node !== 'object') return;
|
|
1163
|
+
if (Array.isArray(node)) { node.forEach((v, i) => visit(v, `${path}[${i}]`)); return; }
|
|
1164
|
+
if (node.Events) collectFromEventsObj(node.Events, path);
|
|
1165
|
+
if (Array.isArray(node.Contents)) {
|
|
1166
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
1167
|
+
}
|
|
1168
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1169
|
+
if (Array.isArray(layouts)) {
|
|
1170
|
+
layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
1171
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const steps = scenario?.Steps ?? {};
|
|
1176
|
+
for (const sid of Object.keys(steps)) {
|
|
1177
|
+
const step = steps[sid];
|
|
1178
|
+
if (step?.Events) collectFromEventsObj(step.Events, `Steps/${sid}`);
|
|
1179
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
1180
|
+
(step?.[ch] ?? []).forEach((g, i) => visit(g, `Steps/${sid}/${ch}/${i}`));
|
|
1181
|
+
}
|
|
1182
|
+
(step?.BottomButtons ?? []).forEach((bb, i) => {
|
|
1183
|
+
if (bb?.Events) collectFromEventsObj(bb.Events, `Steps/${sid}/BottomButtons/${i}`);
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
for (const { name, path } of refs) {
|
|
1188
|
+
if (!definedEvents.has(name)) {
|
|
1189
|
+
errors.push(
|
|
1190
|
+
`${path} 가 참조하는 이벤트 '${name}' 이 scenario.Events 에 정의되지 않음 — ` +
|
|
1191
|
+
`핸들러 키로 추가하거나 참조 제거.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return errors;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/* ------------------------------------------------------------------ */
|
|
1199
|
+
/* [no-inner-block] InnerBlock 생성 금지 (MCP 생성 strict) */
|
|
1200
|
+
/* - UseInnerBlock / InnerBlockKey / RepeatStyle 어떤 형태로도 사용 금지 */
|
|
1201
|
+
/* - AI 가 생성하는 메타에는 InnerBlock 패턴을 박지 말 것 — 데이터 반복 */
|
|
1202
|
+
/* 렌더링은 DataConnection (DataUsage:'query') 그룹으로 표현. */
|
|
1203
|
+
/* - 운영 메타 검증 등에서 허용이 필요하면 strict_no_inner_block:false */
|
|
1204
|
+
/* ------------------------------------------------------------------ */
|
|
1205
|
+
const INNER_BLOCK_FIELDS = ['UseInnerBlock', 'InnerBlockKey', 'RepeatStyle'];
|
|
1206
|
+
|
|
1207
|
+
export function checkNoInnerBlock(scenario, strictNoInnerBlock = true) {
|
|
1208
|
+
if (!strictNoInnerBlock) return [];
|
|
1209
|
+
const errors = [];
|
|
1210
|
+
const visit = (node, path) => {
|
|
1211
|
+
if (!node || typeof node !== 'object') return;
|
|
1212
|
+
if (Array.isArray(node)) { node.forEach((v, i) => visit(v, `${path}[${i}]`)); return; }
|
|
1213
|
+
for (const key of INNER_BLOCK_FIELDS) {
|
|
1214
|
+
if (Object.prototype.hasOwnProperty.call(node, key)) {
|
|
1215
|
+
errors.push(
|
|
1216
|
+
`${path}.${key} is FORBIDDEN — InnerBlock 패턴은 MCP 생성에서 사용 금지. ` +
|
|
1217
|
+
`데이터 반복 렌더링은 DataConnection(DataUsage:'query') 그룹으로 표현. ` +
|
|
1218
|
+
`(strict_no_inner_block=false 로 비활성)`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
for (const v of Object.values(node)) visit(v, path);
|
|
1223
|
+
};
|
|
1224
|
+
visit(scenario, '$');
|
|
1225
|
+
return errors;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/* ------------------------------------------------------------------ */
|
|
1229
|
+
/* [step-nav] N개 Step 흐름의 Step 간 이동 트리거 보장 */
|
|
1230
|
+
/* Step.Next 의 각 타깃은 다음 중 하나로 도달 가능해야 한다: */
|
|
1231
|
+
/* (a) Step.BottomButtons[*] 의 MoveTo:'Next' + Next.MoveStepOrder */
|
|
1232
|
+
/* 에 타깃 StepId 포함, 또는 */
|
|
1233
|
+
/* (b) Step 의 Contents/FixedContentsTop/FixedContentsBottom 하위 */
|
|
1234
|
+
/* 어딘가의 Group 이 UseMove:true + MoveSteps.MoveStepOrder 에 */
|
|
1235
|
+
/* 타깃 포함 (서브그룹/리스트 카드 클릭 이동), 또는 */
|
|
1236
|
+
/* (c) 자손 Control 의 UseMove:true + MoveSteps.MoveStepOrder 에 */
|
|
1237
|
+
/* 타깃 포함. */
|
|
1238
|
+
/* ------------------------------------------------------------------ */
|
|
1239
|
+
export function checkStepNavigationSources(scenario, strictStepNav = true) {
|
|
1240
|
+
if (!strictStepNav) return [];
|
|
1241
|
+
const errors = [];
|
|
1242
|
+
const steps = scenario?.Steps ?? {};
|
|
1243
|
+
const stepIds = new Set(Object.keys(steps));
|
|
1244
|
+
|
|
1245
|
+
const collectMoveTargets = (node, into) => {
|
|
1246
|
+
if (!node || typeof node !== 'object') return;
|
|
1247
|
+
if (Array.isArray(node)) { node.forEach(v => collectMoveTargets(v, into)); return; }
|
|
1248
|
+
if (node.UseMove === true && node.MoveSteps && Array.isArray(node.MoveSteps.MoveStepOrder)) {
|
|
1249
|
+
for (const t of node.MoveSteps.MoveStepOrder) if (typeof t === 'string') into.add(t);
|
|
1250
|
+
}
|
|
1251
|
+
// 탭 Items[].LinkedStepId 도 이동 트리거로 인정 — 탭 헤더 클릭이 그 Step 을 띄움.
|
|
1252
|
+
// (해당 Step 은 reachability 상 여전히 host Step 의 Next 에 있어야 한다 — [reach].)
|
|
1253
|
+
if (node.ControlType === 'Tab' && Array.isArray(node.Items)) {
|
|
1254
|
+
for (const it of node.Items) {
|
|
1255
|
+
if (it && typeof it.LinkedStepId === 'string' && it.LinkedStepId) into.add(it.LinkedStepId);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (Array.isArray(node.Contents)) {
|
|
1259
|
+
node.Contents.forEach(c => collectMoveTargets(c, into));
|
|
1260
|
+
}
|
|
1261
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1262
|
+
if (Array.isArray(layouts)) {
|
|
1263
|
+
layouts.forEach(l => (l?.Controls ?? []).forEach(c => collectMoveTargets(c, into)));
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
for (const sid of Object.keys(steps)) {
|
|
1268
|
+
const step = steps[sid];
|
|
1269
|
+
const nextList = Array.isArray(step?.Next) ? step.Next.filter(t => typeof t === 'string' && t) : [];
|
|
1270
|
+
if (nextList.length === 0) continue;
|
|
1271
|
+
|
|
1272
|
+
const reachable = new Set();
|
|
1273
|
+
for (const bb of step?.BottomButtons ?? []) {
|
|
1274
|
+
if (!bb || typeof bb !== 'object') continue;
|
|
1275
|
+
if (bb.MoveTo === 'Next') {
|
|
1276
|
+
const order = bb?.Next?.MoveStepOrder;
|
|
1277
|
+
if (Array.isArray(order)) for (const t of order) if (typeof t === 'string') reachable.add(t);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
1281
|
+
(step?.[ch] ?? []).forEach(g => collectMoveTargets(g, reachable));
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
for (const target of nextList) {
|
|
1285
|
+
if (!stepIds.has(target)) continue;
|
|
1286
|
+
if (!reachable.has(target)) {
|
|
1287
|
+
errors.push(
|
|
1288
|
+
`Step '${sid}'.Next='${target}' 에 도달할 수 있는 이동 트리거 없음 — ` +
|
|
1289
|
+
`BottomButton(MoveTo:'Next' + Next.MoveStepOrder) 또는 하위 Group/Control 의 ` +
|
|
1290
|
+
`UseMove:true + MoveSteps.MoveStepOrder 에 '${target}' 추가 필요 ` +
|
|
1291
|
+
`(또는 Tab 컨트롤의 Items[].LinkedStepId='${target}' — 탭 헤더 클릭이 트리거이므로 별도 이동 트리거 불요). ` +
|
|
1292
|
+
`(N 개 Step 흐름에서 서브그룹 아이템 클릭 이동 패턴: list 카드 Group 에 UseMove:true + MoveSteps.MoveStepOrder:['${target}']). ` +
|
|
1293
|
+
`strict_step_nav=false 로 비활성.`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return errors;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/* ------------------------------------------------------------------ */
|
|
1302
|
+
/* [tab-move] 탭 조상 Group 의 UseMove 금지 */
|
|
1303
|
+
/* - Tab 컨트롤을 감싸는(조상) Group 에 UseMove:true 가 있으면 탭 헤더 */
|
|
1304
|
+
/* 클릭이 그 Group 의 Step 이동을 트리거해버린다 (탭 전환이 아니라 */
|
|
1305
|
+
/* 화면이 넘어가 버림). */
|
|
1306
|
+
/* - 따라서 Tab 의 어떤 조상 Group 도 UseMove:true 를 두면 안 된다. */
|
|
1307
|
+
/* 이동이 필요하면 LinkedStep 내부 컨트롤/버튼으로 처리. */
|
|
1308
|
+
/* ------------------------------------------------------------------ */
|
|
1309
|
+
export function checkTabAncestorMove(scenario, strictTabMove = true) {
|
|
1310
|
+
if (!strictTabMove) return [];
|
|
1311
|
+
const errors = [];
|
|
1312
|
+
const visit = (node, path, moveAncestorPath) => {
|
|
1313
|
+
if (!node || typeof node !== 'object') return;
|
|
1314
|
+
if (node.ControlType === 'Tab' && moveAncestorPath) {
|
|
1315
|
+
errors.push(
|
|
1316
|
+
`${path} (Tab) 의 조상 Group '${moveAncestorPath}' 에 UseMove:true 가 있음 — ` +
|
|
1317
|
+
`탭 헤더 클릭이 그 Group 의 Step 이동을 트리거해 탭 전환 대신 화면이 넘어가 버림. ` +
|
|
1318
|
+
`탭을 감싸는 Group 에는 UseMove 를 두지 말 것 (이동이 필요하면 LinkedStep 내부 ` +
|
|
1319
|
+
`Button/Label 컨트롤로). strict_tab_move=false 로 비활성.`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
// Group 에 UseMove:true 면 이후 자손에게 "move 조상" 컨텍스트 전파.
|
|
1323
|
+
let nextMoveAncestor = moveAncestorPath;
|
|
1324
|
+
if (node.ContentsType === 'Group' && node.UseMove === true) {
|
|
1325
|
+
nextMoveAncestor = path;
|
|
1326
|
+
}
|
|
1327
|
+
if (Array.isArray(node.Contents)) {
|
|
1328
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, nextMoveAncestor));
|
|
1329
|
+
}
|
|
1330
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1331
|
+
if (Array.isArray(layouts)) {
|
|
1332
|
+
layouts.forEach((layout, li) => {
|
|
1333
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
1334
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, nextMoveAncestor)
|
|
1335
|
+
);
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
forEachStepRoot(scenario, (node, path) => visit(node, path, null));
|
|
1340
|
+
return errors;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/* ------------------------------------------------------------------ */
|
|
1344
|
+
/* Tab 컨트롤이 Items[].LinkedStepId 로 띄우는 Step Id 수집 */
|
|
1345
|
+
/* ------------------------------------------------------------------ */
|
|
1346
|
+
function collectTabLinkedStepIds(scenario) {
|
|
1347
|
+
const linked = new Set();
|
|
1348
|
+
const visit = (node) => {
|
|
1349
|
+
if (!node || typeof node !== 'object') return;
|
|
1350
|
+
if (Array.isArray(node)) { node.forEach(visit); return; }
|
|
1351
|
+
if (node.ControlType === 'Tab' && Array.isArray(node.Items)) {
|
|
1352
|
+
for (const it of node.Items) {
|
|
1353
|
+
if (it && typeof it.LinkedStepId === 'string' && it.LinkedStepId) linked.add(it.LinkedStepId);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach(visit);
|
|
1357
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1358
|
+
if (Array.isArray(layouts)) layouts.forEach(l => (l?.Controls ?? []).forEach(visit));
|
|
1359
|
+
};
|
|
1360
|
+
const steps = scenario?.Steps ?? {};
|
|
1361
|
+
for (const sid of Object.keys(steps)) {
|
|
1362
|
+
const step = steps[sid];
|
|
1363
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
1364
|
+
(step?.[ch] ?? []).forEach(visit);
|
|
1365
|
+
}
|
|
1366
|
+
(step?.BottomButtons ?? []).forEach(visit);
|
|
1367
|
+
}
|
|
1368
|
+
return linked;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/* ------------------------------------------------------------------ */
|
|
1372
|
+
/* [tab-embed] 탭 내부 LinkedStep 의 Embed 컨트롤 금지 */
|
|
1373
|
+
/* - Tab 컨트롤이 Items[].LinkedStepId 로 띄우는 Step 은 Step 이벤트 */
|
|
1374
|
+
/* (Init/Loaded)가 미실행 → Embed 의 DOM 초기화 시점을 잡을 수 없다 */
|
|
1375
|
+
/* (차트/지도/QR 등이 안 뜸). */
|
|
1376
|
+
/* - 따라서 탭 내부에 렌더되는 Step 의 Contents 트리에는 Embed 금지. */
|
|
1377
|
+
/* Embed 가 필요한 화면은 탭이 아닌 별도 Step 으로 분리. */
|
|
1378
|
+
/* ------------------------------------------------------------------ */
|
|
1379
|
+
export function checkTabLinkedStepEmbed(scenario, strictTabEmbed = true) {
|
|
1380
|
+
if (!strictTabEmbed) return [];
|
|
1381
|
+
const errors = [];
|
|
1382
|
+
const steps = scenario?.Steps ?? {};
|
|
1383
|
+
const linked = collectTabLinkedStepIds(scenario);
|
|
1384
|
+
const findEmbed = (node, path, acc) => {
|
|
1385
|
+
if (!node || typeof node !== 'object') return;
|
|
1386
|
+
if (Array.isArray(node)) { node.forEach((n, i) => findEmbed(n, `${path}/${i}`, acc)); return; }
|
|
1387
|
+
if (node.ControlType === 'Embed') acc.push(path);
|
|
1388
|
+
if (Array.isArray(node.Contents)) {
|
|
1389
|
+
node.Contents.forEach((c, i) => findEmbed(c, `${path}/Contents/${i}`, acc));
|
|
1390
|
+
}
|
|
1391
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1392
|
+
if (Array.isArray(layouts)) {
|
|
1393
|
+
layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
1394
|
+
findEmbed(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, acc)));
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
for (const sid of linked) {
|
|
1398
|
+
const step = steps[sid];
|
|
1399
|
+
if (!step || typeof step !== 'object') continue;
|
|
1400
|
+
const acc = [];
|
|
1401
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
1402
|
+
(step?.[ch] ?? []).forEach((g, i) => findEmbed(g, `Steps/${sid}/${ch}/${i}`, acc));
|
|
1403
|
+
}
|
|
1404
|
+
for (const p of acc) {
|
|
1405
|
+
errors.push(
|
|
1406
|
+
`${p} (Embed) — Step '${sid}' 은 Tab 컨트롤 내부에 렌더되는 LinkedStep 이라 Embed 컨트롤 사용 금지. ` +
|
|
1407
|
+
`탭 내부 Step 은 Step 이벤트(Init/Loaded)가 미실행이라 Embed 초기화 시점을 못 잡아 차트/지도/QR 이 안 뜬다. ` +
|
|
1408
|
+
`Embed 가 필요한 화면은 탭이 아닌 별도 Step 으로 분리할 것. strict_tab_embed=false 로 비활성.`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return errors;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/* ------------------------------------------------------------------ */
|
|
1416
|
+
/* [group-dc] 모든 Group 에 UseDataConnection 명시 강제 */
|
|
1417
|
+
/* - Group 에 UseDataConnection 키가 아예 없으면(undefined), 스튜디오 */
|
|
1418
|
+
/* 로드 시 studio-util.js 의 convertTopLevelDataConnection 이 레거시 */
|
|
1419
|
+
/* 그룹으로 간주해 UseDataConnection=false + UseOldDataConnection=true */
|
|
1420
|
+
/* 를 자동 생성한다 → 그룹이 구버전 데이터연결 프로퍼티로 렌더됨. */
|
|
1421
|
+
/* - UseDataConnection 이 명시(false 라도)돼 있으면 그 분기에서 return */
|
|
1422
|
+
/* 하므로 UseOldDataConnection 이 안 생긴다. */
|
|
1423
|
+
/* - 따라서 비연결 그룹도 "UseDataConnection": false 를 반드시 명시. */
|
|
1424
|
+
/* - MCP 경로는 applyGroupDefaults 가 먼저 채우므로 통과; 이 검사는 */
|
|
1425
|
+
/* defaults 미적용 경로(skill CLI 등)의 누락을 잡는다. */
|
|
1426
|
+
/* ------------------------------------------------------------------ */
|
|
1427
|
+
export function checkGroupUseDataConnection(scenario, strictGroupDc = true) {
|
|
1428
|
+
if (!strictGroupDc) return [];
|
|
1429
|
+
const errors = [];
|
|
1430
|
+
const visit = (node, path) => {
|
|
1431
|
+
if (!node || typeof node !== 'object') return;
|
|
1432
|
+
if (node.ContentsType === 'Group' && !Object.prototype.hasOwnProperty.call(node, 'UseDataConnection')) {
|
|
1433
|
+
errors.push(
|
|
1434
|
+
`${path} (Group) 에 UseDataConnection 키 누락 — 비연결 그룹도 "UseDataConnection": false 를 명시할 것. ` +
|
|
1435
|
+
`키가 없으면 스튜디오(convertTopLevelDataConnection)가 레거시로 간주해 UseOldDataConnection:true 를 자동 생성(구버전 데이터연결 프로퍼티로 렌더). ` +
|
|
1436
|
+
`strict_group_dc=false 로 비활성.`
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
if (Array.isArray(node.Contents)) {
|
|
1440
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
1441
|
+
}
|
|
1442
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1443
|
+
if (Array.isArray(layouts)) {
|
|
1444
|
+
layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
1445
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
forEachStepRoot(scenario, (node, path) => visit(node, path));
|
|
1449
|
+
return errors;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/* ------------------------------------------------------------------ */
|
|
1453
|
+
/* [dc-empty] DC 그룹의 자식 트리에 컬렉션키 바인딩이 한 개도 없으면 에러 */
|
|
1454
|
+
/* - UseDataConnection:true + DataConnection 인 Group 은 카테고리 sector */
|
|
1455
|
+
/* 를 가져와 자식 컨트롤이 컬럼키를 표시/입력하는 게 본래 목적이다. */
|
|
1456
|
+
/* - 자식 트리에 Label.LabelCKey / Input·Combo·Search·CheckBox·RadioBox */
|
|
1457
|
+
/* 의 Ckeys / SaveValueKey 가 하나도 없으면 DC 그룹의 의미가 없음 — */
|
|
1458
|
+
/* 스튜디오에서 빈 반복 카드만 그려지거나, 사용자 의도("주문 목록을 */
|
|
1459
|
+
/* 바인딩") 와 실제 메타("아무 컬럼키도 안 박힘") 가 불일치한다. */
|
|
1460
|
+
/* - 중첩 DC 그룹(자식 DC) 의 서브트리는 그쪽 DC 의 컨텍스트로 귀속되므로 */
|
|
1461
|
+
/* 부모 DC 의 카운트에서 제외한다. */
|
|
1462
|
+
/* - strict_dc_empty=false 로 비활성. */
|
|
1463
|
+
/* ------------------------------------------------------------------ */
|
|
1464
|
+
function isDcGroup(node) {
|
|
1465
|
+
return !!node && node.ContentsType === 'Group' && node.UseDataConnection === true && !!node.DataConnection;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function nodeHasCkeyBinding(node) {
|
|
1469
|
+
if (!node || typeof node !== 'object') return false;
|
|
1470
|
+
const ct = node.ControlType;
|
|
1471
|
+
if (ct === 'Label') {
|
|
1472
|
+
return typeof node.LabelCKey === 'string' && node.LabelCKey.length > 0;
|
|
1473
|
+
}
|
|
1474
|
+
if (ct && CKEY_BOUND.has(ct)) {
|
|
1475
|
+
if (Array.isArray(node.Ckeys) && node.Ckeys.length > 0) return true;
|
|
1476
|
+
if (typeof node.SaveValueKey === 'string' && node.SaveValueKey.length > 0) return true;
|
|
1477
|
+
}
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function dcGroupChildren(node) {
|
|
1482
|
+
const out = [];
|
|
1483
|
+
if (Array.isArray(node?.Contents)) {
|
|
1484
|
+
node.Contents.forEach((c, i) => out.push({ child: c, sub: `Contents/${i}` }));
|
|
1485
|
+
}
|
|
1486
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1487
|
+
if (Array.isArray(layouts)) {
|
|
1488
|
+
layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
1489
|
+
out.push({ child: c, sub: `Dialog/Layouts/${li}/Controls/${ci}` })));
|
|
1490
|
+
}
|
|
1491
|
+
return out;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
export function checkGroupDcChildBinding(scenario, strictDcEmpty = true) {
|
|
1495
|
+
if (!strictDcEmpty) return [];
|
|
1496
|
+
const errors = [];
|
|
1497
|
+
|
|
1498
|
+
const countKeysUnder = (dcGroupNode) => {
|
|
1499
|
+
let count = 0;
|
|
1500
|
+
const stack = [];
|
|
1501
|
+
for (const { child } of dcGroupChildren(dcGroupNode)) stack.push(child);
|
|
1502
|
+
while (stack.length) {
|
|
1503
|
+
const cur = stack.pop();
|
|
1504
|
+
if (!cur || typeof cur !== 'object') continue;
|
|
1505
|
+
if (isDcGroup(cur)) continue; // 자식 DC 컨텍스트로 분리 — 부모 카운트에서 제외
|
|
1506
|
+
if (nodeHasCkeyBinding(cur)) count += 1;
|
|
1507
|
+
for (const { child } of dcGroupChildren(cur)) stack.push(child);
|
|
1508
|
+
}
|
|
1509
|
+
return count;
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
const visit = (node, path) => {
|
|
1513
|
+
if (!node || typeof node !== 'object') return;
|
|
1514
|
+
if (isDcGroup(node)) {
|
|
1515
|
+
const cnt = countKeysUnder(node);
|
|
1516
|
+
if (cnt === 0) {
|
|
1517
|
+
const cat = node?.DataConnection?.CategoryName || '(unknown)';
|
|
1518
|
+
const usage = node?.DataConnection?.DataUsage || 'default';
|
|
1519
|
+
errors.push(
|
|
1520
|
+
`${path} (Group UseDataConnection:true, CategoryName='${cat}', DataUsage='${usage}') 의 자식 트리에 컬렉션키 바인딩이 0개 — ` +
|
|
1521
|
+
`DC 그룹은 자식이 컬럼키를 1개 이상 참조해야 의미가 있음 (Label.LabelCKey / Input·Combo·Search·CheckBox·RadioBox 의 Ckeys 또는 SaveValueKey). ` +
|
|
1522
|
+
`반복 카드라면 표시할 키를 라벨/입력으로 1개 이상 노출, 상세 그룹이라면 캡션-값 패턴의 값 라벨에 LabelCKey 를 박을 것. ` +
|
|
1523
|
+
`(중첩 DC 그룹의 자식은 그 안쪽 DC 컨텍스트로 귀속되므로 부모 카운트에 포함되지 않음). strict_dc_empty=false 로 비활성.`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
for (const { child, sub } of dcGroupChildren(node)) visit(child, `${path}/${sub}`);
|
|
1528
|
+
};
|
|
1529
|
+
forEachStepRoot(scenario, (g, p) => visit(g, p));
|
|
1530
|
+
return errors;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/* ------------------------------------------------------------------ */
|
|
1534
|
+
/* [embed-render] Embed(JS 타깃 컨테이너) 의 Loaded 렌더 누락 차단 */
|
|
1535
|
+
/* - <canvas> 또는 빈 id 컨테이너(<div id=..></div> 등)를 둔 Embed 는 */
|
|
1536
|
+
/* 라이브러리만 로드(StyleURLs / f.Script.load)하고 끝내면 빈 영역만 */
|
|
1537
|
+
/* 그려진다 — 반드시 host Step 의 Loaded(또는 Init) 핸들러에서 */
|
|
1538
|
+
/* document.getElementById(...) 로 실제 렌더 코드를 넣어야 한다. */
|
|
1539
|
+
/* - 정적 Embed(<video>/<iframe>/<style> 등 id 없는)는 검사 대상 외. */
|
|
1540
|
+
/* ------------------------------------------------------------------ */
|
|
1541
|
+
function embedJsTarget(html) {
|
|
1542
|
+
if (typeof html !== 'string' || !html) return null;
|
|
1543
|
+
const hasCanvas = /<canvas\b/i.test(html);
|
|
1544
|
+
const emptyIdContainer = /<(div|span|section|canvas)\b[^>]*\bid\s*=\s*["'][^"']+["'][^>]*>\s*<\/(div|span|section|canvas)>/i.test(html);
|
|
1545
|
+
if (!hasCanvas && !emptyIdContainer) return null; // 정적 Embed — 렌더 불요
|
|
1546
|
+
const ids = [];
|
|
1547
|
+
const idRe = /\bid\s*=\s*["']([^"']+)["']/gi;
|
|
1548
|
+
let m;
|
|
1549
|
+
while ((m = idRe.exec(html)) !== null) ids.push(m[1]);
|
|
1550
|
+
return { ids };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export function checkEmbedRender(scenario, strictEmbedRender = true) {
|
|
1554
|
+
if (!strictEmbedRender) return [];
|
|
1555
|
+
const errors = [];
|
|
1556
|
+
const events = scenario?.Events ?? {};
|
|
1557
|
+
const steps = scenario?.Steps ?? {};
|
|
1558
|
+
for (const sid of Object.keys(steps)) {
|
|
1559
|
+
const step = steps[sid];
|
|
1560
|
+
const embeds = [];
|
|
1561
|
+
const visit = (node, path) => {
|
|
1562
|
+
if (!node || typeof node !== 'object') return;
|
|
1563
|
+
if (Array.isArray(node)) { node.forEach((n, i) => visit(n, `${path}/${i}`)); return; }
|
|
1564
|
+
if (node.ControlType === 'Embed') {
|
|
1565
|
+
const t = embedJsTarget(node.Embed);
|
|
1566
|
+
if (t) embeds.push({ path, ...t });
|
|
1567
|
+
}
|
|
1568
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
1569
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1570
|
+
if (Array.isArray(layouts)) layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
1571
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
1572
|
+
};
|
|
1573
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
1574
|
+
(step?.[ch] ?? []).forEach((g, i) => visit(g, `Steps/${sid}/${ch}/${i}`));
|
|
1575
|
+
}
|
|
1576
|
+
if (embeds.length === 0) continue;
|
|
1577
|
+
|
|
1578
|
+
// host Step 의 Loaded / Init 핸들러 스크립트를 합쳐서 렌더 코드 존재 여부 판정.
|
|
1579
|
+
const renderScripts = [];
|
|
1580
|
+
for (const key of ['Loaded', 'Init']) {
|
|
1581
|
+
const hname = step?.Events?.[key];
|
|
1582
|
+
if (typeof hname === 'string' && hname) renderScripts.push(...gatherScriptsFromHandler(events, hname));
|
|
1583
|
+
}
|
|
1584
|
+
const blob = renderScripts.join('\n');
|
|
1585
|
+
const hasDrawApi = /\b(getElementById|querySelector|querySelectorAll)\b/.test(blob);
|
|
1586
|
+
const hasHandler = renderScripts.length > 0 || typeof step?.Events?.Loaded === 'string';
|
|
1587
|
+
|
|
1588
|
+
for (const e of embeds) {
|
|
1589
|
+
const idHit = e.ids.some(id => id && blob.includes(id));
|
|
1590
|
+
if (!hasHandler) {
|
|
1591
|
+
errors.push(
|
|
1592
|
+
`${e.path} (Embed) — canvas/빈 컨테이너가 있는데 Step '${sid}' 에 Loaded 이벤트 핸들러가 없음. ` +
|
|
1593
|
+
`라이브러리는 Scenario.StyleURLs 로 로드하고 Step.Events.Loaded 핸들러에서 document.getElementById(...) 로 ` +
|
|
1594
|
+
`실제 렌더 코드를 넣을 것 — 로드만 하고 그리기를 빼먹으면 빈 영역만 그려진다. strict_embed_render=false 로 비활성.`
|
|
1595
|
+
);
|
|
1596
|
+
} else if (!hasDrawApi && !idHit) {
|
|
1597
|
+
errors.push(
|
|
1598
|
+
`${e.path} (Embed) — Step '${sid}' 의 Loaded/Init 핸들러가 document.getElementById/querySelector 로 그리는 코드가 없음 ` +
|
|
1599
|
+
`(컨테이너 id=[${e.ids.join(', ') || '(없음)'}]). 라이브러리 로드(StyleURLs/f.Script.load)만으로는 안 그려진다 — ` +
|
|
1600
|
+
`Loaded 에서 getElementById 로 DOM 을 잡아 차트/지도 등을 실제로 렌더할 것. strict_embed_render=false 로 비활성.`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return errors;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/* ------------------------------------------------------------------ */
|
|
1609
|
+
/* [embed-bind] Embed HTML 의 섹터 바인딩 정합 */
|
|
1610
|
+
/* ① '{=Field}' display 토큰 금지 — Embed 렌더는 sector 인자 없이 */
|
|
1611
|
+
/* replaceExpressions(embedHTML) 를 타므로 (engine control.js */
|
|
1612
|
+
/* createEmbedVideo) '{=}' 분기가 치환을 못 하고 그대로 노출된다. */
|
|
1613
|
+
/* 섹터 값은 {%return Load.sector.<Field>; %} (LoadScript 토큰). */
|
|
1614
|
+
/* ② Load.sector 참조는 조상 DC 그룹 필수 — Load.sector 는 전역 렌더 */
|
|
1615
|
+
/* 커서(Current.sector) getter 일 뿐이라 (engine publicscript.js), */
|
|
1616
|
+
/* 조상 DC 그룹이 섹터별 렌더 중일 때만 그 행의 섹터를 가리킨다 */
|
|
1617
|
+
/* (engine layout.js generateContents). DC 컨텍스트 밖이면 undefined */
|
|
1618
|
+
/* → {% %} 실행기의 try/catch 가 삼켜 조용히 빈 문자열 렌더. */
|
|
1619
|
+
/* - opts.strictEmbedBind=false 로 비활성. */
|
|
1620
|
+
/* ------------------------------------------------------------------ */
|
|
1621
|
+
export function checkEmbedSectorBinding(scenario, strictEmbedBind = true) {
|
|
1622
|
+
if (!strictEmbedBind) return [];
|
|
1623
|
+
const errors = [];
|
|
1624
|
+
const visit = (node, path, hasAncestorDc) => {
|
|
1625
|
+
if (!node || typeof node !== 'object') return;
|
|
1626
|
+
if (node.ControlType === 'Embed' && typeof node.Embed === 'string') {
|
|
1627
|
+
const tokens = node.Embed.match(/\{=[A-Za-z_$][A-Za-z0-9_.]*\}/g);
|
|
1628
|
+
if (tokens && tokens.length > 0) {
|
|
1629
|
+
const fields = [...new Set(tokens)].join(', ');
|
|
1630
|
+
const ex = [...new Set(tokens)][0].slice(2, -1).replace(/^\$/, '');
|
|
1631
|
+
errors.push(
|
|
1632
|
+
`${path}/Embed 안에 display 토큰 ${fields} 가 있음 — '{=Field}' 는 Embed raw HTML 에서 치환되지 않고 문자 그대로 노출된다. ` +
|
|
1633
|
+
`섹터 값 바인딩은 LoadScript 토큰 '{%return Load.sector.${ex}; %}' 로 쓸 것 ` +
|
|
1634
|
+
`(DC 그룹 안의 Embed 면 그 행의 섹터가 Load.sector 로 주입됨). (strictEmbedBind=false 로 비활성)`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
if (!hasAncestorDc && /\bLoad\.sector\b/.test(node.Embed)) {
|
|
1638
|
+
errors.push(
|
|
1639
|
+
`${path}/Embed 가 Load.sector 를 참조하는데 조상(상위)에 데이터연결 그룹이 없음 — ` +
|
|
1640
|
+
`Load.sector 는 렌더 커서(Current.sector) getter 라 조상 DC 그룹이 섹터별 렌더 중일 때만 그 행의 섹터를 가리킨다. ` +
|
|
1641
|
+
`DC 컨텍스트 밖이면 undefined 로 평가가 실패하고 try/catch 가 삼켜 조용히 빈 문자열이 렌더된다. ` +
|
|
1642
|
+
`Embed 를 UseDataConnection:true + DataConnection.CategoryName 그룹 아래에 둘 것. (strictEmbedBind=false 로 비활성)`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
const childHasDc = hasAncestorDc || (node.UseDataConnection === true && !!node.DataConnection);
|
|
1647
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`, childHasDc));
|
|
1648
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1649
|
+
if (Array.isArray(layouts)) layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) =>
|
|
1650
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, childHasDc)));
|
|
1651
|
+
};
|
|
1652
|
+
forEachStepRoot(scenario, (g, p) => visit(g, p, false));
|
|
1653
|
+
return errors;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/* ------------------------------------------------------------------ */
|
|
1657
|
+
/* [script-allow] 이벤트 스크립트 화이트리스트 강제 */
|
|
1658
|
+
/* - meta-contract/runtime/v1/script/scenario-gen-rules.md 기반 */
|
|
1659
|
+
/* - 허용: f.Collection.addSector / _c.* / Current.step.reload(WithAnim) */
|
|
1660
|
+
/* f.Content(fid).reload(WithAnim) / f.Date / f.Script.load / */
|
|
1661
|
+
/* f.MessageBox().show / f.Event(name).runNext|break */
|
|
1662
|
+
/* - 차단: f.Collection.{updateSector|removeSector|moveCategory| */
|
|
1663
|
+
/* removeCategory|createCategory|filter}, f.Frame.popUp, */
|
|
1664
|
+
/* f.Notification, f.History.*, Current.step.{moveTo*|restart}, */
|
|
1665
|
+
/* Current.step.{id|prev|from} 조회, */
|
|
1666
|
+
/* f.Content().{show|hide|addRow|delRow|selectAll|deselectAll| */
|
|
1667
|
+
/* clickPositionFixed|Scroll}, removeSector('all',...), */
|
|
1668
|
+
/* 컨트롤 단위 reload (정적 분석 한계 — fId 검증은 [reload-target]). */
|
|
1669
|
+
/* ------------------------------------------------------------------ */
|
|
1670
|
+
const SCRIPT_DENY_PATTERNS = [
|
|
1671
|
+
// f.Collection.removeSector 는 ALLOW (단, 'all' 인자 형태만 비우기 idiom 으로 권장). 다른 변형은 차단.
|
|
1672
|
+
{ re: /\bf\.Collection\.(updateSector|moveCategory|removeCategory|createCategory|filter)\s*\(/, msg: "f.Collection.{updateSector|moveCategory|removeCategory|createCategory|filter} 호출 금지 — **intent.script 우선**: 'updateActive' (활성 섹터 일괄 갱신) / 'clearCategory' (카테고리 비우기) / 'deleteActive' (활성 섹터 삭제). raw fallback 은 '_c.<Cat>[n].키 = 값' 또는 'sector.*' (runtime/v1/intent-system/INTENT.md / scenario-gen-rules.md §3)" },
|
|
1673
|
+
{ re: /\bf\.Frame\.popUp\s*\(/, msg: "f.Frame.popUp(...) 호출 금지 — 같은 시나리오 PopUp/SlideUp 은 메타 (StepDialogType) 로 표현" },
|
|
1674
|
+
{ re: /\bf\.Notification\b/, msg: "f.Notification 사용 금지 — 전체 발송 위험. 모달 다이얼로그는 f.MessageBox(type).setTitle().setDescription().addButton/addBtn().show() (runtime/v1/script/f_messagebox.md)" },
|
|
1675
|
+
{ re: /\bf\.History\.[A-Za-z]/, msg: "f.History.* 분기 로직 금지 — 분기는 메타에서 별도 Step / Event 로 분리" },
|
|
1676
|
+
{ re: /\bCurrent\.step\.(moveToNext|moveToPrev|moveToFirst|moveToParent|restart)\s*\(/, msg: "Current.step.{moveToNext|moveToPrev|moveToFirst|moveToParent|restart}() 호출 금지 — Step 이동은 메타 (BottomButton.MoveTo / UseMove + MoveSteps) 채널로 선언적 표현" },
|
|
1677
|
+
{ re: /\bCurrent\.step\.(id|prev|from)\b/, msg: "Current.step.{id|prev|from} 조회 금지 — 분기는 메타에서 별도 Event/Step 으로 분리" },
|
|
1678
|
+
{ re: /\bf\.Content\s*\([^)]*\)\s*\.(show|hide|addRow|delRow|selectAll|deselectAll|clickPositionFixed|Scroll)\b/, msg: "f.Content().{show|hide|addRow|delRow|selectAll|deselectAll|clickPositionFixed|Scroll} 사용 금지 — 표시/숨김은 TargetSector.Filters, 행 추가/삭제는 **intent.mock** (Init mock) / **intent.script.clearCategory** (비우기) + intent.script.reloadGroup (재렌더)" },
|
|
1679
|
+
// ── 가짜 컬렉션/카테고리 API (runtime/v1/collection/_c.md, category.md, sector.md 진실원본) ──
|
|
1680
|
+
{ re: /\b_c\.[A-Za-z_][A-Za-z0-9_]*\.clear\s*\(/, msg: "_c.<카테고리>.clear() 호출 금지 — 존재하지 않는 메서드. **intent.script.clearCategory** 권장: '{ kind:\"clearCategory\", cat:\"<Cat>\", reload?:\"f_N\" }'. raw fallback: f.Collection.removeSector('all', '<Cat>'). 활성 섹터만 비우려면 intent.script.deleteActive 또는 raw '_c.<Cat>.activeSector.release()'" },
|
|
1681
|
+
{ re: /\b_c\.[A-Za-z_][A-Za-z0-9_]*\.deleteActiveSector\s*\(/, msg: "_c.<카테고리>.deleteActiveSector() 호출 금지 — 존재하지 않는 메서드. **intent.script.deleteActive** 권장: '{ kind:\"deleteActive\", cat:\"<Cat>\", reload?:\"f_N\" }' (가드 2단 + cur.release() 자동). raw fallback: '_c.<Cat>.activeSector.release()' (runtime/v1/collection/sector.md §4-4)" },
|
|
1682
|
+
// ── 섹터 .delete() 금지 — deleted 상태로 잔존 (생성 시나리오는 완전삭제 release()) ──
|
|
1683
|
+
{ re: /\.delete\s*\(\s*\)/, msg: "섹터 .delete() 호출 금지 — added 가 아닌 섹터는 deleted 상태로 컬렉션에 잔존(백엔드 동기화용 표시)해 집계/반복 루프를 오염시킨다. 생성 시나리오는 완전삭제 '.release()' 사용 — **intent.script.deleteActive** 권장 (가드 2단 + cur.release() 자동) (runtime/v1/collection/sector.md §4-4)" },
|
|
1684
|
+
// activeSector / sectors / suids / active / status / name 은 모두 프로퍼티 (게터) — 함수 호출 금지
|
|
1685
|
+
{ re: /\.activeSector\s*\(/, msg: "activeSector(...) 호출 금지 — 'activeSector' 는 프로퍼티(게터). '_c.<카테고리>.activeSector.<필드>' 또는 메서드를 부르려면 '.activeSector.set(...)' / '.activeSector.release()' / '.activeSector.setStatus(...)' 처럼 sector 메서드 호출 (runtime/v1/collection/category.md §3, _c.md §3)" },
|
|
1686
|
+
{ re: /\b_c\.[A-Za-z_][A-Za-z0-9_]*\.sectors\s*\(/, msg: "_c.<카테고리>.sectors(...) 호출 금지 — 'sectors' 는 프로퍼티(=this 카테고리 자체). '_c.<카테고리>.sectors' (또는 동등하게 '_c.<카테고리>' / '_c.<카테고리>.forEach(...)') 로 사용 (runtime/v1/collection/category.md §3)" },
|
|
1687
|
+
{ re: /\b_c\.[A-Za-z_][A-Za-z0-9_]*\.(suids|active|name|status)\s*\(/, msg: "_c.<카테고리>.{suids|active|name|status}(...) 호출 금지 — 모두 프로퍼티(게터). 괄호 없이 사용 (runtime/v1/collection/category.md §3)" },
|
|
1688
|
+
// ── f.MessageBox() 가짜 체인 메서드 + 가짜 f.Message() (인스턴스) ──
|
|
1689
|
+
{ re: /\bf\.MessageBox\s*\([^)]*\)\s*\.(chain|send|setBody|addReceiver|addAction|setActionJson|toast|notify|queue)\s*\(/, msg: "f.MessageBox().{chain|send|setBody|addReceiver|addAction|setActionJson|toast|notify|queue}(...) 호출 금지 — 존재하지 않는 메서드. **intent.script.dialog** 권장 ('{ kind:\"dialog\", type, title, body?, buttons:[...] }') — 종결자 .show() / 본문 .setDescription 자동 emit (오타 불가능). raw fallback: 'f.MessageBox(type).setTitle(...).setDescription(...).addButton/addBtn(...).show()' (runtime/v1/script/f_messagebox.md)" },
|
|
1690
|
+
{ re: /\bf\.Message\s*\(/, msg: "f.Message() 호출 금지 — 모달 다이얼로그는 **intent.script.dialog** 권장 또는 raw 'f.MessageBox(type).setTitle()...show()' 채널만 (runtime/v1/script/f_messagebox.md)" },
|
|
1691
|
+
// ── {=Field} 템플릿 토큰은 display 전용 — event Script 본문 금지 (raw=true: 문자열 안에서도 검출) ──
|
|
1692
|
+
{ re: /\{=[A-Za-z_][A-Za-z0-9_.]*\}/, raw: true, msg: "이벤트 Script 본문에서 '{=필드}' 사용 금지 — '{=필드}' 는 Label.labeltext / Button text / ConditionValue 같은 display 토큰. event Script 안에서는 '_c.<카테고리>.activeSector.<필드>' (또는 '_c.<카테고리>[n].<필드>') 로 명시적으로 참조 (runtime/v1/script/scenario-gen-rules.md §2-1.1, schema/v1/data-objects.md '{=Field}')" },
|
|
1693
|
+
];
|
|
1694
|
+
|
|
1695
|
+
const SCRIPT_INIT_DENY_PATTERNS = [
|
|
1696
|
+
{ re: /\bCurrent\.step\.reload(WithAnimation)?\s*\(/, msg: "Init 이벤트에서 Current.step.reload() / reloadWithAnimation() 호출 금지 — Init 은 데이터 박는 단계 (Init/Loaded 자체가 reload 시 재실행되지 않으므로 의미 없음). 화면 갱신은 Loaded 또는 사용자 트리거 이벤트에서." },
|
|
1697
|
+
{ re: /\bf\.Content\s*\([^)]*\)\s*\.reload(WithAnimation)?\s*\(/, msg: "Init 이벤트에서 f.Content(fid).reload() / reloadWithAnimation() 호출 금지 — Init 은 데이터 박는 단계. 부분 갱신은 Loaded/Click 등에서." },
|
|
1698
|
+
{ re: /\bf\.Event\s*\([^)]*\)\s*\.(runNext|break)\s*\(/, msg: "Init 이벤트에서 f.Event(...).runNext()/.break() 호출 금지 — Init 은 단순 mock sector 생성만 허용" },
|
|
1699
|
+
{ re: /\bf\.MessageBox\s*\(/, msg: "Init 이벤트에서 f.MessageBox() 호출 금지 — 모달 다이얼로그는 사용자 트리거 이벤트(Click/Change/Loaded 후)의 intent.script.dialog 또는 raw f.MessageBox 로. Init 은 intent.mock 으로 mock 데이터만." },
|
|
1700
|
+
{ re: /\bf\.Script\.load\s*\(/, msg: "Init 이벤트에서 f.Script.load() 호출 금지 — 외부 스크립트 로드는 Loaded 또는 별도 이벤트에서" },
|
|
1701
|
+
];
|
|
1702
|
+
|
|
1703
|
+
const F_MEMBER_RE = /\bf\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1704
|
+
const F_ALLOWED_TOPLEVEL = new Set(['Collection', 'Date', 'Script', 'MessageBox', 'Event', 'Content']);
|
|
1705
|
+
|
|
1706
|
+
function stripStringsAndComments(src) {
|
|
1707
|
+
// 길이 보존 — 문자열/주석 본문을 같은 길이의 공백으로 치환해
|
|
1708
|
+
// src 와 code 의 인덱스가 일치하도록 한다 (categoriesAddedInScripts 가
|
|
1709
|
+
// code 의 매칭 위치를 그대로 src 에 슬라이스해서 인자를 추출하기 때문).
|
|
1710
|
+
const blank = (m) => ' '.repeat(m.length);
|
|
1711
|
+
return src
|
|
1712
|
+
.replace(/\/\/[^\n]*/g, blank)
|
|
1713
|
+
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
1714
|
+
.replace(/`(?:\\.|[^`\\])*`/g, blank)
|
|
1715
|
+
.replace(/'(?:\\.|[^'\\])*'/g, blank)
|
|
1716
|
+
.replace(/"(?:\\.|[^"\\])*"/g, blank);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function checkScriptBody(src, where, errors, isInit) {
|
|
1720
|
+
if (typeof src !== 'string' || !src.trim()) return;
|
|
1721
|
+
const code = stripStringsAndComments(src);
|
|
1722
|
+
for (const { re, msg, raw } of SCRIPT_DENY_PATTERNS) {
|
|
1723
|
+
if ((raw ? re.test(src) : re.test(code))) errors.push(`${where} 스크립트 — ${msg}`);
|
|
1724
|
+
}
|
|
1725
|
+
if (isInit) {
|
|
1726
|
+
for (const { re, msg, raw } of SCRIPT_INIT_DENY_PATTERNS) {
|
|
1727
|
+
if ((raw ? re.test(src) : re.test(code))) errors.push(`${where} (Init) 스크립트 — ${msg}`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
let m;
|
|
1731
|
+
while ((m = F_MEMBER_RE.exec(code)) !== null) {
|
|
1732
|
+
const top = m[1];
|
|
1733
|
+
if (!F_ALLOWED_TOPLEVEL.has(top)) {
|
|
1734
|
+
errors.push(`${where} 스크립트 — f.${top}.* 사용 금지 — 허용된 f.* 진입점은 ${[...F_ALLOWED_TOPLEVEL].sort().join(', ')} 뿐 (runtime/v1/script/scenario-gen-rules.md §2-3)`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* 모든 Events 의 Script Action 본문을 정적 분석.
|
|
1741
|
+
* 시나리오 레벨 Events 만 검사 (Step/Group/Control.Events 는 핸들러 이름만 가지고
|
|
1742
|
+
* 실제 Script 는 시나리오 Events 에 정의 — [event-ref] 와 정합).
|
|
1743
|
+
*/
|
|
1744
|
+
export function checkScriptAllowlist(scenario, strictScript = true) {
|
|
1745
|
+
if (!strictScript) return [];
|
|
1746
|
+
const errors = [];
|
|
1747
|
+
const events = scenario?.Events;
|
|
1748
|
+
if (!events || typeof events !== 'object' || Array.isArray(events)) return [];
|
|
1749
|
+
|
|
1750
|
+
const initHandlerNames = collectInitHandlerNames(scenario);
|
|
1751
|
+
|
|
1752
|
+
for (const [name, arr] of Object.entries(events)) {
|
|
1753
|
+
if (!Array.isArray(arr)) continue;
|
|
1754
|
+
const isInit = initHandlerNames.has(name);
|
|
1755
|
+
arr.forEach((action, i) => {
|
|
1756
|
+
if (!action || typeof action !== 'object') return;
|
|
1757
|
+
if (action.Action === 'Script') {
|
|
1758
|
+
checkScriptBody(action.Script, `scenario.Events['${name}'][${i}]`, errors, isInit);
|
|
1759
|
+
} else if (action.Action === 'LinkedEvent' && typeof action.Script === 'string' && action.Script) {
|
|
1760
|
+
checkScriptBody(action.Script, `scenario.Events['${name}'][${i}].Script`, errors, isInit);
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
return errors;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/* ------------------------------------------------------------------ */
|
|
1768
|
+
/* [mock-init] 시작 Step 의 Init 이벤트에서 mock 섹터 생성 강제 */
|
|
1769
|
+
/* - 시나리오 안에서 사용되는 카테고리(DataConnection.CategoryName) 를 */
|
|
1770
|
+
/* 모두 수집하고, StartSteps 의 첫 Step 의 Events.Init 핸들러가 모든 */
|
|
1771
|
+
/* 카테고리에 대해 f.Collection.addSector(..., '카테고리') 호출 포함. */
|
|
1772
|
+
/* - 외부 데이터 시스템(Service/API)을 사용하지 않으므로(MCP 생성 모드), */
|
|
1773
|
+
/* 화면이 그릴 데이터는 Init 에서 mock 으로 박는 것이 유일한 출처. */
|
|
1774
|
+
/* - 카테고리 식별은 DataConnection.CategoryName 만 사용 — TargetSector */
|
|
1775
|
+
/* 안에 CategoryName 을 두는 형태는 잘못된 형태로 수집 대상 아님 */
|
|
1776
|
+
/* (checkCategoryNaming 이 별도 에러로 보고). */
|
|
1777
|
+
/* ------------------------------------------------------------------ */
|
|
1778
|
+
function collectUsedCategories(scenario) {
|
|
1779
|
+
// category 이름 → 그 카테고리가 쓰인 DataUsage 값들의 Set.
|
|
1780
|
+
// DataUsage:'new' 는 진입 시 빈 섹터를 자동 생성하므로 Init mock 이 불필요 —
|
|
1781
|
+
// checkMockInit 에서 'new' 로만 쓰인 카테고리는 요구 대상에서 제외한다.
|
|
1782
|
+
const used = new Map();
|
|
1783
|
+
const addUsage = (name, usage) => {
|
|
1784
|
+
if (!used.has(name)) used.set(name, new Set());
|
|
1785
|
+
used.get(name).add(usage);
|
|
1786
|
+
};
|
|
1787
|
+
const visit = (node) => {
|
|
1788
|
+
if (!node || typeof node !== 'object') return;
|
|
1789
|
+
if (Array.isArray(node)) { node.forEach(visit); return; }
|
|
1790
|
+
// ContentsType 체크 없이 DataConnection 객체가 있으면 수집 — 반복 렌더링
|
|
1791
|
+
// 그룹 (UseDataConnection:true + DataConnection.CategoryName) 은 ContentsType 누락
|
|
1792
|
+
// 사례에서도 잡히도록.
|
|
1793
|
+
const dc = node.DataConnection;
|
|
1794
|
+
if (dc && typeof dc === 'object') {
|
|
1795
|
+
if (typeof dc.CategoryName === 'string' && dc.CategoryName) {
|
|
1796
|
+
addUsage(dc.CategoryName, typeof dc.DataUsage === 'string' ? dc.DataUsage : 'default');
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach(visit);
|
|
1800
|
+
const layouts = node?.Dialog?.Layouts;
|
|
1801
|
+
if (Array.isArray(layouts)) {
|
|
1802
|
+
layouts.forEach(l => (l?.Controls ?? []).forEach(visit));
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
// 모든 Step 의 Contents/Fixed* + BottomButtons 까지 순회 (BottomButton 자체에는
|
|
1806
|
+
// 보통 DC 가 없지만 안전을 위해).
|
|
1807
|
+
const steps = scenario?.Steps ?? {};
|
|
1808
|
+
for (const sid of Object.keys(steps)) {
|
|
1809
|
+
const step = steps[sid];
|
|
1810
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
1811
|
+
(step?.[ch] ?? []).forEach(visit);
|
|
1812
|
+
}
|
|
1813
|
+
(step?.BottomButtons ?? []).forEach(visit);
|
|
1814
|
+
}
|
|
1815
|
+
return used;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function collectInitHandlerNames(scenario) {
|
|
1819
|
+
const names = new Set();
|
|
1820
|
+
const startSteps = Array.isArray(scenario?.StartSteps) ? scenario.StartSteps : [];
|
|
1821
|
+
const steps = scenario?.Steps ?? {};
|
|
1822
|
+
for (const sid of startSteps) {
|
|
1823
|
+
const ev = steps?.[sid]?.Events;
|
|
1824
|
+
if (ev && typeof ev === 'object' && typeof ev.Init === 'string' && ev.Init) {
|
|
1825
|
+
names.add(ev.Init);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return names;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// 이벤트 action 배열에서 Script 본문을 수집. LinkedEvent 는 시나리오 레벨
|
|
1832
|
+
// 핸들러를 따라 재귀(순환 방지 visited). 시나리오 레벨 named 핸들러와
|
|
1833
|
+
// Step.Events.Init 인라인 배열(intent.mock materialize 결과) 양쪽에서 재사용.
|
|
1834
|
+
function gatherScriptsFromActions(arr, events, visited = new Set()) {
|
|
1835
|
+
const out = [];
|
|
1836
|
+
if (!Array.isArray(arr)) return out;
|
|
1837
|
+
for (const action of arr) {
|
|
1838
|
+
if (!action || typeof action !== 'object') continue;
|
|
1839
|
+
if (action.Action === 'Script' && typeof action.Script === 'string') {
|
|
1840
|
+
out.push(action.Script);
|
|
1841
|
+
} else if (action.Action === 'LinkedEvent' && typeof action.EventName === 'string') {
|
|
1842
|
+
if (typeof action.Script === 'string' && action.Script) out.push(action.Script);
|
|
1843
|
+
out.push(...gatherScriptsFromHandler(events, action.EventName, visited));
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return out;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function gatherScriptsFromHandler(events, handlerName, visited = new Set()) {
|
|
1850
|
+
if (visited.has(handlerName)) return [];
|
|
1851
|
+
visited.add(handlerName);
|
|
1852
|
+
return gatherScriptsFromActions(events?.[handlerName], events, visited);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// 시작 Step(들)의 Init 에 연결된 모든 Script 본문을 수집.
|
|
1856
|
+
// - Step.Events.Init 가 문자열(named 핸들러) → 시나리오 레벨 Events 에서 펼침.
|
|
1857
|
+
// - Step.Events.Init 가 배열([{Action:'Script', Script:'...addSector...'}], 즉
|
|
1858
|
+
// intent.mock 인라인이 materialize된 형태) → 그 action 배열에서 직접 펼침.
|
|
1859
|
+
// proto / mock-init 양쪽이 "시작 Init 의 addSector mock" 을 동일하게 인식하도록
|
|
1860
|
+
// 단일 진실원본. (collectInitHandlerNames 는 핸들러 '이름'이 필요한
|
|
1861
|
+
// checkScriptAllowlist 전용 — 인라인 배열엔 이름이 없으므로 별개 유지.)
|
|
1862
|
+
function gatherStartStepInitScripts(scenario) {
|
|
1863
|
+
const events = scenario?.Events ?? {};
|
|
1864
|
+
const startSteps = Array.isArray(scenario?.StartSteps) ? scenario.StartSteps : [];
|
|
1865
|
+
const steps = scenario?.Steps ?? {};
|
|
1866
|
+
const out = [];
|
|
1867
|
+
for (const sid of startSteps) {
|
|
1868
|
+
const initVal = steps?.[sid]?.Events?.Init;
|
|
1869
|
+
if (typeof initVal === 'string' && initVal) {
|
|
1870
|
+
out.push(...gatherScriptsFromHandler(events, initVal));
|
|
1871
|
+
} else if (Array.isArray(initVal)) {
|
|
1872
|
+
out.push(...gatherScriptsFromActions(initVal, events));
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return out;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const ADD_SECTOR_RE = /\bf\.Collection\.addSector\s*\(/g;
|
|
1879
|
+
|
|
1880
|
+
function categoriesAddedInScripts(scripts) {
|
|
1881
|
+
const added = new Set();
|
|
1882
|
+
for (const src of scripts) {
|
|
1883
|
+
if (typeof src !== 'string' || !src) continue;
|
|
1884
|
+
const code = stripStringsAndComments(src);
|
|
1885
|
+
let m;
|
|
1886
|
+
ADD_SECTOR_RE.lastIndex = 0;
|
|
1887
|
+
while ((m = ADD_SECTOR_RE.exec(code)) !== null) {
|
|
1888
|
+
const start = m.index + m[0].length;
|
|
1889
|
+
const cat = extractSecondStringArg(src.slice(start));
|
|
1890
|
+
if (cat) added.add(cat);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return added;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function extractSecondStringArg(s) {
|
|
1897
|
+
// 매우 단순한 파서 — 첫 인자를 brace depth 0 에서 ',' 까지 읽고 두 번째 인자가 문자열 리터럴이면 값 반환.
|
|
1898
|
+
let depth = 0;
|
|
1899
|
+
let inStr = null;
|
|
1900
|
+
let i = 0;
|
|
1901
|
+
for (; i < s.length; i++) {
|
|
1902
|
+
const ch = s[i];
|
|
1903
|
+
if (inStr) {
|
|
1904
|
+
if (ch === '\\') { i++; continue; }
|
|
1905
|
+
if (ch === inStr) inStr = null;
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
|
|
1909
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
1910
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
1911
|
+
if (depth === 0) return null;
|
|
1912
|
+
depth--;
|
|
1913
|
+
} else if (ch === ',' && depth === 0) { i++; break; }
|
|
1914
|
+
}
|
|
1915
|
+
// 두 번째 인자 시작
|
|
1916
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
1917
|
+
if (i >= s.length) return null;
|
|
1918
|
+
const q = s[i];
|
|
1919
|
+
if (q !== '"' && q !== "'" && q !== '`') return null;
|
|
1920
|
+
let val = '';
|
|
1921
|
+
for (i++; i < s.length; i++) {
|
|
1922
|
+
const ch = s[i];
|
|
1923
|
+
if (ch === '\\') { val += s[i + 1] ?? ''; i++; continue; }
|
|
1924
|
+
if (ch === q) return val;
|
|
1925
|
+
val += ch;
|
|
1926
|
+
}
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
export function checkMockInit(scenario, strictMockInit = true) {
|
|
1931
|
+
if (!strictMockInit) return [];
|
|
1932
|
+
const errors = [];
|
|
1933
|
+
const catUsage = collectUsedCategories(scenario);
|
|
1934
|
+
// DataUsage:'new' 는 진입 시 빈 섹터 자동 생성(data-objects.md §1.2) → Init mock 불필요.
|
|
1935
|
+
// 'new' 로만 쓰인 카테고리는 제외하고, query/update/detail/default 등 읽기 용도가
|
|
1936
|
+
// 하나라도 있으면 mock 섹터가 필요하다.
|
|
1937
|
+
const usedCats = new Set(
|
|
1938
|
+
[...catUsage.entries()]
|
|
1939
|
+
.filter(([, usages]) => [...usages].some((u) => u !== 'new'))
|
|
1940
|
+
.map(([name]) => name)
|
|
1941
|
+
);
|
|
1942
|
+
if (usedCats.size === 0) return [];
|
|
1943
|
+
|
|
1944
|
+
const startSteps = Array.isArray(scenario?.StartSteps) ? scenario.StartSteps : [];
|
|
1945
|
+
if (startSteps.length === 0) return [];
|
|
1946
|
+
|
|
1947
|
+
// 모든 시작 Step (StartSteps 가 N개일 수도 있음) 의 Init 핸들러를 합쳐서 mock 채움 여부 판단.
|
|
1948
|
+
const events = scenario?.Events ?? {};
|
|
1949
|
+
const allScripts = [];
|
|
1950
|
+
const missingInit = [];
|
|
1951
|
+
for (const sid of startSteps) {
|
|
1952
|
+
const initVal = scenario?.Steps?.[sid]?.Events?.Init;
|
|
1953
|
+
// intent.mock 인라인 (Step.Events.Init 가 [{Action:'Script', Script:'...addSector...'}]
|
|
1954
|
+
// 배열로 materialize된 형태) 도 시작 Init 으로 인정 — proto 게이트와 대칭.
|
|
1955
|
+
if (Array.isArray(initVal)) {
|
|
1956
|
+
allScripts.push(...gatherScriptsFromActions(initVal, events));
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (typeof initVal !== 'string' || !initVal) {
|
|
1960
|
+
missingInit.push(sid);
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
if (!Array.isArray(events[initVal])) {
|
|
1964
|
+
errors.push(
|
|
1965
|
+
`scenario.Events['${initVal}'] 미정의 — 시작 Step '${sid}' 의 Init 핸들러가 가리키는 이벤트가 시나리오에 없음. ` +
|
|
1966
|
+
`(strict_mock_init=false 로 비활성)`
|
|
1967
|
+
);
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
allScripts.push(...gatherScriptsFromHandler(events, initVal));
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// 시작 Step 중 단 하나라도 Init 이 정의되어 있으면 거기서 mock 박을 수 있다는 가정 — 단,
|
|
1974
|
+
// Init 이 한 개도 없는데 카테고리는 사용 중이면 분명한 누락.
|
|
1975
|
+
if (missingInit.length === startSteps.length) {
|
|
1976
|
+
const startSid = startSteps[0];
|
|
1977
|
+
errors.push(
|
|
1978
|
+
`시작 Step '${startSid}'${startSteps.length > 1 ? ` (외 ${startSteps.length - 1}개)` : ''} 의 Events.Init 핸들러 미정의 — 사용 카테고리 [${[...usedCats].join(', ')}] 를 mock 으로 채울 곳이 없음. ` +
|
|
1979
|
+
`Step '${startSid}'.Events.Init = '<핸들러이름>' 추가 + scenario.Events['<핸들러이름>'] 에 ` +
|
|
1980
|
+
`f.Collection.addSector({...}, '<카테고리>') Script Action 들을 박을 것. ` +
|
|
1981
|
+
`(strict_mock_init=false 로 비활성)`
|
|
1982
|
+
);
|
|
1983
|
+
return errors;
|
|
1984
|
+
}
|
|
1985
|
+
if (missingInit.length > 0) {
|
|
1986
|
+
errors.push(
|
|
1987
|
+
`시작 Step [${missingInit.join(', ')}] 에 Events.Init 핸들러 누락 — 시작 Step 마다 Init 을 정의하거나 LinkedEvent 로 공통 핸들러를 부를 것. ` +
|
|
1988
|
+
`(strict_mock_init=false 로 비활성)`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const added = categoriesAddedInScripts(allScripts);
|
|
1993
|
+
const missing = [...usedCats].filter(c => !added.has(c));
|
|
1994
|
+
if (missing.length > 0) {
|
|
1995
|
+
errors.push(
|
|
1996
|
+
`시작 Step [${startSteps.join(', ')}] 의 Init 핸들러(들) 가 카테고리 [${missing.join(', ')}] 에 ` +
|
|
1997
|
+
`f.Collection.addSector(..., '<카테고리>') 호출이 없음 — 외부 서비스를 쓰지 않는 MCP 생성 모드에서는 ` +
|
|
1998
|
+
`시작 Step.Init 에서 모든 사용 카테고리에 mock 섹터를 박아야 화면에 데이터가 나옴 ` +
|
|
1999
|
+
`(스텝 간 이벤트가 없거나 단일 Step 안에서 반복 렌더링 그룹 (UseDataConnection + DataConnection.CategoryName) 만 쓰는 케이스도 동일). ` +
|
|
2000
|
+
`사용 카테고리: [${[...usedCats].join(', ')}], addSector 된 카테고리: [${[...added].join(', ') || '(없음)'}]. ` +
|
|
2001
|
+
`(strict_mock_init=false 로 비활성)`
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
return errors;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* group.schema.json 의 UseDataConnection.default:false 를 실제 인스턴스에 적용.
|
|
2009
|
+
* ajv 의 useDefaults 옵션은 oneOf/allOf 분기와 충돌해 폭주하므로 직접 mutate.
|
|
2010
|
+
*
|
|
2011
|
+
* UseInnerBlock:true 그룹은 면제 (스키마 차원에서 false 로 차단되어 별도 처리 불필요).
|
|
2012
|
+
*
|
|
2013
|
+
* @param {object} scenario
|
|
2014
|
+
* @returns {number} 채워넣은 그룹 수
|
|
2015
|
+
*/
|
|
2016
|
+
export function applyGroupDefaults(scenario) {
|
|
2017
|
+
let added = 0;
|
|
2018
|
+
const visit = (node) => {
|
|
2019
|
+
if (!node || typeof node !== 'object') return;
|
|
2020
|
+
if (Array.isArray(node)) { node.forEach(visit); return; }
|
|
2021
|
+
if (node.ContentsType === 'Group') {
|
|
2022
|
+
if (!Object.prototype.hasOwnProperty.call(node, 'UseDataConnection')) {
|
|
2023
|
+
node.UseDataConnection = false;
|
|
2024
|
+
added += 1;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach(visit);
|
|
2028
|
+
const layouts = node?.Dialog?.Layouts;
|
|
2029
|
+
if (Array.isArray(layouts)) {
|
|
2030
|
+
layouts.forEach(l => (l?.Controls ?? []).forEach(visit));
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
const steps = scenario?.Steps ?? {};
|
|
2034
|
+
for (const sid of Object.keys(steps)) {
|
|
2035
|
+
const step = steps[sid];
|
|
2036
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
2037
|
+
(step?.[ch] ?? []).forEach(visit);
|
|
2038
|
+
}
|
|
2039
|
+
(step?.BottomButtons ?? []).forEach(visit);
|
|
2040
|
+
}
|
|
2041
|
+
return added;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* `f.Collection.addSector(obj, 'Cat')` 호출 본문을 가독성 좋게 reformat.
|
|
2046
|
+
* - 첫 인자가 단순 객체 리터럴이면 키별 줄바꿈
|
|
2047
|
+
* - 호출과 호출 사이는 줄바꿈
|
|
2048
|
+
*
|
|
2049
|
+
* addSector({fId:1, fName:'사과', fPrice:1000}, 'CtgItems');
|
|
2050
|
+
* →
|
|
2051
|
+
* addSector({
|
|
2052
|
+
* fId: 1,
|
|
2053
|
+
* fName: '사과',
|
|
2054
|
+
* fPrice: 1000
|
|
2055
|
+
* }, 'CtgItems');
|
|
2056
|
+
*/
|
|
2057
|
+
function splitTopLevelCommas(s) {
|
|
2058
|
+
const out = [];
|
|
2059
|
+
let depth = 0, inStr = null, start = 0;
|
|
2060
|
+
for (let i = 0; i < s.length; i++) {
|
|
2061
|
+
const ch = s[i];
|
|
2062
|
+
if (inStr) {
|
|
2063
|
+
if (ch === '\\') { i++; continue; }
|
|
2064
|
+
if (ch === inStr) inStr = null;
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
|
|
2068
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
2069
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
2070
|
+
else if (ch === ',' && depth === 0) {
|
|
2071
|
+
out.push(s.slice(start, i).trim());
|
|
2072
|
+
start = i + 1;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const last = s.slice(start).trim();
|
|
2076
|
+
if (last) out.push(last);
|
|
2077
|
+
return out;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function reformatObjectLiteral(src) {
|
|
2081
|
+
const t = src.trim();
|
|
2082
|
+
if (!t.startsWith('{') || !t.endsWith('}')) return src;
|
|
2083
|
+
const body = t.slice(1, -1).trim();
|
|
2084
|
+
if (!body) return '{}';
|
|
2085
|
+
const pairs = splitTopLevelCommas(body);
|
|
2086
|
+
if (pairs.length === 0) return '{}';
|
|
2087
|
+
// key: value 사이 공백 정규화 (key:value → key: value)
|
|
2088
|
+
const normPairs = pairs.map(p => {
|
|
2089
|
+
const m = p.match(/^([A-Za-z_$][\w$]*|"[^"]*"|'[^']*')\s*:\s*(.*)$/s);
|
|
2090
|
+
return m ? `${m[1]}: ${m[2].trim()}` : p;
|
|
2091
|
+
});
|
|
2092
|
+
return '{\n ' + normPairs.join(',\n ') + '\n}';
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function reformatAddSectorCalls(src) {
|
|
2096
|
+
const re = /\bf\.Collection\.addSector\s*\(/g;
|
|
2097
|
+
let out = '', lastEnd = 0, m;
|
|
2098
|
+
re.lastIndex = 0;
|
|
2099
|
+
while ((m = re.exec(src)) !== null) {
|
|
2100
|
+
const callStart = m.index;
|
|
2101
|
+
const argsStart = m.index + m[0].length;
|
|
2102
|
+
let depth = 1, inStr = null, i = argsStart;
|
|
2103
|
+
for (; i < src.length; i++) {
|
|
2104
|
+
const ch = src[i];
|
|
2105
|
+
if (inStr) {
|
|
2106
|
+
if (ch === '\\') { i++; continue; }
|
|
2107
|
+
if (ch === inStr) inStr = null;
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
|
|
2111
|
+
if (ch === '(') depth++;
|
|
2112
|
+
else if (ch === ')') { depth--; if (depth === 0) break; }
|
|
2113
|
+
}
|
|
2114
|
+
if (depth !== 0) break;
|
|
2115
|
+
const argsRaw = src.slice(argsStart, i);
|
|
2116
|
+
const callEnd = i + 1;
|
|
2117
|
+
const args = splitTopLevelCommas(argsRaw);
|
|
2118
|
+
out += src.slice(lastEnd, callStart);
|
|
2119
|
+
if (args.length === 2 || args.length === 3) {
|
|
2120
|
+
const obj = args[0].trim();
|
|
2121
|
+
const formattedObj = obj.startsWith('{') && obj.endsWith('}')
|
|
2122
|
+
? reformatObjectLiteral(obj)
|
|
2123
|
+
: obj;
|
|
2124
|
+
const tail = args.slice(1).map(a => a.trim()).join(', ');
|
|
2125
|
+
out += `f.Collection.addSector(${formattedObj}, ${tail})`;
|
|
2126
|
+
} else {
|
|
2127
|
+
out += src.slice(callStart, callEnd);
|
|
2128
|
+
}
|
|
2129
|
+
lastEnd = callEnd;
|
|
2130
|
+
re.lastIndex = callEnd;
|
|
2131
|
+
}
|
|
2132
|
+
out += src.slice(lastEnd);
|
|
2133
|
+
// 호출 사이 줄바꿈 — `;` 뒤 다음 호출 시작 시 줄바꿈
|
|
2134
|
+
out = out.replace(/;[ \t]*(?=f\.Collection\.addSector\b)/g, ';\n');
|
|
2135
|
+
return out;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/**
|
|
2139
|
+
* AI 생성 금지 키 검증 — MCP 는 **항상 strict** (env 게이트 / opts 토글 없음).
|
|
2140
|
+
*
|
|
2141
|
+
* CLI 의 validate.mjs `checkAiGenerationForbidden` 과 동일 정책. 단, CLI 는
|
|
2142
|
+
* AI_GEN_STRICT=1 환경변수로 운영 메타 검증과 토글하지만 MCP 는 AI 생성 경로
|
|
2143
|
+
* 전용이므로 항상 적용 — 운영 메타를 MCP 로 검증하지 않음.
|
|
2144
|
+
*
|
|
2145
|
+
* 단일 진실원본: meta-contract/schema/v1/control.md §AI 샘플 생성 금지 키.
|
|
2146
|
+
* 금지 키 매트릭스 (description 의 "★ AI 생성 시 미포함" 마커 기준):
|
|
2147
|
+
* - 모든 Control + Group: minWidth / maxWidth / minHeight / maxHeight
|
|
2148
|
+
* - 데이터 연결고리 (Calendar 내부 / List 내부 / Tree): UseDCLink / DCLinkCkey
|
|
2149
|
+
* - Label: UseFullShape / UseTriming / TooltipType / TooltipText / TooltipCKey
|
|
2150
|
+
* - RadioBox: ItemWrap
|
|
2151
|
+
* - Button: UseMove / MoveSteps
|
|
2152
|
+
* - ImageBox (View 모드): DefaultViewGroup
|
|
2153
|
+
* - 모든 Control (FontStyle 자간): FontStyle.LetterSpacing
|
|
2154
|
+
*/
|
|
2155
|
+
const AI_GEN_FORBIDDEN_KEYS_ALL = [
|
|
2156
|
+
'minWidth', 'maxWidth', 'minHeight', 'maxHeight',
|
|
2157
|
+
'UseDCLink', 'DCLinkCkey',
|
|
2158
|
+
];
|
|
2159
|
+
const AI_GEN_FORBIDDEN_KEYS_BY_TYPE = {
|
|
2160
|
+
Label: ['UseFullShape', 'UseTriming', 'TooltipType', 'TooltipText', 'TooltipCKey'],
|
|
2161
|
+
ImageBox: ['DefaultViewGroup'],
|
|
2162
|
+
};
|
|
2163
|
+
export function checkAiGenerationForbidden(scenario) {
|
|
2164
|
+
const errors = [];
|
|
2165
|
+
|
|
2166
|
+
const report = (path, key, reason) => {
|
|
2167
|
+
errors.push(`${path}.${key} ${reason}`);
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
const visit = (node, path) => {
|
|
2171
|
+
if (!node || typeof node !== 'object') return;
|
|
2172
|
+
|
|
2173
|
+
for (const k of AI_GEN_FORBIDDEN_KEYS_ALL) {
|
|
2174
|
+
if (Object.prototype.hasOwnProperty.call(node, k)) {
|
|
2175
|
+
report(path, k, 'AI 생성 시 미포함 — 사용자 수동 설정 전용 (policy.ai-generation-forbidden-keys).');
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
const ct = node.ControlType;
|
|
2179
|
+
const typeKeys = ct && AI_GEN_FORBIDDEN_KEYS_BY_TYPE[ct];
|
|
2180
|
+
if (typeKeys) {
|
|
2181
|
+
for (const k of typeKeys) {
|
|
2182
|
+
if (Object.prototype.hasOwnProperty.call(node, k)) {
|
|
2183
|
+
report(path, k, `(${ct}) AI 생성 시 미포함 — 사용자 수동 설정 전용.`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const fs = node.FontStyle;
|
|
2188
|
+
if (fs && typeof fs === 'object' && Object.prototype.hasOwnProperty.call(fs, 'LetterSpacing')) {
|
|
2189
|
+
report(`${path}.FontStyle`, 'LetterSpacing', 'AI 생성 시 미포함 — 사용자 수동 설정 전용.');
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
if (Array.isArray(node.Contents)) {
|
|
2193
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
2194
|
+
}
|
|
2195
|
+
const layouts = node?.Dialog?.Layouts;
|
|
2196
|
+
if (Array.isArray(layouts)) {
|
|
2197
|
+
layouts.forEach((layout, li) => {
|
|
2198
|
+
(layout?.Controls ?? []).forEach((c, ci) =>
|
|
2199
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)
|
|
2200
|
+
);
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
|
|
2205
|
+
const steps = scenario?.Steps ?? {};
|
|
2206
|
+
for (const sid of Object.keys(steps)) {
|
|
2207
|
+
const step = steps[sid];
|
|
2208
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
2209
|
+
(step?.[ch] ?? []).forEach((g, i) => visit(g, `Steps/${sid}/${ch}/${i}`));
|
|
2210
|
+
}
|
|
2211
|
+
(step?.BottomButtons ?? []).forEach((b, i) => visit(b, `Steps/${sid}/BottomButtons/${i}`));
|
|
2212
|
+
}
|
|
2213
|
+
return errors;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* 모든 Script Action 본문에서 f.Collection.addSector 호출을 다중 줄로 재포맷.
|
|
2218
|
+
* @returns {number} 변경된 Script Action 수
|
|
2219
|
+
*/
|
|
2220
|
+
export function normalizeMockSectorScripts(scenario) {
|
|
2221
|
+
let changed = 0;
|
|
2222
|
+
const events = scenario?.Events;
|
|
2223
|
+
if (!events || typeof events !== 'object' || Array.isArray(events)) return 0;
|
|
2224
|
+
for (const arr of Object.values(events)) {
|
|
2225
|
+
if (!Array.isArray(arr)) continue;
|
|
2226
|
+
for (const action of arr) {
|
|
2227
|
+
if (!action || typeof action !== 'object') continue;
|
|
2228
|
+
if (action.Action !== 'Script' || typeof action.Script !== 'string') continue;
|
|
2229
|
+
if (!action.Script.includes('f.Collection.addSector')) continue;
|
|
2230
|
+
const reformatted = reformatAddSectorCalls(action.Script);
|
|
2231
|
+
if (reformatted !== action.Script) {
|
|
2232
|
+
action.Script = reformatted;
|
|
2233
|
+
changed += 1;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return changed;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/* ------------------------------------------------------------------ */
|
|
2241
|
+
/* [back-button] 가짜 뒤로가기 그룹 검출 */
|
|
2242
|
+
/* - Step.UseBackButton:false 인데 FixedContentsTop/Contents 어딘가에 */
|
|
2243
|
+
/* UseMove:true + MoveSteps 의 한 항목이 MoveTo:"Prev" 인 Group/Control */
|
|
2244
|
+
/* 이 박혀 있으면 "표준 뒤로가기 비활성 + 커스텀 그룹으로 대체" 안티 */
|
|
2245
|
+
/* 패턴. 헤더 백버튼은 step 의 UseBackButton:true + BackButton 구성으로 */
|
|
2246
|
+
/* 표준화해야 한다 (schema/v1/step.md "뒤로가기 사용하기"). */
|
|
2247
|
+
/* ------------------------------------------------------------------ */
|
|
2248
|
+
export function checkBackButtonStandard(scenario, strictBackButton = true) {
|
|
2249
|
+
if (!strictBackButton) return [];
|
|
2250
|
+
const errors = [];
|
|
2251
|
+
const steps = scenario?.Steps ?? {};
|
|
2252
|
+
|
|
2253
|
+
const hasPrevMove = (node) => {
|
|
2254
|
+
if (!node || typeof node !== 'object') return false;
|
|
2255
|
+
if (node.UseMove === true && node.MoveSteps && typeof node.MoveSteps === 'object') {
|
|
2256
|
+
const order = Array.isArray(node.MoveSteps.MoveStepOrder) ? node.MoveSteps.MoveStepOrder : [];
|
|
2257
|
+
for (const sid of order) {
|
|
2258
|
+
const entry = node.MoveSteps[sid];
|
|
2259
|
+
if (entry && entry.MoveTo === 'Prev') return true;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return false;
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
const walk = (node, path, into) => {
|
|
2266
|
+
if (!node || typeof node !== 'object') return;
|
|
2267
|
+
if (Array.isArray(node)) { node.forEach((v, i) => walk(v, `${path}/${i}`, into)); return; }
|
|
2268
|
+
if (hasPrevMove(node)) {
|
|
2269
|
+
const label = node.ContentsName || node.ControlDefaultName || node.ControlName2 || node.ControlType || node.ContentsType || '?';
|
|
2270
|
+
into.push(`${path} (${label})`);
|
|
2271
|
+
}
|
|
2272
|
+
if (Array.isArray(node.Contents)) node.Contents.forEach((c, i) => walk(c, `${path}/Contents/${i}`, into));
|
|
2273
|
+
const layouts = node?.Dialog?.Layouts;
|
|
2274
|
+
if (Array.isArray(layouts)) layouts.forEach((l, li) => (l?.Controls ?? []).forEach((c, ci) => walk(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`, into)));
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
for (const sid of Object.keys(steps)) {
|
|
2278
|
+
const step = steps[sid];
|
|
2279
|
+
if (step?.UseBackButton === true) continue;
|
|
2280
|
+
// FixedContentsTop 에서 발견되는 Prev 이동 트리거는 헤더 백버튼 대체 패턴 — 강하게 안티.
|
|
2281
|
+
const topHits = [];
|
|
2282
|
+
(step?.FixedContentsTop ?? []).forEach((g, i) => walk(g, `Steps/${sid}/FixedContentsTop/${i}`, topHits));
|
|
2283
|
+
if (topHits.length > 0) {
|
|
2284
|
+
errors.push(
|
|
2285
|
+
`Step '${sid}'.UseBackButton:false 이면서 FixedContentsTop 에 커스텀 뒤로가기 그룹 발견 (${topHits.join(', ')}) — ` +
|
|
2286
|
+
`상단 헤더의 뒤로가기는 표준 채널 'UseBackButton:true + BackButton:{Next:"None", IsRestored:true, BackButtonMode:"Step", BackButtonEvent:"undefined"}' 로 표현. ` +
|
|
2287
|
+
`커스텀 Group + UseMove + MoveTo:"Prev" 로 헤더 백버튼을 대체하지 말 것 (schema/v1/step.md "뒤로가기 사용하기"). ` +
|
|
2288
|
+
`strict_back_button=false 로 비활성.`
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return errors;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/* ------------------------------------------------------------------ */
|
|
2296
|
+
/* helper */
|
|
2297
|
+
/* ------------------------------------------------------------------ */
|
|
2298
|
+
function forEachStepRoot(scenario, visitor) {
|
|
2299
|
+
const steps = scenario?.Steps ?? {};
|
|
2300
|
+
for (const sid of Object.keys(steps)) {
|
|
2301
|
+
const step = steps[sid];
|
|
2302
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
2303
|
+
(step?.[ch] ?? []).forEach((g, i) => visitor(g, `Steps/${sid}/${ch}/${i}`));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
/* ------------------------------------------------------------------ */
|
|
2308
|
+
/* [residual] design / intent short-form 미전개 잔존 차단 */
|
|
2309
|
+
/* - design (design-system) 과 intent (runtime intent-system) 는 검증 */
|
|
2310
|
+
/* 진입 전 materialize/intent phase 가 raw 메타로 expand 하고 키를 */
|
|
2311
|
+
/* 제거한다(applyDesign / applyIntent). 그런데 `design` 키는 group/ */
|
|
2312
|
+
/* control 스키마에 허용 필드로 정의돼 있어, materialize 가 도달하지 */
|
|
2313
|
+
/* 못한 위치(정상 컨테이너 밖)의 잔존 design 키는 strict-fields 에도 */
|
|
2314
|
+
/* 안 걸리고 ok:true 로 통과 → "design/intent 안 풀고 끝났다" 는 거짓 */
|
|
2315
|
+
/* 완료. ok:true 면 write-back 이 design 키가 남은 채로 파일을 덮어써 */
|
|
2316
|
+
/* 런타임 렌더가 깨진다. */
|
|
2317
|
+
/* - 이 검사는 모든 phase(materialize+intent) 이후 cross-check 단계에서 */
|
|
2318
|
+
/* 돌므로 잔존 = 진짜 미전개. design/intent 둘 다 전개 후엔 키가 절대 */
|
|
2319
|
+
/* 남지 않으므로 false-positive 없음. */
|
|
2320
|
+
/* ------------------------------------------------------------------ */
|
|
2321
|
+
const RESIDUAL_SHORTFORM = new Map([
|
|
2322
|
+
['design', 'design-system (schema/v1/design-system/materialize.mjs)'],
|
|
2323
|
+
['intent', 'intent-system (runtime/v1/intent-system/materialize.mjs)'],
|
|
2324
|
+
]);
|
|
2325
|
+
|
|
2326
|
+
export function checkNoResidualShortForms(scenario) {
|
|
2327
|
+
const errors = [];
|
|
2328
|
+
const visit = (node, path) => {
|
|
2329
|
+
if (Array.isArray(node)) {
|
|
2330
|
+
node.forEach((v, i) => visit(v, `${path}[${i}]`));
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
if (!node || typeof node !== 'object') return;
|
|
2334
|
+
for (const [k, v] of Object.entries(node)) {
|
|
2335
|
+
if (v !== undefined && RESIDUAL_SHORTFORM.has(k)) {
|
|
2336
|
+
errors.push(
|
|
2337
|
+
`${path}.${k} — '${k}' short-form 이 전개되지 않은 채 남아있음. ` +
|
|
2338
|
+
`${RESIDUAL_SHORTFORM.get(k)} 의 materialize phase 가 이 위치에 도달하지 못함 ` +
|
|
2339
|
+
`(보통 호스트 노드가 정상 컨테이너 — Group Contents / Dialog.Layouts[*].Controls / Step FixedContents — 밖에 있음). ` +
|
|
2340
|
+
`'${k}' 키는 검증 전 raw 메타로 expand 되고 제거되어야 한다 — 잔존하면 런타임 렌더가 깨지므로 ` +
|
|
2341
|
+
`호스트 노드를 정상 컨테이너 안으로 옮기거나 '${k}' 를 raw 필드로 직접 작성할 것. (거짓 완료 방지 게이트)`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
visit(v, `${path}.${k}`);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
visit(scenario, '$');
|
|
2348
|
+
return errors;
|
|
2349
|
+
}
|
|
2350
|
+
/* ---------------------------------------------------------------------- */
|
|
2351
|
+
/* [caption-row] 캡션 컨트롤 + 비캡션 컨트롤이 같은 가로 행에 섞임 검출 */
|
|
2352
|
+
/* ---------------------------------------------------------------------- */
|
|
2353
|
+
/**
|
|
2354
|
+
* top 캡션은 컨트롤의 박스 높이를 캡션만큼 키운다. 가로 행(FlexDirection:'row')
|
|
2355
|
+
* 안에 "캡션 있는 컨트롤(입력 등)"과 "캡션 없는 컨트롤(버튼 등)"이 섞이면 높이가
|
|
2356
|
+
* 달라 세로 정렬이 어긋난다. 캡션을 별도 Label 로 행 위로 올리고, 입력+컨트롤만
|
|
2357
|
+
* 같은 높이로 한 행에 둘 것 (DESIGN.md §Layout "Captioned input in a row").
|
|
2358
|
+
*
|
|
2359
|
+
* 검출 조건 (materialize 된 payload 기준 — design.caption→CaptionStyle, design.flex→DisplayStyle):
|
|
2360
|
+
* - 노드가 가로 행 (DisplayStyle.UseDisplay && FlexDirection==='row')
|
|
2361
|
+
* - 직계 자식 ≥2
|
|
2362
|
+
* - 그 중 ≥1 이 top 캡션 inflate (CaptionStyle.UseDesign===true + CaptionPosition≠'Left' + Caption 비어있지 않음)
|
|
2363
|
+
* - 그 중 ≥1 이 비캡션 → "혼합 행" 만 경보 (입력+입력 둘 다 캡션 / 세로 그룹 / 라벨을 이미 올린 행은 통과)
|
|
2364
|
+
*/
|
|
2365
|
+
export function checkCaptionRowAlignment(scenario, strictCaptionRow = true) {
|
|
2366
|
+
if (!strictCaptionRow) return [];
|
|
2367
|
+
const errors = [];
|
|
2368
|
+
const steps = scenario?.Steps ?? {};
|
|
2369
|
+
|
|
2370
|
+
const isRow = (n) =>
|
|
2371
|
+
n?.DisplayStyle?.UseDisplay === true && n?.DisplayStyle?.FlexDirection === 'row';
|
|
2372
|
+
const captionInflated = (n) => {
|
|
2373
|
+
const cs = n?.CaptionStyle;
|
|
2374
|
+
if (!cs || cs.UseDesign !== true) return false;
|
|
2375
|
+
if (cs.CaptionPosition === 'Left') return false; // 좌측 캡션은 높이를 키우지 않음
|
|
2376
|
+
return typeof n.Caption === 'string' && n.Caption.length > 0; // 비어있는 캡션(숨김)은 제외
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
const visit = (node, path) => {
|
|
2380
|
+
if (!node || typeof node !== 'object') return;
|
|
2381
|
+
const kids = Array.isArray(node.Contents)
|
|
2382
|
+
? node.Contents.filter((c) => c && typeof c === 'object')
|
|
2383
|
+
: [];
|
|
2384
|
+
if (isRow(node) && kids.length >= 2) {
|
|
2385
|
+
const capped = kids.filter(captionInflated);
|
|
2386
|
+
const uncapped = kids.filter((c) => !captionInflated(c));
|
|
2387
|
+
if (capped.length > 0 && uncapped.length > 0) {
|
|
2388
|
+
const names = capped
|
|
2389
|
+
.map((c) => c.ControlName2 || c.ControlName || c.ControlType || c.ContentsType)
|
|
2390
|
+
.join(', ');
|
|
2391
|
+
errors.push(
|
|
2392
|
+
`${path} (가로 행) 안에 캡션 컨트롤(${names})과 비캡션 컨트롤이 섞여 있음 — ` +
|
|
2393
|
+
`top 캡션이 박스 높이를 키워 세로 정렬이 어긋난다. 캡션을 별도 Label 로 행 위에 올리고 ` +
|
|
2394
|
+
`(컬럼 gap), 입력+컨트롤만 같은 높이로 한 행에 둘 것 (DESIGN.md §Layout "Captioned input in a row").`,
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (Array.isArray(node.Contents)) {
|
|
2399
|
+
node.Contents.forEach((c, i) => visit(c, `${path}/Contents/${i}`));
|
|
2400
|
+
}
|
|
2401
|
+
const layouts = node?.Dialog?.Layouts;
|
|
2402
|
+
if (Array.isArray(layouts)) {
|
|
2403
|
+
layouts.forEach((l, li) =>
|
|
2404
|
+
(l?.Controls ?? []).forEach((c, ci) =>
|
|
2405
|
+
visit(c, `${path}/Dialog/Layouts/${li}/Controls/${ci}`)));
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
for (const sid of Object.keys(steps)) {
|
|
2410
|
+
const step = steps[sid];
|
|
2411
|
+
for (const ch of ['Contents', 'FixedContentsTop', 'FixedContentsBottom']) {
|
|
2412
|
+
(step?.[ch] ?? []).forEach((g, i) => visit(g, `Steps/${sid}/${ch}/${i}`));
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return errors;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* 시나리오 전체에 대해 모든 cross-check 실행.
|
|
2420
|
+
* @returns {{ [tag: string]: string[] }} — 검증 태그별 에러 메시지 배열
|
|
2421
|
+
*/
|
|
2422
|
+
export function runAllCrossChecks(scenario, opts = {}) {
|
|
2423
|
+
const {
|
|
2424
|
+
strictSample = true, strictImage = true, strictProto = true,
|
|
2425
|
+
strictV1Controls = true, strictStepNav = true, strictNoInnerBlock = true,
|
|
2426
|
+
strictNoService = true, strictScript = true, strictMockInit = true,
|
|
2427
|
+
strictCategoryName = true, strictDisplayKey = true, strictTabMove = true,
|
|
2428
|
+
strictTabEmbed = true, strictGroupDc = true, strictEmbedRender = true,
|
|
2429
|
+
strictDcEmpty = true, strictBackButton = true, strictComboFixed = true, strictCaptionRow = true,
|
|
2430
|
+
strictComboBodyCkeys = true, strictDcParentKey = true, strictDcParentCtx = true,
|
|
2431
|
+
strictEmbedBind = true,
|
|
2432
|
+
} = opts;
|
|
2433
|
+
return {
|
|
2434
|
+
reach: checkStepReachability(scenario),
|
|
2435
|
+
id: checkIdUniqueness(scenario),
|
|
2436
|
+
date: checkInputDateConsistency(scenario),
|
|
2437
|
+
event: checkDialogEventPlacement(scenario),
|
|
2438
|
+
'dialog-host': checkDialogHostStructure(scenario),
|
|
2439
|
+
sample: checkSampleDataSources(scenario, strictSample),
|
|
2440
|
+
dsref: checkDataSourceReferences(scenario),
|
|
2441
|
+
bind: checkCkeyBinding(scenario),
|
|
2442
|
+
move: checkMoveStepsTargets(scenario),
|
|
2443
|
+
'ds-key': checkComboSearchInnerKeys(scenario),
|
|
2444
|
+
'combo-body-ckeys': checkComboBodyNoCkeys(scenario, strictComboBodyCkeys),
|
|
2445
|
+
'dc-parent-key': checkDataConnectionParentKeys(scenario, strictDcParentKey),
|
|
2446
|
+
'dc-parent-ctx': checkParentFilterAncestorDc(scenario, strictDcParentCtx),
|
|
2447
|
+
'combo-fixed': checkComboFixedKeys(scenario, strictComboFixed),
|
|
2448
|
+
'display-key': checkDisplayCkey(scenario, strictDisplayKey),
|
|
2449
|
+
image: checkImagePathEmpty(scenario, strictImage),
|
|
2450
|
+
proto: checkPrototypeBinding(scenario, strictProto),
|
|
2451
|
+
'v1-only': checkV1ControlsOnly(scenario, strictV1Controls),
|
|
2452
|
+
inner: checkInnerBlockContext(scenario),
|
|
2453
|
+
'no-inner-block': checkNoInnerBlock(scenario, strictNoInnerBlock),
|
|
2454
|
+
'no-service': checkNoService(scenario, strictNoService),
|
|
2455
|
+
'event-ref': checkEventReferences(scenario),
|
|
2456
|
+
'step-nav': checkStepNavigationSources(scenario, strictStepNav),
|
|
2457
|
+
'script-allow': checkScriptAllowlist(scenario, strictScript),
|
|
2458
|
+
'mock-init': checkMockInit(scenario, strictMockInit),
|
|
2459
|
+
'category-name': checkCategoryNaming(scenario, strictCategoryName),
|
|
2460
|
+
'tab-move': checkTabAncestorMove(scenario, strictTabMove),
|
|
2461
|
+
'tab-embed': checkTabLinkedStepEmbed(scenario, strictTabEmbed),
|
|
2462
|
+
'group-dc': checkGroupUseDataConnection(scenario, strictGroupDc),
|
|
2463
|
+
'dc-empty': checkGroupDcChildBinding(scenario, strictDcEmpty),
|
|
2464
|
+
'embed-render': checkEmbedRender(scenario, strictEmbedRender),
|
|
2465
|
+
'embed-bind': checkEmbedSectorBinding(scenario, strictEmbedBind),
|
|
2466
|
+
'back-button': checkBackButtonStandard(scenario, strictBackButton),
|
|
2467
|
+
'caption-row': checkCaptionRowAlignment(scenario, strictCaptionRow),
|
|
2468
|
+
'ai-gen': checkAiGenerationForbidden(scenario),
|
|
2469
|
+
residual: checkNoResidualShortForms(scenario),
|
|
2470
|
+
};
|
|
2471
|
+
}
|