feat(tags): Implement tagged task lists migration system (Part 1/2)

This commit introduces the foundational infrastructure for tagged task lists,
enabling multi-context task management without remote storage to prevent merge conflicts.

CORE ARCHITECTURE:
• Silent migration system transforms tasks.json from old format { "tasks": [...] }
  to new tagged format { "master": { "tasks": [...] } }
• Tag resolution layer provides complete backward compatibility - existing code continues to work
• Automatic configuration and state management for seamless user experience

SILENT MIGRATION SYSTEM:
• Automatic detection and migration of legacy tasks.json format
• Complete project migration: tasks.json + config.json + state.json
• Transparent tag resolution returns old format to maintain compatibility
• Zero breaking changes - all existing functionality preserved

CONFIGURATION MANAGEMENT:
• Added global.defaultTag setting (defaults to 'master')
• New tags section with gitIntegration placeholders for future features
• Automatic config.json migration during first run
• Proper state.json creation with migration tracking

USER EXPERIENCE:
• Clean, one-time FYI notice after migration (no emojis, professional styling)
• Notice appears after 'Suggested Next Steps' and is tracked in state.json
• Silent operation - users unaware migration occurred unless explicitly shown

TECHNICAL IMPLEMENTATION:
• Enhanced readJSON() with automatic migration detection and processing
• New utility functions: getCurrentTag(), resolveTag(), getTasksForTag(), setTasksForTag()
• Complete migration orchestration via performCompleteTagMigration()
• Robust error handling and fallback mechanisms

BACKWARD COMPATIBILITY:
• 100% backward compatibility maintained
• Existing CLI commands and MCP tools continue to work unchanged
• Legacy tasks.json format automatically upgraded on first read
• All existing workflows preserved

TESTING VERIFIED:
• Complete migration from legacy state works correctly
• Config.json properly updated with tagged system settings
• State.json created with correct initial values
• Migration notice system functions as designed
• All existing functionality continues to work normally

Part 2 will implement tag management commands (add-tag, use-tag, list-tags)
and MCP tool updates for full tagged task system functionality.

Related: Task 103 - Implement Tagged Task Lists System for Multi-Context Task Management
This commit is contained in:
Eyal Toledano
2025-06-11 16:36:48 -04:00
parent f3fe481f3f
commit 2328efe482
10 changed files with 7201 additions and 6724 deletions

View File

@@ -1,33 +1,41 @@
{ {
"models": { "models": {
"main": { "main": {
"provider": "anthropic", "provider": "anthropic",
"modelId": "claude-sonnet-4-20250514", "modelId": "claude-sonnet-4-20250514",
"maxTokens": 50000, "maxTokens": 50000,
"temperature": 0.2 "temperature": 0.2
}, },
"research": { "research": {
"provider": "perplexity", "provider": "perplexity",
"modelId": "sonar-pro", "modelId": "sonar-pro",
"maxTokens": 8700, "maxTokens": 8700,
"temperature": 0.1 "temperature": 0.1
}, },
"fallback": { "fallback": {
"provider": "anthropic", "provider": "anthropic",
"modelId": "claude-3-7-sonnet-20250219", "modelId": "claude-3-7-sonnet-20250219",
"maxTokens": 128000, "maxTokens": 128000,
"temperature": 0.2 "temperature": 0.2
} }
}, },
"global": { "global": {
"userId": "1234567890", "userId": "1234567890",
"logLevel": "info", "logLevel": "info",
"debug": false, "debug": false,
"defaultSubtasks": 5, "defaultSubtasks": 5,
"defaultPriority": "medium", "defaultPriority": "medium",
"projectName": "Taskmaster", "projectName": "Taskmaster",
"ollamaBaseURL": "http://localhost:11434/api", "ollamaBaseURL": "http://localhost:11434/api",
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
"azureBaseURL": "https://your-endpoint.azure.com/" "azureBaseURL": "https://your-endpoint.azure.com/",
} "defaultTag": "master"
} },
"tags": {
"autoSwitchOnBranch": false,
"gitIntegration": {
"enabled": false,
"autoSwitchTagWithBranch": false
}
}
}

