Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions library/docs/schemas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ published with a guide, examples, invalid fixtures, and an offline CI check.
| `.map/map.config.json` | [`project.schema.json`](../../schemas/project.schema.json) | [Project manifest](project.md) |
| Architecture decision record | [`decision.schema.json`](../../schemas/decision.schema.json) | [Decision records](decision.md) |
| Pattern and anti-pattern metadata | [`pattern.schema.json`](../../schemas/pattern.schema.json) | [Pattern Schema v1](../../../docs/specifications/PATTERN_SCHEMA.md) |
| Static scan result | [`scan-result.schema.json`](../../schemas/scan-result.schema.json) | [Scan result v1](../../../docs/specifications/SCAN_RESULT.md) |
| Pattern suggestions | [`recommendation-result.schema.json`](../../schemas/recommendation-result.schema.json) | [Recommendation result v1](../../../docs/specifications/RECOMMENDATION_RESULT.md) |

Schemas use JSON Schema draft 2020-12. Stable fields are strict; experiments use an
`x-` prefix. A schema change that alters accepted meaning requires compatibility and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"schemaVersion": 1, "kind": "map.recommendation-result", "scan": { "root": "/workspace", "detectedAt": "2026-09-13T00:00:00Z" },
"recommendations": [{ "pattern": "security.injection", "priority": "high", "rationale": "Review it.", "triggeredBy": ["rag"] }],
"limitations": ["Static signals only."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"schemaVersion": 1, "kind": "map.recommendation-result", "scan": { "root": "/workspace", "detectedAt": "2026-09-13T00:00:00Z" },
"recommendations": [{ "pattern": "security/prompt-injection-defense", "priority": "urgent", "rationale": "Review it.", "triggeredBy": ["rag"] }],
"limitations": ["Static signals only."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"schemaVersion": 1, "kind": "map.recommendation-result", "scan": { "root": "/workspace", "detectedAt": "2026-09-13T00:00:00Z" },
"recommendations": [{ "pattern": "security/prompt-injection-defense", "priority": "high", "rationale": "Review it.", "triggeredBy": [] }],
"limitations": ["Static signals only."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"kind": "map.recommendation-result",
"scan": { "root": "/workspace/example", "detectedAt": "2026-09-13T00:00:00.000Z" },
"recommendations": [{ "pattern": "security/prompt-injection-defense", "priority": "high", "rationale": "Retrieved text crosses a trust boundary.", "triggeredBy": ["rag"] }],
"limitations": ["Suggestions are review candidates, not proof of absence."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"schemaVersion": 1, "kind": "map.scan-result", "root": "/workspace", "detectedAt": "2026-09-13T00:00:00Z",
"analyzers": [], "inspected": [], "concepts": [{ "concept": "rag", "confidence": 2, "certainty": "detected", "evidence": ["package.json: x"] }],
"limitations": ["Static signals only."]
}
4 changes: 4 additions & 0 deletions library/schemas/fixtures/scan-result/invalid/bad-kind.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"schemaVersion": 1, "kind": "map.analysis", "root": "/workspace", "detectedAt": "2026-09-13T00:00:00Z",
"analyzers": [], "inspected": [], "concepts": [], "limitations": ["Static signals only."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"schemaVersion": 1, "kind": "map.scan-result", "root": "/workspace", "detectedAt": "2026-09-13T00:00:00Z",
"analyzers": [], "inspected": [], "concepts": [], "limitations": []
}
10 changes: 10 additions & 0 deletions library/schemas/fixtures/scan-result/valid/detected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"schemaVersion": 1,
"kind": "map.scan-result",
"root": "/workspace/example",
"detectedAt": "2026-09-13T00:00:00.000Z",
"analyzers": ["dependency-manifest"],
"inspected": ["package.json"],
"concepts": [{ "concept": "tool_calling", "confidence": 0.95, "certainty": "detected", "evidence": ["package.json: ai"] }],
"limitations": ["Dependency declarations do not prove runtime use."]
}
36 changes: 36 additions & 0 deletions library/schemas/recommendation-result.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://map.dev/schemas/recommendation-result.schema.json",
"title": "MAP Recommendation Result v1",
"type": "object",
"additionalProperties": false,
"required": ["schemaVersion", "kind", "scan", "recommendations", "limitations"],
"properties": {
"schemaVersion": { "const": 1 },
"kind": { "const": "map.recommendation-result" },
"scan": {
"type": "object",
"additionalProperties": false,
"required": ["root", "detectedAt"],
"properties": {
"root": { "type": "string", "minLength": 1 },
"detectedAt": { "type": "string", "format": "date-time" }
}
},
"recommendations": { "type": "array", "items": { "$ref": "#/$defs/recommendation" } },
"limitations": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
},
"$defs": {
"recommendation": {
"type": "object",
"additionalProperties": false,
"required": ["pattern", "priority", "rationale", "triggeredBy"],
"properties": {
"pattern": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*/[a-z0-9]+(?:-[a-z0-9]+)*$" },
"priority": { "enum": ["high", "medium", "low"] },
"rationale": { "type": "string", "minLength": 1 },
"triggeredBy": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, "uniqueItems": true }
}
}
}
}
31 changes: 31 additions & 0 deletions library/schemas/scan-result.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://map.dev/schemas/scan-result.schema.json",
"title": "MAP Scan Result v1",
"type": "object",
"additionalProperties": false,
"required": ["schemaVersion", "kind", "root", "detectedAt", "analyzers", "inspected", "concepts", "limitations"],
"properties": {
"schemaVersion": { "const": 1 },
"kind": { "const": "map.scan-result" },
"root": { "type": "string", "minLength": 1 },
"detectedAt": { "type": "string", "format": "date-time" },
"analyzers": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
"inspected": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
"concepts": { "type": "array", "items": { "$ref": "#/$defs/concept" } },
"limitations": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
},
"$defs": {
"concept": {
"type": "object",
"additionalProperties": false,
"required": ["concept", "confidence", "certainty", "evidence"],
"properties": {
"concept": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"certainty": { "enum": ["detected", "likely", "unknown"] },
"evidence": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
}
}
}
}
2 changes: 2 additions & 0 deletions library/scripts/validate-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const CONTRACTS: readonly Contract[] = [
{ name: "project", schema: "project.schema.json", fixtures: "project" },
{ name: "decision", schema: "decision.schema.json", fixtures: "decision" },
{ name: "pattern", schema: "pattern.schema.json", fixtures: "pattern" },
{ name: "scan-result", schema: "scan-result.schema.json", fixtures: "scan-result" },
{ name: "recommendation-result", schema: "recommendation-result.schema.json", fixtures: "recommendation-result" },
];

