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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ All notable changes to DevMap are documented in this file.
- Public benchmark results
- Feedback-driven fixes from the `0.2.0` beta

## [0.4.1] - 2026-10-03

### Added

- Analyzer pipeline loading spinners in `createProjectMap`: scanning,
analyzing, dependency mapping, entity/feature extraction, and optional
AI domain inference, with TTY animation, non-TTY fallback, and silent
JSON output

### Changed

- Welcome banner trimmed to the big wordmark plus separator; removed the
`[ DEVMAP CLI ]` label and capabilities line
- `devmap analyze` drops the duplicate scanning line now covered by the
pipeline spinner

## [0.4.0] - 2026-09-13

### Added
Expand Down
45 changes: 44 additions & 1 deletion docs/for-me-personal/DEBUG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1551,5 +1551,48 @@ pm view @flaid/devmap version -> .3.0.
assert bentuk/polanya saja.
- Urutan gate penting: jalankan full test suite SETELAH bump versi sebelum
menagih tag, karena bump bisa merusak asumsi test.
- Tag rilis yang belum ter-publish boleh digeser; setelah publish, tag
- Tag rilis yang belum ter-publish boleh digeser; setelah publish, tag
bersifat permanen.

## 29. Test Onboarding Flaky di PowerShell karena stdin Masih TTY

**Tanggal:** 2026-10-03

### Gejala

`pnpm test:cli` lokal Windows: 272 pass / 1 fail.
`onboarding command renders a snapshot-based guide` fail — output markdown
berbahasa Indonesia (`Tentang project ini`) padahal assertion menunggu
`/What this is/`. Durasi test 321 detik (menggantung). Di sandbox Linux dan
CI, 273 pass semua.

### Akar Masalah

`resolveOnboardingLanguage()` (`packages/cli/src/commands/onboarding.ts`)
kembali `en` hanya bila `!prompt && !process.stdin.isTTY`; selebihnya membuat
prompt asli dan menunggu input. Di bawah pnpm di PowerShell, stdout sudah
di-pipe (spinner jatuh ke fallback `console.log`) tetapi stdin tetap TTY,
jadi test masuk jalur prompt interaktif dan menggantung. Dua test pertama
tidak mengoper `language` maupun prompt palsu, sehingga hasilnya tergantung
lingkungan — lolos di CI/sandbox secara kebetulan, bukan by design.

### Solusi

Buat kedua test hermetic dengan bahasa eksplisit:
`onboardingCommand({ projectRoot, language: "en" })`. Source tidak diubah —
prompt interaktif tanpa `--write` adalah intent dari commit `244373b`.
Test ketiga (bahasa `id` via prompt palsu) sudah hermetic dan tidak diubah.

### Verifikasi

- Targeted: `tsx --test test/onboarding-command.test.ts` → 3 pass / 0 fail
dalam ~2 detik (sebelumnya 1 fail / 321 detik).
- Full: `pnpm test:cli` → 273 pass / 0 fail + `tsc --noEmit` bersih.

### Pelajaran

- Test yang memanggil kode ber-cabang TTY harus menginjeksi input
(prompt palsu / flag eksplisit), jangan mengandalkan `stdin.isTTY`
lingkungan runner.
- Durasi test yang melonjak (detik → menit) adalah sinyal test menunggu
input interaktif, bukan test yang lambat.
24 changes: 23 additions & 1 deletion docs/for-me-personal/PROGRESS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
# Progress DevMap

Terakhir diperbarui: 2026-09-04
Terakhir diperbarui: 2026-10-03

## Update 2026-10-03

### Analyzer Spinners + Welcome Trim — branch `ui-improve` (rilis 0.4.1)

