feat(skills): resolve .ua data directory in helper scripts
Every bundled script now honors the shared rule (.understand-anything/ wins when it already exists, .ua/ otherwise): scripts importing @understand-anything/core use the exported resolveUaDir; standalone figma/dev scripts inline a two-line helper; Python merge/parse scripts inline resolve_ua_dir mirroring core. extract-domain-context skips both directory names when walking, and eslint ignores both. Tests cover fresh-project .ua, legacy .understand-anything, and legacy-wins-when- both for each script family.
This commit is contained in:
@@ -11,6 +11,7 @@ export default tseslint.config(
|
||||
'**/public/**',
|
||||
'**/coverage/**',
|
||||
'**/.understand-anything/**',
|
||||
'**/.ua/**',
|
||||
'**/.claude-plugin/**',
|
||||
'**/.cursor-plugin/**',
|
||||
'**/.copilot-plugin/**',
|
||||
|
||||
@@ -11,11 +11,17 @@
|
||||
* dashboard robustness pipeline (Tier 1-3: null fields, wrong cases,
|
||||
* missing fields, aliases, dangling refs, unrecognizable types).
|
||||
*
|
||||
* Default: 3000 nodes. Writes to .understand-anything/knowledge-graph.json
|
||||
* Default: 3000 nodes. Writes to the project's data dir —
|
||||
* .ua/knowledge-graph.json (or legacy .understand-anything/knowledge-graph.json
|
||||
* when that directory already exists).
|
||||
*/
|
||||
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { resolve, join } from "node:path";
|
||||
|
||||
// Mirror core's resolveUaDir: the legacy `.understand-anything/` dir wins for
|
||||
// both reads and writes when it already exists; otherwise use `.ua/`.
|
||||
const uaDir = (root) => { const legacy = join(root, ".understand-anything"); return existsSync(legacy) ? legacy : join(root, ".ua"); };
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const MESSY = args.includes("--messy");
|
||||
@@ -278,7 +284,7 @@ const graph = {
|
||||
tour: MESSY && Math.random() < 0.5 ? null : tour,
|
||||
};
|
||||
|
||||
const outDir = resolve(process.cwd(), ".understand-anything");
|
||||
const outDir = resolve(uaDir(process.cwd()));
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = resolve(outDir, "knowledge-graph.json");
|
||||
writeFileSync(outPath, JSON.stringify(graph, null, 2));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@@ -25,6 +25,19 @@ function setupProject(fixtureName) {
|
||||
return root;
|
||||
}
|
||||
|
||||
// Variant of setupProject that seeds the fixture into an arbitrary data
|
||||
// directory name (`.ua` for fresh projects, `.understand-anything` for legacy).
|
||||
function setupProjectInDir(fixtureName, dirName) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ua-cb-dir-test-'));
|
||||
mkdirSync(join(root, dirName, 'intermediate'), { recursive: true });
|
||||
const fixturePath = join(FIXTURES, fixtureName);
|
||||
writeFileSync(
|
||||
join(root, dirName, 'intermediate', 'scan-result.json'),
|
||||
readFileSync(fixturePath, 'utf-8'),
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
function readBatches(projectRoot) {
|
||||
const p = join(projectRoot, '.understand-anything', 'intermediate', 'batches.json');
|
||||
return JSON.parse(readFileSync(p, 'utf-8'));
|
||||
@@ -703,3 +716,50 @@ describe('compute-batches.mjs — --changed-files', () => {
|
||||
expect(neighbors.find(n => n.path === 'src/b/middle.ts').batchIndex).toBe(batch.batchIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compute-batches.mjs — data-dir resolution (.ua vs legacy)', () => {
|
||||
let root;
|
||||
|
||||
afterEach(() => {
|
||||
if (root) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('fresh project reads scan-result from .ua/ and writes batches.json there', () => {
|
||||
root = setupProjectInDir('scan-result-3-cliques.json', '.ua');
|
||||
const result = runScript(root);
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
// Output landed in .ua/, and the legacy dir was never created.
|
||||
expect(existsSync(join(root, '.ua', 'intermediate', 'batches.json'))).toBe(true);
|
||||
expect(existsSync(join(root, '.understand-anything'))).toBe(false);
|
||||
|
||||
const batches = JSON.parse(
|
||||
readFileSync(join(root, '.ua', 'intermediate', 'batches.json'), 'utf-8'),
|
||||
);
|
||||
expect(batches.totalFiles).toBe(9);
|
||||
expect(batches.batches.length).toBe(3);
|
||||
});
|
||||
|
||||
it('legacy project keeps using .understand-anything/ (no migration)', () => {
|
||||
// Legacy-compat regression: an existing .understand-anything/ dir wins for
|
||||
// both read and write even though .ua/ is the new default.
|
||||
root = setupProjectInDir('scan-result-3-cliques.json', '.understand-anything');
|
||||
const result = runScript(root);
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
expect(existsSync(join(root, '.understand-anything', 'intermediate', 'batches.json'))).toBe(true);
|
||||
expect(existsSync(join(root, '.ua'))).toBe(false);
|
||||
});
|
||||
|
||||
it('legacy dir wins when both .understand-anything/ and .ua/ exist', () => {
|
||||
root = setupProjectInDir('scan-result-3-cliques.json', '.understand-anything');
|
||||
// A stray empty .ua/ must not divert reads/writes away from the legacy dir.
|
||||
mkdirSync(join(root, '.ua', 'intermediate'), { recursive: true });
|
||||
|
||||
const result = runScript(root);
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
expect(existsSync(join(root, '.understand-anything', 'intermediate', 'batches.json'))).toBe(true);
|
||||
expect(existsSync(join(root, '.ua', 'intermediate', 'batches.json'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1292,5 +1292,62 @@ class TestEmptyBatchGuard(unittest.TestCase):
|
||||
self.assertNotIn("contributed 0 nodes and 0 edges", stderr)
|
||||
|
||||
|
||||
class TestUaDirResolution(unittest.TestCase):
|
||||
"""The merge script reads/writes under the resolved data dir: `.ua/` for
|
||||
fresh projects, legacy `.understand-anything/` when that dir already exists
|
||||
(no migration). Exercised end-to-end via subprocess.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
import tempfile
|
||||
self.tmp = Path(tempfile.mkdtemp(prefix="ua-mbg-uadir-"))
|
||||
|
||||
def tearDown(self) -> None:
|
||||
import shutil
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def _write_batch(self, dir_name: str, name: str, nodes: list) -> Path:
|
||||
import json as _j
|
||||
inter = self.tmp / dir_name / "intermediate"
|
||||
inter.mkdir(parents=True, exist_ok=True)
|
||||
(inter / name).write_text(_j.dumps({"nodes": nodes, "edges": []}), encoding="utf-8")
|
||||
return inter
|
||||
|
||||
def _run(self) -> int:
|
||||
import subprocess
|
||||
return subprocess.run(
|
||||
[sys.executable, str(_MODULE_PATH), str(self.tmp)],
|
||||
capture_output=True, text=True,
|
||||
).returncode
|
||||
|
||||
def test_fresh_project_uses_dot_ua(self) -> None:
|
||||
self._write_batch(".ua", "batch-1.json", [_file_node("src/a.ts")])
|
||||
rc = self._run()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertTrue((self.tmp / ".ua" / "intermediate" / "assembled-graph.json").is_file())
|
||||
# Legacy dir must not be created for a fresh project.
|
||||
self.assertFalse((self.tmp / ".understand-anything").exists())
|
||||
|
||||
def test_legacy_project_keeps_understand_anything(self) -> None:
|
||||
self._write_batch(".understand-anything", "batch-1.json", [_file_node("src/a.ts")])
|
||||
rc = self._run()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertTrue(
|
||||
(self.tmp / ".understand-anything" / "intermediate" / "assembled-graph.json").is_file()
|
||||
)
|
||||
self.assertFalse((self.tmp / ".ua").exists())
|
||||
|
||||
def test_legacy_dir_wins_when_both_present(self) -> None:
|
||||
self._write_batch(".understand-anything", "batch-1.json", [_file_node("src/a.ts")])
|
||||
# A stray empty .ua/ must not divert the merge away from the legacy dir.
|
||||
(self.tmp / ".ua" / "intermediate").mkdir(parents=True, exist_ok=True)
|
||||
rc = self._run()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertTrue(
|
||||
(self.tmp / ".understand-anything" / "intermediate" / "assembled-graph.json").is_file()
|
||||
)
|
||||
self.assertFalse((self.tmp / ".ua" / "intermediate" / "assembled-graph.json").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -475,6 +475,50 @@ describe('scan-project.mjs — .understandignore handling', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('scan-project.mjs — data-dir resolution (.ua vs legacy)', () => {
|
||||
let projectRoot;
|
||||
|
||||
afterEach(() => {
|
||||
if (projectRoot) {
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
projectRoot = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('honors .ua/.understandignore in a fresh project (no legacy dir)', () => {
|
||||
// scan-project delegates ignore handling to core's createIgnoreFilter,
|
||||
// which reads <resolveUaDir>/.understandignore — .ua/ for fresh projects.
|
||||
projectRoot = setupTree({
|
||||
'.ua/.understandignore': 'fixtures/\n',
|
||||
'src/index.ts': 'export const x = 1;\n',
|
||||
'fixtures/snap1.json': '{ "a": 1 }\n',
|
||||
'fixtures/snap2.json': '{ "b": 2 }\n',
|
||||
});
|
||||
const r = runScript(projectRoot);
|
||||
expect(r.status).toBe(0);
|
||||
expect(byPath(r.output, 'fixtures/snap1.json')).toBeUndefined();
|
||||
expect(byPath(r.output, 'fixtures/snap2.json')).toBeUndefined();
|
||||
// Counted as user-driven drops (dual-filter accounting saw the ua ignore).
|
||||
expect(r.output.filteredByIgnore).toBe(2);
|
||||
});
|
||||
|
||||
it('honors legacy .understand-anything/.understandignore (legacy-compat)', () => {
|
||||
// Legacy-compat regression: projects with an existing
|
||||
// .understand-anything/ keep using it for the .understandignore lookup.
|
||||
projectRoot = setupTree({
|
||||
'.understand-anything/.understandignore': 'fixtures/\n',
|
||||
'src/index.ts': 'export const x = 1;\n',
|
||||
'fixtures/snap1.json': '{ "a": 1 }\n',
|
||||
'fixtures/snap2.json': '{ "b": 2 }\n',
|
||||
});
|
||||
const r = runScript(projectRoot);
|
||||
expect(r.status).toBe(0);
|
||||
expect(byPath(r.output, 'fixtures/snap1.json')).toBeUndefined();
|
||||
expect(byPath(r.output, 'fixtures/snap2.json')).toBeUndefined();
|
||||
expect(r.output.filteredByIgnore).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scan-project.mjs — special-file recognition', () => {
|
||||
let projectRoot;
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ Usage:
|
||||
python extract-domain-context.py <project-root>
|
||||
|
||||
Output:
|
||||
<project-root>/.understand-anything/intermediate/domain-context.json
|
||||
<ua-dir>/intermediate/domain-context.json, where <ua-dir> is `.ua/` (or
|
||||
legacy `.understand-anything/` when that directory already exists).
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -19,6 +20,12 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def resolve_ua_dir(root: Path) -> Path:
|
||||
"""Mirror core resolveUaDir: legacy .understand-anything/ wins if present."""
|
||||
legacy = root / ".understand-anything"
|
||||
return legacy if legacy.is_dir() else root / ".ua"
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
|
||||
MAX_FILE_TREE_DEPTH = 6
|
||||
@@ -52,7 +59,7 @@ SKIP_DIRS = {
|
||||
"node_modules", ".git", ".svn", ".hg", "__pycache__", ".tox",
|
||||
"venv", ".venv", "env", ".env", "dist", "build", "out", ".next",
|
||||
".nuxt", "target", "vendor", ".idea", ".vscode", "coverage",
|
||||
".understand-anything", ".pytest_cache", ".mypy_cache",
|
||||
".understand-anything", ".ua", ".pytest_cache", ".mypy_cache",
|
||||
"Pods", "DerivedData", ".gradle", "bin", "obj",
|
||||
}
|
||||
|
||||
@@ -385,7 +392,7 @@ def main() -> None:
|
||||
|
||||
try:
|
||||
# Ensure output directory exists
|
||||
output_dir = project_root / ".understand-anything" / "intermediate"
|
||||
output_dir = resolve_ua_dir(project_root) / "intermediate"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / "domain-context.json"
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
|
||||
import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { mergeDesignGraph } from "@understand-anything/core/figma";
|
||||
|
||||
// Mirror core's resolveUaDir: the legacy `.understand-anything/` dir wins for
|
||||
// both reads and writes when it already exists; otherwise use `.ua/`.
|
||||
const uaDir = (root) => { const legacy = join(root, ".understand-anything"); return existsSync(legacy) ? legacy : join(root, ".ua"); };
|
||||
|
||||
const [, , projectRoot] = process.argv;
|
||||
const interDir = join(projectRoot, ".understand-anything", "intermediate");
|
||||
const interDir = join(uaDir(projectRoot), "intermediate");
|
||||
const manifest = JSON.parse(readFileSync(join(interDir, "scan-manifest.json"), "utf8"));
|
||||
const analyses = readdirSync(interDir)
|
||||
.filter((f) => /^analysis-batch-.*\.json$/.test(f))
|
||||
@@ -20,7 +24,7 @@ if (!result.success || !result.data) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outDir = join(projectRoot, ".understand-anything");
|
||||
const outDir = uaDir(projectRoot);
|
||||
writeFileSync(join(outDir, "knowledge-graph.json"), JSON.stringify(result.data, null, 2));
|
||||
writeFileSync(join(outDir, "meta.json"), JSON.stringify({
|
||||
lastAnalyzedAt: new Date().toISOString(),
|
||||
|
||||
@@ -3,6 +3,10 @@ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parseFileKey, FigmaApiSource, parseDocument, extractTokens, applyScreenThumbnails } from "@understand-anything/core/figma";
|
||||
|
||||
// Mirror core's resolveUaDir: the legacy `.understand-anything/` dir wins for
|
||||
// both reads and writes when it already exists; otherwise use `.ua/`.
|
||||
const uaDir = (root) => { const legacy = join(root, ".understand-anything"); return existsSync(legacy) ? legacy : join(root, ".ua"); };
|
||||
|
||||
const [, , projectRoot, urlOrKey] = process.argv;
|
||||
if (!projectRoot || !urlOrKey) {
|
||||
console.error("usage: figma-scan.mjs <projectRoot> <figmaUrlOrKey>");
|
||||
@@ -13,7 +17,7 @@ const fileKey = parseFileKey(urlOrKey);
|
||||
const source = new FigmaApiSource(fileKey); // reads FIGMA_TOKEN from env; throws a friendly error if missing
|
||||
const doc = await source.fetchDocument();
|
||||
|
||||
const metaPath = join(projectRoot, ".understand-anything", "meta.json");
|
||||
const metaPath = join(uaDir(projectRoot), "meta.json");
|
||||
let prevVersion = null;
|
||||
if (existsSync(metaPath)) {
|
||||
try {
|
||||
@@ -64,7 +68,7 @@ const manifest = {
|
||||
edges,
|
||||
};
|
||||
|
||||
const interDir = join(projectRoot, ".understand-anything", "intermediate");
|
||||
const interDir = join(uaDir(projectRoot), "intermediate");
|
||||
mkdirSync(interDir, { recursive: true });
|
||||
writeFileSync(join(interDir, "scan-manifest.json"), JSON.stringify(manifest, null, 2));
|
||||
|
||||
@@ -83,7 +87,7 @@ console.error(
|
||||
* Best-effort: never throw (thumbnails are optional).
|
||||
*/
|
||||
async function refreshThumbnailsInPlace(projectRoot, source) {
|
||||
const graphPath = join(projectRoot, ".understand-anything", "knowledge-graph.json");
|
||||
const graphPath = join(uaDir(projectRoot), "knowledge-graph.json");
|
||||
if (!existsSync(graphPath)) return;
|
||||
try {
|
||||
const graph = JSON.parse(readFileSync(graphPath, "utf8"));
|
||||
|
||||
@@ -12,7 +12,9 @@ Usage:
|
||||
python merge-knowledge-graph.py <wiki-directory>
|
||||
|
||||
Output:
|
||||
Writes assembled-graph.json to <wiki-directory>/.understand-anything/intermediate/
|
||||
Writes assembled-graph.json to <wiki-directory>/<ua-dir>/intermediate/, where
|
||||
<ua-dir> is `.ua/` (or legacy `.understand-anything/` when that directory
|
||||
already exists).
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -23,6 +25,12 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def resolve_ua_dir(root: Path) -> Path:
|
||||
"""Mirror core resolveUaDir: legacy .understand-anything/ wins if present."""
|
||||
legacy = root / ".understand-anything"
|
||||
return legacy if legacy.is_dir() else root / ".ua"
|
||||
|
||||
|
||||
def _find_markdown_case_insensitive(parent: Path, name: str) -> Path:
|
||||
"""Resolve a known markdown filename case-insensitively within one directory.
|
||||
|
||||
@@ -110,7 +118,7 @@ def normalize_entity_name(name: str) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def merge(root: Path) -> dict:
|
||||
intermediate = root / ".understand-anything" / "intermediate"
|
||||
intermediate = resolve_ua_dir(root) / "intermediate"
|
||||
manifest_path = intermediate / "scan-manifest.json"
|
||||
|
||||
if not manifest_path.is_file():
|
||||
|
||||
@@ -10,7 +10,9 @@ Usage:
|
||||
python parse-knowledge-base.py <wiki-directory>
|
||||
|
||||
Output:
|
||||
Writes scan-manifest.json to <wiki-directory>/.understand-anything/intermediate/
|
||||
Writes scan-manifest.json to <wiki-directory>/<ua-dir>/intermediate/, where
|
||||
<ua-dir> is `.ua/` (or legacy `.understand-anything/` when that directory
|
||||
already exists).
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -19,6 +21,12 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def resolve_ua_dir(root: Path) -> Path:
|
||||
"""Mirror core resolveUaDir: legacy .understand-anything/ wins if present."""
|
||||
legacy = root / ".understand-anything"
|
||||
return legacy if legacy.is_dir() else root / ".ua"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regex patterns
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -538,7 +546,7 @@ def main():
|
||||
manifest = parse_wiki(root)
|
||||
|
||||
# Write output
|
||||
out_dir = root / ".understand-anything" / "intermediate"
|
||||
out_dir = resolve_ua_dir(root) / "intermediate"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / "scan-manifest.json"
|
||||
out_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*
|
||||
* Builds the structural-fingerprint baseline used by auto-update's
|
||||
* incremental change detection. Runs once per /understand full rebuild
|
||||
* (Phase 7 step 2.5), generating .understand-anything/fingerprints.json.
|
||||
* (Phase 7 step 2.5), generating fingerprints.json in the project's data dir
|
||||
* (`.ua/`, or legacy `.understand-anything/` — resolved by core's
|
||||
* saveFingerprints via resolveUaDir).
|
||||
*
|
||||
* Replaces the LLM-written fingerprint script that previously sat in
|
||||
* SKILL.md as a code example — that example had the wrong signature
|
||||
@@ -17,7 +19,8 @@
|
||||
* Input JSON:
|
||||
* { projectRoot: string, sourceFilePaths: string[], gitCommitHash: string }
|
||||
*
|
||||
* Writes: <projectRoot>/.understand-anything/fingerprints.json
|
||||
* Writes: <projectRoot>/.ua/fingerprints.json (or legacy
|
||||
* <projectRoot>/.understand-anything/fingerprints.json when that dir exists)
|
||||
* Exit code: 0 on success (including 0 files analyzed); non-zero on error.
|
||||
*/
|
||||
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
* Usage:
|
||||
* node compute-batches.mjs <project-root> [--changed-files=<path>]
|
||||
*
|
||||
* Input: <project-root>/.understand-anything/intermediate/scan-result.json
|
||||
* Output: <project-root>/.understand-anything/intermediate/batches.json
|
||||
* Input/output live under the project's data dir (`.ua/`, or legacy
|
||||
* `.understand-anything/` when that directory already exists — resolved by
|
||||
* core's resolveUaDir):
|
||||
* Input: <ua-dir>/intermediate/scan-result.json
|
||||
* Output: <ua-dir>/intermediate/batches.json
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
|
||||
@@ -36,7 +39,7 @@ try {
|
||||
} catch {
|
||||
core = await import(pathToFileURL(resolve(PLUGIN_ROOT, 'packages/core/dist/index.js')).href);
|
||||
}
|
||||
const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core;
|
||||
const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers, resolveUaDir } = core;
|
||||
|
||||
import Graph from 'graphology';
|
||||
import louvain from 'graphology-communities-louvain';
|
||||
@@ -369,7 +372,8 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const scanPath = join(projectRoot, '.understand-anything', 'intermediate', 'scan-result.json');
|
||||
const uaDir = resolveUaDir(projectRoot);
|
||||
const scanPath = join(uaDir, 'intermediate', 'scan-result.json');
|
||||
if (!existsSync(scanPath)) {
|
||||
process.stderr.write(`Error: scan-result.json not found at ${scanPath}\n`);
|
||||
process.exit(1);
|
||||
@@ -567,7 +571,7 @@ async function main() {
|
||||
batches: finalBatches,
|
||||
};
|
||||
|
||||
const outPath = join(projectRoot, '.understand-anything', 'intermediate', 'batches.json');
|
||||
const outPath = join(uaDir, 'intermediate', 'batches.json');
|
||||
writeFileSync(outPath, JSON.stringify(output, null, 2), 'utf-8');
|
||||
const batchSizes = finalBatches.map(b => b.files.length);
|
||||
const maxSize = batchSizes.length ? Math.max(...batchSizes) : 0;
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
/**
|
||||
* generate-ignore.mjs
|
||||
*
|
||||
* Writes a starter `.understand-anything/.understandignore` for the target
|
||||
* project by delegating to `generateStarterIgnoreFile` in
|
||||
* `@understand-anything/core`. Invoked from SKILL.md Phase 0.5; replaces the
|
||||
* inline `node -e "…"` block that previously duplicated the generator logic.
|
||||
* Writes a starter `.understandignore` into the project's data directory
|
||||
* (`.ua/`, or legacy `.understand-anything/` when that directory already
|
||||
* exists — see core's resolveUaDir) by delegating to
|
||||
* `generateStarterIgnoreFile` in `@understand-anything/core`. Invoked from
|
||||
* SKILL.md Phase 0.5; replaces the inline `node -e "…"` block that previously
|
||||
* duplicated the generator logic.
|
||||
*
|
||||
* Usage:
|
||||
* node generate-ignore.mjs <projectRoot>
|
||||
*
|
||||
* Behaviour:
|
||||
* - Exits 0 with a stderr notice if the target file already exists.
|
||||
* - Creates `<projectRoot>/.understand-anything/` if missing.
|
||||
* - Creates the resolved data dir (`.ua/` or legacy
|
||||
* `.understand-anything/`) if missing.
|
||||
* - Emits a one-line stderr summary on success.
|
||||
*
|
||||
* Mirrors the @understand-anything/core resolution dance used by
|
||||
@@ -50,10 +53,10 @@ try {
|
||||
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
|
||||
}
|
||||
|
||||
const { generateStarterIgnoreFile } = core;
|
||||
const { generateStarterIgnoreFile, resolveUaDir } = core;
|
||||
|
||||
const projectRoot = resolve(process.argv[2] ?? process.cwd());
|
||||
const outDir = join(projectRoot, '.understand-anything');
|
||||
const outDir = resolveUaDir(projectRoot);
|
||||
const outPath = join(outDir, '.understandignore');
|
||||
|
||||
if (existsSync(outPath)) {
|
||||
|
||||
@@ -11,11 +11,10 @@ then reviews the output for semantic issues the script cannot catch.
|
||||
Usage:
|
||||
python merge-batch-graphs.py <project-root>
|
||||
|
||||
Input:
|
||||
<project-root>/.understand-anything/intermediate/batch-*.json
|
||||
|
||||
Output:
|
||||
<project-root>/.understand-anything/intermediate/assembled-graph.json
|
||||
Input/output live under the project's data dir (`.ua/`, or legacy
|
||||
`.understand-anything/` when that directory already exists):
|
||||
Input: <ua-dir>/intermediate/batch-*.json
|
||||
Output: <ua-dir>/intermediate/assembled-graph.json
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -27,6 +26,12 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def resolve_ua_dir(root: Path) -> Path:
|
||||
"""Mirror core resolveUaDir: legacy .understand-anything/ wins if present."""
|
||||
legacy = root / ".understand-anything"
|
||||
return legacy if legacy.is_dir() else root / ".ua"
|
||||
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────────
|
||||
|
||||
VALID_NODE_PREFIXES = {
|
||||
@@ -1026,7 +1031,7 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
project_root = Path(sys.argv[1]).resolve()
|
||||
intermediate_dir = project_root / ".understand-anything" / "intermediate"
|
||||
intermediate_dir = resolve_ua_dir(project_root) / "intermediate"
|
||||
|
||||
if not intermediate_dir.is_dir():
|
||||
print(f"Error: {intermediate_dir} does not exist", file=sys.stderr)
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"""
|
||||
merge-subdomain-graphs.py — Merge subdomain knowledge-graph files into one.
|
||||
|
||||
Auto-discovers *knowledge-graph*.json files in .understand-anything/
|
||||
(excluding knowledge-graph.json itself), loads the existing
|
||||
Auto-discovers *knowledge-graph*.json files in the project's data dir
|
||||
(`.ua/`, or legacy `.understand-anything/` when that directory already exists)
|
||||
excluding knowledge-graph.json itself, loads the existing
|
||||
knowledge-graph.json as a base if present, and merges everything
|
||||
into a single knowledge-graph.json.
|
||||
|
||||
@@ -15,7 +16,7 @@ knowledge-graph.json is loaded as a base but never as a discovery input
|
||||
(prevents self-merging on repeated runs).
|
||||
|
||||
Output:
|
||||
<project-root>/.understand-anything/knowledge-graph.json
|
||||
<ua-dir>/knowledge-graph.json
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -24,6 +25,12 @@ from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def resolve_ua_dir(root: Path) -> Path:
|
||||
"""Mirror core resolveUaDir: legacy .understand-anything/ wins if present."""
|
||||
legacy = root / ".understand-anything"
|
||||
return legacy if legacy.is_dir() else root / ".ua"
|
||||
|
||||
# Edge types that carry the domain hierarchy. Dropping one of these changes
|
||||
# downstream graph traversal (unlike a routine `related` edge), so they are
|
||||
# warned about loudly and re-tried on later runs via merge-report.json —
|
||||
@@ -308,7 +315,7 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
project_root = Path(sys.argv[1]).resolve()
|
||||
ua_dir = project_root / ".understand-anything"
|
||||
ua_dir = resolve_ua_dir(project_root)
|
||||
|
||||
if not ua_dir.is_dir():
|
||||
print(f"Error: {ua_dir} does not exist", file=sys.stderr)
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
*
|
||||
* What this script owns:
|
||||
* - File enumeration (git ls-files preferred, recursive walk fallback)
|
||||
* - `.understandignore` filtering (delegated to core's createIgnoreFilter)
|
||||
* - `.understandignore` filtering (delegated to core's createIgnoreFilter,
|
||||
* which reads the resolved data dir — `.ua/`, or legacy
|
||||
* `.understand-anything/` when that directory already exists)
|
||||
* - Per-file language detection (extension + filename table)
|
||||
* - Per-file category assignment (priority-ordered rules from
|
||||
* project-scanner.md Step 4)
|
||||
@@ -81,7 +83,7 @@ try {
|
||||
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
|
||||
}
|
||||
|
||||
const { createIgnoreFilter } = core;
|
||||
const { createIgnoreFilter, resolveUaDir } = core;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Language detection
|
||||
@@ -602,11 +604,15 @@ function buildDefaultsOnlyFilter() {
|
||||
* Determine whether `projectRoot` has any user .understandignore files.
|
||||
* When neither file exists, the combined and defaults-only filters are
|
||||
* identical, so we can skip the dual-filter accounting entirely.
|
||||
*
|
||||
* Mirrors core's createIgnoreFilter, which reads the resolved data dir —
|
||||
* `.ua/`, or legacy `.understand-anything/` when that directory already
|
||||
* exists (see resolveUaDir).
|
||||
*/
|
||||
function hasUserIgnoreFile(projectRoot) {
|
||||
return (
|
||||
existsSync(join(projectRoot, '.understandignore'))
|
||||
|| existsSync(join(projectRoot, '.understand-anything', '.understandignore'))
|
||||
|| existsSync(join(resolveUaDir(projectRoot), '.understandignore'))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -202,3 +202,55 @@ describe("merge-batch-graphs.py imports recovery", () => {
|
||||
expect(assembled.edges.filter((e) => e.type === "imports")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merge-batch-graphs.py data-dir resolution (.ua vs legacy)", () => {
|
||||
// Self-contained: uses its own temp roots rather than the module-global
|
||||
// .understand-anything projectRoot wired up in the top-level beforeEach.
|
||||
function runIn(root) {
|
||||
const result = spawnSync(PYTHON.command, [...PYTHON.args, MERGE_SCRIPT, root], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
it("fresh project reads/writes under .ua/", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "ua-merge-uadir-"));
|
||||
try {
|
||||
const inter = join(root, ".ua", "intermediate");
|
||||
mkdirSync(inter, { recursive: true });
|
||||
writeFileSync(
|
||||
join(inter, "batch-0.json"),
|
||||
JSON.stringify({ nodes: [fileNode("src/a.py")], edges: [] }),
|
||||
);
|
||||
const result = runIn(root);
|
||||
expect(result.status).toBe(0);
|
||||
// Output landed in .ua/, legacy dir never created.
|
||||
const out = JSON.parse(
|
||||
readFileSync(join(inter, "assembled-graph.json"), "utf-8"),
|
||||
);
|
||||
expect(out.nodes.map((n) => n.id)).toContain("file:src/a.py");
|
||||
expect(existsSync(join(root, ".understand-anything"))).toBe(false);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("legacy .understand-anything/ wins even when .ua/ also exists", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "ua-merge-legacy-"));
|
||||
try {
|
||||
const legacyInter = join(root, ".understand-anything", "intermediate");
|
||||
mkdirSync(legacyInter, { recursive: true });
|
||||
mkdirSync(join(root, ".ua", "intermediate"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(legacyInter, "batch-0.json"),
|
||||
JSON.stringify({ nodes: [fileNode("src/a.py")], edges: [] }),
|
||||
);
|
||||
const result = runIn(root);
|
||||
expect(result.status).toBe(0);
|
||||
expect(existsSync(join(legacyInter, "assembled-graph.json"))).toBe(true);
|
||||
expect(existsSync(join(root, ".ua", "intermediate", "assembled-graph.json"))).toBe(false);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user