x, enabling SQL queries for reporting.
4. CI Integration: Validation runs as a pre-commit and PR check to enforce schema compliance.
Step-by-Step Implementation
1. Define the Product Schema
Create a strict schema that captures the dimensions of the digital asset matrix: identity, lifecycle, assets, dependencies, and policies.
// src/schema/product-manifest.ts
import { z } from 'zod';
export const ProductManifestSchema = z.object({
apiVersion: z.literal('cc20-3-1-digital-asset-matrix/v1'),
kind: z.literal('Product'),
metadata: z.object({
id: z.string().regex(/^[a-z0-9-]+$/),
name: z.string().min(3),
owner: z.object({
team: z.string(),
slackChannel: z.string().optional(),
email: z.string().email(),
}),
lifecycle: z.enum(['experimental', 'active', 'maintenance', 'deprecated']),
tags: z.record(z.string()).optional(),
}),
assets: z.object({
repositories: z.array(z.string().url()),
infra: z.object({
provider: z.enum(['aws', 'gcp', 'azure', 'k8s']),
region: z.string(),
costCenter: z.string(),
}),
telemetry: z.object({
logs: z.string().url(),
metrics: z.string().url(),
traces: z.string().url().optional(),
}),
}),
dependencies: z.array(z.object({
product_id: z.string(),
type: z.enum(['runtime', 'build', 'data']),
criticality: z.enum(['mandatory', 'optional']),
})).default([]),
policies: z.object({
security_scan: z.boolean().default(true),
performance_budget_ms: z.number().optional(),
sla_percent: z.number().min(90).max(99.999).optional(),
}).default({}),
});
export type ProductManifest = z.infer<typeof ProductManifestSchema>;
2. Build the Portfolio Registry
The registry parses manifests and constructs the matrix. It handles dependency resolution and provides query interfaces.
// src/registry/portfolio-registry.ts
import { ProductManifest, ProductManifestSchema } from './schema/product-manifest';
export class PortfolioRegistry {
private products: Map<string, ProductManifest> = new Map();
private dependencyGraph: Map<string, Set<string>> = new Map();
async ingestManifest(manifest: unknown): Promise<ProductManifest> {
const validated = ProductManifestSchema.parse(manifest);
if (this.products.has(validated.metadata.id)) {
throw new Error(`Duplicate product ID: ${validated.metadata.id}`);
}
this.products.set(validated.metadata.id, validated);
this.updateDependencyGraph(validated);
return validated;
}
private updateDependencyGraph(product: ProductManifest): void {
const deps = new Set<string>();
product.dependencies.forEach(dep => deps.add(dep.product_id));
this.dependencyGraph.set(product.metadata.id, deps);
}
// Query: Get all products with 'active' lifecycle
getActiveProducts(): ProductManifest[] {
return Array.from(this.products.values()).filter(
p => p.metadata.lifecycle === 'active'
);
}
// Query: Detect circular dependencies
detectCircularDependencies(): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const stack = new Set<string>();
const dfs = (node: string, path: string[]) => {
if (stack.has(node)) {
const cycleStart = path.indexOf(node);
cycles.push(path.slice(cycleStart).concat(node));
return;
}
if (visited.has(node)) return;
visited.add(node);
stack.add(node);
path.push(node);
const deps = this.dependencyGraph.get(node) || new Set();
deps.forEach(dep => dfs(dep, [...path]));
stack.delete(node);
};
this.products.forEach((_, id) => dfs(id, []));
return cycles;
}
// Query: Aggregate cost by team
getCostByTeam(): Record<string, number> {
const teamCosts: Record<string, number> = {};
this.products.forEach(p => {
const team = p.metadata.owner.team;
// In production, this would query a cost API using p.assets.infra.costCenter
const estimatedCost = 1000; // Placeholder for API integration
teamCosts[team] = (teamCosts[team] || 0) + estimatedCost;
});
return teamCosts;
}
}
3. CLI Tool for Developers
Provide a CLI to validate manifests locally and query the portfolio.
// src/cli/index.ts
import { Command } from 'commander';
import fs from 'fs/promises';
import { PortfolioRegistry } from '../registry/portfolio-registry';
const program = new Command();
program
.name('portfolio-cli')
.description('Manage your digital product portfolio');
program
.command('validate')
.argument('<path>', 'Path to product-manifest.yaml')
.action(async (path: string) => {
try {
const content = await fs.readFile(path, 'utf-8');
const yaml = await import('js-yaml');
const manifest = yaml.load(content);
const registry = new PortfolioRegistry();
await registry.ingestManifest(manifest);
console.log('✅ Manifest valid and ingested.');
} catch (error) {
console.error('❌ Validation failed:', error.message);
process.exit(1);
}
});
program
.command('graph')
.action(async () => {
// Logic to load all manifests and output DOT graph
console.log('Generating dependency graph...');
});
program.parse();
4. CI/CD Integration
Enforce the matrix in the pipeline.
# .github/workflows/portfolio-check.yaml
name: Portfolio Validation
on:
pull_request:
paths:
- 'product-manifest.yaml'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx portfolio-cli validate ./product-manifest.yaml
- name: Check Dependencies
run: |
# Run dependency check against registry
npm run check:deps
Pitfall Guide
Common Mistakes
-
The YAML Trap: Over-Specification
- Mistake: Defining 50+ fields in the manifest, including runtime configurations or deployment parameters.
- Impact: Developers treat the manifest as a configuration file rather than a portfolio descriptor. Changes become frequent and error-prone.
- Correction: Separate portfolio metadata from deployment configuration. The manifest should describe what the product is, not how to deploy it.
-
Ignoring Dependency Criticality
- Mistake: Listing dependencies without marking criticality.
- Impact: During outages, teams cannot prioritize. A non-critical data dependency failure blocks deployment of a critical service.
- Correction: Enforce
criticality in the schema. Use this data to implement fallback strategies and blast radius analysis.
-
Static Lifecycle States
- Mistake: Setting lifecycle to "active" and never updating it.
- Impact: Technical debt accumulates in "zombie" products. Resources are not reclaimed.
- Correction: Automate lifecycle transitions. If a product has no deployments in 90 days, flag it for
maintenance review.
-
Cost Attribution Without Granularity
- Mistake: Tagging resources at the account level instead of the product level.
- Impact: Cost variance remains high. Teams cannot optimize spend.
- Correction: Mandate
costCenter in the manifest. Use infrastructure-as-code to inject this tag into all resources provisioned for the product.
-
Dependency Drift
- Mistake: Updating code dependencies without updating the manifest.
- Impact: The registry becomes inaccurate. Dependency graphs are useless.
- Correction: Integrate manifest updates into the dependency upgrade workflow. If
package.json changes, the CI should verify the manifest reflects new runtime dependencies.
-
Security Silos
- Mistake: Treating security as a separate scan rather than a portfolio policy.
- Impact: Vulnerabilities are reported per repo, not per product. Risk assessment is fragmented.
- Correction: Aggregate security findings by
product_id. Block deployments if the aggregate product risk exceeds the policy threshold.
-
Neglecting Developer Experience
- Mistake: Building a complex registry with no DX tools.
- Impact: Developers bypass the system. Shadow portfolios emerge.
- Correction: Provide autocomplete, validation, and clear error messages in the CLI. Integrate with IDEs. Make compliance easier than non-compliance.
Best Practices from Production
- Schema Versioning: Always include
apiVersion in the manifest. This allows you to evolve the schema without breaking existing products.
- Read-Only Registry: The registry should be built from manifests. Never allow direct writes to the registry. The source of truth is the Git repository.
- Graph Database for Scale: For portfolios >200 products, migrate the dependency graph to a graph database (Neo4j) for efficient traversal and impact analysis.
- Automated Onboarding: Create a
portfolio init command that scaffolds the manifest with sensible defaults based on detected tech stack.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| < 20 Products | File-based Registry + Local CLI | Low overhead. Git diff provides audit trail. No DB required. | Low |
| 20-100 Products | SQLite Registry + API | Enables querying and reporting. SQLite is sufficient for this scale. | Medium |
| > 100 Products | Postgres/Neo4j + Microservice | High concurrency, complex graph queries, and multi-region support needed. | High |
| Monorepo Structure | Centralized Manifest Directory | Single source of truth. Easy to enforce cross-product policies. | Low |
| Polyrepo Structure | Distributed Manifests + Aggregator | Decouples teams. Aggregator pulls manifests via API or Git. | Medium |
| Regulated Industry | Immutable Registry + Audit Log | Compliance requires tamper-proof records of product states and changes. | High |
Configuration Template
Copy this template to product-manifest.yaml in your repository root.
# product-manifest.yaml
apiVersion: cc20-3-1-digital-asset-matrix/v1
kind: Product
metadata:
id: user-auth-service
name: User Authentication Service
owner:
team: identity-platform
slackChannel: "#eng-identity"
email: team-identity@company.com
lifecycle: active
tags:
tier: critical
data-classification: pii
assets:
repositories:
- https://github.com/company/user-auth-service
infra:
provider: aws
region: us-east-1
costCenter: CC-IDENTITY-001
telemetry:
logs: https://logs.company.com/query?service=user-auth
metrics: https://metrics.company.com/dashboard?service=user-auth
traces: https://traces.company.com/trace?service=user-auth
dependencies:
- product_id: user-db
type: runtime
criticality: mandatory
- product_id: audit-logger
type: data
criticality: optional
policies:
security_scan: true
performance_budget_ms: 200
sla_percent: 99.95
Quick Start Guide
- Initialize: Run
npx @codcompass/portfolio-cli init in your repository. This creates a valid product-manifest.yaml with detected defaults.
- Edit: Fill in
metadata.owner, assets.infra.costCenter, and dependencies.
- Validate: Run
npx @codcompass/portfolio-cli validate ./product-manifest.yaml to ensure schema compliance.
- Commit: Push the manifest to your repository. The CI pipeline will automatically ingest it into the portfolio registry.
- Verify: Query the registry API or dashboard to confirm your product appears in the matrix with correct tags and dependencies.