Code Standards
Construct maintains high code quality through consistent standards across frontend, backend, and infrastructure code. All contributions must follow these guidelines.
TypeScript Standards
Strict Mode
All TypeScript code must use strict mode:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}This means:
- No
anytype unless absolutely unavoidable (and must be documented with// @ts-ignore) - Explicit null/undefined handling - Check for nulls before accessing properties
- Type all function parameters - No implicit
anyparameters
Type Annotations
Always provide explicit types:
// Good
const users: User[] = [];
function fetchUser(id: string): Promise<User> {
// ...
}
// Avoid
const users = []; // Unknown type
function fetchUser(id) { // No parameter type
// ...
}Interfaces over Types
Prefer interface for object shapes, type for unions:
// Good - Interface for objects
interface User {
id: string;
email: string;
name: string;
}
// Good - Type for unions
type Status = "active" | "inactive" | "pending";
// Avoid - Type for objects (less extensible)
type User = {
id: string;
email: string;
};Vue Standards
Composition API with Setup Script
Always use <script setup lang="ts">:
<!-- Good -->
<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('Hello')
</script>
<!-- Avoid - Options API -->
<script lang="ts">
export default {
data() {
return { message: 'Hello' }
}
}
</script>Composables over Mixins
Use composables for shared logic:
// Good - Composable
export function useCount() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// Avoid - Mixin
const countMixin = {
data() { return { count: 0 } },
methods: { increment() { this.count++ } }
}Component Organization
<template>
<!-- Template first, concise -->
<div class="component">
<h1>{{ title }}</h1>
<Button @click="handleClick">Click Me</Button>
</div>
</template>
<script setup lang="ts">
// Imports
import { ref, computed } from 'vue'
import Button from './Button.vue'
// Props
interface Props {
initialTitle?: string
}
const props = withDefaults(defineProps<Props>(), {
initialTitle: 'Default'
})
// Emits
const emit = defineEmits<{
click: [value: string]
}>()
// State
const title = ref(props.initialTitle)
// Computed
const upperTitle = computed(() => title.value.toUpperCase())
// Methods
function handleClick() {
emit('click', title.value)
}
</script>
<style scoped>
.component {
padding: 1rem;
}
</style>Props with TypeScript
Always use withDefaults and explicit types:
interface Props {
title: string
count?: number
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
disabled: false
})Component Naming
- File names: PascalCase (
UserProfile.vue) - Component names: Same as file name
- Slots: kebab-case (
<slot name="header-action">)
Go Standards
Code Quality
All Go code must pass:
# Run before committing
go vet ./...
go test ./...
# Optional but recommended
go fmt ./...
golangci-lint runError Handling
Always handle errors explicitly:
// Good
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
// Avoid
result, _ := operation() // Silently ignoring errorsInterface Definition
Define small, focused interfaces:
// Good - Small, focused interface
type Reader interface {
Read([]byte) (int, error)
}
// Avoid - Large, bloated interface
type AllOperations interface {
Read() ([]byte, error)
Write([]byte) error
Delete(string) error
Update(map[string]interface{}) error
// ... 20 more methods
}Concurrency
Use goroutines and channels safely:
// Good - Clear channel usage
type Service struct {
results chan Result
done chan struct{}
}
// Good - Error handling with context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Avoid - Goroutines without cancellation
go func() { /* runs forever */ }()Testing
Test files in same package:
operator/
internal/
state/
store.go
store_test.go // Same package, same directoryExample test:
func TestStoreGet(t *testing.T) {
store := NewStore(t.TempDir())
store.Set("key", "value")
value, err := store.Get("key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if value != "value" {
t.Errorf("expected 'value', got %q", value)
}
}Frontend Structure
Directory Organization
frontend/
src/
components/ # Reusable Vue components
Button.vue
Modal.vue
composables/ # Reusable logic
useAuth.ts
useStorage.ts
stores/ # Pinia stores
auth.ts
project.ts
pages/ # Route components
Dashboard.vue
Settings.vue
utils/ # Helper functions
format.ts
validate.ts
router/ # Route definitions
index.ts
types/ # TypeScript types
agent.ts
space.ts
styles/ # Global styles
main.css
App.vue
main.tsImport Alias
Always use @/ alias for frontend imports:
// Good - Use alias
import { useAuth } from '@/composables/useAuth'
import Button from '@/components/Button.vue'
// Avoid - Relative paths
import { useAuth } from '../../../composables/useAuth'
import Button from '../../components/Button.vue'Configure in vite.config.ts:
export default {
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
}Auto-imports
Enable auto-imports in vite.config.ts to avoid verbose imports:
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
export default {
plugins: [
AutoImport({
imports: ['vue', 'vue-router'],
dirs: ['src/composables', 'src/stores'],
vueTemplate: true
}),
Components({
dirs: ['src/components'],
dts: 'src/components.d.ts'
})
]
}Then use without imports:
<script setup lang="ts">
// useAuth, Button already available
const auth = useAuth()
</script>Operator Code
AI Logic in Operator Only
Keep all AI interaction logic in the Go operator, never in frontend:
// Good - Frontend uses SDK
const result = await dispatch({
messages: [...],
agentID: 'architect'
})
// Avoid - Frontend calling AI directly
const response = await fetch('https://api.anthropic.com/...')This ensures:
- Consistent AI logic across clients
- Easier to update models/providers
- Better security (API keys server-side)
- Session management centralized
SDK Import Rules
Always import from npm packages, never use file: paths:
// Good - From npm
import { useAuth, useStorage } from '@construct-space/sdk'
import { Button, Modal } from '@construct-space/ui'
// Avoid - File paths break in production
import { useAuth } from 'file:../../../construct-sdk/src/composables'Alignment Requirements
Two files must stay synchronized:
desktop/src/lib.rs
Tauri-bridge-enabled functions:
#[tauri::command]
pub fn show_file_picker(title: String) -> Result<Vec<String>> {
// Implementation
}frontend/lib/appPaths.ts
Frontend-accessible paths must be declared:
// Must list all available commands from Tauri bridge
export const appPaths = {
showFilePicker: 'show_file_picker',
// Add new Tauri commands here
}Critical: Always update both files when adding Tauri commands.
Documentation
Code Comments
Comments explain "why", not "what":
// Good - Explains reasoning
// Use Handlebars to support dynamic context in system prompts
const compiled = Handlebars.compile(template)
// Avoid - Explains what code obviously does
// Increment count
count++Function Documentation
Document public functions:
/**
* Dispatch a request to an agent and stream results.
*
* @param request - The dispatch request with messages and agent ID
* @returns Promise resolving to session ID for streaming
* @throws Error if agent not found or request invalid
*
* @example
* const sessionId = await dispatch({
* messages: [{ role: 'user', content: [...] }],
* agentID: 'architect'
* })
*/
export async function dispatch(request: DispatchRequest): Promise<string> {
// Implementation
}README Files
Each major module should have a README explaining:
- What it does
- How to use it
- Key concepts
- Examples
Linting & Formatting
ESLint
Frontend must pass ESLint:
npm run lint # Check for issues
npm run lint:fix # Fix automaticallyConfiguration in .eslintrc.json:
- No
anytypes - No unused variables
- No console in production
- Consistent naming conventions
Prettier
All frontend code is formatted with Prettier:
npm run format # Format all filesConfiguration in .prettierrc:
- 2-space indentation
- Single quotes
- Semicolons required
- Line width: 80 characters
Go fmt
Go code must be formatted:
go fmt ./...No configuration needed - standard Go style.
Performance
Frontend
- Lazy load routes with Vue Router's dynamic imports
- Use computed() for derived state (not methods)
- Memoize expensive computations
- Avoid unnecessary re-renders (use v-show for toggling)
Operator
- Cache file reads when possible
- Use goroutines for I/O-bound operations
- Profile before optimizing
- Keep tight loops fast
Security
Never in Frontend
- API keys
- Tokens
- Secrets
- Authentication logic
Environment Variables
Use .env.local for development secrets (never committed):
# .env.local (in .gitignore)
VITE_OPERATOR_URL=http://localhost:60100Access via import.meta.env:
const operatorUrl = import.meta.env.VITE_OPERATOR_URLDependency Updates
Keep dependencies current but tested:
npm outdated # Check for updates
npm update # Update safely
npm audit # Check for vulnerabilitiesTesting Requirements
See Testing Guide for complete details.
Quick summary:
- Unit tests for utilities and composables
- Integration tests for complex flows
- E2E tests for critical user paths
- All new features require tests
Checklist Before Commit
- [ ] Code passes linter (
npm run lint,go vet) - [ ] Tests pass locally (
npm run test,go test ./...) - [ ] Build succeeds (
npm run build) - [ ] No console errors in development
- [ ] Code follows style standards (run formatter)
- [ ] TypeScript: no
anytypes without explanation - [ ] Commit message follows conventions
- [ ] No hardcoded secrets or API keys