1. `output.spinner()` baru di `packages/cli/src/utils/output.ts`: animasi
TTY, fallback satu-baris untuk non-TTY, silent untuk `--json`.
2. `createProjectMap` membungkus 5 tahap (scan, analyze, dependency map,
entity/feature extraction, AI domain inference) dengan spinner;
`analyze.ts` menghapus baris `Scanning` duplikat.
3. Banner welcome dipangkas ke wordmark besar + separator (hapus label
`[ DEVMAP CLI ]` dan capabilities); `welcome.test.ts` disesuaikan.
4. `CHANGELOG.md`: entry `[0.4.1] - 2026-10-03` (Added/Changed).
5. Fix flake `onboarding-command.test.ts`: dua test pertama oper
`language: "en"` eksplisit agar tidak tergantung `stdin.isTTY`
(detail di `DEBUG.md` #29).

**Verifikasi**: `pnpm test:cli` → 273 pass / 0 fail + type-check bersih.
Rencana rilis: PR `ui-improve` → `main`, merge, lalu `npm version patch`
di `main` + `push --follow-tags` (publish via OIDC).

---

## Update 2026-09-04

Expand Down
14 changes: 14 additions & 0 deletions docs/for-me-personal/TEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ Ada beberapa versi DevMap yang dapat diuji:
| npm link | CLI global sementara | Menguji command `devmap` dari folder mana pun |
| CI/runtime | OS dan versi Node berbeda | Verifikasi lintas platform sebelum release |

## Onboarding TTY flake (2026-10-03)

Dua test onboarding (`renders a snapshot-based guide`,
`writes ONBOARDING.md`) sekarang mengoper `language: "en"` eksplisit agar
tidak membuka prompt bahasa saat `stdin.isTTY` true (PowerShell lokal —
menggantung ~5 menit lalu fail). Targeted run:

```powershell
pnpm --filter @flaid/devmap exec tsx --test test/onboarding-command.test.ts
```

Expected: `3 pass / 0 fail` dalam hitungan detik. Kalau ada test baru yang
memanggil `onboardingCommand`, selalu oper `language` atau prompt palsu.

## Custom Provider & Registry (2026-08-26)

Focused automated tests:
Expand Down
241 changes: 132 additions & 109 deletions packages/cli/src/analyzers/pipeline/projectMap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { output, type Spinner } from "../../utils/output.js";
import { hashContent } from "../../cache/fileHash.js";
import { REASON_TAGS } from "./reasonTags.js";
import { analyzeFiles } from "./analyzerRegistry.js";
Expand Down Expand Up @@ -168,120 +169,142 @@ export async function createProjectMap(
*/
callAI?: (prompt: string) => Promise<string>
): Promise<ProjectMap> {
const files = await scanFiles(projectRoot);
const analyses = await analyzeFiles(files);
const aliasMappings = await loadAliasMappings(projectRoot);
const { graph, diagnostics: graphDiagnostics } = buildDependencyGraph(files, analyses, aliasMappings);
const references = countReferences(graph);
const detectedFramework = detectFramework(files);
const frameworks = detectFrameworks(files);
const project = detectProjectMetadata(
projectRoot,
detectedFramework,
files,
frameworks
);
const framework = project.framework;
const entryPoints = detectEntryPoints(graph);
const routes = detectRoutes(files, project.frameworks, graph);
const database = detectDatabase(files);

// Step 1: Extract entities dari schema (Prisma dll) atau route fallback
const entityGraph = extractEntities(files, routes);

// Step 2: Detect capabilities dari route patterns + HTTP methods
const capabilities = detectCapabilities(routes, entityGraph);

// Step 3: Detect features — consume entityGraph + capabilities
const featureResult = detectFeatures(files, analyses, routes, database, entityGraph, capabilities, graph);
const features = attachFeatureEntryPoints(
featureResult.features,
routes,
entryPoints,
graph,
analyses
);
// Step 4: AI domain inference (optional — hanya jalan kalau callAI disediakan)
// Kirim structured metadata ke AI, dapat domain summary + domain-specific features.
// Kalau gagal atau callAI tidak ada, static features tetap lengkap.
let domain: DomainInferenceResult | undefined;
if (callAI) {
const inferenceInput = buildDomainInferenceInput(
entityGraph,
capabilities,
features,
framework,
routes.length
let activeSpinner: Spinner | undefined;

try {
activeSpinner = output.spinner(`Scanning ${projectRoot}`);
const files = await scanFiles(projectRoot);
activeSpinner.succeed(`Scanned ${files.length} files`);

activeSpinner = output.spinner("Analyzing files");
const analyses = await analyzeFiles(files);
activeSpinner.succeed(`Analyzed ${files.length} files`);

activeSpinner = output.spinner("Mapping dependencies");
const aliasMappings = await loadAliasMappings(projectRoot);
const { graph, diagnostics: graphDiagnostics } = buildDependencyGraph(files, analyses, aliasMappings);
const references = countReferences(graph);
const detectedFramework = detectFramework(files);
const frameworks = detectFrameworks(files);
const project = detectProjectMetadata(
projectRoot,
detectedFramework,
files,
frameworks
);
const result = await inferDomain(inferenceInput, callAI, projectRoot);
if (result) {
domain = result;
// Merge domain-specific features ke features list.
// Pakai similarity engine — bukan name equality — sehingga
// "Customizable Plans" tidak duplicate "Plan Management" yang sudah ada.
// Canonical name (first-seen) dipertahankan oleh mergeDomainFeatures.
const domainFeatures = domainFeaturesToFeatureInfo(result.domainFeatures);
mergeDomainFeatures(features, domainFeatures);
features.sort((a, b) => a.name.localeCompare(b.name));
const framework = project.framework;
const entryPoints = detectEntryPoints(graph);
const routes = detectRoutes(files, project.frameworks, graph);
const database = detectDatabase(files);
activeSpinner.succeed("Dependencies mapped");

activeSpinner = output.spinner("Extracting entities & features");
// Step 1: Extract entities dari schema (Prisma dll) atau route fallback
const entityGraph = extractEntities(files, routes);

// Step 2: Detect capabilities dari route patterns + HTTP methods
const capabilities = detectCapabilities(routes, entityGraph);

// Step 3: Detect features — consume entityGraph + capabilities
const featureResult = detectFeatures(files, analyses, routes, database, entityGraph, capabilities, graph);
const features = attachFeatureEntryPoints(
featureResult.features,
routes,
entryPoints,
graph,
analyses
);
activeSpinner.succeed(`Detected ${features.length} features`);

// Step 4: AI domain inference (optional — hanya jalan kalau callAI disediakan)
// Kirim structured metadata ke AI, dapat domain summary + domain-specific features.
// Kalau gagal atau callAI tidak ada, static features tetap lengkap.
let domain: DomainInferenceResult | undefined;
if (callAI) {
activeSpinner = output.spinner("Inferring domain with AI");
const inferenceInput = buildDomainInferenceInput(
entityGraph,
capabilities,
features,
framework,
routes.length
);
const result = await inferDomain(inferenceInput, callAI, projectRoot);
if (result) {
domain = result;
// Merge domain-specific features ke features list.
// Pakai similarity engine — bukan name equality — sehingga
// "Customizable Plans" tidak duplicate "Plan Management" yang sudah ada.
// Canonical name (first-seen) dipertahankan oleh mergeDomainFeatures.
const domainFeatures = domainFeaturesToFeatureInfo(result.domainFeatures);
mergeDomainFeatures(features, domainFeatures);
features.sort((a, b) => a.name.localeCompare(b.name));
}
activeSpinner.succeed(domain ? `Domain inferred: ${domain.domain}` : "Domain inference skipped");
}
}

const criticalFiles = rankCriticalFiles(files, analyses, references, entryPoints);
const fileIndex = Object.fromEntries(files.map((file) => [
file.path,
createFileIndexEntry(
file,
analyses[file.path],
graph[file.path] ?? [],
references,
activeSpinner = output.spinner("Building project index");
const criticalFiles = rankCriticalFiles(files, analyses, references, entryPoints);
const fileIndex = Object.fromEntries(files.map((file) => [
file.path,
createFileIndexEntry(
file,
analyses[file.path],
graph[file.path] ?? [],
references,
entryPoints,
criticalFiles,
features
)
]));

const flows = generateMinimalFlows(features, fileIndex, routes, graph);
activeSpinner.succeed("Project index built");

return {
version: SNAPSHOT_SCHEMA_VERSION,
generatedAt: new Date().toISOString(),
agentInstructions: createAgentInstructions(),
fingerprint: createProjectFingerprint(files),
projectRoot,
framework,
project,
stats: {
// A pre-filter filesystem count is not collected in schema v1.
totalFiles: files.length,
relevantFiles: files.length,
totalLines: files.reduce((sum, file) => sum + file.lines, 0)
},
entryPoints,
criticalFiles,
features
)
]));

const flows = generateMinimalFlows(features, fileIndex, routes, graph);

return {
version: SNAPSHOT_SCHEMA_VERSION,
generatedAt: new Date().toISOString(),
agentInstructions: createAgentInstructions(),
fingerprint: createProjectFingerprint(files),
projectRoot,
framework,
project,
stats: {
// A pre-filter filesystem count is not collected in schema v1.
totalFiles: files.length,
relevantFiles: files.length,
totalLines: files.reduce((sum, file) => sum + file.lines, 0)
},
entryPoints,
criticalFiles,
routes,
apiRoutes: routes.filter((route) => route.kind === "api"),
externalServices: detectExternalServices(files),
...(database ? { database } : {}),
features,
...(entityGraph.source !== "empty" ? { entityGraph } : {}),
...(capabilities.length > 0 ? { capabilities } : {}),
...(domain ? { domain } : {}),
flows,
onboarding: {
recommendedPath: buildOnboardingPath(files, entryPoints, criticalFiles, fileIndex)
},
changeImpact: buildChangeImpact(fileIndex, features, flows, graph),
warnings: detectAnalysisWarnings(files, entryPoints, criticalFiles, features),
...(graphDiagnostics.unresolvedAliases.length > 0 || graphDiagnostics.parserFallbacks.length > 0
? { diagnostics: graphDiagnostics }
: {}),
...(featureResult.mergeDecisions.length > 0 || featureResult.rejectedCandidateIds.length > 0
? { featureDiagnostics: { mergeDecisions: featureResult.mergeDecisions, rejectedCandidateIds: featureResult.rejectedCandidateIds } }
: {}),
dependencies: readPackageDependencies(files),
fileGraph: graph,
fileIndex
};
routes,
apiRoutes: routes.filter((route) => route.kind === "api"),
externalServices: detectExternalServices(files),
...(database ? { database } : {}),
features,
...(entityGraph.source !== "empty" ? { entityGraph } : {}),
...(capabilities.length > 0 ? { capabilities } : {}),
...(domain ? { domain } : {}),
flows,
onboarding: {
recommendedPath: buildOnboardingPath(files, entryPoints, criticalFiles, fileIndex)
},
changeImpact: buildChangeImpact(fileIndex, features, flows, graph),
warnings: detectAnalysisWarnings(files, entryPoints, criticalFiles, features),
...(graphDiagnostics.unresolvedAliases.length > 0 || graphDiagnostics.parserFallbacks.length > 0
? { diagnostics: graphDiagnostics }
: {}),
...(featureResult.mergeDecisions.length > 0 || featureResult.rejectedCandidateIds.length > 0
? { featureDiagnostics: { mergeDecisions: featureResult.mergeDecisions, rejectedCandidateIds: featureResult.rejectedCandidateIds } }
: {}),
dependencies: readPackageDependencies(files),
fileGraph: graph,
fileIndex
};
} catch (error) {
activeSpinner?.fail();
throw error;
}
}

function createAgentInstructions(): ProjectMap["agentInstructions"] {
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ async function runAnalyze(
const projectRoot = resolve(target);

output.section("DevMap Analyze");
output.step(`Scanning ${projectRoot}`);

// Build callAI wrapper — dipakai oleh domain inference (Step 5) di createProjectMap.
// Dibuat di sini biar analyze command yang kontrol config + client lifecycle,
Expand Down
Loading
Loading