@claudetools/tools 0.4.0 → 0.5.1

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.
@@ -0,0 +1,706 @@
1
+ # CodeDNA Modern Stack - Complete Implementation Guide
2
+
3
+ ## Overview
4
+
5
+ CodeDNA now supports complete modern full-stack development with:
6
+ - **Tanstack Start + Drizzle** - Full-stack framework with type-safe ORM
7
+ - **React 19 + ShadcnUI** - Modern components with latest hooks
8
+ - **Tanstack Router + Query** - Type-safe routing and data fetching
9
+ - **Resend + React Email** - Transactional email system
10
+ - **Cloudflare** - Edge deployment (Pages, Workers, D1, KV, R2)
11
+ - **Code Quality** - ESLint, Prettier, TypeScript strict mode
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { EntityParser } from '@claudetools/tools/codedna';
17
+ import { TanstackStartDrizzleGenerator } from '@claudetools/tools/codedna/generators/tanstack-start-drizzle';
18
+ import { React19ShadcnGenerator } from '@claudetools/tools/codedna/generators/react19-shadcn';
19
+
20
+ // Parse entity
21
+ const parser = new EntityParser();
22
+ const entity = parser.parse(`
23
+ Product(
24
+ name:string:required:length(3,200),
25
+ sku:string:unique:required:pattern(/^[A-Z]{3}-\\d{6}$/),
26
+ price:decimal:required:min(0),
27
+ stock:integer:min(0):default(0),
28
+ category:refOne(Category),
29
+ tags:refMany(Tag),
30
+ description:string:nullable,
31
+ images:array(string),
32
+ metadata:json:nullable,
33
+ status:enum(active,inactive,discontinued):default(active)
34
+ )
35
+ `);
36
+
37
+ // Generate full-stack app
38
+ const fullStackGen = new TanstackStartDrizzleGenerator({
39
+ database: 'postgresql',
40
+ deployment: 'cloudflare',
41
+ ssr: true,
42
+ });
43
+
44
+ const componentsGen = new React19ShadcnGenerator({
45
+ componentPattern: 'compound',
46
+ includeTable: true,
47
+ includeModal: true,
48
+ });
49
+
50
+ const fullStackFiles = await fullStackGen.generate(entity);
51
+ const componentFiles = await componentsGen.generate(entity);
52
+
53
+ // Result: 20+ production-ready files
54
+ ```
55
+
56
+ ## Generated Architecture
57
+
58
+ ### File Structure
59
+ ```
60
+ my-app/
61
+ ├── src/
62
+ │ ├── db/
63
+ │ │ ├── schema/
64
+ │ │ │ └── product.ts # Drizzle schema
65
+ │ │ └── client.ts # DB connection
66
+ │ ├── queries/
67
+ │ │ └── product.ts # Type-safe queries
68
+ │ ├── mutations/
69
+ │ │ └── product.ts # Server actions
70
+ │ ├── routes/
71
+ │ │ └── product/
72
+ │ │ ├── index.tsx # List (SSR)
73
+ │ │ ├── $id.tsx # Detail (SSR)
74
+ │ │ ├── create.tsx # Create form
75
+ │ │ └── $id/
76
+ │ │ └── edit.tsx # Edit form
77
+ │ ├── components/
78
+ │ │ ├── ProductForm.tsx # React 19 form
79
+ │ │ ├── ProductTable.tsx # Sortable table
80
+ │ │ ├── ProductModal.tsx # Dialog
81
+ │ │ └── ProductCard.tsx # Card
82
+ │ └── types/
83
+ │ └── product.ts # TypeScript types
84
+ ├── drizzle/
85
+ │ └── migrations/ # SQL migrations
86
+ ├── wrangler.toml # Cloudflare config
87
+ ├── drizzle.config.ts # Drizzle config
88
+ └── package.json # Dependencies
89
+ ```
90
+
91
+ ## 1. Tanstack Router + Query Integration
92
+
93
+ ### Route with Data Loading
94
+ ```typescript
95
+ // src/routes/product/index.tsx
96
+ import { createFileRoute } from '@tanstack/react-router';
97
+ import { useQuery } from '@tanstack/react-query';
98
+ import { getAllProducts } from '@/queries/product';
99
+
100
+ export const Route = createFileRoute('/product/')({
101
+ // Server-side loader
102
+ loader: async () => {
103
+ const products = await getAllProducts();
104
+ return { products };
105
+ },
106
+ component: ProductsPage,
107
+ });
108
+
109
+ function ProductsPage() {
110
+ const { products } = Route.useLoaderData();
111
+
112
+ // Client-side query (optional, for real-time updates)
113
+ const { data, refetch } = useQuery({
114
+ queryKey: ['products'],
115
+ queryFn: getAllProducts,
116
+ initialData: products,
117
+ staleTime: 5000,
118
+ });
119
+
120
+ return <ProductTable data={data} />;
121
+ }
122
+ ```
123
+
124
+ ### Mutations with Optimistic Updates
125
+ ```typescript
126
+ // src/hooks/useProductMutations.ts
127
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
128
+ import { createProduct, updateProduct, deleteProduct } from '@/mutations/product';
129
+
130
+ export function useProductMutations() {
131
+ const queryClient = useQueryClient();
132
+
133
+ const create = useMutation({
134
+ mutationFn: createProduct,
135
+ onMutate: async (newProduct) => {
136
+ // Cancel outgoing refetches
137
+ await queryClient.cancelQueries({ queryKey: ['products'] });
138
+
139
+ // Snapshot previous value
140
+ const previous = queryClient.getQueryData(['products']);
141
+
142
+ // Optimistically update
143
+ queryClient.setQueryData(['products'], (old: any) => [...old, newProduct]);
144
+
145
+ return { previous };
146
+ },
147
+ onError: (err, newProduct, context) => {
148
+ // Rollback on error
149
+ queryClient.setQueryData(['products'], context?.previous);
150
+ },
151
+ onSettled: () => {
152
+ // Refetch after mutation
153
+ queryClient.invalidateQueries({ queryKey: ['products'] });
154
+ },
155
+ });
156
+
157
+ const update = useMutation({
158
+ mutationFn: ({ id, data }: { id: number; data: any }) => updateProduct(id, data),
159
+ onSuccess: () => {
160
+ queryClient.invalidateQueries({ queryKey: ['products'] });
161
+ },
162
+ });
163
+
164
+ const remove = useMutation({
165
+ mutationFn: deleteProduct,
166
+ onSuccess: () => {
167
+ queryClient.invalidateQueries({ queryKey: ['products'] });
168
+ },
169
+ });
170
+
171
+ return { create, update, remove };
172
+ }
173
+ ```
174
+
175
+ ### Prefetching
176
+ ```typescript
177
+ // Prefetch on hover
178
+ import { useQueryClient } from '@tanstack/react-query';
179
+ import { getProductById } from '@/queries/product';
180
+
181
+ function ProductLink({ id }: { id: number }) {
182
+ const queryClient = useQueryClient();
183
+
184
+ const prefetch = () => {
185
+ queryClient.prefetchQuery({
186
+ queryKey: ['product', id],
187
+ queryFn: () => getProductById(id),
188
+ staleTime: 5000,
189
+ });
190
+ };
191
+
192
+ return (
193
+ <a
194
+ href={`/product/${id}`}
195
+ onMouseEnter={prefetch}
196
+ onFocus={prefetch}
197
+ >
198
+ View Product
199
+ </a>
200
+ );
201
+ }
202
+ ```
203
+
204
+ ## 2. Email System (Resend + React Email)
205
+
206
+ ### Email Templates
207
+ ```typescript
208
+ // src/emails/WelcomeEmail.tsx
209
+ import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
210
+
211
+ interface WelcomeEmailProps {
212
+ name: string;
213
+ loginUrl: string;
214
+ }
215
+
216
+ export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
217
+ return (
218
+ <Html>
219
+ <Head />
220
+ <Body style={{ backgroundColor: '#f6f9fc' }}>
221
+ <Container style={{ padding: '20px' }}>
222
+ <Text style={{ fontSize: '24px', fontWeight: 'bold' }}>
223
+ Welcome, {name}!
224
+ </Text>
225
+ <Text>
226
+ Thanks for signing up. Get started by logging in:
227
+ </Text>
228
+ <Button
229
+ href={loginUrl}
230
+ style={{
231
+ backgroundColor: '#007bff',
232
+ color: '#fff',
233
+ padding: '12px 20px',
234
+ borderRadius: '4px',
235
+ }}
236
+ >
237
+ Log In
238
+ </Button>
239
+ </Container>
240
+ </Body>
241
+ </Html>
242
+ );
243
+ }
244
+ ```
245
+
246
+ ### Resend Integration
247
+ ```typescript
248
+ // src/lib/email.ts
249
+ import { Resend } from 'resend';
250
+ import { WelcomeEmail } from '@/emails/WelcomeEmail';
251
+ import { render } from '@react-email/render';
252
+
253
+ const resend = new Resend(process.env.RESEND_API_KEY);
254
+
255
+ export async function sendWelcomeEmail(to: string, name: string) {
256
+ const html = render(<WelcomeEmail name={name} loginUrl="https://app.example.com" />);
257
+
258
+ await resend.emails.send({
259
+ from: 'noreply@example.com',
260
+ to,
261
+ subject: 'Welcome to Our Platform',
262
+ html,
263
+ });
264
+ }
265
+
266
+ // Development preview
267
+ export async function previewEmail() {
268
+ const html = render(<WelcomeEmail name="John Doe" loginUrl="https://localhost:3000" />);
269
+ return html;
270
+ }
271
+ ```
272
+
273
+ ### Generated Email Templates (3)
274
+ 1. **Welcome Email** - User registration confirmation
275
+ 2. **Password Reset** - Secure reset link with expiry
276
+ 3. **Notification Email** - Generic transactional notifications
277
+
278
+ ## 3. Cloudflare Deployment
279
+
280
+ ### Wrangler Configuration
281
+ ```toml
282
+ # wrangler.toml
283
+ name = "my-app"
284
+ main = "src/index.ts"
285
+ compatibility_date = "2024-12-01"
286
+
287
+ # D1 Database
288
+ [[ d1_databases ]]
289
+ binding = "DB"
290
+ database_name = "production-db"
291
+ database_id = "YOUR_D1_DATABASE_ID"
292
+
293
+ # KV Storage (sessions, cache)
294
+ [[ kv_namespaces ]]
295
+ binding = "KV"
296
+ id = "YOUR_KV_ID"
297
+
298
+ # R2 Storage (file uploads)
299
+ [[ r2_buckets ]]
300
+ binding = "UPLOADS"
301
+ bucket_name = "uploads"
302
+
303
+ # Environment Variables
304
+ [vars]
305
+ ENVIRONMENT = "production"
306
+
307
+ # Secrets (set via: wrangler secret put RESEND_API_KEY)
308
+ # RESEND_API_KEY
309
+ # DATABASE_URL
310
+ ```
311
+
312
+ ### GitHub Actions CI/CD
313
+ ```yaml
314
+ # .github/workflows/deploy.yml
315
+ name: Deploy to Cloudflare
316
+
317
+ on:
318
+ push:
319
+ branches: [main]
320
+
321
+ jobs:
322
+ deploy:
323
+ runs-on: ubuntu-latest
324
+ steps:
325
+ - uses: actions/checkout@v4
326
+
327
+ - uses: actions/setup-node@v4
328
+ with:
329
+ node-version: '20'
330
+
331
+ - name: Install dependencies
332
+ run: npm ci
333
+
334
+ - name: Build
335
+ run: npm run build
336
+
337
+ - name: Run migrations
338
+ run: npx wrangler d1 migrations apply production-db --remote
339
+ env:
340
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
341
+
342
+ - name: Deploy
343
+ run: npx wrangler deploy
344
+ env:
345
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
346
+ ```
347
+
348
+ ### Deployment Scripts
349
+ ```json
350
+ {
351
+ "scripts": {
352
+ "dev": "wrangler dev",
353
+ "build": "vinxi build",
354
+ "deploy": "npm run build && wrangler deploy",
355
+ "deploy:preview": "npm run build && wrangler deploy --env preview",
356
+ "db:migrate": "wrangler d1 migrations apply production-db --remote",
357
+ "db:migrate:local": "wrangler d1 migrations apply production-db --local"
358
+ }
359
+ }
360
+ ```
361
+
362
+ ## 4. Code Quality & Patterns Library
363
+
364
+ ### ESLint Configuration (Strict)
365
+ ```javascript
366
+ // eslint.config.js
367
+ export default {
368
+ extends: [
369
+ 'eslint:recommended',
370
+ 'plugin:@typescript-eslint/strict',
371
+ 'plugin:react/recommended',
372
+ 'plugin:react-hooks/recommended',
373
+ ],
374
+ rules: {
375
+ '@typescript-eslint/no-explicit-any': 'error',
376
+ '@typescript-eslint/no-unused-vars': 'error',
377
+ 'react/prop-types': 'off', // Using TypeScript
378
+ 'react/react-in-jsx-scope': 'off', // React 19
379
+ },
380
+ };
381
+ ```
382
+
383
+ ### TypeScript Strict Mode
384
+ ```json
385
+ {
386
+ "compilerOptions": {
387
+ "strict": true,
388
+ "noUncheckedIndexedAccess": true,
389
+ "noImplicitOverride": true,
390
+ "exactOptionalPropertyTypes": true,
391
+ "noFallthroughCasesInSwitch": true,
392
+ "noImplicitReturns": true,
393
+ "noPropertyAccessFromIndexSignature": true,
394
+ "noUnusedLocals": true,
395
+ "noUnusedParameters": true
396
+ }
397
+ }
398
+ ```
399
+
400
+ ### Pattern: Compound Components
401
+ ```typescript
402
+ // ✅ GOOD: Compound component pattern
403
+ import { createContext, useContext } from 'react';
404
+
405
+ interface AccordionContextValue {
406
+ openItem: string | null;
407
+ setOpenItem: (id: string | null) => void;
408
+ }
409
+
410
+ const AccordionContext = createContext<AccordionContextValue | null>(null);
411
+
412
+ export function Accordion({ children }: { children: React.ReactNode }) {
413
+ const [openItem, setOpenItem] = useState<string | null>(null);
414
+
415
+ return (
416
+ <AccordionContext.Provider value={{ openItem, setOpenItem }}>
417
+ <div className="accordion">{children}</div>
418
+ </AccordionContext.Provider>
419
+ );
420
+ }
421
+
422
+ Accordion.Item = function AccordionItem({
423
+ id,
424
+ children
425
+ }: {
426
+ id: string;
427
+ children: React.ReactNode
428
+ }) {
429
+ return <div data-item-id={id}>{children}</div>;
430
+ };
431
+
432
+ Accordion.Trigger = function AccordionTrigger({
433
+ id,
434
+ children
435
+ }: {
436
+ id: string;
437
+ children: React.ReactNode
438
+ }) {
439
+ const context = useContext(AccordionContext);
440
+ if (!context) throw new Error('Trigger must be used within Accordion');
441
+
442
+ return (
443
+ <button onClick={() => context.setOpenItem(context.openItem === id ? null : id)}>
444
+ {children}
445
+ </button>
446
+ );
447
+ };
448
+
449
+ Accordion.Content = function AccordionContent({
450
+ id,
451
+ children
452
+ }: {
453
+ id: string;
454
+ children: React.ReactNode
455
+ }) {
456
+ const context = useContext(AccordionContext);
457
+ if (!context) throw new Error('Content must be used within Accordion');
458
+
459
+ if (context.openItem !== id) return null;
460
+ return <div>{children}</div>;
461
+ };
462
+
463
+ // Usage
464
+ <Accordion>
465
+ <Accordion.Item id="1">
466
+ <Accordion.Trigger id="1">Section 1</Accordion.Trigger>
467
+ <Accordion.Content id="1">Content 1</Accordion.Content>
468
+ </Accordion.Item>
469
+ </Accordion>
470
+ ```
471
+
472
+ ### Anti-Pattern: AI Slop
473
+ ```typescript
474
+ // ❌ BAD: AI-generated slop with unnecessary complexity
475
+ export const createUserWithValidationAndErrorHandlingAndLogging = async (
476
+ userData: Partial<User> | undefined | null,
477
+ options?: {
478
+ validate?: boolean;
479
+ log?: boolean;
480
+ retry?: number;
481
+ timeout?: number;
482
+ fallback?: User;
483
+ }
484
+ ): Promise<User | null | undefined> => {
485
+ try {
486
+ if (!userData) {
487
+ console.log('No user data provided');
488
+ return options?.fallback ?? null;
489
+ }
490
+
491
+ if (options?.validate !== false) {
492
+ // Over-engineered validation
493
+ const isValid = validateUserData(userData);
494
+ if (!isValid) {
495
+ throw new Error('Validation failed');
496
+ }
497
+ }
498
+
499
+ // Unnecessary retry logic for simple operation
500
+ let attempts = 0;
501
+ while (attempts < (options?.retry ?? 3)) {
502
+ try {
503
+ const user = await db.insert(users).values(userData);
504
+ if (options?.log !== false) {
505
+ console.log('User created:', user.id);
506
+ }
507
+ return user;
508
+ } catch (err) {
509
+ attempts++;
510
+ if (attempts >= (options?.retry ?? 3)) throw err;
511
+ }
512
+ }
513
+ } catch (error) {
514
+ console.error('Error creating user:', error);
515
+ return options?.fallback ?? null;
516
+ }
517
+ };
518
+
519
+ // ✅ GOOD: Clean, maintainable code
520
+ export async function createUser(data: CreateUserInput): Promise<User> {
521
+ const user = await db.insert(users).values(data).returning();
522
+ return user[0];
523
+ }
524
+ ```
525
+
526
+ ## 5. Complete Example Applications
527
+
528
+ ### Blog Platform
529
+ ```typescript
530
+ // Entity DSL
531
+ const blogEntities = `
532
+ User(
533
+ email:email:unique:required,
534
+ username:string:unique:required:length(3,20),
535
+ password:string:hashed:required,
536
+ bio:string:length(10,500):nullable,
537
+ avatar:url:nullable
538
+ )
539
+
540
+ Post(
541
+ title:string:required:length(3,200),
542
+ slug:string:unique:required:index,
543
+ content:string:required,
544
+ excerpt:string:length(10,500):nullable,
545
+ author:refOne(User),
546
+ category:refOne(Category),
547
+ tags:refMany(Tag),
548
+ status:enum(draft,published,archived):default(draft),
549
+ publishedAt:datetime:nullable,
550
+ viewCount:integer:default(0)
551
+ )
552
+
553
+ Category(
554
+ name:string:unique:required,
555
+ slug:string:unique:required:index,
556
+ parent:ref(Category)
557
+ )
558
+
559
+ Tag(
560
+ name:string:unique:required,
561
+ slug:string:unique:required:index
562
+ )
563
+
564
+ Comment(
565
+ content:string:required:length(1,1000),
566
+ author:refOne(User),
567
+ post:refOne(Post),
568
+ replyTo:ref(Comment),
569
+ status:enum(pending,approved,spam):default(pending)
570
+ )
571
+ `;
572
+
573
+ // Generate: 5 entities × 15 files = 75 files
574
+ // Token savings: 5 × 11,300 = 56,500 tokens (~$0.85 saved)
575
+ ```
576
+
577
+ ### E-Commerce Store
578
+ ```typescript
579
+ const ecommerceEntities = `
580
+ Product(
581
+ name:string:required,
582
+ sku:string:unique:required:pattern(/^[A-Z]{3}-\\d{6}$/),
583
+ price:decimal:required:min(0),
584
+ compareAtPrice:decimal:min(0):nullable,
585
+ stock:integer:min(0):default(0),
586
+ category:refOne(Category),
587
+ images:array(string),
588
+ metadata:json:nullable,
589
+ status:enum(active,inactive,discontinued):default(active)
590
+ )
591
+
592
+ Order(
593
+ orderNumber:string:unique:required:immutable,
594
+ customer:refOne(Customer),
595
+ items:refMany(Product),
596
+ subtotal:decimal:required:min(0),
597
+ tax:decimal:required:min(0),
598
+ shipping:decimal:required:min(0),
599
+ total:computed(subtotal + tax + shipping),
600
+ status:enum(pending,paid,processing,shipped,delivered):default(pending)
601
+ )
602
+
603
+ Customer(
604
+ email:email:unique:required,
605
+ firstName:string:required,
606
+ lastName:string:required,
607
+ phone:string:pattern(/^\\+1\\d{10}$/):nullable,
608
+ addresses:json:nullable
609
+ )
610
+ `;
611
+
612
+ // Generate: 3 entities × 15 files = 45 files
613
+ // Includes: Stripe integration, order tracking emails, admin dashboard
614
+ ```
615
+
616
+ ### SaaS Dashboard
617
+ ```typescript
618
+ const saasEntities = `
619
+ Organization(
620
+ name:string:required:unique,
621
+ slug:string:unique:required:pattern(/^[a-z0-9-]+$/),
622
+ plan:enum(free,starter,pro,enterprise):default(free),
623
+ settings:json:nullable
624
+ )
625
+
626
+ User(
627
+ email:email:unique:required,
628
+ password:string:hashed:required,
629
+ organization:refOne(Organization),
630
+ role:enum(owner,admin,member,guest):default(member),
631
+ permissions:array(string)
632
+ )
633
+
634
+ Project(
635
+ name:string:required,
636
+ organization:refOne(Organization),
637
+ owner:refOne(User),
638
+ members:refMany(User),
639
+ status:enum(active,archived):default(active)
640
+ )
641
+
642
+ Task(
643
+ title:string:required,
644
+ project:refOne(Project),
645
+ assignee:refOne(User),
646
+ status:enum(todo,in_progress,review,done):default(todo),
647
+ priority:enum(low,medium,high,critical):default(medium),
648
+ dueDate:datetime:nullable
649
+ )
650
+ `;
651
+
652
+ // Generate: 4 entities × 15 files = 60 files
653
+ // Includes: Team management, billing, analytics, notifications
654
+ ```
655
+
656
+ ## Token Savings Summary
657
+
658
+ | Feature | Traditional | CodeDNA | Savings |
659
+ |---------|------------|---------|---------|
660
+ | Full-stack app (1 entity) | ~11,500 | ~200 | 98% |
661
+ | Components (1 entity) | ~2,500 | ~50 | 98% |
662
+ | Email templates (3) | ~1,500 | ~75 | 95% |
663
+ | Deployment config | ~800 | ~25 | 97% |
664
+ | **Blog (5 entities)** | **~80,000** | **~1,500** | **98%** |
665
+ | **E-commerce (3 entities)** | **~48,000** | **~900** | **98%** |
666
+ | **SaaS (4 entities)** | **~64,000** | **~1,200** | **98%** |
667
+
668
+ ## Cost Savings (Claude Sonnet)
669
+
670
+ **Blog Platform:**
671
+ - Traditional: ~$1.20
672
+ - CodeDNA: ~$0.02
673
+ - **Saved: $1.18 (98%)**
674
+
675
+ **E-Commerce:**
676
+ - Traditional: ~$0.72
677
+ - CodeDNA: ~$0.01
678
+ - **Saved: $0.71 (98%)**
679
+
680
+ **SaaS Dashboard:**
681
+ - Traditional: ~$0.96
682
+ - CodeDNA: ~$0.02
683
+ - **Saved: $0.94 (98%)**
684
+
685
+ ## Best Practices
686
+
687
+ 1. **Start with Entity DSL** - Design your data model first
688
+ 2. **Use Type Safety** - Enable TypeScript strict mode
689
+ 3. **Leverage Relations** - Use refOne/refMany for clean schemas
690
+ 4. **Add Validation** - Use pattern, length, min/max constraints
691
+ 5. **Deploy to Edge** - Cloudflare for global performance
692
+ 6. **Monitor Errors** - Use error boundaries and logging
693
+ 7. **Optimize Queries** - Use Tanstack Query caching
694
+ 8. **Email Responsively** - Test emails in all clients
695
+
696
+ ## Next Steps
697
+
698
+ 1. Generate your first entity
699
+ 2. Review generated code
700
+ 3. Customize as needed
701
+ 4. Deploy to Cloudflare
702
+ 5. Monitor and iterate
703
+
704
+ ---
705
+
706
+ **CodeDNA Modern Stack: Build production apps in minutes, not weeks.**