const fs = require('fs/promises');
const path = require('path');
class MatrixFileSystem {
constructor(basePath = process.cwd()) {
this.basePath = basePath;
this.matrixRoot = path.join(basePath, '.matrix_pattern');
this.matrixDir = path.join(this.matrixRoot, 'matrix');
this.metadataDir = path.join(this.matrixRoot, 'metadata');
this.horizontalsDir = path.join(this.metadataDir, 'horizontals');
this.syncReportsDir = path.join(this.metadataDir, 'sync-reports');
this.verticalsFile = path.join(this.metadataDir, 'verticals.json');
}
/**
* Initialize the matrix directory structure
*/
async initialize() {
try {
// Create base directories
await fs.mkdir(this.matrixRoot, { recursive: true });
await fs.mkdir(this.matrixDir, { recursive: true });
await fs.mkdir(this.metadataDir, { recursive: true });
await fs.mkdir(this.horizontalsDir, { recursive: true });
await fs.mkdir(this.syncReportsDir, { recursive: true });
// Initialize verticals.json if it doesn't exist
try {
await fs.access(this.verticalsFile);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.writeFile(this.verticalsFile, JSON.stringify([], null, 2));
}
}
} catch (error) {
throw new Error(`Failed to initialize matrix structure: ${error.message}`);
}
}
/**
* Create a markdown cell with content
* @param {string} vertical - The vertical identifier
* @param {string} horizontal - The horizontal identifier
* @param {string} content - The markdown content
*/
async createCell(vertical, horizontal, content) {
const verticalDir = path.join(this.matrixDir, vertical);
const cellFile = path.join(verticalDir, `${horizontal}.md`);
try {
await fs.mkdir(verticalDir, { recursive: true });
await fs.writeFile(cellFile, content, 'utf8');
// Register the vertical if it doesn't exist
await this.registerVertical(vertical);
} catch (error) {
throw new Error(`Failed to create cell [${vertical}, ${horizontal}]: ${error.message}`);
}
}
/**
* Create an empty cell marker
* @param {string} vertical - The vertical identifier
* @param {string} horizontal - The horizontal identifier
* @param {string} reason - Reason why the cell is empty
*/
async createEmptyCell(vertical, horizontal, reason = '') {
const verticalDir = path.join(this.matrixDir, vertical);
const emptyFile = path.join(verticalDir, `${horizontal}.empty`);
try {
await fs.mkdir(verticalDir, { recursive: true });
await fs.writeFile(emptyFile, reason, 'utf8');
// Register the vertical if it doesn't exist
await this.registerVertical(vertical);
} catch (error) {
throw new Error(`Failed to create empty cell [${vertical}, ${horizontal}]: ${error.message}`);
}
}
/**
* Read cell content
* @param {string} vertical - The vertical identifier
* @param {string} horizontal - The horizontal identifier
* @returns {Object} Cell data with content, isEmpty, and reason properties
*/
async readCell(vertical, horizontal) {
const verticalDir = path.join(this.matrixDir, vertical);
const cellFile = path.join(verticalDir, `${horizontal}.md`);
const emptyFile = path.join(verticalDir, `${horizontal}.empty`);
try {
// Try to read markdown file first
try {
const content = await fs.readFile(cellFile, 'utf8');
return {
content,
isEmpty: false,
reason: null
};
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
// Try to read empty file
try {
const reason = await fs.readFile(emptyFile, 'utf8');
return {
content: null,
isEmpty: true,
reason
};
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
// Cell doesn't exist
return {
content: null,
isEmpty: false,
reason: null
};
} catch (error) {
throw new Error(`Failed to read cell [${vertical}, ${horizontal}]: ${error.message}`);
}
}
/**
* Check if a cell exists (either as markdown or empty)
* @param {string} vertical - The vertical identifier
* @param {string} horizontal - The horizontal identifier
* @returns {boolean} True if cell exists
*/
async cellExists(vertical, horizontal) {
const verticalDir = path.join(this.matrixDir, vertical);
const cellFile = path.join(verticalDir, `${horizontal}.md`);
const emptyFile = path.join(verticalDir, `${horizontal}.empty`);
try {
await fs.access(cellFile);
return true;
} catch (error) {
if (error.code !== 'ENOENT') {
throw new Error(`Failed to check cell existence: ${error.message}`);
}
}
try {
await fs.access(emptyFile);
return true;
} catch (error) {
if (error.code !== 'ENOENT') {
throw new Error(`Failed to check empty cell existence: ${error.message}`);
}
}
return false;
}
/**
* List all registered verticals
* @returns {Array<string>} Array of vertical identifiers
*/
async listVerticals() {
try {
const content = await fs.readFile(this.verticalsFile, 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
return [];
}
throw new Error(`Failed to read verticals list: ${error.message}`);
}
}
/**
* List all horizontals by scanning matrix directories
* @returns {Array<string>} Array of horizontal identifiers
*/
async listHorizontals() {
try {
const verticals = await this.listVerticals();
const horizontalsSet = new Set();
for (const vertical of verticals) {
const verticalDir = path.join(this.matrixDir, vertical);
try {
const files = await fs.readdir(verticalDir);
for (const file of files) {
// Extract horizontal from filename (remove .md or .empty extension)
if (file.endsWith('.md')) {
horizontalsSet.add(file.slice(0, -3));
} else if (file.endsWith('.empty')) {
horizontalsSet.add(file.slice(0, -6));
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
// Directory doesn't exist, skip
}
}
return Array.from(horizontalsSet).sort();
} catch (error) {
throw new Error(`Failed to list horizontals: ${error.message}`);
}
}
/**
* Register a vertical in verticals.json
* @param {string} vertical - The vertical identifier to register
*/
async registerVertical(vertical) {
try {
const verticals = await this.listVerticals();
if (!verticals.includes(vertical)) {
verticals.push(vertical);
verticals.sort(); // Keep sorted
await fs.writeFile(this.verticalsFile, JSON.stringify(verticals, null, 2));
}
} catch (error) {
throw new Error(`Failed to register vertical '${vertical}': ${error.message}`);
}
}
/**
* Read horizontal instructions
* @param {string} horizontal - The horizontal identifier
* @returns {string|null} Instructions content or null if not found
*/
async readHorizontalInstructions(horizontal) {
const instructionsFile = path.join(this.horizontalsDir, horizontal, 'instructions.md');
try {
return await fs.readFile(instructionsFile, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw new Error(`Failed to read horizontal instructions for '${horizontal}': ${error.message}`);
}
}
/**
* Write horizontal instructions
* @param {string} horizontal - The horizontal identifier
* @param {string} content - The instructions content
*/
async writeHorizontalInstructions(horizontal, content) {
const horizontalDir = path.join(this.horizontalsDir, horizontal);
const instructionsFile = path.join(horizontalDir, 'instructions.md');
try {
await fs.mkdir(horizontalDir, { recursive: true });
await fs.writeFile(instructionsFile, content, 'utf8');
} catch (error) {
throw new Error(`Failed to write horizontal instructions for '${horizontal}': ${error.message}`);
}
}
/**
* Get all cells for a specific horizontal
* @param {string} horizontal - The horizontal identifier
* @returns {Array<Object>} Array of cell objects with vertical, content, isEmpty, reason
*/
async getCellsForHorizontal(horizontal) {
try {
const verticals = await this.listVerticals();
const cells = [];
for (const vertical of verticals) {
const cellData = await this.readCell(vertical, horizontal);
if (cellData.content !== null || cellData.isEmpty) {
cells.push({
vertical,
horizontal,
content: cellData.content,
isEmpty: cellData.isEmpty,
reason: cellData.reason
});
}
}
return cells;
} catch (error) {
throw new Error(`Failed to get cells for horizontal '${horizontal}': ${error.message}`);
}
}
/**
* Get all cells for a specific vertical
* @param {string} vertical - The vertical identifier
* @returns {Array<Object>} Array of cell objects with horizontal, content, isEmpty, reason
*/
async getCellsForVertical(vertical) {
try {
const verticalDir = path.join(this.matrixDir, vertical);
const cells = [];
try {
const files = await fs.readdir(verticalDir);
for (const file of files) {
let horizontal;
if (file.endsWith('.md')) {
horizontal = file.slice(0, -3);
} else if (file.endsWith('.empty')) {
horizontal = file.slice(0, -6);
} else {
continue; // Skip non-matrix files
}
const cellData = await this.readCell(vertical, horizontal);
cells.push({
vertical,
horizontal,
content: cellData.content,
isEmpty: cellData.isEmpty,
reason: cellData.reason
});
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
// Directory doesn't exist, return empty array
}
return cells.sort((a, b) => a.horizontal.localeCompare(b.horizontal));
} catch (error) {
throw new Error(`Failed to get cells for vertical '${vertical}': ${error.message}`);
}
}
/**
* Delete a cell (both markdown and empty versions)
* @param {string} vertical - The vertical identifier
* @param {string} horizontal - The horizontal identifier
*/
async deleteCell(vertical, horizontal) {
const verticalDir = path.join(this.matrixDir, vertical);
const cellFile = path.join(verticalDir, `${horizontal}.md`);
const emptyFile = path.join(verticalDir, `${horizontal}.empty`);
try {
// Try to delete markdown file
try {
await fs.unlink(cellFile);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
// Try to delete empty file
try {
await fs.unlink(emptyFile);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
} catch (error) {
throw new Error(`Failed to delete cell [${vertical}, ${horizontal}]: ${error.message}`);
}
}
/**
* Get matrix statistics
* @returns {Object} Statistics about the matrix
*/
async getMatrixStats() {
try {
const verticals = await this.listVerticals();
const horizontals = await this.listHorizontals();
let totalCells = 0;
let filledCells = 0;
let emptyCells = 0;
for (const vertical of verticals) {
for (const horizontal of horizontals) {
if (await this.cellExists(vertical, horizontal)) {
totalCells++;
const cellData = await this.readCell(vertical, horizontal);
if (cellData.isEmpty) {
emptyCells++;
} else {
filledCells++;
}
}
}
}
return {
totalVerticals: verticals.length,
totalHorizontals: horizontals.length,
totalPossibleCells: verticals.length * horizontals.length,
totalExistingCells: totalCells,
filledCells,
emptyCells,
completionRate: verticals.length * horizontals.length > 0 ?
(totalCells / (verticals.length * horizontals.length) * 100).toFixed(2) + '%' : '0%'
};
} catch (error) {
throw new Error(`Failed to get matrix statistics: ${error.message}`);
}
}
}
module.exports = { MatrixFileSystem };