6
.taskmaster/state.json Normal file
View File

@@ -0,0 +1,6 @@
{
"currentTag": "master",
"lastSwitched": "2025-06-11T20:26:12.598Z",
"branchTagMapping": {},
"migrationNoticeShown": true
}

View File

@@ -25,13 +25,13 @@
- Run automated and manual tests for all new and modified commands, including edge cases (e.g., duplicate tag names, tag deletion with tasks). - Run automated and manual tests for all new and modified commands, including edge cases (e.g., duplicate tag names, tag deletion with tasks).
# Subtasks: # Subtasks:
## 1. Design Extended tasks.json Schema for Tag Support [pending] ## 1. Design Extended tasks.json Schema for Tag Support [done]
### Dependencies: None ### Dependencies: None
### Description: Define and document the updated tasks.json schema to include a 'tags' structure, ensuring 'master' is the default tag containing all existing tasks. ### Description: Define and document the updated tasks.json schema to include a 'tags' structure, ensuring 'master' is the default tag containing all existing tasks.
### Details: ### Details:
Create a schema that supports multiple tags, with backward compatibility for users without tags. Create a schema that supports multiple tags, with backward compatibility for users without tags.
## 2. Implement Seamless Migration for Existing Users [pending] ## 2. Implement Seamless Migration for Existing Users [done]
### Dependencies: 103.1 ### Dependencies: 103.1
### Description: Develop a migration script or logic to move existing tasks into the 'master' tag for users upgrading from previous versions. ### Description: Develop a migration script or logic to move existing tasks into the 'master' tag for users upgrading from previous versions.
### Details: ### Details:
@@ -112,7 +112,7 @@ Ensure all features work as intended and meet quality standards, with specific f
### Details: ### Details:
## 15. Implement Tasks.json Migration Logic [pending] ## 15. Implement Tasks.json Migration Logic [done]
### Dependencies: 103.1, 103.2 ### Dependencies: 103.1, 103.2
### Description: Create specific migration logic to transform existing tasks.json format (array of tasks) to the new tagged format ({tags: {master: {tasks: [...]}}}). Include validation and rollback capabilities. ### Description: Create specific migration logic to transform existing tasks.json format (array of tasks) to the new tagged format ({tags: {master: {tasks: [...]}}}). Include validation and rollback capabilities.
### Details: ### Details:

File diff suppressed because one or more lines are too long

View File

@@ -29,5 +29,12 @@
"ollamaBaseURL": "http://localhost:11434/api", "ollamaBaseURL": "http://localhost:11434/api",
"azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/", "azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/",
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com" "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com"
},
"tags": {
"autoSwitchOnBranch": false,
"gitIntegration": {
"enabled": false,
"autoSwitchTagWithBranch": false
}
} }
} }

51
commit_message.txt Normal file
View File

