Procházet zdrojové kódy

docs(architecture): add repo structure, build flow and packaging spec (@controlstack/oac)

darrenhinde před 2 týdny
rodič
revize
145b80d5b9

+ 222 - 0
docs/architecture/canonical-refactor/08-STRUCTURE-AND-PACKAGING.md

@@ -0,0 +1,222 @@
+# 08 — Repo Structure, Build Flow & Packaging
+
+> **Status:** Decided — closes `04`'s Open Questions 1, 2, 3, 4, 5, 6, 9.
+> **Date:** 2026-07-15 · Companion to `07-EXECUTION-PLAN.md`.
+> One picture of where everything lives, how a change flows from author to user,
+> and exactly what npm ships.
+
+---
+
+## 1. Target repo layout (the monorepo)
+
+```
+OpenAgentsControl/
+├── content/                        ← THE source of truth (tool-neutral, hand-authored)
+│   ├── agents/                     #   merged agents (*.md, YAML frontmatter = IR fields)
+│   ├── skills/                     #   union of all skills; `targets:[]` gates applicability
+│   ├── commands/
+│   ├── context/                    #   MVI HTML-comment metadata preserved on disk
+│   ├── hooks/                      #   session-start etc. (harvested from CC plugin)
+│   └── registry.json               #   component catalog + profiles + dependency graph
+│
+├── packages/                       ← pnpm workspace (all TypeScript, Node ≥ 20, zero Bun)
+│   ├── core/                       #   @controlstack/core (PRIVATE): Zod IR, MVI parser, serializers,
+│   │                               #   capability resolution, dependency/wildcard/alias graph
+│   ├── adapters/                   #   @controlstack/adapters (PRIVATE): BaseAdapter + opencode |
+│   │                               #   claude | agents-md (| cursor, experimental)
+│   └── cli/                        #   @controlstack/oac (PUBLISHED): commands, installer
+│       ├── src/commands/           #   init | add | remove | update | build | doctor | status | list
+│       ├── src/lib/                #   installer, manifest, config, paths, registry, ide-detect
+│       └── bin/oac.js              #   #!/usr/bin/env node (moves here from repo root /bin)
+│
+├── evals/                          ← behavioral test harness (unchanged; the end-to-end gate)
+├── docs/                           ← user + architecture docs (planning archived)
+├── scripts/                        ← repo tooling only (registry validation, release)
+│
+├── .opencode/                      ← GENERATED (oac build --target opencode) — dogfooding;
+├── .claude-plugin/ + plugins/      ← GENERATED (oac build --target claude) — committed until
+│                                   #   Stage 6, then built in CI for marketplace publishing
+│
+├── pnpm-workspace.yaml             #   packages/* + evals/framework
+├── pnpm-lock.yaml                  #   the ONE lockfile (bun.lock + package-lock.json deleted)
+└── package.json                    #   root = the published package (see §4)
+```
+
+Mapping from today: `packages/compatibility-layer/{types,core,mappers}` → `packages/core`;
+`packages/compatibility-layer/adapters` → `packages/adapters`; `packages/cli` stays (de-Bunned);
+`packages/plugin-abilities` unchanged for now (CC plugin runtime, revisit post-Stage 6);
+`.opencode/` demoted from source to build output. The `skill`/`skills`, `plugin`/`plugins`
+singular-plural schism dies with `.opencode/`-as-source.
+
+**Dependency direction (enforced by lint):** `cli → adapters → core`. `core` has no I/O
+beyond `node:fs`, no tool knowledge; `adapters` know tools but not installation; `cli` owns
+files-on-disk, manifests, and UX. Nothing imports upward.
+
+---
+
+## 2. Build flow — one pipeline, two operators
+
+The same pipeline runs in two places; only *who runs it* and *what's in `content/`* differ.
+
+### 2.1 Maintainer flow (this repo)
+
+```
+content/**  ──parse──▶  IR (Zod)  ──resolve deps──▶  adapt per target  ──▶  write + manifest
+   ▲                                                        │
+   └────────── golden snapshots + corpus tests ◀── validate ┘
+```
+
+- `pnpm build` compiles packages; `oac build --all` regenerates `.opencode/`, the CC plugin
+  tree, and `AGENTS.md` from `content/`.
+- CI gate: `oac build --check` (build to temp, diff, fail on drift) — generated trees can
+  never silently diverge from source again.
+- Releases: CI builds targets fresh, packs the tarball, publishes to npm + regenerates the
+  CC marketplace entry. Humans never hand-edit generated trees.
+
+### 2.2 User flow (their project)
+
+```
+npx @controlstack/oac init --profile developer --target claude,opencode
+        │
+        ├─ 1. copy profile's component set (+ dependency closure) from the BUNDLED
+        │     content/ into  <project>/content/          ← theirs to edit
+        ├─ 2. oac build  → generates .opencode/, .claude/…, AGENTS.md from THEIR content/
+        ├─ 3. write .oac/manifest.json (sha256 ledger) + .oac.json (discovery)
+        └─ 4. oac doctor --verify → confirms each tool actually loads the output
+```
+
+User project after init:
+
+```
+myproject/
+├── content/            ← their editable source (team standards live here, in git)
+├── .oac/               ← manifest.json (sha256), config.json, backups/<ts>/
+├── .oac.json           ← discovery: { "context": { "root": "content" } }
+├── .opencode/          ← generated (never hand-edit; rebuilt by oac build)
+├── .claude/ | plugin   ← generated
+└── AGENTS.md           ← generated (agents-md target — works with any tool)
+```
+
+The loop that makes it sticky: **edit `content/` → `oac build` → every tool updates.**
+`oac update` refreshes upstream content sha256-safely (user-edited files are never
+clobbered without `--yolo`); `oac add context:security` pulls a component + its dependency
+closure and rebuilds affected targets.
+
+**Closes `04` Q9:** yes, users keep an editable `content/` in their project. Without it,
+"author once, build many" dies at the user boundary — the CLI therefore ships the full
+parser + adapters, not pre-built output.
+
+---
+
+## 3. Package topology — one published package
+
+**Closes `04` Q2.** Publish **exactly one** package under a fresh scope:
+**`@controlstack/oac`** (bin: `oac`). `@controlstack/core` and `@controlstack/adapters` are
+`"private": true` workspace packages **bundled into the CLI's `dist/` at build time**
+(tsup/esbuild, `noExternal` for workspace deps).
+
+> ⚠️ **Action before Stage 6:** claim the `controlstack` org on npmjs.com (and ideally the
+> GitHub org) — scope names are first-come. Until it's claimed, docs/code keep the name
+> behind a single constant so a rename is one commit.
+
+Why one package:
+- **No version skew** — a manifest's `oacVersion` unambiguously identifies parser + adapters
+  + content that produced any output (the current 5-versions-across-7-files drift is the
+  counter-example).
+- **One install, one tarball, offline-complete** — nothing to resolve at runtime.
+- **Smaller supply-chain surface** — users audit one package, not three.
+- All three existing names — `@nextsystems/oac` (0.7.1), `@nextsystems/oac-cli` (1.0.0),
+  and `@openagents-control/compatibility-layer` (0.1.0) — are **deprecated on npm** with
+  pointers at `@controlstack/oac` (`npm deprecate` keeps old installs working while steering
+  new ones). If programmatic consumers show up later, publishing `@controlstack/core`
+  separately is an additive, non-breaking step.
+
+---
+
+## 4. What npm ships (the tarball)
+
+```jsonc
+// root package.json (the published one)
+{
+  "name": "@controlstack/oac",
+  "version": "<one version, everywhere>",
+  "bin": { "oac": "packages/cli/bin/oac.js" },   // plain #!/usr/bin/env node
+  "engines": { "node": ">=20" },
+  "files": [
+    "packages/cli/bin/",
+    "packages/cli/dist/",     // bundled CLI (core + adapters inlined)
+    "content/",               // the neutral source, shipped whole
+    "LICENSE", "README.md", "CHANGELOG.md"
+  ]
+}
+```
+
+- `registry.json` ships *inside* `content/` — one bundled root, one root-detection anchor.
+- Package root found by an explicit check (`package.json` `name === "@controlstack/oac"`),
+  with `OAC_PACKAGE_ROOT` env override for the monorepo/dev case. This replaces `bundled.ts`'s
+  registry-absence heuristic, which breaks post-refactor on **two** anchors (`06` G4).
+- Explicitly NOT shipped: `.opencode/` (generated), `install.sh`/`update.sh` (deleted),
+  `evals/`, `docs/`, any `node_modules` (enforced in `files` globs *and* the runtime walker).
+- **No `postinstall` script. No install-time network.** `npx @controlstack/oac init` works
+  offline after download — the modern replacement for `curl | bash`.
+- Symlinks: none in `content/` (converted to `aliases[]` at Stage 1) — so tarball behavior
+  is identical on every platform.
+
+**Closes `04` Q1:** Node-portable build (option A). Bun is gone even for dev (pnpm + vitest);
+one runtime story.
+**Closes `04` Q6:** registry fully bundled; no remote registry in v1 (a remote catalog
+pointing at unbundled components is meaningless).
+
+---
+
+## 5. pnpm — workspace and security posture
+
+```yaml
+# pnpm-workspace.yaml
+packages:
+  - packages/*
+  - evals/framework
+```
+
+- **One lockfile** (`pnpm-lock.yaml`); `bun.lock`, root `package-lock.json`, and
+  `.opencode/package-lock.json` all deleted.
+- **Strict by default:** pnpm's non-flat `node_modules` eliminates phantom dependencies —
+  every import must be declared, which is exactly the discipline the IR packages need.
+- **Scripts off:** `pnpm.onlyBuiltDependencies: []` in root `package.json` — dependency
+  lifecycle scripts (the #1 npm supply-chain vector) are ignored unless explicitly
+  allowlisted. CI runs `pnpm install --frozen-lockfile` always.
+- **Cooldown:** `minimumReleaseAge: 4320` (3 days) in `.npmrc`/pnpm settings — newly
+  published dependency versions are not picked up immediately, blunting
+  compromised-release attacks.
+- **Publish integrity:** `pnpm publish` from CI only, with **npm provenance**
+  (`--provenance`) so users can verify the tarball was built from this repo by CI.
+- Renovate/dependabot PRs run the full package test matrix (Stage 0 CI gate) before merge.
+
+Dev loop: `pnpm install` → `pnpm -r build` → `pnpm -r test` → `pnpm pack` (tarball smoke
+test: install the packed tarball into a temp dir on 3 OSes in CI and run
+`oac init && oac doctor --verify` — this IS the Stage 5 e2e gate).
+
+---
+
+## 6. Remaining `04` question closures
+
+| Q | Decision |
+|---|---|
+| Q3 interactivity | Flags-first, fully non-interactive under CI. When stdin is a TTY and required flags are absent, `oac init` asks profile/target via `@clack/prompts` (keeps `install.sh`'s guided feel without bash menus). Every prompt has a flag equivalent. |
+| Q4 default target | No tool detected + no `--target` + no TTY → default `agents-md` (universal, harmless); with TTY → prompt. Never guess a heavyweight target. |
+| Q5 Windows global path | `getGlobalDir()` = `%LOCALAPPDATA%\oac` on Windows, `$XDG_CONFIG_HOME/oac` else `~/.config/oac`. Existing `~/.config/opencode` installs detected and adopted by `oac migrate`. |
+| Q7 `apply` alias | Kept one minor release after Stage 6, printing a deprecation pointer to `oac build`. |
+| Q8 backup retention | `oac clean` prunes `.oac/backups/` older than 30 days; never auto-deletes on update. |
+
+---
+
+## 7. Stage hooks (where this lands in `07`)
+
+- **Stage 0** creates `pnpm-workspace.yaml`, deletes the competing lockfiles, moves dev
+  scripts to pnpm.
+- **Stage 2** creates `packages/core` in this shape (the compatibility-layer split).
+- **Stage 3** creates `packages/adapters` + the `agents-md` target.
+- **Stage 5** re-shapes `packages/cli`, retargets `bundled.ts`, implements §4's tarball and
+  the pack-and-verify e2e.
+- **Stage 6** flips `files`, deprecates the two old npm packages, and turns on provenance
+  publishing.