Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f5ee3de
chore: remove dead exports and un-export internal constant
carlos-alm Mar 17, 2026
f236b55
Merge remote-tracking branch 'origin/main' into worktree-titan-recon
carlos-alm Mar 17, 2026
17cdcb0
refactor: extract shared findNodes utility from cfg and dataflow feat…
carlos-alm Mar 17, 2026
a09740d
fix: replace empty catch blocks in db connection and migrations
carlos-alm Mar 17, 2026
b691fcc
fix: replace empty catch blocks in domain analysis layer
carlos-alm Mar 17, 2026
dadb383
fix: replace empty catch blocks in parser.js
carlos-alm Mar 17, 2026
22d94f4
fix: replace empty catch blocks in features layer
carlos-alm Mar 17, 2026
3b36534
refactor: decompose extractSymbolsWalk into per-category handlers
carlos-alm Mar 17, 2026
e1d7ee0
refactor: decompose extractPythonSymbols into per-category handlers
carlos-alm Mar 17, 2026
3a656bb
refactor: decompose extractJavaSymbols into per-category handlers
carlos-alm Mar 17, 2026
bf5b986
refactor: decompose remaining language extractors
carlos-alm Mar 17, 2026
eafdf19
refactor: decompose AST analysis visitors and engine into focused hel…
carlos-alm Mar 17, 2026
46a95ae
refactor: decompose domain builder stages into focused helpers
carlos-alm Mar 17, 2026
0a3fbc7
refactor: decompose domain analysis functions into focused helpers
carlos-alm Mar 17, 2026
b2f89f1
refactor: decompose buildComplexityMetrics
carlos-alm Mar 17, 2026
cb82258
refactor: decompose buildStructure into traversal, cohesion, and clas…
carlos-alm Mar 17, 2026
54b0067
refactor: decompose buildCFGData and buildDataflowEdges
carlos-alm Mar 17, 2026
7030e7f
refactor: decompose sequenceData into BFS and message construction
carlos-alm Mar 17, 2026
b4d8a0d
refactor: decompose explain() into section renderers
carlos-alm Mar 17, 2026
ae805d5
refactor: decompose stats() into section printers
carlos-alm Mar 17, 2026
3aa2e4b
fix: address quality issues in features (boundaries, communities, tri…
carlos-alm Mar 17, 2026
4bb2ea5
Merge remote-tracking branch 'origin/main' into fix/review-492
carlos-alm Mar 17, 2026
330d33a
fix: include remaining merge changes from main
carlos-alm Mar 17, 2026
fec9ac3
fix: resolve merge conflicts with base branch
carlos-alm Mar 17, 2026
bb2729e
Merge remote-tracking branch 'origin/refactor/titan-domain-features' …
carlos-alm Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 172 additions & 189 deletions src/domain/analysis/context.js

Large diffs are not rendered by default.

346 changes: 200 additions & 146 deletions src/domain/analysis/dependencies.js

Large diffs are not rendered by default.

418 changes: 266 additions & 152 deletions src/domain/analysis/impact.js

Large diffs are not rendered by default.

467 changes: 246 additions & 221 deletions src/domain/analysis/module-map.js

Large diffs are not rendered by default.

181 changes: 98 additions & 83 deletions src/features/boundaries.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,104 +94,119 @@ export function resolveModules(boundaryConfig) {
// ─── Validation ──────────────────────────────────────────────────────

/**
* Validate a boundary configuration object.
* @param {object} config - The `manifesto.boundaries` config
* @returns {{ valid: boolean, errors: string[] }}
* Validate the `modules` section of a boundary config.
* @param {object} modules
* @param {string[]} errors - Mutated: push any validation errors
*/
export function validateBoundaryConfig(config) {
const errors = [];
function validateModules(modules, errors) {
if (!modules || typeof modules !== 'object' || Object.keys(modules).length === 0) {
errors.push('boundaries.modules must be a non-empty object');
return;
}
for (const [name, value] of Object.entries(modules)) {
if (typeof value === 'string') continue;
if (value && typeof value === 'object' && typeof value.match === 'string') continue;
errors.push(`boundaries.modules.${name}: must be a glob string or { match: "<glob>" }`);
}
}

if (!config || typeof config !== 'object') {
return { valid: false, errors: ['boundaries config must be an object'] };
/**
* Validate the `preset` field of a boundary config.
* @param {string|null|undefined} preset
* @param {string[]} errors - Mutated: push any validation errors
*/
function validatePreset(preset, errors) {
if (preset == null) return;
if (typeof preset !== 'string' || !PRESETS[preset]) {
errors.push(
`boundaries.preset: must be one of ${Object.keys(PRESETS).join(', ')} (got "${preset}")`,
);
}
}

// Validate modules
if (
!config.modules ||
typeof config.modules !== 'object' ||
Object.keys(config.modules).length === 0
) {
errors.push('boundaries.modules must be a non-empty object');
} else {
for (const [name, value] of Object.entries(config.modules)) {
if (typeof value === 'string') continue;
if (value && typeof value === 'object' && typeof value.match === 'string') continue;
errors.push(`boundaries.modules.${name}: must be a glob string or { match: "<glob>" }`);
/**
* Validate a single rule's target list (`notTo` or `onlyTo`).
* @param {*} list - The target list value
* @param {string} field - "notTo" or "onlyTo"
* @param {number} idx - Rule index for error messages
* @param {Set<string>} moduleNames
* @param {string[]} errors - Mutated
*/
function validateTargetList(list, field, idx, moduleNames, errors) {
if (!Array.isArray(list)) {
errors.push(`boundaries.rules[${idx}]: "${field}" must be an array`);
return;
}
for (const target of list) {
if (!moduleNames.has(target)) {
errors.push(`boundaries.rules[${idx}]: "${field}" references unknown module "${target}"`);
}
}
}

// Validate preset
if (config.preset != null) {
if (typeof config.preset !== 'string' || !PRESETS[config.preset]) {
errors.push(
`boundaries.preset: must be one of ${Object.keys(PRESETS).join(', ')} (got "${config.preset}")`,
);
/**
* Validate the `rules` array of a boundary config.
* @param {Array} rules
* @param {object|undefined} modules - The modules config (for cross-referencing names)
* @param {string[]} errors - Mutated
*/
function validateRules(rules, modules, errors) {
if (!rules) return;
if (!Array.isArray(rules)) {
errors.push('boundaries.rules must be an array');
return;
}
const moduleNames = modules ? new Set(Object.keys(modules)) : new Set();
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (!rule.from) {
errors.push(`boundaries.rules[${i}]: missing "from" field`);
} else if (!moduleNames.has(rule.from)) {
errors.push(`boundaries.rules[${i}]: "from" references unknown module "${rule.from}"`);
}
if (rule.notTo && rule.onlyTo) {
errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
}
if (!rule.notTo && !rule.onlyTo) {
errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
}
if (rule.notTo) validateTargetList(rule.notTo, 'notTo', i, moduleNames, errors);
if (rule.onlyTo) validateTargetList(rule.onlyTo, 'onlyTo', i, moduleNames, errors);
}
}

// Validate rules
if (config.rules) {
if (!Array.isArray(config.rules)) {
errors.push('boundaries.rules must be an array');
} else {
const moduleNames = config.modules ? new Set(Object.keys(config.modules)) : new Set();
for (let i = 0; i < config.rules.length; i++) {
const rule = config.rules[i];
if (!rule.from) {
errors.push(`boundaries.rules[${i}]: missing "from" field`);
} else if (!moduleNames.has(rule.from)) {
errors.push(`boundaries.rules[${i}]: "from" references unknown module "${rule.from}"`);
}
if (rule.notTo && rule.onlyTo) {
errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
}
if (!rule.notTo && !rule.onlyTo) {
errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
}
if (rule.notTo) {
if (!Array.isArray(rule.notTo)) {
errors.push(`boundaries.rules[${i}]: "notTo" must be an array`);
} else {
for (const target of rule.notTo) {
if (!moduleNames.has(target)) {
errors.push(
`boundaries.rules[${i}]: "notTo" references unknown module "${target}"`,
);
}
}
}
}
if (rule.onlyTo) {
if (!Array.isArray(rule.onlyTo)) {
errors.push(`boundaries.rules[${i}]: "onlyTo" must be an array`);
} else {
for (const target of rule.onlyTo) {
if (!moduleNames.has(target)) {
errors.push(
`boundaries.rules[${i}]: "onlyTo" references unknown module "${target}"`,
);
}
}
}
}
}
/**
* Validate that module layer assignments match preset layers.
* @param {object} config
* @param {string[]} errors - Mutated
*/
function validateLayerAssignments(config, errors) {
if (!config.preset || !PRESETS[config.preset] || !config.modules) return;
const presetLayers = new Set(PRESETS[config.preset].layers);
for (const [name, value] of Object.entries(config.modules)) {
if (typeof value === 'object' && value.layer && !presetLayers.has(value.layer)) {
errors.push(
`boundaries.modules.${name}: layer "${value.layer}" not in preset "${config.preset}" (valid: ${[...presetLayers].join(', ')})`,
);
}
}
}

// Validate preset + layer assignments
if (config.preset && PRESETS[config.preset] && config.modules) {
const presetLayers = new Set(PRESETS[config.preset].layers);
for (const [name, value] of Object.entries(config.modules)) {
if (typeof value === 'object' && value.layer) {
if (!presetLayers.has(value.layer)) {
errors.push(
`boundaries.modules.${name}: layer "${value.layer}" not in preset "${config.preset}" (valid: ${[...presetLayers].join(', ')})`,
);
}
}
}
/**
* Validate a boundary configuration object.
* @param {object} config - The `manifesto.boundaries` config
* @returns {{ valid: boolean, errors: string[] }}
*/
export function validateBoundaryConfig(config) {
if (!config || typeof config !== 'object') {
return { valid: false, errors: ['boundaries config must be an object'] };
}

const errors = [];
validateModules(config.modules, errors);
validatePreset(config.preset, errors);
validateRules(config.rules, config.modules, errors);
validateLayerAssignments(config, errors);
return { valid: errors.length === 0, errors };
}

Expand Down
Loading
Loading