@@ -0,0 +1,51 @@
feat(tags): Implement tagged task lists migration system (Part 1/2)
This commit introduces the foundational infrastructure for tagged task lists,
enabling multi-context task management without remote storage to prevent merge conflicts.
CORE ARCHITECTURE:
• Silent migration system transforms tasks.json from old format { "tasks": [...] }
to new tagged format { "master": { "tasks": [...] } }
• Tag resolution layer provides complete backward compatibility - existing code continues to work
• Automatic configuration and state management for seamless user experience
SILENT MIGRATION SYSTEM:
• Automatic detection and migration of legacy tasks.json format
• Complete project migration: tasks.json + config.json + state.json
• Transparent tag resolution returns old format to maintain compatibility
• Zero breaking changes - all existing functionality preserved
CONFIGURATION MANAGEMENT:
• Added global.defaultTag setting (defaults to 'master')
• New tags section with gitIntegration placeholders for future features
• Automatic config.json migration during first run
• Proper state.json creation with migration tracking
USER EXPERIENCE:
• Clean, one-time FYI notice after migration (no emojis, professional styling)
• Notice appears after 'Suggested Next Steps' and is tracked in state.json
• Silent operation - users unaware migration occurred unless explicitly shown
TECHNICAL IMPLEMENTATION:
• Enhanced readJSON() with automatic migration detection and processing
• New utility functions: getCurrentTag(), resolveTag(), getTasksForTag(), setTasksForTag()
• Complete migration orchestration via performCompleteTagMigration()
• Robust error handling and fallback mechanisms
BACKWARD COMPATIBILITY:
• 100% backward compatibility maintained
• Existing CLI commands and MCP tools continue to work unchanged
• Legacy tasks.json format automatically upgraded on first read
• All existing workflows preserved
TESTING VERIFIED:
• Complete migration from legacy state works correctly
• Config.json properly updated with tagged system settings
• State.json created with correct initial values
• Migration notice system functions as designed
• All existing functionality continues to work normally
Part 2 will implement tag management commands (add-tag, use-tag, list-tags)
and MCP tool updates for full tagged task system functionality.
Related: Task 103 - Implement Tagged Task Lists System for Multi-Context Task Management

View File

