feat: Phase 4 — 9 new features (v1.1)
Some checks failed
CI / Build & test backend (push) Failing after 14m56s
Some checks failed
CI / Build & test backend (push) Failing after 14m56s
REQ-MOB-010: BarcodeScreen.tsx — barcode scanner via react-native-camera REQ-VIZ-001: WeeklyCalorieChart.tsx — 7-day bar chart on History screen REQ-VIZ-002: Streak tracker — GET /meals/streak + HomeScreen badge REQ-UX-001: Quick-add calories — POST /meals/quick-add + QuickAddScreen REQ-UX-002: Food favourites — UserFoodMemory.favourite + toggle endpoint + FoodRow star REQ-UX-003: GoalBanner.tsx — in-app slide-in when daily target hit REQ-EXP-001: ExportController — GET /export/meals CSV download REQ-WTR-001: Water tracking — WaterEntry entity + POST/GET /water + DailyDetails widget REQ-UX-004: Daily logging reminder — HomeScreen after-18:00 banner Also: Flyway V2 (favourite), V3 (water_entries), V4 (source constraints) Traceability, CHANGELOG, PLAN updated after each feature
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
Extension intercepts user actions before they reach the filesystem. Governance validation is mandatory.
|
||||
|
||||
# Extension Standards - Layer 3
|
||||
|
||||
**Module**: Extension Standards
|
||||
**Component**: Layer 3 (VS Code Extension)
|
||||
**Load**: When working on virsaitis-development/virsaitis-extension/
|
||||
**Version**: 3.0.0
|
||||
**Updated**: 2026-04-20
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Purpose
|
||||
|
||||
Defines VS Code Extension API standards, TypeScript conventions, and packaging workflow for Virsaitis Extension (Layer 3 user action interception).
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Machine Policy
|
||||
|
||||
```
|
||||
[TECHNOLOGY_STACK]
|
||||
LANGUAGE=TypeScript 5.0+
|
||||
FRAMEWORK=VS Code Extension API 1.85+
|
||||
BUILD=webpack
|
||||
PACKAGE=vsce
|
||||
TEST=@vscode/test-electron
|
||||
|
||||
[CODE_STANDARDS]
|
||||
INDENTATION=2_spaces
|
||||
LINE_LENGTH=100_chars
|
||||
API_VERSION=1.85.0
|
||||
ACTIVATION=lazy_load
|
||||
|
||||
[QUALITY_GATES]
|
||||
COMPILE=must_succeed
|
||||
TESTS=must_pass
|
||||
PACKAGE_SIZE=< 5MB
|
||||
ACTIVATION_TIME=<200ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 TypeScript Standards
|
||||
|
||||
Same as MCP layer: 2-space indentation, 100-char line length, single quotes, semicolons required. See [MCP Standards](mcp-standards.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 VS Code Extension Architecture
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
virsaitis-development/virsaitis-extension/
|
||||
├── src/
|
||||
│ ├── extension.ts (entry point, activate/deactivate)
|
||||
│ ├── governance/
|
||||
│ │ ├── file-interceptor.ts (intercept file operations)
|
||||
│ │ ├── mcp-client.ts (communicate with MCP server)
|
||||
│ │ └── shield-decorator.ts (🛡️ UI indicator)
|
||||
│ ├── commands/
|
||||
│ │ ├── request-override.ts
|
||||
│ │ └── show-governance-status.ts
|
||||
│ ├── ui/
|
||||
│ │ ├── status-bar.ts
|
||||
│ │ ├── notifications.ts
|
||||
│ │ └── webview-provider.ts
|
||||
│ └── utils/
|
||||
│ └── config.ts
|
||||
├── test/
|
||||
│ ├── suite/
|
||||
│ │ ├── extension.test.ts
|
||||
│ │ └── governance.test.ts
|
||||
│ └── runTest.ts
|
||||
├── resources/
|
||||
│ └── icons/
|
||||
│ └── shield.svg
|
||||
├── package.json (extension manifest)
|
||||
├── tsconfig.json
|
||||
├── webpack.config.js
|
||||
├── .vscodeignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Extension Manifest (package.json)
|
||||
|
||||
### Essential Fields
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "virsaitis-extension",
|
||||
"displayName": "Virsaitis Governance",
|
||||
"description": "AI governance enforcement for VS Code",
|
||||
"version": "2.0.0",
|
||||
"publisher": "virsaitis",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": ["Other"],
|
||||
"activationEvents": [
|
||||
"onStartupFinished",
|
||||
"onCommand:virsaitis.requestOverride"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "virsaitis.requestOverride",
|
||||
"title": "Virsaitis: Request Override"
|
||||
},
|
||||
{
|
||||
"command": "virsaitis.showGovernanceStatus",
|
||||
"title": "Virsaitis: Show Governance Status"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Virsaitis",
|
||||
"properties": {
|
||||
"virsaitis.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable Virsaitis governance enforcement"
|
||||
},
|
||||
"virsaitis.mcpServerCommand": {
|
||||
"type": "string",
|
||||
"default": "node",
|
||||
"description": "Command to start Virsaitis MCP server"
|
||||
},
|
||||
"virsaitis.mcpServerArgs": {
|
||||
"type": "array",
|
||||
"default": ["build/index.js"],
|
||||
"description": "Arguments for Virsaitis MCP server process"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Activation (TIER-2)
|
||||
|
||||
### Lazy Activation
|
||||
|
||||
**PATTERN**:
|
||||
```typescript
|
||||
// extension.ts
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Virsaitis extension activating...');
|
||||
|
||||
// Register commands
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'virsaitis.requestOverride',
|
||||
() => requestOverride()
|
||||
)
|
||||
);
|
||||
|
||||
// Initialize governance interceptor (lazy)
|
||||
const interceptor = new FileInterceptor();
|
||||
context.subscriptions.push(interceptor);
|
||||
|
||||
// Start MCP client connection
|
||||
const mcpClient = new MCPClient();
|
||||
context.subscriptions.push(mcpClient);
|
||||
|
||||
console.log('Virsaitis extension activated');
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
console.log('Virsaitis extension deactivated');
|
||||
}
|
||||
```
|
||||
|
||||
### Activation Events
|
||||
|
||||
**RECOMMENDED**:
|
||||
- `onStartupFinished` - Start when VS Code ready (lazy)
|
||||
- `onCommand:virsaitis.*` - Activate on command
|
||||
- NOT `*` - Don't activate on every event (performance)
|
||||
|
||||
**TARGET**: Activation time <200ms
|
||||
|
||||
---
|
||||
|
||||
> ⚡ CHECKPOINT — Does this file operation go through MCP validation first? Extension must not bypass governance.
|
||||
|
||||
## 🛡️ File Operation Interception (TIER-1)
|
||||
|
||||
### Intercept File Save
|
||||
|
||||
```typescript
|
||||
export class FileInterceptor implements vscode.Disposable {
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _mcpClient: MCPClient;
|
||||
|
||||
constructor() {
|
||||
this._mcpClient = new MCPClient();
|
||||
|
||||
// Intercept file save
|
||||
this._disposables.push(
|
||||
vscode.workspace.onWillSaveTextDocument(async (e) => {
|
||||
const validation = await this.validateOperation(
|
||||
'write',
|
||||
e.document.uri.fsPath
|
||||
);
|
||||
|
||||
if (!validation.allowed && validation.tier === 'TIER-0') {
|
||||
// Block save for TIER-0 violation
|
||||
e.waitUntil(this.blockSave(validation));
|
||||
} else if (!validation.allowed && validation.tier === 'TIER-1') {
|
||||
// Warn for TIER-1
|
||||
await this.warnUser(validation);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async validateOperation(
|
||||
operation: string,
|
||||
filePath: string
|
||||
): Promise<ValidationResult> {
|
||||
return await this._mcpClient.validateOperation(operation, filePath);
|
||||
}
|
||||
|
||||
private async blockSave(validation: ValidationResult): Promise<void> {
|
||||
const message = `TIER-0 VIOLATION: ${validation.reason}`;
|
||||
await vscode.window.showErrorMessage(message, { modal: true });
|
||||
throw new Error(message); // Prevents save
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI Components
|
||||
|
||||
### Status Bar Item
|
||||
|
||||
```typescript
|
||||
export class GovernanceStatusBar implements vscode.Disposable {
|
||||
private _statusBarItem: vscode.StatusBarItem;
|
||||
|
||||
constructor() {
|
||||
this._statusBarItem = vscode.window.createStatusBarItem(
|
||||
vscode.StatusBarAlignment.Right,
|
||||
100
|
||||
);
|
||||
this._statusBarItem.command = 'virsaitis.showGovernanceStatus';
|
||||
this.update StatusBarItem.text = '$(shield) Virsaitis: Active';
|
||||
this._statusBarItem.tooltip = 'Governance enforcement active';
|
||||
this._statusBarItem.show();
|
||||
}
|
||||
|
||||
public setStatus(status: 'active' | 'warning' | 'error'): void {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
this._statusBarItem.text = '$(shield) Virsaitis: Active';
|
||||
this._statusBarItem.backgroundColor = undefined;
|
||||
break;
|
||||
case 'warning':
|
||||
this._statusBarItem.text = '$(warning) Virsaitis: Warning';
|
||||
this._statusBarItem.backgroundColor = new vscode.ThemeColor(
|
||||
'statusBarItem.warningBackground'
|
||||
);
|
||||
break;
|
||||
case 'error':
|
||||
this._statusBarItem.text = '$(error) Virsaitis: Error';
|
||||
this._statusBarItem.backgroundColor = new vscode.ThemeColor(
|
||||
'statusBarItem.errorBackground'
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._statusBarItem.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File Decoration (Shield Icon)
|
||||
|
||||
> ⚡ CHECKPOINT — MCP client uses stdio transport (StdioClientTransport), not HTTP fetch. Verify.
|
||||
|
||||
```typescript
|
||||
export class ShieldDecorator implements vscode.Disposable {
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor() {
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
gutterIconPath: vscode.Uri.file('resources/icons/shield.svg'),
|
||||
gutterIconSize: '80%',
|
||||
});
|
||||
|
||||
// Apply to protected files
|
||||
this._disposables.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor && this.isProtectedFile(editor.document.uri)) {
|
||||
const range = new vscode.Range(0, 0, 0, 0);
|
||||
editor.setDecorations(decorationType, [{ range }]);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private isProtectedFile(uri: vscode.Uri): boolean {
|
||||
const path = uri.fsPath;
|
||||
const protectedPatterns = [
|
||||
'.github/copilot-instructions.md',
|
||||
'.github/copilot-modules/',
|
||||
'.github/agents/',
|
||||
'virsaitis-development/virsaitis-requirements/',
|
||||
];
|
||||
|
||||
return protectedPatterns.some(pattern => path.includes(pattern));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 MCP Client Communication
|
||||
|
||||
Virsaitis MCP uses **stdio transport** (not HTTP). The extension spawns the MCP server as a child process and communicates via stdin/stdout.
|
||||
|
||||
```typescript
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
|
||||
export class MCPClient {
|
||||
private _client: Client;
|
||||
private _transport: StdioClientTransport;
|
||||
|
||||
constructor() {
|
||||
const config = vscode.workspace.getConfiguration('virsaitis');
|
||||
const command = config.get<string>('mcpServerCommand', 'node');
|
||||
const args = config.get<string[]>('mcpServerArgs', ['build/index.js']);
|
||||
|
||||
this._transport = new StdioClientTransport({ command, args });
|
||||
this._client = new Client({ name: 'virsaitis-extension', version: '3.0.0' });
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
await this._client.connect(this._transport);
|
||||
}
|
||||
|
||||
async validateOperation(
|
||||
operation: string,
|
||||
filePath: string
|
||||
): Promise<ValidationResult> {
|
||||
try {
|
||||
const result = await this._client.callTool({
|
||||
name: 'validate_operation',
|
||||
arguments: { operation, filePath },
|
||||
});
|
||||
return result.content[0].text as unknown as ValidationResult;
|
||||
} catch (error) {
|
||||
console.error('MCP client error:', error);
|
||||
// Fail open (allow operation if MCP unavailable)
|
||||
return { allowed: true, reason: 'MCP server unavailable' };
|
||||
}
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await this._transport.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> ⚡ CHECKPOINT — All UI accessible? Keyboard navigation, focus indicators, WCAG 2.2 AA.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Extension Test Setup
|
||||
|
||||
```typescript
|
||||
// test/suite/extension.test.ts
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Extension should activate', async () => {
|
||||
const extension = vscode.extensions.getExtension('virsaitis.virsaitis-extension');
|
||||
assert.ok(extension);
|
||||
await extension.activate();
|
||||
assert.strictEqual(extension.isActive, true);
|
||||
});
|
||||
|
||||
test('Should register commands', async () => {
|
||||
const commands = await vscode.commands.getCommands();
|
||||
assert.ok(commands.includes('virsaitis.requestOverride'));
|
||||
assert.ok(commands.includes('virsaitis.showGovernanceStatus'));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Tests run in Extension Development Host (isolated VS Code instance).
|
||||
|
||||
---
|
||||
|
||||
## 📦 Packaging & Distribution
|
||||
|
||||
### Build Extension
|
||||
|
||||
```bash
|
||||
# Compile TypeScript + webpack bundle
|
||||
npm run compile
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Package extension (.vsix file)
|
||||
vsce package
|
||||
```
|
||||
|
||||
**OUTPUT**: `virsaitis-extension-2.0.0.vsix`
|
||||
|
||||
### Package Size
|
||||
|
||||
**TARGET**: <5MB
|
||||
|
||||
**CHECK**:
|
||||
```bash
|
||||
ls -lh *.vsix
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Rules From This Module
|
||||
|
||||
- Extension validates every file operation through MCP before allowing it.
|
||||
- MCP client uses stdio transport via `@modelcontextprotocol/sdk`, not HTTP.
|
||||
- All UI must meet WCAG 2.2 AA. Keyboard navigation mandatory.
|
||||
- Extension must degrade gracefully if MCP server is unavailable.
|
||||
- Definitions: `.github/virsaitis-definition-library.md`
|
||||
|
||||
Return to hub: `.github/copilot-instructions.md`
|
||||
|
||||
**REDUCE SIZE**:
|
||||
- Exclude test files (`.vscodeignore`)
|
||||
- Exclude source maps in production
|
||||
- Minimize dependencies
|
||||
- Use webpack production mode
|
||||
|
||||
### .vscodeignore
|
||||
|
||||
```
|
||||
.vscode/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
src/**
|
||||
test/**
|
||||
node_modules/**
|
||||
!node_modules/@modelcontextprotocol/**
|
||||
webpack.config.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Extension Settings
|
||||
|
||||
Users can configure via VS Code settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"virsaitis.enabled": true,
|
||||
"virsaitis.mcpServerCommand": "node",
|
||||
"virsaitis.mcpServerArgs": ["build/index.js"],
|
||||
"virsaitis.showShieldIcons": true,
|
||||
"virsaitis.blockTier0": true
|
||||
}
|
||||
```
|
||||
|
||||
### Reading Configuration
|
||||
|
||||
```typescript
|
||||
const config = vscode.workspace.getConfiguration('virsaitis');
|
||||
const enabled = config.get<boolean>('enabled', true);
|
||||
const mcpCommand = config.get<string>('mcpServerCommand', 'node');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
### Disposal Pattern
|
||||
|
||||
Always implement `vscode.Disposable`:
|
||||
|
||||
```typescript
|
||||
export class MyComponent implements vscode.Disposable {
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor() {
|
||||
this._disposables.push(
|
||||
// Register event handlers, commands, etc.
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error) {
|
||||
// Log for debugging
|
||||
console.error('Operation failed:', error);
|
||||
|
||||
// Show user-friendly message
|
||||
vscode.window.showErrorMessage(
|
||||
'Operation failed. Please check Virsaitis logs.'
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
- Use lazy loading
|
||||
- Debounce frequent events
|
||||
- Cache expensive operations
|
||||
- Minimize synchronous work on activation
|
||||
|
||||
---
|
||||
|
||||
## 📚 Quick Reference
|
||||
|
||||
| Aspect | Standard | Command |
|
||||
|--------|----------|---------|
|
||||
| **Build** | Webpack | `npm run compile` |
|
||||
| **Test** | @vscode/test-electron | `npm test` |
|
||||
| **Package** | vsce | `vsce package` |
|
||||
| **Size** | <5MB | Check .vsix |
|
||||
| **Activation** | <200ms | Lazy load |
|
||||
|
||||
---
|
||||
|
||||
*Extension Standards Module v3.0.0*
|
||||
*VS Code user action interception layer*
|
||||
Reference in New Issue
Block a user