micro-contracts 0.9.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 +22 -0
- package/README.md +351 -0
- package/dist/cli/templates.d.ts +16 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +377 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +978 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/dependencyGenerator.d.ts +43 -0
- package/dist/generator/dependencyGenerator.d.ts.map +1 -0
- package/dist/generator/dependencyGenerator.js +159 -0
- package/dist/generator/dependencyGenerator.js.map +1 -0
- package/dist/generator/domainGenerator.d.ts +16 -0
- package/dist/generator/domainGenerator.d.ts.map +1 -0
- package/dist/generator/domainGenerator.js +212 -0
- package/dist/generator/domainGenerator.js.map +1 -0
- package/dist/generator/index.d.ts +37 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +747 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/linter.d.ts +24 -0
- package/dist/generator/linter.d.ts.map +1 -0
- package/dist/generator/linter.js +202 -0
- package/dist/generator/linter.js.map +1 -0
- package/dist/generator/overlayProcessor.d.ts +90 -0
- package/dist/generator/overlayProcessor.d.ts.map +1 -0
- package/dist/generator/overlayProcessor.js +532 -0
- package/dist/generator/overlayProcessor.js.map +1 -0
- package/dist/generator/schemaGenerator.d.ts +10 -0
- package/dist/generator/schemaGenerator.d.ts.map +1 -0
- package/dist/generator/schemaGenerator.js +299 -0
- package/dist/generator/schemaGenerator.js.map +1 -0
- package/dist/generator/templateProcessor.d.ts +178 -0
- package/dist/generator/templateProcessor.d.ts.map +1 -0
- package/dist/generator/templateProcessor.js +607 -0
- package/dist/generator/templateProcessor.js.map +1 -0
- package/dist/generator/typeGenerator.d.ts +9 -0
- package/dist/generator/typeGenerator.d.ts.map +1 -0
- package/dist/generator/typeGenerator.js +395 -0
- package/dist/generator/typeGenerator.js.map +1 -0
- package/dist/guardrails/allowlist.d.ts +45 -0
- package/dist/guardrails/allowlist.d.ts.map +1 -0
- package/dist/guardrails/allowlist.js +261 -0
- package/dist/guardrails/allowlist.js.map +1 -0
- package/dist/guardrails/config.d.ts +40 -0
- package/dist/guardrails/config.d.ts.map +1 -0
- package/dist/guardrails/config.js +174 -0
- package/dist/guardrails/config.js.map +1 -0
- package/dist/guardrails/docs.d.ts +24 -0
- package/dist/guardrails/docs.d.ts.map +1 -0
- package/dist/guardrails/docs.js +138 -0
- package/dist/guardrails/docs.js.map +1 -0
- package/dist/guardrails/drift.d.ts +23 -0
- package/dist/guardrails/drift.d.ts.map +1 -0
- package/dist/guardrails/drift.js +127 -0
- package/dist/guardrails/drift.js.map +1 -0
- package/dist/guardrails/index.d.ts +19 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +25 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/lint.d.ts +20 -0
- package/dist/guardrails/lint.d.ts.map +1 -0
- package/dist/guardrails/lint.js +274 -0
- package/dist/guardrails/lint.js.map +1 -0
- package/dist/guardrails/manifest.d.ts +43 -0
- package/dist/guardrails/manifest.d.ts.map +1 -0
- package/dist/guardrails/manifest.js +231 -0
- package/dist/guardrails/manifest.js.map +1 -0
- package/dist/guardrails/runner.d.ts +31 -0
- package/dist/guardrails/runner.d.ts.map +1 -0
- package/dist/guardrails/runner.js +268 -0
- package/dist/guardrails/runner.js.map +1 -0
- package/dist/guardrails/security.d.ts +31 -0
- package/dist/guardrails/security.d.ts.map +1 -0
- package/dist/guardrails/security.js +181 -0
- package/dist/guardrails/security.js.map +1 -0
- package/dist/guardrails/typecheck.d.ts +15 -0
- package/dist/guardrails/typecheck.d.ts.map +1 -0
- package/dist/guardrails/typecheck.js +104 -0
- package/dist/guardrails/typecheck.js.map +1 -0
- package/dist/guardrails/types.d.ts +196 -0
- package/dist/guardrails/types.d.ts.map +1 -0
- package/dist/guardrails/types.js +8 -0
- package/dist/guardrails/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +489 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +297 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.svg +226 -0
- package/docs/development-guardrails.md +541 -0
- package/docs/guardrails-concept.svg +252 -0
- package/docs/overlays-deep-dive.md +298 -0
- package/package.json +66 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 1195" font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif">
|
|
2
|
+
<defs>
|
|
3
|
+
<!-- Gradients -->
|
|
4
|
+
<linearGradient id="headerGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
5
|
+
<stop offset="0%" style="stop-color:#1e3a5f"/>
|
|
6
|
+
<stop offset="100%" style="stop-color:#2d5a87"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="specLaneGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
9
|
+
<stop offset="0%" style="stop-color:#dbeafe"/>
|
|
10
|
+
<stop offset="100%" style="stop-color:#eff6ff"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<linearGradient id="implLaneGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
13
|
+
<stop offset="0%" style="stop-color:#fee2e2"/>
|
|
14
|
+
<stop offset="100%" style="stop-color:#fef2f2"/>
|
|
15
|
+
</linearGradient>
|
|
16
|
+
<linearGradient id="generatorGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
17
|
+
<stop offset="0%" style="stop-color:#a855f7"/>
|
|
18
|
+
<stop offset="100%" style="stop-color:#7c3aed"/>
|
|
19
|
+
</linearGradient>
|
|
20
|
+
<linearGradient id="mergeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
21
|
+
<stop offset="0%" style="stop-color:#22c55e"/>
|
|
22
|
+
<stop offset="100%" style="stop-color:#16a34a"/>
|
|
23
|
+
</linearGradient>
|
|
24
|
+
<linearGradient id="taskGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
25
|
+
<stop offset="0%" style="stop-color:#67e8f9"/>
|
|
26
|
+
<stop offset="100%" style="stop-color:#22d3ee"/>
|
|
27
|
+
</linearGradient>
|
|
28
|
+
|
|
29
|
+
<!-- Drop shadow filter -->
|
|
30
|
+
<filter id="shadow" x="-5%" y="-5%" width="110%" height="110%">
|
|
31
|
+
<feDropShadow dx="2" dy="3" stdDeviation="3" flood-opacity="0.15"/>
|
|
32
|
+
</filter>
|
|
33
|
+
<filter id="shadowSmall" x="-5%" y="-5%" width="110%" height="110%">
|
|
34
|
+
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-opacity="0.1"/>
|
|
35
|
+
</filter>
|
|
36
|
+
|
|
37
|
+
<!-- Arrow markers -->
|
|
38
|
+
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
39
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b"/>
|
|
40
|
+
</marker>
|
|
41
|
+
</defs>
|
|
42
|
+
|
|
43
|
+
<!-- Background -->
|
|
44
|
+
<rect width="1100" height="1195" fill="#f8fafc"/>
|
|
45
|
+
|
|
46
|
+
<!-- Header -->
|
|
47
|
+
<rect x="20" y="15" width="1060" height="70" rx="8" fill="url(#headerGrad)" filter="url(#shadow)"/>
|
|
48
|
+
<text x="550" y="45" text-anchor="middle" fill="white" font-size="22" font-weight="600">AI-Driven Development Guardrails</text>
|
|
49
|
+
<text x="550" y="68" text-anchor="middle" fill="#94a3b8" font-size="13">Contract-first quality gates for TypeScript API development</text>
|
|
50
|
+
|
|
51
|
+
<!-- Two-Lane Headers -->
|
|
52
|
+
<rect x="20" y="100" width="530" height="35" rx="6" fill="#3b82f6"/>
|
|
53
|
+
<text x="285" y="123" text-anchor="middle" fill="white" font-size="14" font-weight="600">Spec Level (Contract Verification)</text>
|
|
54
|
+
|
|
55
|
+
<rect x="550" y="100" width="530" height="35" rx="6" fill="#ef4444"/>
|
|
56
|
+
<text x="815" y="123" text-anchor="middle" fill="white" font-size="14" font-weight="600">Implementation Level (Code Verification)</text>
|
|
57
|
+
|
|
58
|
+
<!-- Lane backgrounds -->
|
|
59
|
+
<rect x="20" y="135" width="530" height="770" rx="0 0 8 8" fill="url(#specLaneGrad)" stroke="#93c5fd" stroke-width="1"/>
|
|
60
|
+
<rect x="550" y="135" width="530" height="770" rx="0 0 8 8" fill="url(#implLaneGrad)" stroke="#fca5a5" stroke-width="1"/>
|
|
61
|
+
|
|
62
|
+
<!-- ==================== TASK: Human writes OpenAPI spec ==================== -->
|
|
63
|
+
<rect x="40" y="150" width="490" height="45" rx="22" fill="url(#taskGrad)" filter="url(#shadowSmall)"/>
|
|
64
|
+
<text x="285" y="178" text-anchor="middle" fill="#0e7490" font-size="12" font-weight="600">👤 Human writes OpenAPI spec</text>
|
|
65
|
+
|
|
66
|
+
<!-- Arrow down -->
|
|
67
|
+
<line x1="285" y1="195" x2="285" y2="215" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
68
|
+
|
|
69
|
+
<!-- ==================== GATE 1: Change Allowlist ==================== -->
|
|
70
|
+
<rect x="40" y="225" width="490" height="70" rx="6" fill="white" stroke="#3b82f6" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
71
|
+
<rect x="570" y="225" width="490" height="70" rx="6" fill="white" stroke="#ef4444" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
72
|
+
|
|
73
|
+
<text x="285" y="252" text-anchor="middle" fill="#1e40af" font-size="13" font-weight="600">Change Allowlist</text>
|
|
74
|
+
<rect x="60" y="262" width="200" height="24" rx="4" fill="#dbeafe" stroke="#3b82f6" stroke-width="1"/>
|
|
75
|
+
<text x="160" y="278" text-anchor="middle" fill="#1e40af" font-size="10">packages/** edit blocked</text>
|
|
76
|
+
<rect x="270" y="262" width="200" height="24" rx="4" fill="#dbeafe" stroke="#3b82f6" stroke-width="1"/>
|
|
77
|
+
<text x="370" y="278" text-anchor="middle" fill="#1e40af" font-size="10">*.generated.* edit blocked</text>
|
|
78
|
+
|
|
79
|
+
<text x="815" y="252" text-anchor="middle" fill="#991b1b" font-size="13" font-weight="600">Protected Paths</text>
|
|
80
|
+
<rect x="590" y="262" width="220" height="24" rx="4" fill="#fee2e2" stroke="#ef4444" stroke-width="1"/>
|
|
81
|
+
<text x="700" y="278" text-anchor="middle" fill="#991b1b" font-size="10">server/src/_shared/overlays/**</text>
|
|
82
|
+
<rect x="820" y="262" width="220" height="24" rx="4" fill="#fef3c7" stroke="#f59e0b" stroke-width="1"/>
|
|
83
|
+
<text x="930" y="278" text-anchor="middle" fill="#92400e" font-size="10">.github/** (approval req)</text>
|
|
84
|
+
|
|
85
|
+
<!-- Gate 1 badge (after boxes for z-index) -->
|
|
86
|
+
<rect x="515" y="246" width="70" height="28" rx="14" fill="#1e293b"/>
|
|
87
|
+
<text x="550" y="265" text-anchor="middle" fill="white" font-size="14" font-weight="bold">Gate 1</text>
|
|
88
|
+
|
|
89
|
+
<!-- Arrow down -->
|
|
90
|
+
<line x1="550" y1="295" x2="550" y2="315" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
91
|
+
|
|
92
|
+
<!-- ==================== GATE 2: OpenAPI Spec Validation ==================== -->
|
|
93
|
+
<rect x="40" y="325" width="490" height="70" rx="6" fill="white" stroke="#3b82f6" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
94
|
+
<rect x="570" y="325" width="490" height="70" rx="6" fill="#f8fafc" stroke="#d1d5db" stroke-width="1" stroke-dasharray="4"/>
|
|
95
|
+
|
|
96
|
+
<text x="285" y="352" text-anchor="middle" fill="#1e40af" font-size="13" font-weight="600">OpenAPI Spec Validation</text>
|
|
97
|
+
|
|
98
|
+
<rect x="100" y="362" width="200" height="24" rx="4" fill="#dc2626"/>
|
|
99
|
+
<text x="200" y="378" text-anchor="middle" fill="white" font-size="9" font-weight="600">Spec Lint (Spectral)</text>
|
|
100
|
+
|
|
101
|
+
<rect x="320" y="362" width="180" height="24" rx="4" fill="#f59e0b"/>
|
|
102
|
+
<text x="410" y="378" text-anchor="middle" fill="white" font-size="9" font-weight="600">Breaking Changes (oasdiff)</text>
|
|
103
|
+
|
|
104
|
+
<text x="815" y="365" text-anchor="middle" fill="#9ca3af" font-size="11" font-style="italic">No implementation checks at this gate</text>
|
|
105
|
+
|
|
106
|
+
<!-- Gate 2 badge (after boxes for z-index) -->
|
|
107
|
+
<rect x="515" y="346" width="70" height="28" rx="14" fill="#1e293b"/>
|
|
108
|
+
<text x="550" y="365" text-anchor="middle" fill="white" font-size="14" font-weight="bold">Gate 2</text>
|
|
109
|
+
|
|
110
|
+
<!-- Arrow down -->
|
|
111
|
+
<line x1="550" y1="395" x2="550" y2="415" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
112
|
+
|
|
113
|
+
<!-- ==================== GENERATOR ==================== -->
|
|
114
|
+
<rect x="200" y="425" width="700" height="50" rx="25" fill="url(#generatorGrad)" filter="url(#shadow)"/>
|
|
115
|
+
<text x="550" y="455" text-anchor="middle" fill="white" font-size="14" font-weight="600">micro-contracts generate: spec/** → packages/**</text>
|
|
116
|
+
|
|
117
|
+
<!-- Arrow down -->
|
|
118
|
+
<line x1="550" y1="475" x2="550" y2="495" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
119
|
+
|
|
120
|
+
<!-- ==================== GATE 3: Artifact Integrity ==================== -->
|
|
121
|
+
<rect x="40" y="505" width="490" height="115" rx="6" fill="white" stroke="#3b82f6" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
122
|
+
<rect x="570" y="505" width="490" height="115" rx="6" fill="#f8fafc" stroke="#d1d5db" stroke-width="1" stroke-dasharray="4"/>
|
|
123
|
+
|
|
124
|
+
<text x="285" y="532" text-anchor="middle" fill="#1e40af" font-size="13" font-weight="600">Generated Artifact Integrity</text>
|
|
125
|
+
|
|
126
|
+
<rect x="60" y="542" width="140" height="24" rx="4" fill="#dc2626"/>
|
|
127
|
+
<text x="130" y="558" text-anchor="middle" fill="white" font-size="9" font-weight="600">Drift Check (git diff)</text>
|
|
128
|
+
|
|
129
|
+
<rect x="210" y="542" width="140" height="24" rx="4" fill="#dc2626"/>
|
|
130
|
+
<text x="280" y="558" text-anchor="middle" fill="white" font-size="9" font-weight="600">Manifest Verification</text>
|
|
131
|
+
|
|
132
|
+
<rect x="360" y="542" width="140" height="24" rx="4" fill="#dc2626"/>
|
|
133
|
+
<text x="430" y="558" text-anchor="middle" fill="white" font-size="9" font-weight="600">Package Typecheck (tsc)</text>
|
|
134
|
+
|
|
135
|
+
<rect x="60" y="577" width="180" height="24" rx="4" fill="#dc2626"/>
|
|
136
|
+
<text x="150" y="593" text-anchor="middle" fill="white" font-size="9" font-weight="600">Project Rules (Spectral)</text>
|
|
137
|
+
|
|
138
|
+
<rect x="250" y="577" width="250" height="24" rx="4" fill="#dc2626"/>
|
|
139
|
+
<text x="375" y="593" text-anchor="middle" fill="white" font-size="9" font-weight="600">Published Breaking Changes (oasdiff)</text>
|
|
140
|
+
|
|
141
|
+
<text x="815" y="565" text-anchor="middle" fill="#9ca3af" font-size="11" font-style="italic">No implementation checks at this gate</text>
|
|
142
|
+
|
|
143
|
+
<!-- Gate 3 badge (after boxes for z-index) -->
|
|
144
|
+
<rect x="515" y="549" width="70" height="28" rx="14" fill="#1e293b"/>
|
|
145
|
+
<text x="550" y="568" text-anchor="middle" fill="white" font-size="14" font-weight="bold">Gate 3</text>
|
|
146
|
+
|
|
147
|
+
<!-- Arrow down -->
|
|
148
|
+
<line x1="550" y1="620" x2="550" y2="640" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
149
|
+
|
|
150
|
+
<!-- ==================== TASK: AI/Human implements code ==================== -->
|
|
151
|
+
<rect x="570" y="650" width="490" height="45" rx="22" fill="url(#taskGrad)" filter="url(#shadowSmall)"/>
|
|
152
|
+
<text x="815" y="678" text-anchor="middle" fill="#0e7490" font-size="12" font-weight="600">🤖👤 AI or Human implements code</text>
|
|
153
|
+
|
|
154
|
+
<!-- Arrow down from task -->
|
|
155
|
+
<line x1="815" y1="695" x2="815" y2="715" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
156
|
+
|
|
157
|
+
<!-- ==================== GATE 4: Code Quality ==================== -->
|
|
158
|
+
<rect x="40" y="735" width="490" height="70" rx="6" fill="#f8fafc" stroke="#d1d5db" stroke-width="1" stroke-dasharray="4"/>
|
|
159
|
+
<rect x="570" y="735" width="490" height="70" rx="6" fill="white" stroke="#ef4444" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
160
|
+
|
|
161
|
+
<text x="285" y="775" text-anchor="middle" fill="#9ca3af" font-size="11" font-style="italic">No spec checks at this gate</text>
|
|
162
|
+
|
|
163
|
+
<text x="815" y="762" text-anchor="middle" fill="#991b1b" font-size="13" font-weight="600">Code Quality</text>
|
|
164
|
+
|
|
165
|
+
<rect x="590" y="772" width="110" height="24" rx="4" fill="#dc2626"/>
|
|
166
|
+
<text x="645" y="788" text-anchor="middle" fill="white" font-size="9" font-weight="600">TypeScript Lint (ESLint)</text>
|
|
167
|
+
|
|
168
|
+
<rect x="710" y="772" width="100" height="24" rx="4" fill="#dc2626"/>
|
|
169
|
+
<text x="760" y="788" text-anchor="middle" fill="white" font-size="9" font-weight="600">Type Check (tsc)</text>
|
|
170
|
+
|
|
171
|
+
<rect x="820" y="772" width="80" height="24" rx="4" fill="#dc2626"/>
|
|
172
|
+
<text x="860" y="788" text-anchor="middle" fill="white" font-size="9" font-weight="600">Unit Tests</text>
|
|
173
|
+
|
|
174
|
+
<rect x="910" y="772" width="120" height="24" rx="4" fill="#f59e0b"/>
|
|
175
|
+
<text x="970" y="788" text-anchor="middle" fill="white" font-size="9" font-weight="600">Format Check (Prettier)</text>
|
|
176
|
+
|
|
177
|
+
<!-- Gate 4 badge (after boxes for z-index) -->
|
|
178
|
+
<rect x="515" y="756" width="70" height="28" rx="14" fill="#1e293b"/>
|
|
179
|
+
<text x="550" y="775" text-anchor="middle" fill="white" font-size="14" font-weight="bold">Gate 4</text>
|
|
180
|
+
|
|
181
|
+
<!-- Arrow down -->
|
|
182
|
+
<line x1="550" y1="805" x2="550" y2="825" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
183
|
+
|
|
184
|
+
<!-- ==================== GATE 5: Doc Consistency & Static Analysis ==================== -->
|
|
185
|
+
<rect x="40" y="835" width="490" height="70" rx="6" fill="white" stroke="#3b82f6" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
186
|
+
<rect x="570" y="835" width="490" height="70" rx="6" fill="white" stroke="#ef4444" stroke-width="2" filter="url(#shadowSmall)"/>
|
|
187
|
+
|
|
188
|
+
<text x="285" y="862" text-anchor="middle" fill="#1e40af" font-size="13" font-weight="600">Doc Consistency</text>
|
|
189
|
+
|
|
190
|
+
<rect x="60" y="872" width="200" height="24" rx="4" fill="#dc2626"/>
|
|
191
|
+
<text x="160" y="888" text-anchor="middle" fill="white" font-size="9" font-weight="600">Markdown Sync (embedoc)</text>
|
|
192
|
+
|
|
193
|
+
<rect x="270" y="872" width="120" height="24" rx="4" fill="#dc2626"/>
|
|
194
|
+
<text x="330" y="888" text-anchor="middle" fill="white" font-size="9" font-weight="600">Links Validity</text>
|
|
195
|
+
|
|
196
|
+
<text x="815" y="862" text-anchor="middle" fill="#991b1b" font-size="13" font-weight="600">Architectural Integrity</text>
|
|
197
|
+
|
|
198
|
+
<rect x="690" y="872" width="250" height="24" rx="4" fill="#dc2626"/>
|
|
199
|
+
<text x="815" y="888" text-anchor="middle" fill="white" font-size="9" font-weight="600">Static Analysis (CodeQL)</text>
|
|
200
|
+
|
|
201
|
+
<!-- Gate 5 badge (after boxes for z-index) -->
|
|
202
|
+
<rect x="515" y="856" width="70" height="28" rx="14" fill="#1e293b"/>
|
|
203
|
+
<text x="550" y="875" text-anchor="middle" fill="white" font-size="14" font-weight="bold">Gate 5</text>
|
|
204
|
+
|
|
205
|
+
<!-- Arrow down -->
|
|
206
|
+
<line x1="550" y1="905" x2="550" y2="925" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
207
|
+
|
|
208
|
+
<!-- ==================== TASK: Human reviews docs/code ==================== -->
|
|
209
|
+
<rect x="40" y="935" width="1020" height="45" rx="22" fill="url(#taskGrad)" filter="url(#shadow)"/>
|
|
210
|
+
<text x="550" y="963" text-anchor="middle" fill="#0e7490" font-size="12" font-weight="600">👤 Human reviews docs & code</text>
|
|
211
|
+
|
|
212
|
+
<!-- Arrow down -->
|
|
213
|
+
<line x1="550" y1="980" x2="550" y2="1000" stroke="#64748b" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
214
|
+
|
|
215
|
+
<!-- ==================== MERGE GATE ==================== -->
|
|
216
|
+
<rect x="40" y="1010" width="1020" height="55" rx="6" fill="url(#mergeGrad)" filter="url(#shadow)"/>
|
|
217
|
+
<text x="550" y="1035" text-anchor="middle" fill="white" font-size="15" font-weight="600">Merge Gate (All Required Status Checks Must Pass)</text>
|
|
218
|
+
<text x="550" y="1055" text-anchor="middle" fill="#bbf7d0" font-size="11">Gate 1 + Gate 2 + Gate 3 + Gate 4 + Gate 5 → PR can merge to main</text>
|
|
219
|
+
|
|
220
|
+
<!-- ==================== LEGEND ==================== -->
|
|
221
|
+
<rect x="40" y="1080" width="1020" height="95" rx="8" fill="white" stroke="#e2e8f0" stroke-width="1"/>
|
|
222
|
+
<text x="550" y="1105" text-anchor="middle" fill="#1e293b" font-size="14" font-weight="600">Legend</text>
|
|
223
|
+
|
|
224
|
+
<!-- Severity legend -->
|
|
225
|
+
<rect x="60" y="1120" width="80" height="22" rx="4" fill="#dc2626"/>
|
|
226
|
+
<text x="100" y="1135" text-anchor="middle" fill="white" font-size="10" font-weight="600">ERROR</text>
|
|
227
|
+
<text x="155" y="1135" fill="#475569" font-size="10">Blocks merge</text>
|
|
228
|
+
|
|
229
|
+
<rect x="230" y="1120" width="80" height="22" rx="4" fill="#f59e0b"/>
|
|
230
|
+
<text x="270" y="1135" text-anchor="middle" fill="white" font-size="10" font-weight="600">WARNING</text>
|
|
231
|
+
<text x="325" y="1135" fill="#475569" font-size="10">Advisory</text>
|
|
232
|
+
|
|
233
|
+
<rect x="400" y="1120" width="80" height="22" rx="4" fill="#f8fafc" stroke="#d1d5db" stroke-width="1" stroke-dasharray="4"/>
|
|
234
|
+
<text x="440" y="1135" text-anchor="middle" fill="#9ca3af" font-size="10">Empty</text>
|
|
235
|
+
<text x="495" y="1135" fill="#475569" font-size="10">No checks</text>
|
|
236
|
+
|
|
237
|
+
<rect x="570" y="1120" width="80" height="22" rx="25" fill="url(#generatorGrad)"/>
|
|
238
|
+
<text x="610" y="1135" text-anchor="middle" fill="white" font-size="10" font-weight="600">Process</text>
|
|
239
|
+
<text x="665" y="1135" fill="#475569" font-size="10">Transformation</text>
|
|
240
|
+
|
|
241
|
+
<rect x="750" y="1120" width="80" height="22" rx="11" fill="url(#taskGrad)"/>
|
|
242
|
+
<text x="790" y="1135" text-anchor="middle" fill="#0e7490" font-size="10" font-weight="600">Task</text>
|
|
243
|
+
<text x="845" y="1135" fill="#475569" font-size="10">Human/AI work</text>
|
|
244
|
+
|
|
245
|
+
<!-- Lane description -->
|
|
246
|
+
<rect x="60" y="1150" width="16" height="12" rx="2" fill="#3b82f6"/>
|
|
247
|
+
<text x="85" y="1160" fill="#475569" font-size="10">Spec Level: OpenAPI contracts, generated artifacts, documentation</text>
|
|
248
|
+
|
|
249
|
+
<rect x="550" y="1150" width="16" height="12" rx="2" fill="#ef4444"/>
|
|
250
|
+
<text x="575" y="1160" fill="#475569" font-size="10">Impl Level: TypeScript code, tests, static analysis (CodeQL)</text>
|
|
251
|
+
|
|
252
|
+
</svg>
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# OpenAPI Overlays (Deep Dive)
|
|
2
|
+
|
|
3
|
+
> **Navigation**: [← Back to README](../README.md) | [Examples](../examples/)
|
|
4
|
+
|
|
5
|
+
**Overlays** use the standard [OpenAPI Overlay Specification 1.0.0](https://www.openapis.org/blog/2024/10/22/announcing-overlay-specification) to define how `x-*` extensions transform the OpenAPI spec. This enables cross-cutting concerns (middleware, auth, rate limiting) without repeating definitions.
|
|
6
|
+
|
|
7
|
+
**Why Overlays?**
|
|
8
|
+
|
|
9
|
+
- **Standard format**: Uses official OpenAPI Overlay Specification — [learn more](https://learn.openapis.org/overlay/)
|
|
10
|
+
- **Transparent transformation**: Clear what gets injected where
|
|
11
|
+
- **Composable**: Chain multiple overlays (shared + module-specific)
|
|
12
|
+
- **Tool compatibility**: Works with other Overlay-aware tools ([Redocly CLI](https://redocly.com/docs/cli/), etc.)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## How Overlays Work
|
|
17
|
+
|
|
18
|
+
1. **Mark operations** with `x-middleware` (or custom extensions) in OpenAPI
|
|
19
|
+
2. **Define overlay** that adds params/responses when extension is present
|
|
20
|
+
3. **Generator applies overlays** and produces `openapi.generated.yaml`
|
|
21
|
+
4. **Generate code** from the generated spec (types, routes, overlay interfaces)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Overlay Scope
|
|
26
|
+
|
|
27
|
+
| Scope | Overlay Location | Use Case |
|
|
28
|
+
|-------|------------------|----------|
|
|
29
|
+
| **Cross-module** | `spec/_shared/overlays/` | Auth, rate limiting, tenant isolation |
|
|
30
|
+
| **Intra-module** | `spec/{module}/overlays/` | Module-specific validation (e.g., payment validation in billing) |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Writing Overlay Definitions
|
|
35
|
+
|
|
36
|
+
### 1. Mark Operations in OpenAPI
|
|
37
|
+
|
|
38
|
+
Add extension markers to operations that need cross-cutting behavior:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# spec/core/openapi/core.yaml
|
|
42
|
+
paths:
|
|
43
|
+
/api/tenant/data:
|
|
44
|
+
get:
|
|
45
|
+
operationId: getTenantData
|
|
46
|
+
x-micro-contracts-domain: Tenant
|
|
47
|
+
x-micro-contracts-method: getTenantData
|
|
48
|
+
x-middleware: # ← Extension marker
|
|
49
|
+
- tenantIsolation
|
|
50
|
+
- requireAuth
|
|
51
|
+
- rateLimit
|
|
52
|
+
responses:
|
|
53
|
+
'200':
|
|
54
|
+
description: Success
|
|
55
|
+
content:
|
|
56
|
+
application/json:
|
|
57
|
+
schema:
|
|
58
|
+
$ref: '#/components/schemas/TenantData'
|
|
59
|
+
# Note: 400, 401, 429 are NOT defined here
|
|
60
|
+
# They will be injected by the overlay
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Define Overlay
|
|
64
|
+
|
|
65
|
+
Create an overlay file that injects parameters and responses based on markers:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# spec/_shared/overlays/middleware.overlay.yaml
|
|
69
|
+
overlay: 1.0.0
|
|
70
|
+
info:
|
|
71
|
+
title: Middleware Extension Overlay
|
|
72
|
+
version: 1.0.0
|
|
73
|
+
|
|
74
|
+
actions:
|
|
75
|
+
# requireAuth: Inject Authorization header + 401 response
|
|
76
|
+
- target: "$.paths[*][*][?(@.x-middleware contains 'requireAuth')]"
|
|
77
|
+
x-micro-contracts-overlay-name: requireAuth # ← Name for type generation
|
|
78
|
+
update:
|
|
79
|
+
parameters:
|
|
80
|
+
- name: Authorization
|
|
81
|
+
in: header
|
|
82
|
+
schema: { type: string }
|
|
83
|
+
description: Bearer token for authentication
|
|
84
|
+
responses:
|
|
85
|
+
'401':
|
|
86
|
+
$ref: '../openapi/problem-details.yaml#/components/responses/Unauthorized'
|
|
87
|
+
|
|
88
|
+
# tenantIsolation: Inject X-Tenant-Id header + 400 response
|
|
89
|
+
- target: "$.paths[*][*][?(@.x-middleware contains 'tenantIsolation')]"
|
|
90
|
+
x-micro-contracts-overlay-name: tenantIsolation
|
|
91
|
+
update:
|
|
92
|
+
parameters:
|
|
93
|
+
- name: X-Tenant-Id
|
|
94
|
+
in: header
|
|
95
|
+
required: true
|
|
96
|
+
schema:
|
|
97
|
+
type: string
|
|
98
|
+
format: uuid
|
|
99
|
+
description: Tenant identifier for multi-tenant isolation
|
|
100
|
+
responses:
|
|
101
|
+
'400':
|
|
102
|
+
description: Missing or invalid X-Tenant-Id header
|
|
103
|
+
content:
|
|
104
|
+
application/problem+json:
|
|
105
|
+
schema:
|
|
106
|
+
$ref: '../openapi/problem-details.yaml#/components/schemas/ProblemDetails'
|
|
107
|
+
|
|
108
|
+
# rateLimit: Inject 429 response with rate limit headers
|
|
109
|
+
- target: "$.paths[*][*][?(@.x-middleware contains 'rateLimit')]"
|
|
110
|
+
x-micro-contracts-overlay-name: rateLimit
|
|
111
|
+
update:
|
|
112
|
+
responses:
|
|
113
|
+
'429':
|
|
114
|
+
description: Too Many Requests
|
|
115
|
+
headers:
|
|
116
|
+
X-RateLimit-Limit:
|
|
117
|
+
schema: { type: integer }
|
|
118
|
+
description: Rate limit ceiling for this endpoint
|
|
119
|
+
Retry-After:
|
|
120
|
+
schema: { type: integer }
|
|
121
|
+
description: Seconds until rate limit resets
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> **📦 Full example**: See [`examples/spec/_shared/overlays/middleware.overlay.yaml`](../examples/spec/_shared/overlays/middleware.overlay.yaml)
|
|
125
|
+
|
|
126
|
+
### $ref Paths in Overlays
|
|
127
|
+
|
|
128
|
+
Specify `$ref` paths **relative to the overlay file's location**. micro-contracts automatically rebases these paths so that the generated `openapi.generated.yaml` can resolve them correctly.
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
# spec/_shared/overlays/middleware.overlay.yaml
|
|
132
|
+
$ref: '../openapi/problem-details.yaml#/components/responses/Unauthorized'
|
|
133
|
+
# ↑ Resolves to spec/_shared/openapi/problem-details.yaml
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Rebase example:**
|
|
137
|
+
|
|
138
|
+
| File | $ref path |
|
|
139
|
+
|------|-----------|
|
|
140
|
+
| Overlay (source) | `../openapi/problem-details.yaml#/...` |
|
|
141
|
+
| Generated spec (`packages/contract/core/docs/openapi.generated.yaml`) | `../../../../spec/_shared/openapi/problem-details.yaml#/...` |
|
|
142
|
+
|
|
143
|
+
The generated path is relative to `openapi.generated.yaml`'s location, ensuring the reference resolves correctly.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Implementing Overlay Handlers
|
|
148
|
+
|
|
149
|
+
Overlay handlers **do not directly access HTTP request/response objects** (e.g., Fastify's `req`/`reply`). Parameters are extracted by the generated adapter and passed as typed input. However, they are aware of HTTP concepts (header names, status codes) unlike Domain implementations which are purely business logic:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// server/src/core/overlays/requireAuth.ts
|
|
153
|
+
import type {
|
|
154
|
+
RequireAuthOverlayInput,
|
|
155
|
+
OverlayResult
|
|
156
|
+
} from '@project/contract/core/overlays/index.js';
|
|
157
|
+
|
|
158
|
+
export async function requireAuth(input: RequireAuthOverlayInput): Promise<OverlayResult> {
|
|
159
|
+
const authHeader = input['Authorization'];
|
|
160
|
+
|
|
161
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: { status: 401, message: 'Missing or invalid authorization header' },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// In real app: validate JWT token and extract user info
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
context: { userId: 'user-123', role: 'user' },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> **📦 Full examples**: See [`examples/server/src/core/overlays/`](../examples/server/src/core/overlays/)
|
|
177
|
+
|
|
178
|
+
### Layer Responsibilities
|
|
179
|
+
|
|
180
|
+
| Layer | Responsibility | Touches HTTP objects | Knows HTTP concepts |
|
|
181
|
+
|-------|----------------|---------------------|---------------------|
|
|
182
|
+
| **Generated routes** | Extract params, wire handlers | ✅ Yes | ✅ Yes |
|
|
183
|
+
| **Overlay impl** | Handle cross-cutting concern | ❌ No | ✅ Yes (headers, status codes) |
|
|
184
|
+
| **Domain impl** | Business logic only | ❌ No | ❌ No |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Configuration
|
|
189
|
+
|
|
190
|
+
Configure overlays in `micro-contracts.config.yaml`:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
# micro-contracts.config.yaml
|
|
194
|
+
defaults:
|
|
195
|
+
overlays:
|
|
196
|
+
# Shared overlays applied to all modules (in order)
|
|
197
|
+
shared:
|
|
198
|
+
- spec/_shared/overlays/middleware.overlay.yaml
|
|
199
|
+
collision: error # error | warn | last-wins (default: error)
|
|
200
|
+
|
|
201
|
+
modules:
|
|
202
|
+
core:
|
|
203
|
+
openapi: openapi/core.yaml
|
|
204
|
+
overlays: # Module-specific overlays (applied after shared)
|
|
205
|
+
- overlays/cache.overlay.yaml
|
|
206
|
+
|
|
207
|
+
billing:
|
|
208
|
+
openapi: openapi/billing.yaml
|
|
209
|
+
overlays:
|
|
210
|
+
- overlays/payment.overlay.yaml
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Overlay Policies
|
|
216
|
+
|
|
217
|
+
### 1. Collision Policy
|
|
218
|
+
|
|
219
|
+
When multiple overlays inject the same key (e.g., `401` response):
|
|
220
|
+
|
|
221
|
+
| Scenario | Policy | Rationale |
|
|
222
|
+
|----------|--------|-----------|
|
|
223
|
+
| Same key, **identical content** | ✅ Allow | No ambiguity |
|
|
224
|
+
| Same key, **different content** | ❌ Error | Ambiguous intent |
|
|
225
|
+
|
|
226
|
+
Configure in `defaults.overlays.collision`: `error` (default) | `warn` | `last-wins`
|
|
227
|
+
|
|
228
|
+
### 2. Application Order
|
|
229
|
+
|
|
230
|
+
Order is **deterministic**:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
1. defaults.overlays.shared (in array order)
|
|
234
|
+
2. modules.{name}.overlays (in array order)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Generation logs what was injected:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npx micro-contracts generate -m core
|
|
241
|
+
|
|
242
|
+
[overlay] Applying spec/_shared/overlays/middleware.overlay.yaml
|
|
243
|
+
→ /api/tenant/data GET: +X-Tenant-Id, +400, +401, +429
|
|
244
|
+
[output] packages/contract/core/docs/openapi.generated.yaml
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 3. JSONPath Patterns
|
|
248
|
+
|
|
249
|
+
micro-contracts uses a restricted JSONPath dialect for reliable parsing:
|
|
250
|
+
|
|
251
|
+
| Pattern | Meaning |
|
|
252
|
+
|---------|---------|
|
|
253
|
+
| `$.paths[*][*]` | All operations |
|
|
254
|
+
| `[?(@.x-ext)]` | Has extension |
|
|
255
|
+
| `[?(@.x-ext contains 'value')]` | Array contains value |
|
|
256
|
+
|
|
257
|
+
**Use `contains` for array membership:**
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
# ✅ Correct - portable
|
|
261
|
+
- target: "$.paths[*][*][?(@.x-middleware contains 'requireAuth')]"
|
|
262
|
+
|
|
263
|
+
# ❌ Avoid - implementation-dependent
|
|
264
|
+
- target: "$.paths[*][*][?(@.x-middleware[*] == 'requireAuth')]"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 4. Marker vs Injection Responsibility
|
|
268
|
+
|
|
269
|
+
| Layer | Responsibility |
|
|
270
|
+
|-------|----------------|
|
|
271
|
+
| **OpenAPI (source)** | Business responses (200, 201, 204) |
|
|
272
|
+
| **OpenAPI (source)** | Extension markers (`x-middleware: [requireAuth]`) |
|
|
273
|
+
| **Overlay** | Cross-cutting responses (400, 401, 403, 429) |
|
|
274
|
+
|
|
275
|
+
**Anti-pattern**: Don't manually add `401` to operations that have `x-middleware: [requireAuth]`. Let the overlay inject it.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Summary
|
|
280
|
+
|
|
281
|
+
| What | Generated | Notes |
|
|
282
|
+
|------|-----------|-------|
|
|
283
|
+
| Overlay application | ✅ Yes | Transform OpenAPI (inject params/responses) |
|
|
284
|
+
| Overlay interfaces | ✅ Yes | HTTP-agnostic (`OverlayRegistry`, `*OverlayInput`) |
|
|
285
|
+
| Contract types | ✅ Yes | Types from OpenAPI schemas |
|
|
286
|
+
| Route wiring | ✅ Via template | Template decides how to wire |
|
|
287
|
+
| Overlay impl | ❌ Human | Implement generated interface |
|
|
288
|
+
| Domain impl | ❌ Human | Business logic |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Related Documentation
|
|
293
|
+
|
|
294
|
+
| Document | Description |
|
|
295
|
+
|----------|-------------|
|
|
296
|
+
| **[Examples](../examples/)** | Complete working project with multiple modules and overlays |
|
|
297
|
+
| **[Guardrails](development-guardrails.md)** | CI integration, security checks |
|
|
298
|
+
| **[README](../README.md)** | Core concepts, configuration |
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "micro-contracts",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Contract-first OpenAPI toolchain that keeps TypeScript UI and microservices aligned via code generation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"micro-contracts": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"docs",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsx watch src/cli.ts",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"generate": "tsx src/cli.ts generate",
|
|
23
|
+
"docs:sync": "npx embedoc build",
|
|
24
|
+
"docs:check": "npx embedoc build && git diff --exit-code README.md docs/",
|
|
25
|
+
"prepublishOnly": "npm run build && npm test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"openapi",
|
|
29
|
+
"contract-first",
|
|
30
|
+
"microservices",
|
|
31
|
+
"code-generation",
|
|
32
|
+
"fastify",
|
|
33
|
+
"typescript",
|
|
34
|
+
"api",
|
|
35
|
+
"guardrails",
|
|
36
|
+
"ai-development"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/foo-ogawa/micro-contracts.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/foo-ogawa/micro-contracts#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/foo-ogawa/micro-contracts/issues"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"commander": "^12.1.0",
|
|
50
|
+
"glob": "^10.3.10",
|
|
51
|
+
"handlebars": "^4.7.8",
|
|
52
|
+
"js-yaml": "^4.1.0",
|
|
53
|
+
"yaml": "^2.8.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/js-yaml": "^4.0.9",
|
|
57
|
+
"@types/node": "^20.11.0",
|
|
58
|
+
"embedoc": "^0.9.0",
|
|
59
|
+
"tsx": "^4.7.0",
|
|
60
|
+
"typescript": "^5.3.3",
|
|
61
|
+
"vitest": "^1.2.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|