aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
305 lines • 10.7 kB
JavaScript
/**
* Artifact Index Types
*
* Shared TypeScript types for the artifact indexing system.
* Used by index-builder, query-engine, dep-graph, and stats modules.
*
* @implements #420
* @source @src/artifacts/cli.ts
* @tests @test/unit/artifacts/index-builder.test.ts
*/
import fs from 'fs';
import { load as loadYaml } from 'js-yaml';
/**
* Normalize a raw edge value to TypedEdge.
* Handles backward compatibility: plain strings become { path, type: "depends-on" }.
*/
export function normalizeEdge(edge) {
if (typeof edge === 'string')
return { path: edge, type: 'depends-on' };
return edge;
}
/**
* Normalize an array of raw edges to TypedEdge[].
*/
export function normalizeEdges(edges) {
return edges.map(normalizeEdge);
}
/**
* Phase name to directory mapping
*/
export const PHASE_DIRECTORIES = {
requirements: '.aiwg/requirements',
architecture: '.aiwg/architecture',
testing: '.aiwg/testing',
security: '.aiwg/security',
deployment: '.aiwg/deployment',
risks: '.aiwg/risks',
planning: '.aiwg/planning',
intake: '.aiwg/intake',
reports: '.aiwg/reports',
};
/**
* Default index output directory
*/
export const INDEX_DIR = '.aiwg/.index';
/**
* Current index format version
*/
export const INDEX_VERSION = '1.0.0';
/**
* Built-in graph definitions
*/
export const BUILTIN_GRAPH_CONFIGS = {
framework: {
type: 'framework',
// Includes `agentic/code/extensions` and `agentic/code/behaviors`
// (#1221) so extension bundles (sys, net, it, sec, stream, dev) and
// top-level behaviors (concierge, security-sentinel, ...) appear in
// `aiwg discover` alongside frameworks and addons. `inferType()` in
// index-builder.ts uses nearest-type-dir ancestor matching to handle
// every nested layout (slug vs flat, frameworks/<f>/extensions/<sub>,
// research-complete/elaboration/{agents,commands}, etc.).
scanDirs: [
'agentic/code/frameworks',
'agentic/code/addons',
'agentic/code/extensions',
'agentic/code/agents',
'agentic/code/behaviors',
'docs',
],
extensions: ['.md', '.yaml', '.json'],
shared: true,
// Not built by `aiwg index build` (no flag) — that would write to
// the shared XDG location from any project. Freshness is guaranteed
// by the explicit post-deploy rebuild in `useHandler` (#1212/#1214)
// and by `aiwg index build --graph framework` for manual rebuilds.
defaultBuild: false,
},
project: {
type: 'project',
scanDirs: ['.aiwg'],
extensions: ['.md', '.yaml', '.json'],
shared: false,
defaultBuild: true,
},
codebase: {
type: 'codebase',
scanDirs: ['src', 'test', 'tools'],
extensions: ['.ts', '.mts', '.js', '.mjs', '.json', '.yaml'],
shared: false,
defaultBuild: true,
},
};
/**
* Mutable graph configs — starts with built-ins, extended by user config
*
* @implements #426
*/
export const GRAPH_CONFIGS = { ...BUILTIN_GRAPH_CONFIGS };
/**
* Normalize metadataSupplements entries.
*
* Accepts the shorthand `match: "frontmatter.<field>"` and expands it to
* `matchOn` + `nodeKey`. This lets users write the compact form in config:
*
* match: frontmatter.ref
*
* instead of the explicit two-field form:
*
* matchOn: frontmatter.ref
* nodeKey: ref
*
* @implements #738
*/
function normalizeSupplements(raw) {
return raw.map((entry) => {
let matchOn = entry.matchOn;
let nodeKey = entry.nodeKey;
// Accept "match" shorthand: derive matchOn and nodeKey from it
if (!matchOn && typeof entry.match === 'string') {
matchOn = entry.match;
}
// Derive nodeKey from matchOn if not explicitly provided
// e.g. "frontmatter.ref" -> nodeKey "ref"
if (!nodeKey && matchOn) {
nodeKey = matchOn.replace(/^frontmatter\./, '');
}
return {
scanDir: entry.scanDir,
matchOn: matchOn ?? '',
nodeKey: nodeKey ?? '',
mergeFields: Array.isArray(entry.mergeFields) ? entry.mergeFields : [],
};
});
}
/**
* Parse a raw graph definition object into a GraphConfig.
*
* Shared between loadUserGraphConfigs and loadModuleGraphConfigs.
*/
function parseGraphDef(name, graphDef) {
if (!Array.isArray(graphDef.scanDirs))
return null;
return {
type: name,
scanDirs: graphDef.scanDirs,
extensions: Array.isArray(graphDef.extensions) ? graphDef.extensions : ['.md', '.yaml', '.json'],
shared: graphDef.shared === true,
defaultBuild: graphDef.defaultBuild !== false,
edgeExtraction: graphDef.edgeExtraction,
nodeStrategy: graphDef.nodeStrategy,
filenamePattern: typeof graphDef.filenamePattern === 'string' ? graphDef.filenamePattern : undefined,
metadataSupplements: Array.isArray(graphDef.metadataSupplements)
? normalizeSupplements(graphDef.metadataSupplements)
: undefined,
graphBackend: typeof graphDef.graphBackend === 'string'
? graphDef.graphBackend
: undefined,
};
}
/**
* Load graph configs declared in framework/addon manifest.json files.
*
* Reads `.aiwg/frameworks/registry.json` to find installed modules,
* then loads each module's manifest and merges `index.graphs` declarations
* into GRAPH_CONFIGS. Module-declared graphs cannot override built-in names.
*
* This runs before operator config so that .aiwg/config.yaml can override
* module-declared graphs.
*
* @param cwd - Project root directory
* @returns Names of module-declared graphs that were loaded
*
* @implements #726
*/
export function loadModuleGraphConfigs(cwd) {
const registryPath = `${cwd}/.aiwg/frameworks/registry.json`;
const loaded = [];
try {
if (!fs.existsSync(registryPath))
return loaded;
const registryContent = fs.readFileSync(registryPath, 'utf-8');
const registry = JSON.parse(registryContent);
if (!Array.isArray(registry.frameworks))
return loaded;
// Search paths for manifest.json (framework source locations)
const searchRoots = [
`${cwd}/agentic/code/frameworks`,
`${cwd}/agentic/code/addons`,
];
for (const entry of registry.frameworks) {
const id = entry.id;
let manifestData = null;
// Try each search root to find the manifest
for (const root of searchRoots) {
const manifestPath = `${root}/${id}/manifest.json`;
if (fs.existsSync(manifestPath)) {
try {
manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
}
catch {
// Malformed manifest — skip
}
break;
}
}
if (!manifestData)
continue;
// Extract index.graphs from manifest
const indexSection = manifestData.index;
if (!indexSection || typeof indexSection !== 'object')
continue;
const graphs = indexSection.graphs;
if (!graphs || typeof graphs !== 'object')
continue;
for (const [name, def] of Object.entries(graphs)) {
if (name in BUILTIN_GRAPH_CONFIGS)
continue;
// Module graphs don't override already-loaded graphs (first module wins)
if (name in GRAPH_CONFIGS && !(name in BUILTIN_GRAPH_CONFIGS))
continue;
const config = parseGraphDef(name, def);
if (config) {
GRAPH_CONFIGS[name] = config;
loaded.push(name);
}
}
}
}
catch {
// Module config loading is best-effort
}
return loaded;
}
/**
* Load user-defined graph configs from .aiwg/config.yaml
*
* Also loads module-declared graphs from installed framework manifests.
* Module graphs are loaded first; operator config overrides them.
* Neither can override built-in graph names.
*
* @param cwd - Project root directory
* @returns Names of user-defined graphs that were loaded (includes module graphs)
*
* @implements #426 #726
*/
export function loadUserGraphConfigs(cwd) {
// Load module-declared graphs first (frameworks/addons)
const moduleLoaded = loadModuleGraphConfigs(cwd);
const configPath = `${cwd}/.aiwg/config.yaml`;
const loaded = [...moduleLoaded];
try {
if (!fs.existsSync(configPath))
return loaded;
const content = fs.readFileSync(configPath, 'utf-8');
const config = loadYaml(content);
if (!config || typeof config !== 'object')
return loaded;
const indexConfig = config.index;
if (!indexConfig || typeof indexConfig !== 'object')
return loaded;
const graphs = indexConfig.graphs;
if (!graphs || typeof graphs !== 'object')
return loaded;
for (const [name, def] of Object.entries(graphs)) {
if (name in BUILTIN_GRAPH_CONFIGS) {
// Cannot override built-in graph names
continue;
}
const graphConfig = parseGraphDef(name, def);
if (!graphConfig)
continue;
// Operator config overrides module-declared graphs (emit warning if overriding)
if (moduleLoaded.includes(name)) {
// Operator override of a module-declared graph
GRAPH_CONFIGS[name] = graphConfig;
// Already in loaded list from module phase
}
else {
GRAPH_CONFIGS[name] = graphConfig;
loaded.push(name);
}
}
}
catch {
// Config loading is best-effort
}
return loaded;
}
/**
* Get the index output directory for a given graph type
*
* @param cwd - Project root
* @param graphType - Graph type
* @returns Absolute path to the graph's index directory
*/
export function getGraphIndexDir(cwd, graphType) {
if (graphType === 'framework') {
// Shared across projects — XDG data directory
const xdgData = process.env.XDG_DATA_HOME ?? `${process.env.HOME}/.local/share`;
return `${xdgData}/aiwg/index/framework`;
}
return `${cwd}/.aiwg/.index/${graphType}`;
}
//# sourceMappingURL=types.js.map