const failures: string[] = [];
Expand Down
7 changes: 3 additions & 4 deletions tooling/packages/cli/src/analyzer/analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/**
* Module 2 — Analyzer (interfaces only for now).
* Module 2 — Analyzer ports and registry.
*
* An `Analyzer` inspects a project and reports the AI-architecture concepts it finds.
* Analyzers are pluggable and language-scoped (TypeScript, Python, Go, Java, ...).
* The registry picks the analyzers that apply to a given project.
*
* TODO(module-2): implement a first analyzer (e.g. TypeScript) that detects
* embeddings, vector search, RAG, tool calling, streaming, memory, model routing,
* and prompt guards. Keep detection rules data-driven so new signals are cheap.
* The dependency-manifest implementation is the deterministic baseline. Additional
* source or runtime analyzers implement this port without changing domain results.
*/

import type { DetectedConcept } from "../domain/index.ts";
Expand Down
8 changes: 8 additions & 0 deletions tooling/packages/cli/src/analyzer/dependency-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ interface ManifestSpec {
readonly parse: (contents: string) => readonly string[];
}

export const SUPPORTED_MANIFEST_FILES = [
"package.json",
"requirements.txt",
"pyproject.toml",
"go.mod",
"Cargo.toml",
] as const;

const MANIFESTS: readonly ManifestSpec[] = [
{ file: "package.json", ecosystem: "npm", parse: parsePackageJson },
{ file: "requirements.txt", ecosystem: "pypi", parse: parseRequirementsTxt },
Expand Down
6 changes: 5 additions & 1 deletion tooling/packages/cli/src/analyzer/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export type { Analyzer, AnalyzerContext } from "./analyzer.ts";
export { AnalyzerRegistry } from "./analyzer.ts";
export { DependencyManifestAnalyzer, mergeConcepts } from "./dependency-manifest.ts";
export {
DependencyManifestAnalyzer,
SUPPORTED_MANIFEST_FILES,
mergeConcepts,
} from "./dependency-manifest.ts";
export type { DependencySignal, Ecosystem } from "./signals.ts";
export { DEPENDENCY_SIGNALS } from "./signals.ts";
export {
Expand Down
63 changes: 52 additions & 11 deletions tooling/packages/cli/src/cli/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { join, resolve } from "node:path";
import type { Command, CommandContext, CommandResult } from "../command.ts";
import { OK } from "../command.ts";
import type { AnalyzerContext } from "../../analyzer/index.ts";
import { mergeConcepts } from "../../analyzer/index.ts";
import { mergeConcepts, SUPPORTED_MANIFEST_FILES } from "../../analyzer/index.ts";
import { MAP_DIR } from "../../config/index.ts";
import { CONCEPTS } from "../../domain/index.ts";
import type { DetectedArchitecture, DetectedConcept } from "../../domain/index.ts";
import { certaintyForConfidence } from "../../domain/index.ts";
import type { DetectedConcept, ScanResult } from "../../domain/index.ts";
import type { Services } from "../../services.ts";
import type { Reporter } from "../../reporting/index.ts";

Expand All @@ -23,20 +24,26 @@ export const analyzeCommand: Command = {
summary: "Scan the project and detect AI architecture concepts.",
usage: "map analyze [path]",
args: "[path]",
options: [{ flags: "--json", description: "machine-readable scan result" }],

async run(ctx: CommandContext): Promise<CommandResult> {
const { reporter, services } = ctx;
const root = resolve(ctx.cwd, ctx.args[0] ?? ".");

const architecture = await detectArchitecture(root, services);
if (architecture === undefined) {
const architecture = await scanArchitecture(root, services);
if (ctx.flags["json"] === true) {
reporter.info(JSON.stringify(architecture, null, 2));
await saveReport(architecture, services, reporter, false);
return OK;
}
if (architecture.analyzers.length === 0) {
reporter.warn(`No applicable analyzers for ${root}.`);
reporter.info("Supported signals: dependency manifests (package.json, requirements.txt, pyproject.toml, go.mod, Cargo.toml).");
return OK;
}

reportConcepts(architecture.concepts, reporter);
await saveReport(architecture, services, reporter);
await saveReport(architecture, services, reporter, true);

return OK;
},
Expand All @@ -49,20 +56,45 @@ export const analyzeCommand: Command = {
export async function detectArchitecture(
root: string,
services: Services,
): Promise<DetectedArchitecture | undefined> {
): Promise<ScanResult | undefined> {
const result = await scanArchitecture(root, services);
return result.analyzers.length === 0 ? undefined : result;
}

export async function scanArchitecture(
root: string,
services: Services,
): Promise<ScanResult> {
const context: AnalyzerContext = { root };
const applicable = await services.analyzers.applicable(context);
if (applicable.length === 0) return undefined;

const detections: DetectedConcept[] = [];
for (const analyzer of applicable) {
detections.push(...(await analyzer.analyze(context)));
}

const inspected: string[] = [];
for (const file of SUPPORTED_MANIFEST_FILES) {
if (await services.storage.exists(join(root, file))) inspected.push(file);
}

return {
schemaVersion: 1,
kind: "map.scan-result",
root,
detectedAt: new Date().toISOString(),
concepts: mergeConcepts(detections),
analyzers: applicable.map((analyzer) => analyzer.id).sort(),
inspected: inspected.sort(),
concepts: mergeConcepts(detections).map((detection) => ({
...detection,
certainty: certaintyForConfidence(detection.confidence),
})),
limitations: [
applicable.length === 0
? "No analyzer supports the files at this project root; architecture is unknown."
: "Dependency manifests indicate declared packages, not whether or how code uses them.",
"The MVP scanner does not inspect source code, runtime behavior, nested workspaces, or secret values.",
],
};
}

Expand All @@ -81,14 +113,16 @@ function reportConcepts(
CONCEPTS.find((concept) => concept.id === detection.concept)?.name ??
detection.concept;
const confidence = `${Math.round(detection.confidence * 100)}%`;
reporter.info(` ${name} (${confidence}) — ${detection.evidence.join(", ")}`);
const certainty = "certainty" in detection ? `, ${String(detection.certainty)}` : "";
reporter.info(` ${name} (${confidence}${certainty}) — ${detection.evidence.join(", ")}`);
}
}

async function saveReport(
architecture: DetectedArchitecture,
architecture: ScanResult,
services: Services,
reporter: Reporter,
announce: boolean,
): Promise<void> {
const { storage } = services;
const mapDir = join(architecture.root, MAP_DIR);
Expand All @@ -99,5 +133,12 @@ async function saveReport(
await storage.writeFile(path, `${JSON.stringify(architecture, null, 2)}\n`, {
overwrite: true,
});
reporter.success(`Report saved to ${path}`);
if (announce) reporter.success(`Report saved to ${path}`);
}

export const scanCommand: Command = {
...analyzeCommand,
name: "scan",
summary: "Scan the project and return evidence-backed AI architecture signals.",
usage: "map scan [path] [--json]",
};
15 changes: 14 additions & 1 deletion tooling/packages/cli/src/cli/commands/explain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const explainCommand: Command = {
summary: "Explain a pattern: what it is, when to use it, trade-offs.",
usage: "map explain <pattern-id>",
args: "<pattern-id>",
options: [{ flags: "--json", description: "machine-readable pattern metadata" }],

async run(ctx: CommandContext): Promise<CommandResult> {
const { reporter, services } = ctx;
Expand Down Expand Up @@ -45,11 +46,23 @@ export const explainCommand: Command = {
return FAILED;
}

render(ctx, entry);
if (ctx.flags["json"] === true) {
const { files: _files, ...metadata } = entry;
reporter.info(JSON.stringify(metadata, null, 2));
} else {
render(ctx, entry);
}
return OK;
},
};

export const showCommand: Command = {
...explainCommand,
name: "show",
summary: "Show one pattern, its trade-offs, score, and relationships.",
usage: "map show <pattern-id> [--json]",
};

function render(ctx: CommandContext, entry: CatalogEntry): void {
const { reporter } = ctx;

Expand Down
36 changes: 36 additions & 0 deletions tooling/packages/cli/src/cli/commands/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Command, CommandContext, CommandResult } from "../command.ts";
import { FAILED, OK } from "../command.ts";
import { buildPatternGraph } from "../../graph/index.ts";

export const graphCommand: Command = {
name: "graph",
summary: "Inspect pattern nodes and typed relationships.",
usage: "map graph [pattern-id] [--json]",
args: "[pattern-id]",
options: [{ flags: "--json", description: "machine-readable graph projection" }],

async run(ctx: CommandContext): Promise<CommandResult> {
const graph = buildPatternGraph(await ctx.services.catalog.entries());
const id = ctx.args[0];
if (id !== undefined && !graph.hasNode(id)) {
ctx.reporter.error(`Unknown pattern id '${id}'.`);
return FAILED;
}
const nodes = id === undefined ? [...graph.nodes()].sort() : [id];
const edges = (id === undefined ? graph.edges() : graph.neighbors(id)).slice().sort(
(a, b) => a.from.localeCompare(b.from) || a.type.localeCompare(b.type) || a.to.localeCompare(b.to),
);
if (ctx.flags["json"] === true) {
ctx.reporter.info(JSON.stringify({ schemaVersion: 1, kind: "map.pattern-graph", nodes, edges }, null, 2));
return OK;
}
if (id === undefined) {
ctx.reporter.info(`Pattern graph: ${nodes.length} node(s), ${edges.length} edge(s).`);
ctx.reporter.info("Inspect one node with 'map graph <pattern-id>'.");
return OK;
}
ctx.reporter.info(`${id}: ${edges.length} outgoing relationship(s)`);
for (const edge of edges) ctx.reporter.info(` ${edge.type} -> ${edge.to}${edge.note ? ` — ${edge.note}` : ""}`);
return OK;
},
};
Loading
Loading