@@ -198,12 +198,8 @@ function createInitialStateFile(targetDir) {
const initialState = { const initialState = {
currentTag: 'master', currentTag: 'master',
lastSwitched: new Date().toISOString(), lastSwitched: new Date().toISOString(),
autoSwitchOnBranch: false, // Future feature for git branch integration branchTagMapping: {},
gitIntegration: { migrationNoticeShown: false
enabled: false,
autoCreateTags: false,
branchTagMapping: {}
}
}; };
try { try {

View File

@@ -13,7 +13,7 @@ import http from 'http';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import ora from 'ora'; // Import ora import ora from 'ora'; // Import ora
import { log, readJSON, findProjectRoot } from './utils.js'; import { log, readJSON, writeJSON, findProjectRoot } from './utils.js';
import { import {
parsePRD, parsePRD,
updateTasks, updateTasks,
@@ -74,7 +74,8 @@ import {
displayAvailableModels, displayAvailableModels,
displayApiKeyStatus, displayApiKeyStatus,
displayAiUsageSummary, displayAiUsageSummary,
displayMultipleTasksSummary displayMultipleTasksSummary,
displayTaggedTasksFYI
} from './ui.js'; } from './ui.js';
import { initializeProject } from '../init.js'; import { initializeProject } from '../init.js';
@@ -3266,6 +3267,42 @@ async function runCLI(argv = process.argv) {
updateInfo.latestVersion updateInfo.latestVersion
); );
} }
// Check if migration has occurred and show FYI notice once
try {
const projectRoot = findProjectRoot() || '.';
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(tasksPath)) {
// Read raw file to check if it has master key (bypassing tag resolution)
const rawData = fs.readFileSync(tasksPath, 'utf8');
const parsedData = JSON.parse(rawData);
if (parsedData && parsedData.master) {
// Migration has occurred, check if we've shown the notice
let stateData = { migrationNoticeShown: false };
if (fs.existsSync(statePath)) {
stateData = readJSON(statePath) || stateData;
}
if (!stateData.migrationNoticeShown) {
displayTaggedTasksFYI({ _migrationHappened: true });
// Mark as shown
stateData.migrationNoticeShown = true;
writeJSON(statePath, stateData);
}
}
}
} catch (error) {
// Silently ignore errors checking for migration notice
}
} catch (error) { } catch (error) {
// ** Specific catch block for missing configuration file ** // ** Specific catch block for missing configuration file **
if (error instanceof ConfigurationError) { if (error instanceof ConfigurationError) {

View File

@@ -34,6 +34,30 @@ import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']); const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']);
const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']); const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);
/**
* Display FYI notice about tagged task lists (only if migration occurred)
* @param {Object} data - Data object that may contain _migrationHappened flag
*/
function displayTaggedTasksFYI(data) {
if (isSilentMode() || !data || !data._migrationHappened) return;
console.log(
boxen(
chalk.white.bold('FYI: ') +
chalk.gray('Taskmaster now supports separate task lists per tag. ') +
chalk.cyan(
'Use the --tag flag to create/read/update/filter tasks by tag.'
),
{
padding: { top: 0, bottom: 0, left: 2, right: 2 },
borderColor: 'cyan',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
/** /**
* Display a fancy banner for the CLI * Display a fancy banner for the CLI
*/ */
@@ -1093,6 +1117,9 @@ async function displayNextTask(tasksPath, complexityReportPath = null) {
margin: { top: 1 } margin: { top: 1 }
}) })
); );
// Show FYI notice if migration occurred
displayTaggedTasksFYI(data);
} }
/** /**
@@ -1554,6 +1581,9 @@ async function displayTaskById(
} }
) )
); );
// Show FYI notice if migration occurred
displayTaggedTasksFYI(data);
} }
/** /**
@@ -2452,7 +2482,7 @@ async function displayMultipleTasksSummary(
) )
); );
break; break;
case '5': case '5': {
// Show dependency visualization // Show dependency visualization
console.log(chalk.white.bold('\nDependency Relationships:')); console.log(chalk.white.bold('\nDependency Relationships:'));
let hasDependencies = false; let hasDependencies = false;
@@ -2470,6 +2500,7 @@ async function displayMultipleTasksSummary(
console.log(chalk.gray('No dependencies found for selected tasks')); console.log(chalk.gray('No dependencies found for selected tasks'));
} }
break; break;
}
case '6': case '6':
console.log(chalk.blue(`\n→ Command: task-master generate`)); console.log(chalk.blue(`\n→ Command: task-master generate`));
console.log( console.log(
@@ -2515,6 +2546,7 @@ async function displayMultipleTasksSummary(
// Export UI functions // Export UI functions
export { export {
displayBanner, displayBanner,
displayTaggedTasksFYI,
startLoadingIndicator, startLoadingIndicator,
stopLoadingIndicator, stopLoadingIndicator,
createProgressBar, createProgressBar,

View File

@@ -194,7 +194,7 @@ function log(level, ...args) {
} }
/** /**
* Reads and parses a JSON file * Reads and parses a JSON file with automatic tag migration for tasks.json
* @param {string} filepath - Path to the JSON file * @param {string} filepath - Path to the JSON file
* @returns {Object|null} Parsed JSON data or null if error occurs * @returns {Object|null} Parsed JSON data or null if error occurs
*/ */
@@ -212,7 +212,103 @@ function readJSON(filepath) {
try { try {
const rawData = fs.readFileSync(filepath, 'utf8'); const rawData = fs.readFileSync(filepath, 'utf8');
return JSON.parse(rawData); let data = JSON.parse(rawData);
// Silent migration for tasks.json files: Transform old format to tagged format
// Only migrate if: 1) has "tasks" array at top level, 2) no "master" key exists
// 3) filepath indicates this is likely a tasks.json file
const isTasksFile =
filepath.includes('tasks.json') ||
path.basename(filepath) === 'tasks.json';
if (
data &&
data.tasks &&
Array.isArray(data.tasks) &&
!data.master &&
isTasksFile
) {
// Migrate from old format { "tasks": [...] } to new format { "master": { "tasks": [...] } }
const migratedData = {
master: {
tasks: data.tasks
}
};
// Write the migrated format back using writeJSON for consistency
try {
writeJSON(filepath, migratedData);
if (isDebug) {
log(
'debug',
`Silently migrated tasks.json to tagged format: ${filepath}`
);
}
// Set global flag for CLI notice
global.taskMasterMigrationOccurred = true;
// Also perform complete project migration (config.json and state.json)
performCompleteTagMigration(filepath);
// Return the migrated data
data = migratedData;
} catch (writeError) {
// If we can't write back, log the error but continue with migrated data in memory
if (isDebug) {
log(
'warn',
`Could not write migrated tasks.json to ${filepath}: ${writeError.message}`
);
}
// Set global flag even on write failure
global.taskMasterMigrationOccurred = true;
// Still attempt other migrations
performCompleteTagMigration(filepath);
data = migratedData;
}
}
// Tag resolution: If data has tagged format, resolve the current tag and return old format
// This makes tag support completely transparent to existing code
if (data && !data.tasks && typeof data === 'object') {
// Check if this looks like tagged format (has tag-like keys)
const hasTaggedFormat = Object.keys(data).some(
(key) => data[key] && data[key].tasks && Array.isArray(data[key].tasks)
);
if (hasTaggedFormat) {
// This is tagged format - resolve which tag to use
// Derive project root from filepath to get correct tag context
const projectRoot =
findProjectRoot(path.dirname(filepath)) || path.dirname(filepath);
const resolvedTag = resolveTag({ projectRoot });
if (data[resolvedTag] && data[resolvedTag].tasks) {
// Return data in old format so existing code continues to work
data = {
tag: resolvedTag,
tasks: data[resolvedTag].tasks,
_rawTaggedData: data // Keep reference to full tagged data if needed
};
} else {
// Tag doesn't exist, create empty tasks array and log warning
if (isDebug) {
log(
'warn',
`Tag "${resolvedTag}" not found in tasks file, using empty tasks array`
);
}
data = { tasks: [], tag: resolvedTag, _rawTaggedData: data };
}
}
}
return data;
} catch (error) { } catch (error) {
log('error', `Error reading JSON file ${filepath}:`, error.message); log('error', `Error reading JSON file ${filepath}:`, error.message);
if (isDebug) { if (isDebug) {
@@ -224,6 +320,145 @@ function readJSON(filepath) {
} }
} }
/**
* Performs complete tag migration including config.json and state.json updates
* @param {string} tasksJsonPath - Path to the tasks.json file that was migrated
*/
function performCompleteTagMigration(tasksJsonPath) {
try {
// Derive project root from tasks.json path
const projectRoot =
findProjectRoot(path.dirname(tasksJsonPath)) ||
path.dirname(tasksJsonPath);
// 1. Migrate config.json - add defaultTag and tags section
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
migrateConfigJson(configPath);
}
// 2. Create state.json if it doesn't exist
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
if (getDebugFlag()) {
log(
'debug',
`Complete tag migration performed for project: ${projectRoot}`
);
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error during complete tag migration: ${error.message}`);
}
}
}
/**
* Migrates config.json to add tagged task system configuration
* @param {string} configPath - Path to the config.json file
*/
function migrateConfigJson(configPath) {
try {
const configData = readJSON(configPath);
if (!configData) return;
let needsUpdate = false;
// Add defaultTag to global section if missing
if (!configData.global) {
configData.global = {};
}
if (!configData.global.defaultTag) {
configData.global.defaultTag = 'master';
needsUpdate = true;
}
// Add tags section if missing
if (!configData.tags) {
configData.tags = {
autoSwitchOnBranch: false,
gitIntegration: {
enabled: false,
autoSwitchTagWithBranch: false
}
};
needsUpdate = true;
}
if (needsUpdate) {
writeJSON(configPath, configData);
if (getDebugFlag()) {
log(
'debug',
`Migrated config.json with tagged task system configuration`
);
}
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error migrating config.json: ${error.message}`);
}
}
}
/**
* Creates initial state.json file for tagged task system
* @param {string} statePath - Path where state.json should be created
*/
function createStateJson(statePath) {
try {
const initialState = {
currentTag: 'master',
lastSwitched: new Date().toISOString(),
branchTagMapping: {},
migrationNoticeShown: false
};
writeJSON(statePath, initialState);
if (getDebugFlag()) {
log('debug', `Created initial state.json for tagged task system`);
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error creating state.json: ${error.message}`);
}
}
}
/**
* Marks in state.json that migration occurred and notice should be shown
* @param {string} tasksJsonPath - Path to the tasks.json file that was migrated
*/
function markMigrationForNotice(tasksJsonPath) {
try {
const projectRoot =
findProjectRoot(path.dirname(tasksJsonPath)) ||
path.dirname(tasksJsonPath);
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
// Ensure state.json exists
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
// Read and update state to mark migration occurred
const stateData = readJSON(statePath) || {};
if (stateData.migrationNoticeShown !== false) {
// Set to false to trigger notice display
stateData.migrationNoticeShown = false;
writeJSON(statePath, stateData);
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error marking migration for notice: ${error.message}`);
}
}
}
/** /**
* Writes data to a JSON file * Writes data to a JSON file
* @param {string} filepath - Path to the JSON file * @param {string} filepath - Path to the JSON file
@@ -661,6 +896,101 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
return aggregated; return aggregated;
} }
/**
* Gets the current tag from state.json or falls back to defaultTag from config
* @param {string} projectRoot - The project root directory
* @returns {string} The current tag name
*/
function getCurrentTag(projectRoot = process.cwd()) {
try {
// Try to read current tag from state.json
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(statePath)) {
const stateData = JSON.parse(fs.readFileSync(statePath, 'utf8'));
if (stateData && stateData.currentTag) {
return stateData.currentTag;
}
}
} catch (error) {
// Ignore errors, fall back to default
}
// Fall back to defaultTag from config or hardcoded default
try {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag;
}
}
} catch (error) {
// Ignore errors, use hardcoded default
}
// Final fallback
return 'master';
}
/**
* Resolves which tag to use based on context
* @param {Object} options - Options object
* @param {string} [options.tag] - Explicit tag from --tag flag
* @param {string} [options.projectRoot] - Project root directory
* @returns {string} The resolved tag name
*/
function resolveTag(options = {}) {
// Priority: explicit tag > current tag from state > defaultTag from config > 'master'
if (options.tag) {
return options.tag;
}
return getCurrentTag(options.projectRoot);
}
/**
* Gets the tasks array for a specific tag from tagged tasks.json data
* @param {Object} data - The parsed tasks.json data (after migration)
* @param {string} tagName - The tag name to get tasks for
* @returns {Array} The tasks array for the specified tag, or empty array if not found
*/
function getTasksForTag(data, tagName) {
if (!data || !tagName) {
return [];
}
// Handle migrated format: { "master": { "tasks": [...] }, "otherTag": { "tasks": [...] } }
if (
data[tagName] &&
data[tagName].tasks &&
Array.isArray(data[tagName].tasks)
) {
return data[tagName].tasks;
}
return [];
}
/**
* Sets the tasks array for a specific tag in the data structure
* @param {Object} data - The tasks.json data object
* @param {string} tagName - The tag name to set tasks for
* @param {Array} tasks - The tasks array to set
* @returns {Object} The updated data object
*/
function setTasksForTag(data, tagName, tasks) {
if (!data) {
data = {};
}
if (!data[tagName]) {
data[tagName] = {};
}
data[tagName].tasks = tasks || [];
return data;
}
// Export all utility functions and configuration // Export all utility functions and configuration
export { export {
LOG_LEVELS, LOG_LEVELS,
@@ -684,5 +1014,13 @@ export {
addComplexityToTask, addComplexityToTask,
resolveEnvVariable, resolveEnvVariable,
findProjectRoot, findProjectRoot,
aggregateTelemetry aggregateTelemetry,
getCurrentTag,
resolveTag,
getTasksForTag,
setTasksForTag,
performCompleteTagMigration,
migrateConfigJson,
createStateJson,
markMigrationForNotice
}; };