diff --git a/ADOPTING.md b/ADOPTING.md index 2d232d4..68f5f91 100644 --- a/ADOPTING.md +++ b/ADOPTING.md @@ -26,7 +26,7 @@ What you do **not** copy: HANDBOOK.md, the `methodology` domain, `examples/`, `t curl -fsSL https://raw.githubusercontent.com/emafriedrich/knowledge-driven-engineering/main/install.sh | bash -s -- ``` -`install.sh` performs the manual steps below, is idempotent, and never overwrites an existing file (it skips and tells you). It detects your package manager (npm, pnpm — workspaces included, yarn, bun) for the `yaml` dependency, and a dependency failure warns instead of aborting the install. Run it from the root of your repository; pass your first domain name as the argument. The argument only seeds a fresh install: on a repository that already has `knowledge/index.yaml` the installer creates no domain and points you to `npm run knowledge -- domain add `. Installing, upgrading (`--upgrade`) and adding a domain are three separate actions. Offline installs work from a local clone: `KDE_SOURCE=/path/to/clone bash install.sh `. To pin a release instead of tracking `main`, set `KDE_REF=v0.1.0`. +`install.sh` performs the manual steps below, is idempotent, and never overwrites an existing file (it skips and tells you). It detects your package manager (npm, pnpm — workspaces included, yarn, bun) for the `yaml` dependency, and a dependency failure warns instead of aborting the install. Run it from the root of your repository; pass your first domain name as the argument. It refuses to run in a subdirectory of a repository that already runs KDE — a second catalog nothing reads — and names the directory to run it from. The argument only seeds a fresh install: on a repository that already has `knowledge/index.yaml` the installer creates no domain and points you to `npm run knowledge -- domain add `. Installing, upgrading (`--upgrade`) and adding a domain are three separate actions. Offline installs work from a local clone: `KDE_SOURCE=/path/to/clone bash install.sh `. To pin a release instead of tracking `main`, set `KDE_REF=v0.1.0`. ## Upgrading diff --git a/install.sh b/install.sh index 94c731f..c169730 100755 --- a/install.sh +++ b/install.sh @@ -39,6 +39,23 @@ say() { printf '%s\n' "$*"; } add() { say " add $1"; } skip() { say " skip $1 (already exists)"; } +# A repository runs one knowledge catalog. Installing below a directory of the +# same git repository that already has one creates a second, empty KDE that +# nothing reads — refuse and name the right directory instead. +TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || true)" +if [ -n "$TOPLEVEL" ]; then + HERE="$(pwd -P)" + TOPLEVEL="$(cd "$TOPLEVEL" && pwd -P)" + dir="$HERE" + while [ "$dir" != "$TOPLEVEL" ] && [ "$dir" != "/" ]; do + dir="$(dirname "$dir")" + if [ -f "$dir/knowledge/index.yaml" ] && [ -f "$dir/tools/knowledge-check.mts" ]; then + say "install.sh: ${dir} already runs Knowledge-Driven Engineering; run the installer from there, not from ${HERE}." >&2 + exit 1 + fi + done +fi + # Version of the framework-owned files a repository runs, read from the # kde-version marker the installer stamps on line 1 of every copy. installed_version() { @@ -403,7 +420,10 @@ fi say "" say "Done. Next steps:" say " 1. npm run knowledge:check" -say " 2. Seed current truth with one decision your team already made:" +say " 2. Existing codebase? Recover the behavior that lives only in code, one domain at a time:" +say " npm run knowledge -- backfill ${DOMAIN:-}" +say " then ask your coding agent to \"backfill the ${DOMAIN:-} domain\" — it follows tools/backfill-protocol.md." +say " Starting fresh? Seed current truth with one decision your team already made:" say " npm run knowledge -- new decision ${DOMAIN:-} \"\" --author <you>" say " npm run knowledge -- promote <ID> --by <you>" say " 3. Enable branch protection with code-owner review so knowledge promotion needs a human." diff --git a/package.json b/package.json index b581ac2..dbe7af6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-driven-engineering", - "version": "0.7.1", + "version": "0.7.2", "private": true, "type": "module", "scripts": { diff --git a/tests/install.test.ts b/tests/install.test.ts index bfa6631..0c21f59 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { appendFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import test from 'node:test'; @@ -83,6 +83,24 @@ test('installer stamps framework-owned files with the package.json version and r } }); +test('installer refuses a subdirectory of a repository that already runs KDE, and suggests backfill as a next step', () => { + const root = mkdtempSync(join(tmpdir(), 'kde-install-test-')); + try { + execFileSync('git', ['init', '-q'], { cwd: root }); + const fresh = install(root, 'shop'); + assert.match(fresh, /Existing codebase\? Recover the behavior that lives only in code[\s\S]*npm run knowledge -- backfill shop[\s\S]*"backfill the shop domain" — it follows tools\/backfill-protocol\.md/); + assert.match(fresh, /Starting fresh\? Seed current truth/); + + const nested = join(root, 'apps', 'api'); + mkdirSync(nested, { recursive: true }); + assert.throws(() => install(nested, '--upgrade'), /already runs Knowledge-Driven Engineering; run the installer from there, not from .*apps\/api/); + assert.equal(existsSync(join(nested, 'knowledge')), false, 'nothing is written in the subdirectory'); + assert.equal(existsSync(join(nested, 'tools')), false); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('installer rejects unknown options', () => { const root = mkdtempSync(join(tmpdir(), 'kde-install-test-')); try { diff --git a/tests/knowledge.test.ts b/tests/knowledge.test.ts index c1a15b4..42bc853 100644 --- a/tests/knowledge.test.ts +++ b/tests/knowledge.test.ts @@ -334,6 +334,12 @@ test('prompt: new scaffolds from the template and promote sets current — promp }); }); +test('a command run where the catalog has no domains says the directory may be the wrong one', () => { + withFixture({ 'knowledge/index.yaml': 'domains: {}\n' }, (root) => { + assert.throws(() => commandBackfill(root, 'orders'), /has no domains at all — if this repository runs KDE from another directory, run the command there/); + }); +}); + test('cli: usage errors are CommandErrors, and flags with several values are collected', () => { withFixture({}, (root) => { assert.throws(() => run(root, ['promote']), /usage:/); diff --git a/tools/knowledge.mts b/tools/knowledge.mts index 85a2b40..aa4f94f 100644 --- a/tools/knowledge.mts +++ b/tools/knowledge.mts @@ -124,10 +124,14 @@ function slugify(text: string): string { // --- Repository lookups ----------------------------------------------------- function findDomain(root: string, name: string): { catalog: Catalog; domain: CatalogDomain } { - for (const catalog of loadCatalogs(root)) { + const catalogs = loadCatalogs(root); + for (const catalog of catalogs) { const domain = catalog.domains.get(name); if (domain) return { catalog, domain }; } + if (catalogs.every((catalog) => catalog.domains.size === 0)) { + throw new CommandError(`domain ${name} is not in any catalog, and the catalog in ${root} has no domains at all — if this repository runs KDE from another directory, run the command there; otherwise create it: npm run knowledge -- domain add ${name} --description "<one line>"`); + } throw new CommandError(`domain ${name} is not in any catalog — create it: npm run knowledge -- domain add ${name} --description "<one line>"`); }