@planu/cli 0.97.2 → 0.98.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/dist/config/license-plans.json +6 -2
- package/dist/config/mobile-ci-templates/android-github-actions.yml +149 -0
- package/dist/config/mobile-ci-templates/ios-github-actions.yml +120 -0
- package/dist/engine/api-validation/graphql-schema-validator.d.ts +3 -0
- package/dist/engine/api-validation/graphql-schema-validator.d.ts.map +1 -0
- package/dist/engine/api-validation/graphql-schema-validator.js +134 -0
- package/dist/engine/api-validation/graphql-schema-validator.js.map +1 -0
- package/dist/engine/api-validation/index.d.ts +3 -0
- package/dist/engine/api-validation/index.d.ts.map +1 -0
- package/dist/engine/api-validation/index.js +4 -0
- package/dist/engine/api-validation/index.js.map +1 -0
- package/dist/engine/api-validation/openapi-impl-validator.d.ts +3 -0
- package/dist/engine/api-validation/openapi-impl-validator.d.ts.map +1 -0
- package/dist/engine/api-validation/openapi-impl-validator.js +205 -0
- package/dist/engine/api-validation/openapi-impl-validator.js.map +1 -0
- package/dist/engine/ci-generator/android-jobs.d.ts +17 -0
- package/dist/engine/ci-generator/android-jobs.d.ts.map +1 -0
- package/dist/engine/ci-generator/android-jobs.js +168 -0
- package/dist/engine/ci-generator/android-jobs.js.map +1 -0
- package/dist/engine/ci-generator/ios-jobs.d.ts +17 -0
- package/dist/engine/ci-generator/ios-jobs.d.ts.map +1 -0
- package/dist/engine/ci-generator/ios-jobs.js +151 -0
- package/dist/engine/ci-generator/ios-jobs.js.map +1 -0
- package/dist/engine/ci-generator/yaml-builder.d.ts +10 -1
- package/dist/engine/ci-generator/yaml-builder.d.ts.map +1 -1
- package/dist/engine/ci-generator/yaml-builder.js +46 -1
- package/dist/engine/ci-generator/yaml-builder.js.map +1 -1
- package/dist/engine/coverage-gap-analyzer.d.ts +6 -0
- package/dist/engine/coverage-gap-analyzer.d.ts.map +1 -0
- package/dist/engine/coverage-gap-analyzer.js +177 -0
- package/dist/engine/coverage-gap-analyzer.js.map +1 -0
- package/dist/engine/dashboard/templates-kanban.d.ts.map +1 -1
- package/dist/engine/dashboard/templates-kanban.js +22 -0
- package/dist/engine/dashboard/templates-kanban.js.map +1 -1
- package/dist/engine/dashboard/templates-layout.d.ts.map +1 -1
- package/dist/engine/dashboard/templates-layout.js +59 -7
- package/dist/engine/dashboard/templates-layout.js.map +1 -1
- package/dist/engine/mutation-config-generator.d.ts +6 -0
- package/dist/engine/mutation-config-generator.d.ts.map +1 -0
- package/dist/engine/mutation-config-generator.js +111 -0
- package/dist/engine/mutation-config-generator.js.map +1 -0
- package/dist/engine/tdd-scaffold-generator.d.ts +7 -0
- package/dist/engine/tdd-scaffold-generator.d.ts.map +1 -0
- package/dist/engine/tdd-scaffold-generator.js +252 -0
- package/dist/engine/tdd-scaffold-generator.js.map +1 -0
- package/dist/engine/version-resolver.d.ts +10 -0
- package/dist/engine/version-resolver.d.ts.map +1 -0
- package/dist/engine/version-resolver.js +144 -0
- package/dist/engine/version-resolver.js.map +1 -0
- package/dist/engine/web-fetcher/stack-advisor.d.ts.map +1 -1
- package/dist/engine/web-fetcher/stack-advisor.js +32 -4
- package/dist/engine/web-fetcher/stack-advisor.js.map +1 -1
- package/dist/index.js +7 -11
- package/dist/index.js.map +1 -1
- package/dist/tools/checkpoint-handler.d.ts.map +1 -1
- package/dist/tools/checkpoint-handler.js +24 -1
- package/dist/tools/checkpoint-handler.js.map +1 -1
- package/dist/tools/dashboard.d.ts.map +1 -1
- package/dist/tools/dashboard.js +2 -0
- package/dist/tools/dashboard.js.map +1 -1
- package/dist/tools/register-spec-322-tools.d.ts +3 -0
- package/dist/tools/register-spec-322-tools.d.ts.map +1 -0
- package/dist/tools/register-spec-322-tools.js +41 -0
- package/dist/tools/register-spec-322-tools.js.map +1 -0
- package/dist/tools/register-spec-323-tools.d.ts +3 -0
- package/dist/tools/register-spec-323-tools.d.ts.map +1 -0
- package/dist/tools/register-spec-323-tools.js +57 -0
- package/dist/tools/register-spec-323-tools.js.map +1 -0
- package/dist/tools/tdd-scaffold-handler.d.ts +5 -0
- package/dist/tools/tdd-scaffold-handler.d.ts.map +1 -0
- package/dist/tools/tdd-scaffold-handler.js +92 -0
- package/dist/tools/tdd-scaffold-handler.js.map +1 -0
- package/dist/tools/validate-api-contract-handler.d.ts +3 -0
- package/dist/tools/validate-api-contract-handler.d.ts.map +1 -0
- package/dist/tools/validate-api-contract-handler.js +74 -0
- package/dist/tools/validate-api-contract-handler.js.map +1 -0
- package/dist/tools/validate.d.ts.map +1 -1
- package/dist/tools/validate.js +22 -2
- package/dist/tools/validate.js.map +1 -1
- package/dist/types/api-contract.d.ts +58 -0
- package/dist/types/api-contract.d.ts.map +1 -1
- package/dist/types/ci.d.ts +8 -0
- package/dist/types/ci.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/testing.d.ts +60 -0
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/version-resolution.d.ts +23 -0
- package/dist/types/version-resolution.d.ts.map +1 -0
- package/dist/types/version-resolution.js +3 -0
- package/dist/types/version-resolution.js.map +1 -0
- package/package.json +1 -1
- package/src/config/license-plans.json +6 -2
- package/src/config/mobile-ci-templates/android-github-actions.yml +149 -0
- package/src/config/mobile-ci-templates/ios-github-actions.yml +120 -0
|
@@ -87,7 +87,8 @@
|
|
|
87
87
|
"check_spec_lock",
|
|
88
88
|
"configure_desktop_notifications",
|
|
89
89
|
"desktop_notification_status",
|
|
90
|
-
"test_notification"
|
|
90
|
+
"test_notification",
|
|
91
|
+
"tdd_scaffold"
|
|
91
92
|
],
|
|
92
93
|
"proTools": [
|
|
93
94
|
"audit",
|
|
@@ -273,7 +274,10 @@
|
|
|
273
274
|
"configure_compliance",
|
|
274
275
|
"apply_spec_template",
|
|
275
276
|
"publish_spec_template",
|
|
276
|
-
"search_spec_templates"
|
|
277
|
+
"search_spec_templates",
|
|
278
|
+
"validate_api_contract",
|
|
279
|
+
"coverage_gap_analyzer",
|
|
280
|
+
"run_mutation_hints"
|
|
277
281
|
],
|
|
278
282
|
"alwaysAllowed": [
|
|
279
283
|
"activate_license",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
name: Android CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
|
|
12
|
+
android-test:
|
|
13
|
+
name: Android — Gradle Unit Tests
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup JDK
|
|
20
|
+
uses: actions/setup-java@v4
|
|
21
|
+
with:
|
|
22
|
+
java-version: '17'
|
|
23
|
+
distribution: temurin
|
|
24
|
+
|
|
25
|
+
- name: Cache Gradle packages
|
|
26
|
+
uses: actions/cache@v4
|
|
27
|
+
with:
|
|
28
|
+
path: |
|
|
29
|
+
~/.gradle/caches
|
|
30
|
+
~/.gradle/wrapper
|
|
31
|
+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
32
|
+
restore-keys: ${{ runner.os }}-gradle-
|
|
33
|
+
|
|
34
|
+
- name: Grant Gradle execute permission
|
|
35
|
+
run: chmod +x gradlew
|
|
36
|
+
|
|
37
|
+
- name: Run unit tests
|
|
38
|
+
run: ./gradlew :{{module}}:testDebugUnitTest
|
|
39
|
+
|
|
40
|
+
- name: Upload test reports
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: test-results
|
|
44
|
+
path: '{{module}}/build/reports/tests/'
|
|
45
|
+
if-no-files-found: warn
|
|
46
|
+
|
|
47
|
+
android-sign:
|
|
48
|
+
name: Android — Sign Release AAB
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
needs: android-test
|
|
51
|
+
if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
|
|
52
|
+
env:
|
|
53
|
+
KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
|
|
54
|
+
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
|
55
|
+
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
56
|
+
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
57
|
+
steps:
|
|
58
|
+
- name: Checkout code
|
|
59
|
+
uses: actions/checkout@v4
|
|
60
|
+
|
|
61
|
+
- name: Setup JDK
|
|
62
|
+
uses: actions/setup-java@v4
|
|
63
|
+
with:
|
|
64
|
+
java-version: '17'
|
|
65
|
+
distribution: temurin
|
|
66
|
+
|
|
67
|
+
- name: Cache Gradle packages
|
|
68
|
+
uses: actions/cache@v4
|
|
69
|
+
with:
|
|
70
|
+
path: |
|
|
71
|
+
~/.gradle/caches
|
|
72
|
+
~/.gradle/wrapper
|
|
73
|
+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
74
|
+
restore-keys: ${{ runner.os }}-gradle-
|
|
75
|
+
|
|
76
|
+
- name: Grant Gradle execute permission
|
|
77
|
+
run: chmod +x gradlew
|
|
78
|
+
|
|
79
|
+
- name: Decode keystore
|
|
80
|
+
run: echo "$KEYSTORE_FILE" | base64 --decode > keystore.jks
|
|
81
|
+
env:
|
|
82
|
+
KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
|
|
83
|
+
|
|
84
|
+
- name: Build signed release AAB
|
|
85
|
+
run: |
|
|
86
|
+
./gradlew :{{module}}:bundleRelease \
|
|
87
|
+
-Pandroid.injected.signing.store.file=$(pwd)/keystore.jks \
|
|
88
|
+
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
|
|
89
|
+
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
|
|
90
|
+
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
|
|
91
|
+
env:
|
|
92
|
+
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
|
93
|
+
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
94
|
+
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
95
|
+
|
|
96
|
+
- name: Upload signed AAB
|
|
97
|
+
uses: actions/upload-artifact@v4
|
|
98
|
+
with:
|
|
99
|
+
name: release-aab
|
|
100
|
+
path: '{{module}}/build/outputs/bundle/release/*.aab'
|
|
101
|
+
|
|
102
|
+
android-deploy:
|
|
103
|
+
name: Android — Play Console Deploy
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
needs: android-sign
|
|
106
|
+
if: github.ref == 'refs/heads/main'
|
|
107
|
+
env:
|
|
108
|
+
PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
|
|
109
|
+
steps:
|
|
110
|
+
- name: Checkout code
|
|
111
|
+
uses: actions/checkout@v4
|
|
112
|
+
|
|
113
|
+
- name: Download signed AAB
|
|
114
|
+
uses: actions/download-artifact@v4
|
|
115
|
+
with:
|
|
116
|
+
name: release-aab
|
|
117
|
+
path: ./release
|
|
118
|
+
|
|
119
|
+
- name: Setup Ruby for Fastlane
|
|
120
|
+
uses: ruby/setup-ruby@v1
|
|
121
|
+
with:
|
|
122
|
+
ruby-version: 'bundler-cache: true'
|
|
123
|
+
|
|
124
|
+
- name: Install Fastlane
|
|
125
|
+
run: gem install fastlane --no-document
|
|
126
|
+
|
|
127
|
+
- name: Decode Play Store JSON key
|
|
128
|
+
run: echo "$PLAY_STORE_JSON_KEY" > play-store-key.json
|
|
129
|
+
env:
|
|
130
|
+
PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
|
|
131
|
+
|
|
132
|
+
- name: Upload to Play Console (internal track)
|
|
133
|
+
run: |
|
|
134
|
+
fastlane supply \
|
|
135
|
+
--aab ./release/{{module}}/build/outputs/bundle/release/*.aab \
|
|
136
|
+
--package_name "{{bundleId}}" \
|
|
137
|
+
--track internal \
|
|
138
|
+
--json_key ./play-store-key.json
|
|
139
|
+
|
|
140
|
+
# Required GitHub Secrets:
|
|
141
|
+
# KEYSTORE_FILE — Base64-encoded .jks keystore file
|
|
142
|
+
# KEYSTORE_PASSWORD — Password for the keystore file
|
|
143
|
+
# KEY_ALIAS — Alias of the signing key in the keystore
|
|
144
|
+
# KEY_PASSWORD — Password for the signing key
|
|
145
|
+
# PLAY_STORE_JSON_KEY — JSON content of the Google Play service account key
|
|
146
|
+
#
|
|
147
|
+
# Placeholders:
|
|
148
|
+
# {{module}} — Gradle module name (e.g. app)
|
|
149
|
+
# {{bundleId}} — Android application ID (e.g. com.example.myapp)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
name: iOS CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
|
|
12
|
+
ios-test:
|
|
13
|
+
name: iOS — Xcode Build & Test
|
|
14
|
+
runs-on: macos-latest
|
|
15
|
+
env:
|
|
16
|
+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
17
|
+
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
|
18
|
+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout code
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Select Xcode version
|
|
24
|
+
run: sudo xcode-select -s /Applications/Xcode.app
|
|
25
|
+
|
|
26
|
+
- name: Build and test (Xcode)
|
|
27
|
+
run: |
|
|
28
|
+
xcodebuild clean build test \
|
|
29
|
+
-scheme "{{scheme}}" \
|
|
30
|
+
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
|
|
31
|
+
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \
|
|
32
|
+
| xcpretty && exit "${PIPESTATUS[0]}"
|
|
33
|
+
|
|
34
|
+
ios-testflight:
|
|
35
|
+
name: iOS — TestFlight Upload
|
|
36
|
+
runs-on: macos-latest
|
|
37
|
+
needs: ios-test
|
|
38
|
+
if: github.ref == 'refs/heads/develop'
|
|
39
|
+
env:
|
|
40
|
+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
41
|
+
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
|
42
|
+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
43
|
+
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
44
|
+
steps:
|
|
45
|
+
- name: Checkout code
|
|
46
|
+
uses: actions/checkout@v4
|
|
47
|
+
|
|
48
|
+
- name: Select Xcode version
|
|
49
|
+
run: sudo xcode-select -s /Applications/Xcode.app
|
|
50
|
+
|
|
51
|
+
- name: Setup Ruby for Fastlane
|
|
52
|
+
uses: ruby/setup-ruby@v1
|
|
53
|
+
with:
|
|
54
|
+
ruby-version: 'bundler-cache: true'
|
|
55
|
+
|
|
56
|
+
- name: Install Fastlane
|
|
57
|
+
run: gem install fastlane --no-document
|
|
58
|
+
|
|
59
|
+
- name: Build IPA
|
|
60
|
+
run: |
|
|
61
|
+
fastlane gym \
|
|
62
|
+
--scheme "{{scheme}}" \
|
|
63
|
+
--export_method app-store \
|
|
64
|
+
--output_directory ./build
|
|
65
|
+
env:
|
|
66
|
+
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
67
|
+
|
|
68
|
+
- name: Upload to TestFlight
|
|
69
|
+
run: fastlane pilot upload --ipa ./build/*.ipa --skip_waiting_for_build_processing true
|
|
70
|
+
|
|
71
|
+
ios-appstore:
|
|
72
|
+
name: iOS — App Store Submission
|
|
73
|
+
runs-on: macos-latest
|
|
74
|
+
needs: ios-test
|
|
75
|
+
if: github.ref == 'refs/heads/main'
|
|
76
|
+
env:
|
|
77
|
+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
78
|
+
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
|
79
|
+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
80
|
+
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
81
|
+
steps:
|
|
82
|
+
- name: Checkout code
|
|
83
|
+
uses: actions/checkout@v4
|
|
84
|
+
|
|
85
|
+
- name: Select Xcode version
|
|
86
|
+
run: sudo xcode-select -s /Applications/Xcode.app
|
|
87
|
+
|
|
88
|
+
- name: Setup Ruby for Fastlane
|
|
89
|
+
uses: ruby/setup-ruby@v1
|
|
90
|
+
with:
|
|
91
|
+
ruby-version: 'bundler-cache: true'
|
|
92
|
+
|
|
93
|
+
- name: Install Fastlane
|
|
94
|
+
run: gem install fastlane --no-document
|
|
95
|
+
|
|
96
|
+
- name: Build IPA (production)
|
|
97
|
+
run: |
|
|
98
|
+
fastlane gym \
|
|
99
|
+
--scheme "{{scheme}}" \
|
|
100
|
+
--configuration Release \
|
|
101
|
+
--export_method app-store \
|
|
102
|
+
--output_directory ./build
|
|
103
|
+
env:
|
|
104
|
+
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
105
|
+
|
|
106
|
+
- name: Submit to App Store
|
|
107
|
+
run: |
|
|
108
|
+
fastlane deliver \
|
|
109
|
+
--ipa ./build/*.ipa \
|
|
110
|
+
--submit_for_review false \
|
|
111
|
+
--automatic_release false
|
|
112
|
+
|
|
113
|
+
# Required GitHub Secrets:
|
|
114
|
+
# APPLE_API_KEY — Base64-encoded .p8 private key from App Store Connect
|
|
115
|
+
# APPLE_API_ISSUER_ID — Issuer ID from App Store Connect API keys page
|
|
116
|
+
# APPLE_API_KEY_ID — Key ID from App Store Connect API keys page
|
|
117
|
+
# MATCH_PASSWORD — Encryption passphrase for Fastlane Match certificates
|
|
118
|
+
#
|
|
119
|
+
# Placeholders:
|
|
120
|
+
# {{scheme}} — Replace with your Xcode scheme name (e.g. MyApp)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-schema-validator.d.ts","sourceRoot":"","sources":["../../../src/engine/api-validation/graphql-schema-validator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,iBAAiB,EAIlB,MAAM,sBAAsB,CAAC;AAuF9B,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAgD5B"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// engine/api-validation/graphql-schema-validator.ts — Validate GraphQL schema vs resolvers (SPEC-322)
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// GraphQL SDL parser (minimal — extracts Query/Mutation/Subscription fields)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const OPERATION_TYPE_REGEX = /type\s+(Query|Mutation|Subscription)\s*\{([^}]+)\}/gs;
|
|
8
|
+
const FIELD_NAME_REGEX = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\([^)]*\))?\s*:/m;
|
|
9
|
+
function parseGraphQLOperations(sdl) {
|
|
10
|
+
const operations = [];
|
|
11
|
+
let typeMatch;
|
|
12
|
+
const typeRegex = new RegExp(OPERATION_TYPE_REGEX.source, OPERATION_TYPE_REGEX.flags);
|
|
13
|
+
while ((typeMatch = typeRegex.exec(sdl)) !== null) {
|
|
14
|
+
const kind = typeMatch[1];
|
|
15
|
+
const body = typeMatch[2] ?? '';
|
|
16
|
+
const lines = body.split('\n');
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const fieldMatch = FIELD_NAME_REGEX.exec(line);
|
|
19
|
+
if (fieldMatch?.[1]) {
|
|
20
|
+
operations.push({ name: fieldMatch[1], kind });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return operations;
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Resolver scanner
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const RESOLVER_PATTERNS = [
|
|
30
|
+
// Apollo: { Query: { resolverName() } }
|
|
31
|
+
/['"`]([a-zA-Z_][a-zA-Z0-9_]*)['"`]\s*:\s*(?:async\s+)?(?:function|\(|[a-zA-Z_])/g,
|
|
32
|
+
// Direct assignment: const resolverNameResolver = async () =>
|
|
33
|
+
/(?:const|let)\s+([a-zA-Z_][a-zA-Z0-9_]*Resolver)\s*=/g,
|
|
34
|
+
// Object method shorthand: resolverName(parent, args, ctx)
|
|
35
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*(?:parent|root|_),?\s*(?:args|_args)/g,
|
|
36
|
+
];
|
|
37
|
+
async function scanSourceFilesForResolvers(projectPath) {
|
|
38
|
+
const patterns = [
|
|
39
|
+
`${projectPath}/**/*.ts`,
|
|
40
|
+
`${projectPath}/**/*.tsx`,
|
|
41
|
+
`${projectPath}/**/*.js`,
|
|
42
|
+
`${projectPath}/**/*.jsx`,
|
|
43
|
+
];
|
|
44
|
+
const ignores = ['**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**'];
|
|
45
|
+
const resolvers = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const files = await glob(patterns, { ignore: ignores });
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
const content = await readFile(file, 'utf-8').catch(() => '');
|
|
50
|
+
if (!content) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!content.includes('resolver') &&
|
|
54
|
+
!content.includes('Query') &&
|
|
55
|
+
!content.includes('Mutation')) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const pattern of RESOLVER_PATTERNS) {
|
|
59
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = re.exec(content)) !== null) {
|
|
62
|
+
const name = match[1];
|
|
63
|
+
if (name && !seen.has(name)) {
|
|
64
|
+
seen.add(name);
|
|
65
|
+
resolvers.push({ name, file });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return resolvers;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Main validator
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
export async function validateGraphQLSchema(schemaPath, projectPath) {
|
|
76
|
+
const sdl = await readFile(schemaPath, 'utf-8');
|
|
77
|
+
const operations = parseGraphQLOperations(sdl);
|
|
78
|
+
const resolvers = await scanSourceFilesForResolvers(projectPath);
|
|
79
|
+
const resolverNames = new Set(resolvers.map((r) => r.name));
|
|
80
|
+
const missingEndpoints = [];
|
|
81
|
+
const undocumentedEndpoints = [];
|
|
82
|
+
for (const op of operations) {
|
|
83
|
+
if (!resolverNames.has(op.name)) {
|
|
84
|
+
missingEndpoints.push({
|
|
85
|
+
path: `${op.kind}.${op.name}`,
|
|
86
|
+
method: op.kind,
|
|
87
|
+
severity: 'critical',
|
|
88
|
+
message: `GraphQL ${op.kind} "${op.name}" is defined in the schema but no resolver implementation was detected in the project source files.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const opNames = new Set(operations.map((o) => o.name));
|
|
93
|
+
for (const resolver of resolvers) {
|
|
94
|
+
if (!opNames.has(resolver.name) && isLikelyOperationResolver(resolver.name)) {
|
|
95
|
+
undocumentedEndpoints.push({
|
|
96
|
+
path: resolver.name,
|
|
97
|
+
method: 'resolver',
|
|
98
|
+
severity: 'info',
|
|
99
|
+
message: `Resolver "${resolver.name}" found in ${resolver.file} does not correspond to any operation in the GraphQL schema.`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const findings = [...missingEndpoints, ...undocumentedEndpoints];
|
|
104
|
+
return {
|
|
105
|
+
contractFormat: 'graphql',
|
|
106
|
+
contractVersion: 'SDL',
|
|
107
|
+
contractTitle: 'GraphQL Schema',
|
|
108
|
+
totalContractEndpoints: operations.length,
|
|
109
|
+
totalImplEndpoints: resolvers.length,
|
|
110
|
+
missingEndpoints,
|
|
111
|
+
undocumentedEndpoints,
|
|
112
|
+
schemaDrift: [],
|
|
113
|
+
breakingChanges: [],
|
|
114
|
+
findings,
|
|
115
|
+
summary: buildSummary(missingEndpoints.length, undocumentedEndpoints.length),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function isLikelyOperationResolver(name) {
|
|
119
|
+
return /^(?:get|create|update|delete|list|find|add|remove|fetch|load|save|set)[A-Z]/.test(name);
|
|
120
|
+
}
|
|
121
|
+
function buildSummary(missing, undocumented) {
|
|
122
|
+
if (missing === 0 && undocumented === 0) {
|
|
123
|
+
return 'GraphQL schema and resolver implementations are fully aligned.';
|
|
124
|
+
}
|
|
125
|
+
const parts = [];
|
|
126
|
+
if (missing > 0) {
|
|
127
|
+
parts.push(`${missing} operation(s) in schema have no resolver implementation`);
|
|
128
|
+
}
|
|
129
|
+
if (undocumented > 0) {
|
|
130
|
+
parts.push(`${undocumented} resolver(s) found that are not in the schema`);
|
|
131
|
+
}
|
|
132
|
+
return parts.join('; ') + '.';
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=graphql-schema-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-schema-validator.js","sourceRoot":"","sources":["../../../src/engine/api-validation/graphql-schema-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/engine/api-validation/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/engine/api-validation/index.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAE5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-impl-validator.d.ts","sourceRoot":"","sources":["../../../src/engine/api-validation/openapi-impl-validator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,iBAAiB,EAIlB,MAAM,sBAAsB,CAAC;AAsJ9B,wBAAsB,uBAAuB,CAC3C,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CA6D5B"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// engine/api-validation/openapi-impl-validator.ts — Validate OpenAPI contract vs implementation (SPEC-322)
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Route pattern detectors
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const ROUTE_PATTERNS = [
|
|
8
|
+
// Express: app.get('/path', ...) or router.post('/path')
|
|
9
|
+
{
|
|
10
|
+
regex: /(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
11
|
+
framework: 'express',
|
|
12
|
+
},
|
|
13
|
+
// Fastify: fastify.get('/path')
|
|
14
|
+
{
|
|
15
|
+
regex: /fastify\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
16
|
+
framework: 'fastify',
|
|
17
|
+
},
|
|
18
|
+
// Next.js API routes: export async function GET/POST/PUT/DELETE
|
|
19
|
+
{
|
|
20
|
+
regex: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g,
|
|
21
|
+
framework: 'nextjs',
|
|
22
|
+
},
|
|
23
|
+
// FastAPI: @router.get('/path') or @app.get('/path')
|
|
24
|
+
{
|
|
25
|
+
regex: /@(?:router|app)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
26
|
+
framework: 'fastapi',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
function normalizeMethod(m) {
|
|
33
|
+
return m.toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
function normalizePath(p) {
|
|
36
|
+
// Convert :param → {param} for comparison with OpenAPI style
|
|
37
|
+
return p.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
38
|
+
}
|
|
39
|
+
function pathsMatch(contractPath, implPath) {
|
|
40
|
+
const norm = normalizePath(implPath);
|
|
41
|
+
if (contractPath === norm || contractPath === implPath) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const contractParts = contractPath.split('/');
|
|
45
|
+
const implParts = norm.split('/');
|
|
46
|
+
if (contractParts.length !== implParts.length) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return contractParts.every((part, i) => {
|
|
50
|
+
if (part.startsWith('{') && part.endsWith('}')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return part === implParts[i];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function scanSourceFilesForRoutes(projectPath) {
|
|
57
|
+
const patterns = [
|
|
58
|
+
`${projectPath}/**/*.ts`,
|
|
59
|
+
`${projectPath}/**/*.tsx`,
|
|
60
|
+
`${projectPath}/**/*.js`,
|
|
61
|
+
`${projectPath}/**/*.jsx`,
|
|
62
|
+
`${projectPath}/**/*.py`,
|
|
63
|
+
];
|
|
64
|
+
const ignores = ['**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**'];
|
|
65
|
+
const routes = [];
|
|
66
|
+
const files = await glob(patterns, { ignore: ignores });
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const content = await readFile(file, 'utf-8').catch(() => '');
|
|
69
|
+
if (!content) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const { regex, framework } of ROUTE_PATTERNS) {
|
|
73
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
74
|
+
let match;
|
|
75
|
+
while ((match = re.exec(content)) !== null) {
|
|
76
|
+
if (framework === 'nextjs') {
|
|
77
|
+
const relPath = file
|
|
78
|
+
.replace(projectPath, '')
|
|
79
|
+
.replace(/\\/g, '/')
|
|
80
|
+
.replace(/\/app\/|\/pages\/api\//, '/')
|
|
81
|
+
.replace(/\/route\.(ts|js|tsx|jsx)$/, '')
|
|
82
|
+
.replace(/\.(ts|js|tsx|jsx)$/, '');
|
|
83
|
+
routes.push({
|
|
84
|
+
method: normalizeMethod(match[1] ?? 'GET'),
|
|
85
|
+
path: relPath || '/',
|
|
86
|
+
framework,
|
|
87
|
+
file,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const rawMethod = match[1] ?? '';
|
|
92
|
+
const rawPath = match[2] ?? '';
|
|
93
|
+
if (rawMethod && rawPath) {
|
|
94
|
+
routes.push({
|
|
95
|
+
method: normalizeMethod(rawMethod),
|
|
96
|
+
path: rawPath,
|
|
97
|
+
framework,
|
|
98
|
+
file,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Deduplicate by method+path (multiple patterns may match the same route)
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
return routes.filter((r) => {
|
|
108
|
+
const key = `${r.method}:${r.path}`;
|
|
109
|
+
if (seen.has(key)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
seen.add(key);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// OpenAPI contract reader
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
async function readOpenApiDoc(contractPath) {
|
|
120
|
+
const content = await readFile(contractPath, 'utf-8');
|
|
121
|
+
if (contractPath.endsWith('.json')) {
|
|
122
|
+
return JSON.parse(content);
|
|
123
|
+
}
|
|
124
|
+
// Minimal YAML fallback: strip comments and attempt JSON parse
|
|
125
|
+
const stripped = content
|
|
126
|
+
.split('\n')
|
|
127
|
+
.filter((line) => !line.trim().startsWith('#'))
|
|
128
|
+
.join('\n');
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(stripped);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Main validator
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
const HTTP_METHODS_LIST = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
140
|
+
export async function validateOpenApiContract(contractPath, projectPath) {
|
|
141
|
+
const doc = await readOpenApiDoc(contractPath);
|
|
142
|
+
const implRoutes = await scanSourceFilesForRoutes(projectPath);
|
|
143
|
+
const contractEndpoints = [];
|
|
144
|
+
const paths = doc.paths ?? {};
|
|
145
|
+
for (const [apiPath, pathItem] of Object.entries(paths)) {
|
|
146
|
+
for (const method of HTTP_METHODS_LIST) {
|
|
147
|
+
if (pathItem[method] !== undefined) {
|
|
148
|
+
contractEndpoints.push({ path: apiPath, method: method.toUpperCase() });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const missingEndpoints = [];
|
|
153
|
+
for (const endpoint of contractEndpoints) {
|
|
154
|
+
const found = implRoutes.some((r) => r.method === endpoint.method && pathsMatch(endpoint.path, r.path));
|
|
155
|
+
if (!found) {
|
|
156
|
+
missingEndpoints.push({
|
|
157
|
+
path: endpoint.path,
|
|
158
|
+
method: endpoint.method,
|
|
159
|
+
severity: 'critical',
|
|
160
|
+
message: `Endpoint ${endpoint.method} ${endpoint.path} is documented in the contract but no implementation was found in the project source files.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const undocumentedEndpoints = [];
|
|
165
|
+
for (const route of implRoutes) {
|
|
166
|
+
const found = contractEndpoints.some((e) => e.method === route.method && pathsMatch(e.path, route.path));
|
|
167
|
+
if (!found) {
|
|
168
|
+
undocumentedEndpoints.push({
|
|
169
|
+
path: route.path,
|
|
170
|
+
method: route.method,
|
|
171
|
+
severity: 'warning',
|
|
172
|
+
message: `Endpoint ${route.method} ${route.path} is implemented (found in ${route.file}) but is not documented in the API contract.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const findings = [...missingEndpoints, ...undocumentedEndpoints];
|
|
177
|
+
const contractInfo = doc.info ?? {};
|
|
178
|
+
return {
|
|
179
|
+
contractFormat: 'openapi',
|
|
180
|
+
contractVersion: contractInfo.version ?? 'unknown',
|
|
181
|
+
contractTitle: contractInfo.title ?? 'Unknown API',
|
|
182
|
+
totalContractEndpoints: contractEndpoints.length,
|
|
183
|
+
totalImplEndpoints: implRoutes.length,
|
|
184
|
+
missingEndpoints,
|
|
185
|
+
undocumentedEndpoints,
|
|
186
|
+
schemaDrift: [],
|
|
187
|
+
breakingChanges: [],
|
|
188
|
+
findings,
|
|
189
|
+
summary: buildSummary(missingEndpoints.length, undocumentedEndpoints.length),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function buildSummary(missing, undocumented) {
|
|
193
|
+
if (missing === 0 && undocumented === 0) {
|
|
194
|
+
return 'Contract and implementation are fully aligned. No issues found.';
|
|
195
|
+
}
|
|
196
|
+
const parts = [];
|
|
197
|
+
if (missing > 0) {
|
|
198
|
+
parts.push(`${missing} endpoint(s) documented in the contract but missing from implementation`);
|
|
199
|
+
}
|
|
200
|
+
if (undocumented > 0) {
|
|
201
|
+
parts.push(`${undocumented} endpoint(s) implemented but not documented in the contract`);
|
|
202
|
+
}
|
|
203
|
+
return parts.join('; ') + '.';
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=openapi-impl-validator.js.map
|