From e81793de123df5c48040ffa2d3652b68844a677a Mon Sep 17 00:00:00 2001 From: sameera Date: Sun, 23 Aug 2026 11:08:41 -0400 Subject: [PATCH 1/2] =?UTF-8?q?close:=20typescript-capabilities-as-verbs?= =?UTF-8?q?=20=E2=80=94=20born-at-close=20epic,=20close=20record,=20lesson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012GpZHcHhAojBJjvu4DySrr --- .nexus/queue/epic-247/close-record.md | 38 +++++ .nexus/queue/epic-247/epic.md | 160 ++++++++++++++++++ ...-08-23-typescript-capabilities-as-verbs.md | 25 +++ 3 files changed, 223 insertions(+) create mode 100644 .nexus/queue/epic-247/close-record.md create mode 100644 .nexus/queue/epic-247/epic.md create mode 100644 docs/delivery/lessons/2026-08-23-typescript-capabilities-as-verbs.md diff --git a/.nexus/queue/epic-247/close-record.md b/.nexus/queue/epic-247/close-record.md new file mode 100644 index 0000000..fecd7d2 --- /dev/null +++ b/.nexus/queue/epic-247/close-record.md @@ -0,0 +1,38 @@ +--- +title: "Close Record: Collapse the component-invoked TypeScript capabilities into verbs on one named executable" +epic: "#247" +feature: "Component Distribution" +date: 2026-08-23 +analyze: ran 2026-08-23 @ 31fdeaae5ede15c394849509321c9c45a962d2c5 +record: "#277" +record_hash: ce3bf375199c26537c5007c06bbc41c22aabde938646178282f944f964d97455 +range: + - repo: github.com/sameera/nexus + base: 696237948ed63725bb9c0bec62c305935ddb737e + head: ca0553675f0bfb2cf1ce23679c10572f2e0467ce +--- + +# Close Record: Collapse the component-invoked TypeScript capabilities into verbs on one named executable + +## Key Decisions + +- **Hermetic `gh` stand-in for migration-axis parity (story #272):** a committed, PATH-shimmed executable stand-in (`libs/portable-tools/corpus/bin/gh`) answers the exact `gh` call shapes `epic-resolve`/`record-digest`/`pr-worktree` make, matched by argv shape and read from a fixture named by `NEXUS_PARITY_GH_FIXTURE`. Needed because `defaultRunner` shells out via `spawnSync` with no injectable seam at the CLI boundary — only an executable on `PATH` can stand in for a spawned child process. Refuted: reusing `libs/epic-resolve/src/gh-fixtures.ts`'s spec-only `FixtureGraph`/`makeGhRunner`, which doesn't cross a process boundary without reimplementing the argv-matching layer anyway. +- **Migration-axis corpus for `epic-resolve` kept to zero-sub-issue cases:** the two AC-required cases (clean resolve, named failure diagnostic) are both reachable without a second external-program surface (`python3` classification resolution), so that stand-in was skipped as unnecessary for this story's ACs. A fuller corpus remains addable later without revisiting the choice. +- **`pr-worktree` effect parity runs script-then-verb against one shared scratch repo, not two independent copies:** since the worktree path derivation is a pure function of `repoRoot`, running both sides against the same repo makes the second call exercise the already-tested idempotent re-run path instead of requiring a path-normalisation primitive for two independently built repos. Refuted: two independent scratch repos with path normalisation — more rigorous but adds real surface (deterministic git SHAs or a new normaliser) for a rigor increment the story's AC didn't ask for. +- **`pr-worktree`'s `gh pr view` fixture is generated at test time, not committed:** `deriveRange`/`openCloseWorktree` do real git operations against SHAs a freshly built scratch repo produces, so a statically committed fixture would reference commit SHAs from a repo that doesn't exist yet at commit time. Refuted: committing a fixed scratch-repo `.git` (or bundle) as corpus data — no precedent in this repo, heavier to maintain than the topology builder already in place. +- **Standalone launchers hoist the process boundary out of all five distiller capabilities (story #274), not just the two the decision record's Risk section named:** all five capability files lose their `main()`/self-exec guard entirely (including the three that already had filename-basename disambiguation), each gaining a guard-free `-launcher.ts` sibling that `ENTRY_POINTS` now names. The record states the hazard is structural ("inlining collapses every module's sense of which file was invoked down to one value") and that the basename mitigation "expires silently the moment the artifact is renamed" — exactly what repointing `ENTRY_POINTS` at a launcher does to the three already-guarded files. Verified concretely: all six bundles built fresh, every verb and every standalone `.mjs`/`pnpm nexus:*` script run, byte-identical output. +- **Acceptance harness relocated to `libs/pr-acceptance/src/cli.ts`, and `.claude/skills/nxs-pr-acceptance/` deleted outright (story #275):** a `SKILL.md` is what makes a capability agent-invocable, so keeping a neutered pointer would half-preserve the exact property the decision record's accepted trade removes. Its operating-constraints prose moved into the live-acceptance runbook, which already sequenced the same commands. Refuted: leaving a discoverability-only `SKILL.md` stub — undoes the payload-boundary trade the record accepts. +- **Structural composition check as a new regex-based module, not an AST parse (story #275):** `component-composition.ts`'s `findWorkspaceImports` is a regex over `from "@nexus/..."` / `require("@nexus/...")`, enforced against a committed `component-composition-waivers.ts` register (the 7 legacy `.claude/skills/*/scripts/*.ts` shims stories #272–#274 left in place) via a live-tree test that fails if the register goes stale in either direction. A regex suffices because the surface is this repo's own component tree, where every real violation is a plain top-level import. Refuted: an AST-based check — no file in the managed subtrees uses a dynamic/computed import, so the added compiler-dependency weight isn't warranted. +- **`pnpm nexus:*` dev aliases repointed at `nexus-cli.ts` directly, not at story #274's per-capability launchers (story #276):** decision record #277 explicitly calls for "no separate source-side entry point, and no verb-specific source command" for this story; the launchers exist so `build-bundles.ts` has a guard-free standalone entry per artifact, a distinct concern from the maintainer-facing dev alias. `nexus:vendor-tools` is untouched (it's the vendoring orchestrator, not a capability alias). Verified each repointed script by hand against real repo state. + +## Deviation Rationale + +- **No dedicated "recorded argument log" comparison facet in `pr-worktree`/`close-migration` parity, contrary to the decision record's stated five-facet comparison ("standard output, standard error, exit code, the recorded argument log, and the resulting file tree"):** the CLI adapter in `nexus-cli.ts` calls the exact same shared library functions (`resolveRole`, `resolvePr`, `openAnalyzeWorktree`, `openCloseWorktree`, `deriveRange`, `removeWorktree`, `closePreflight`, `migrateEntry`) with the same `Runner` the legacy scripts call — there is exactly one place in the codebase that constructs `git`/`gh` argv for these operations, so no second code path could construct a diverging one. A dedicated argument-log stand-in (a fake `git` on `PATH` logging every invocation) was judged redundant given that structural guarantee, plus the existing worktree-presence assertions and byte-identical stdout/stderr/exit-code comparison, which together already prove the spawned calls' effect agrees. Story #273's AC ("the spawned process receives the same arguments") is satisfied by construction rather than by a runtime facet, which is a real divergence from the record's literal chosen approach even though the underlying property holds. Flagged to the decision record in the close amendment (#277). + +## Deferred Scope + +None. All follow-on work this epic's Out-of-Scope section names (#248–#254, #257) was already filed before this epic started; nothing new surfaced during implementation that isn't already tracked. + +## Process Lesson + +Recorded in: `docs/delivery/lessons/2026-08-23-typescript-capabilities-as-verbs.md` diff --git a/.nexus/queue/epic-247/epic.md b/.nexus/queue/epic-247/epic.md new file mode 100644 index 0000000..6b0c7cf --- /dev/null +++ b/.nexus/queue/epic-247/epic.md @@ -0,0 +1,160 @@ +--- +feature: "Component Distribution" +feature_path: docs/features/component-distribution +epic: "Collapse the component-invoked TypeScript capabilities into verbs on one named executable" +slug: typescript-capabilities-as-verbs +created: 2026-08-23 +type: enhancement +complexity: M +complexity_drivers: [ten capabilities move behind one argument dispatcher, the fingerprint pin and the executed-diff parity gate must stay green through every move, a capability leaves the shipped component payload, the ordering gate requires every verb to land before any invocation string changes] +concepts: [portable-tooling, pr-worktree, record-digest, distiller, workspace-resolution] +link: "#247" +record: "#277" +record_state: closed +--- + +# Epic: Collapse the component-invoked TypeScript capabilities into verbs on one named executable + +## Description + +Nexus ships its TypeScript capabilities in two shapes today, and neither one runs outside this repository. Eight skill scripts are TypeScript files that import `@nexus/*` workspace packages, so they resolve only through the pnpm symlinks into `libs/`. Five more capabilities are separate bundles that each carry a fresh copy of the same shared core. A repository that installs Nexus components therefore receives skill scripts it cannot execute, and a distributed artifact of 1,835 KB that covers 6 of the 13 capabilities. + +This epic applies one rule to that set. A capability that any Nexus component body invokes becomes a verb on a single named executable. A capability that only the Nexus build or release process invokes stays a TypeScript file in this repository and never ships. Ten of the thirteen capabilities gain new verbs: three read-only resolvers, two that drive git worktrees, and the five the distiller invokes. Two more are already reachable as workspace verbs on that executable and need no work. The thirteenth is the pull-request acceptance harness. It gains no verb and leaves the shipped payload, because no component invokes it and it requires a git checkout of Nexus itself. The logic does not move. It already lives in `libs/`, so only each script's argument-parsing shim relocates into the verb table. + +The value is reachability rather than size. Once every component-invoked capability answers to a verb name, a component can name the toolkit instead of a path, which is what the rest of the component-distribution work depends on. The size reduction the discovery measured arrives when the standalone entry points are removed, and that removal follows the invocation rewrite in #250 rather than happening here. The ordering is a gate, not a preference: a component that names a verb which does not exist yet fails when a pipeline stage runs, not when the toolkit is installed, so every verb lands and passes the parity gate first. + +## Success Metrics + +- All ten component-invoked TypeScript capabilities run as verbs on one named executable, on a bare `node` binary, in a checkout with no installed packages. +- Every verb produces the same standard output, the same standard error and the same exit code as the script or bundle it replaces, for every case in the committed corpus. +- The bundle fingerprint pin matches a freshly built bundle after every capability has moved. +- The pull-request acceptance harness is absent from the vendored component payload, and the payload fingerprint matches a fresh build of the managed component set. +- The single executable carrying all ten newly added verbs stays under 450 KB. +- A maintainer runs any verb directly from the TypeScript source, with no build step between an edit and the next run. + +## Personas + +Per `docs/product/context.md`. + +## User Stories + +### Story #272: The three read-only resolver capabilities run as verbs + +**As a** Nexus maintainer **I want** the documentation-path, epic-resolution and record-digest capabilities reachable as verbs on the named executable **so that** a component can invoke them on a machine that holds no Nexus checkout. + +## Acceptance Criteria + +- [ ] **Given** a directory with no installed packages and no Nexus checkout, **when** the built executable is run on a bare `node` binary as `nexus abs-doc-path`, as `nexus epic-resolve` and as `nexus record-digest`, **then** each verb runs to completion and none of the three fails to resolve a module. +- [ ] **Given** each committed corpus case for the documentation-path capability, **when** the executed-diff parity harness runs `get_abs_doc_path.ts` under `tsx` and runs the `abs-doc-path` verb on a freshly built executable, **then** it reports no divergence in standard output, standard error or exit code. +- [ ] **Given** each committed corpus case for the epic-resolution capability, **when** the parity harness compares `epic_resolve.ts` against the `epic-resolve` verb, **then** it reports no divergence, including for the cases that print a named failure diagnostic on standard error. +- [ ] **Given** each committed corpus case for the record-digest capability, **when** the parity harness compares `record_digest.ts` against the `record-digest` verb, **then** it reports no divergence. +- [ ] **Given** the three verbs have landed, **when** the bundle fingerprint check runs, **then** the committed bundle fingerprint pin matches the freshly built executable's hash. +- [ ] **Given** the executable is run with `--help` or with no verb, **when** the usage text is printed, **then** it names all three verbs and their required arguments. + +## Notes + +The logic already lives in `libs/`. Only each script's argument-parsing shim relocates into the verb table. + +The existing script files stay in place. They are removed after the invocation rewrite in #250, because a component that still names a script path must keep finding one. + +This story establishes how a verb is registered and how its parity coverage is written. The later verb stories follow that pattern. + +### Story #273: The worktree and migration capabilities run as verbs + +**As a** Nexus maintainer **I want** the pull-request worktree capability and the close-migration capability reachable as verbs **so that** the two stages that manage a git worktree stop depending on the target repository's Node toolchain. + +## Acceptance Criteria + +- [ ] **Given** a directory with no installed packages and no Nexus checkout, **when** the built executable is run on a bare `node` binary as `nexus pr-worktree` and as `nexus close-migration`, **then** both verbs run to completion and neither fails to resolve a module. +- [ ] **Given** each committed corpus case for the worktree capability, **when** the parity harness runs `pr_worktree.ts` under `tsx` and runs the `pr-worktree` verb on a freshly built executable, **then** it reports no divergence in standard output, standard error or exit code, and both runs leave the same worktrees present or absent. +- [ ] **Given** each committed corpus case for the close-migration capability, **when** the parity harness compares `close_migration.ts` against the `close-migration` verb, **then** it reports no divergence, including for the inputs the capability refuses. +- [ ] **Given** both verbs have landed, **when** the bundle fingerprint check runs, **then** the committed bundle fingerprint pin matches the freshly built executable's hash. +- [ ] **Given** either verb spawns `git` or `gh`, **when** it runs from the built executable, **then** the spawned process receives the same arguments the TypeScript source form spawns it with. + +## Notes + +These two capabilities write to the filesystem and drive git, so parity must cover the effect as well as the output. + +Extends #272, which establishes the verb registration and parity pattern. + +### Story #274: The five distiller capabilities run as verbs + +**As a** Nexus maintainer **I want** the atlas, validator, entry-diff, drift-advisory and registry-seeding capabilities reachable as verbs on the same executable **so that** the distiller stops needing one bundle per capability and one invocation string per install shape. + +## Acceptance Criteria + +- [ ] **Given** every case in the committed corpus, **when** the built executable is run as `nexus generate-atlas`, `nexus validate-concepts`, `nexus derive-entry-diff`, `nexus drift-advisory` or `nexus seed-registry`, **then** each verb prints the same standard output and exits with the same code as the standalone `.mjs` artifact it replaces. +- [ ] **Given** the atlas verb and the registry-seeding verb write files, **when** they run from the executable over the corpus, **then** the bytes they write are identical to the bytes the standalone `.mjs` artifact writes. +- [ ] **Given** the entry-diff capability, which has no executed-diff coverage today, **when** the parity harness runs it, **then** the harness compares the TypeScript source against the built form over at least one corpus case and reports no divergence. +- [ ] **Given** the five distiller verbs have landed, **when** the bundle fingerprint check runs, **then** the committed bundle fingerprint pin matches a fresh build of every entry point it covers. + +## Notes + +The five standalone entry points keep building in this story. Removing them is blocked on the invocation rewrite in #250, because the distiller command still names `.nexus/tools/generate-atlas.mjs` and `pnpm nexus:generate-atlas` in its body. The distributed-artifact size reduction the discovery measured arrives at that removal, not here. + +Extends #272, which establishes the verb registration and parity pattern. + +### Story #275: The acceptance harness leaves the shipped component payload + +**As a** Nexus maintainer **I want** the pull-request acceptance harness out of the vendored component payload **so that** an adopter never receives a capability that only runs inside a Nexus checkout. + +## Acceptance Criteria + +- [ ] **Given** a freshly vendored component payload, **when** its file list is inspected, **then** it contains no file belonging to the acceptance harness. +- [ ] **Given** the harness now lives outside the three vendored component subtrees, **when** a maintainer follows the acceptance runbook, **then** the runbook names that location and the command that runs the harness there. +- [ ] **Given** the vendored component payload has changed, **when** the payload fingerprint check runs, **then** the committed payload fingerprint pin matches a fresh hash of the vendored component payload. +- [ ] **Given** the harness in its new location, **when** it is run inside a Nexus checkout, **then** it provisions, drives and tears down its scenarios exactly as it does today. + +## Notes + +The harness gains no verb. No component body invokes it, and it archives the Nexus checkout it walks up to find, so it cannot work from an installed toolkit. + +The requirement is that it sits outside the three vendored component subtrees. Which directory it lands in is an implementation choice for the decision record. + +### Story #276: A maintainer runs any verb from the TypeScript source + +**As a** Nexus maintainer **I want** to run any verb directly from the TypeScript source **so that** editing a capability and rerunning it does not gain a build step. + +## Acceptance Criteria + +- [ ] **Given** the contributor documentation, **when** a maintainer reads it, **then** it states one command shape that runs any verb from the TypeScript source. +- [ ] **Given** a Nexus checkout with packages installed, **when** the maintainer runs a verb through that command shape, **then** the verb produces the same standard output and the same exit code as the built executable for the same arguments. +- [ ] **Given** the maintainer edits a capability in `libs/`, **when** they rerun that verb through that command shape, **then** the edited behaviour takes effect with no rebuild of the executable. +- [ ] **Given** a verb name that does not exist, **when** it is run through that command shape, **then** the same usage text and the same exit code are produced as the built executable produces. + +## Notes + +The parity harness already runs the source under `tsx` at build time, so this path exists and needs to be kept and documented rather than invented. + +This story is the mitigation the runtime-shape decision named load-bearing for its recommendation. It covers the whole verb table, so it follows #273 and #274. + +## Assumptions + +- The verbs keep each capability's current argument names, output shape and exit codes. Changing any of them would break the components that still invoke the script or bundle form, and no component is rewritten in this epic. +- Verb names are flat and carry no compatibility tiering. The version-pinning decision refused a public and internal split, because the release has no consumer outside itself. +- The bundle keeps inlining its one third-party dependency, so no verb introduces a package install at run time. +- Every verb resolves the repository it acts on the way its script does today. Replacing the six target-root conventions in use is #248 and happens after this epic. +- A verb that spawns `git` or `gh` keeps doing so. Those are external programs, not bundled code. + +## Out of Scope + +- Rewriting any component body to name a verb instead of a script path or a bundle path. That is #250, and it must follow this epic, because a component naming a verb that does not exist fails inside a running pipeline stage. +- Deleting the eight TypeScript skill script files and the five standalone bundle entry points. Both deletions are blocked on the invocation rewrite, and both belong with it. The distributed-artifact size reduction the discovery measured arrives at that point. +- Making the Python capabilities reachable by name. That is #249. The Python toolkit stays a second named toolkit, and Nexus requires both `node` and `python3`. +- Passing the target root explicitly instead of reading the working directory. That is #248. +- The version identity, the writer stamp and the build-time gate that checks every invocation string against the declared verb set. Those are #251 and #250. +- Publishing the release, installing it, and removing it. Those are #252, #253 and #254. +- Retiring the hub's vendored tools directory. That is #257. +- The stale component archive at `libs/origin/v1/.claude`. Issue #60 already rules it out of the managed set. + +## Open Questions + +## Implementation Sequence + +| Issue | blocked_by | +|---|---| +| #272 | none | +| #273 | #272 | +| #274 | #272 | +| #275 | none | +| #276 | #273, #274 | diff --git a/docs/delivery/lessons/2026-08-23-typescript-capabilities-as-verbs.md b/docs/delivery/lessons/2026-08-23-typescript-capabilities-as-verbs.md new file mode 100644 index 0000000..4bbb799 --- /dev/null +++ b/docs/delivery/lessons/2026-08-23-typescript-capabilities-as-verbs.md @@ -0,0 +1,25 @@ +--- +date: 2026-08-23 +epic: "Collapse the component-invoked TypeScript capabilities into verbs on one named executable" +source: "#247" +--- + +# Lesson: a blocked_by chain across stories does not predict the delivery unit + +The epic decomposed into five stories with an explicit sequencing table (`#273`/`#274` blocked by +`#272`, `#276` blocked by both), which read as an intent for incremental, independently reviewable +landings. What actually shipped was one PR (#278) carrying all five stories' code as a single +2,800-line commit, reviewed and merged as one unit. The blocked_by graph was real and useful — it +governed *build order* inside that one PR (the registry pattern from #272 had to exist before +#273/#274 could extend it) — but it never became five separate review increments, because the five +capabilities share one file (`nexus-cli.ts`'s `REGISTRY`) and one parity harness whose facets grow +incrementally across the same stories. Splitting the PR would have meant each intermediate PR +shipped a registry with some verbs still missing their parity coverage. + +For the next epic with this shape — several stories whose stated purpose is "extend the same shared +structure another story establishes" — size the epic's *story* decomposition around what will +actually become separate GitHub issues for planning/tracking granularity, but expect (and tell +the lead to expect) a single PR at delivery time rather than five. Filing five story issues bought +five clean acceptance-criteria checkpoints and five closeable units on the epic's sub-issue gate; +it did not buy five smaller, independently mergeable diffs, and estimating review effort as if it +would have overstated the coordination cost. From 7ecf0551bcbb49348a104923ae9475e15fc0d5e2 Mon Sep 17 00:00:00 2001 From: sameera Date: Sun, 23 Aug 2026 11:29:28 -0400 Subject: [PATCH 2/2] distill: typescript-capabilities-as-verbs (#247) Creates verb-reachability, capturing the declarative verb-registry pattern that collapses ten component-invoked TypeScript capabilities onto one executable (eager import, process-boundary hoisting, migration-axis parity, hermetic effect parity). Updates portable-tooling (distributable growth, structural payload-composition check, acceptance-harness relocation) and nexus-setup-cli (shared-registry dispatch, workspace-status stream fix), and fans out reciprocal links to pr-worktree, close-entry-migration, record-digest, distiller, and issue-sourced-planning. Drains .nexus/queue/epic-247, consumed by this commit. --- .nexus/anchors/close-entry-migration.md | 4 +- .nexus/anchors/distiller.md | 4 +- .nexus/anchors/issue-sourced-planning.md | 4 +- .nexus/anchors/nexus-setup-cli.md | 7 +- .nexus/anchors/portable-tooling.md | 11 +- .nexus/anchors/pr-worktree.md | 4 +- .nexus/anchors/record-digest.md | 4 +- .nexus/anchors/verb-reachability.md | 30 +++ .nexus/concepts/close-entry-migration.md | 9 +- .nexus/concepts/distiller.md | 9 +- .nexus/concepts/issue-sourced-planning.md | 9 +- .nexus/concepts/nexus-setup-cli.md | 9 +- .nexus/concepts/portable-tooling.md | 11 +- .nexus/concepts/pr-worktree.md | 9 +- .nexus/concepts/record-digest.md | 9 +- .nexus/concepts/verb-reachability.md | 40 +++ .nexus/queue/epic-247/close-record.md | 38 --- .nexus/queue/epic-247/epic.md | 160 ----------- .../sameera/decisions-feat-externalize-nxs.md | 251 ------------------ docs/concepts.md | 13 +- 20 files changed, 148 insertions(+), 487 deletions(-) create mode 100644 .nexus/anchors/verb-reachability.md create mode 100644 .nexus/concepts/verb-reachability.md delete mode 100644 .nexus/queue/epic-247/close-record.md delete mode 100644 .nexus/queue/epic-247/epic.md delete mode 100644 .nexus/queue/epic-247/sameera/decisions-feat-externalize-nxs.md diff --git a/.nexus/anchors/close-entry-migration.md b/.nexus/anchors/close-entry-migration.md index b955560..20c23ef 100644 --- a/.nexus/anchors/close-entry-migration.md +++ b/.nexus/anchors/close-entry-migration.md @@ -1,7 +1,7 @@ --- concept: close-entry-migration -source_sha: b8d5deba61114fe14e2cbaab98a4df798e612f48 -generated: 2026-07-31 +source_sha: ca0553675f0bfb2cf1ce23679c10572f2e0467ce +generated: 2026-08-23 --- + +# Code Anchors: Verb Reachability + +- `libs/portable-tools/src/nexus-cli.ts` — the declarative `VerbEntry` registry (summary, usage, runnable) every reachable capability is registered in; eager static imports of all ten newly reachable capabilities; the composed usage text and the dispatcher lookup both read from the same registry object. +- `libs/portable-tools/src/nexus-cli.spec.ts` — verb dispatch coverage for every newly reachable capability. +- `libs/portable-tools/src/parity.spec.ts` — the migration axis: legacy form vs. verb, extended to capabilities already believed migrated (the workspace-status stream-divergence case), plus the five-facet effect comparison for capabilities driving external programs. +- `libs/portable-tools/src/build-bundles.ts` — `ENTRY_POINTS` now names each standalone artifact's launcher file, not the capability file directly, so the capability itself carries no process boundary. +- `libs/portable-tools/src/build-bundles.spec.ts` — pins the launcher-fronted `ENTRY_POINTS` mapping. +- `libs/portable-tools/src/generate-atlas-launcher.ts` — the guard-free capability's one remaining unconditional-invoke entry, for the standalone build only. +- `libs/portable-tools/src/validate-concepts-launcher.ts` — same role, for the validator's standalone build. +- `libs/portable-tools/src/derive-entry-diff-launcher.ts` — same role, for the diff-derivation tool's standalone build. +- `libs/portable-tools/src/drift-advisory-launcher.ts` — same role, for the drift advisory's standalone build. +- `libs/portable-tools/src/seed-registry-launcher.ts` — same role, for the registry seeder's standalone build. +- `libs/portable-tools/src/component-composition.ts` — the structural payload-boundary check: no vendored component file may import a workspace package, unless waived. +- `libs/portable-tools/src/component-composition-waivers.ts` — the shrink-only waiver register naming the legacy component scripts still awaiting the invocation rewrite. +- `libs/portable-tools/src/component-composition.spec.ts` — composition-check coverage. +- `libs/portable-tools/corpus/bin/gh` — the hermetic, PATH-shimmed `gh` stand-in the migration axis matches by argv shape, answering from committed fixtures. +- `libs/portable-tools/corpus/epic-resolve/success.json`, `libs/portable-tools/corpus/epic-resolve/not-found.json` — fixtures the `gh` stand-in answers from for the epic-resolve migration-axis cases. +- `libs/portable-tools/corpus/record-digest/approved.json`, `libs/portable-tools/corpus/record-digest/not-found.json`, `libs/portable-tools/corpus/record-digest/not-planned.json` — same, for the record-digest migration-axis cases. +- `libs/abs-doc-path/src/resolve.ts`, `libs/abs-doc-path/src/settings.ts`, `libs/abs-doc-path/src/repo-root.ts`, `libs/abs-doc-path/src/normalize.ts` — the documentation-path capability's own logic, lifted from its script into a library ahead of becoming a verb, keeping its hand-rolled manifest parser unchanged for parity. +- `libs/pr-acceptance/src/cli.ts` — the acceptance harness's entry point, relocated beside its own library, outside every vendored component subtree; gains no verb. +- `.claude/skills/nxs-pr-acceptance/SKILL.md` — deleted; the harness stops being agent-invocable. diff --git a/.nexus/concepts/close-entry-migration.md b/.nexus/concepts/close-entry-migration.md index 30edcce..995ab8c 100644 --- a/.nexus/concepts/close-entry-migration.md +++ b/.nexus/concepts/close-entry-migration.md @@ -1,8 +1,8 @@ --- title: "Close-Entry Migration" aliases: ["queue-entry migration", "cross-repo close tail", "close range stamping", "hub queue migration", "migrate-verify-remove"] -touches: ["workspace-resolution", "committed-queue", "distiller", "remote-identity-normalization", "ephemeral-handoff-entry", "scratch-capture"] -last_updated_by: "#170" +touches: ["workspace-resolution", "committed-queue", "distiller", "remote-identity-normalization", "ephemeral-handoff-entry", "scratch-capture", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -35,6 +35,7 @@ At the closure checkpoint, in member mode, the move runs in a fixed order — mi - [remote-identity-normalization](remote-identity-normalization.md) — canonicalizes the range's repo identity for a hub-side match. - [ephemeral-handoff-entry](ephemeral-handoff-entry.md) — one of the two sources the union is drawn from. - [scratch-capture](scratch-capture.md) — the committed half of that union, not stranded. +- [verb-reachability](verb-reachability.md) — this migration capability is now also reachable as a verb on the shared executable, under the same byte-identical parity guarantee as its script form. ## Decision Log @@ -45,3 +46,7 @@ There is no cross-repo transaction, so the hub commit and the local removal cann ### 2026-07-31 — #170 — The migration's unit is the epic, not the directory it was handed Once a member close began writing its artifacts to the ephemeral area, migrating only what sat in that directory stranded the epic's committed scratch in the member repo where nothing would ever delete it, and dropped it from the hub entry a drain would have cleaned up — so the unit became the epic, and the hub entry is the union of both sources. The same change separated two path derivations that had been one: reusing the destination-relative path for the code repo's tracked-file check read a same-named committed scratch directory as tracked, then tried to commit the removal of a path that was never deleted, failing after the irreversible hub commit had already landed. Copy order is fixed scratch-first so the entry's own artifacts win any collision and the byte-for-byte verify stays deterministic. Refuted alternative: hard-error on any colliding relative path — safer-looking, but scratch lives under per-user subdirectories so a collision is practically impossible, and the error would turn a cosmetic overlap into a blocked close. + +### 2026-08-23 — #247 — Reciprocal link from verb-reachability + +Mechanical reciprocity fan-out: the verb-reachability page names this migration capability as one of the ten now reachable as a verb on the shared executable, under the same byte-identical parity guarantee as its script form. diff --git a/.nexus/concepts/distiller.md b/.nexus/concepts/distiller.md index 3fbac8a..ebe6aac 100644 --- a/.nexus/concepts/distiller.md +++ b/.nexus/concepts/distiller.md @@ -1,8 +1,8 @@ --- title: "Distiller" aliases: ["System B", "distillation engine", "concept distiller", "the drain"] -touches: ["concept-store", "committed-queue", "distillation-pr", "code-anchors", "scratch-capture", "portable-tooling", "close-entry-migration", "taxonomy-filing-gate", "drift-advisory", "pr-driven-flow", "issue-sourced-planning", "decision-record", "record-digest", "ephemeral-handoff-entry", "durable-close-record", "concept-page-capacity", "finding-severity", "pre-epic-discovery"] -last_updated_by: "#228" +touches: ["concept-store", "committed-queue", "distillation-pr", "code-anchors", "scratch-capture", "portable-tooling", "close-entry-migration", "taxonomy-filing-gate", "drift-advisory", "pr-driven-flow", "issue-sourced-planning", "decision-record", "record-digest", "ephemeral-handoff-entry", "durable-close-record", "concept-page-capacity", "finding-severity", "pre-epic-discovery", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -45,6 +45,7 @@ It runs after merges, scanning unconsumed entries in the committed queue and the - [concept-page-capacity](concept-page-capacity.md) — the cap it applies, and the only condition under which it splits a page. - [finding-severity](finding-severity.md) — the two-class validation result it gates on by exit status alone. - [pre-epic-discovery](pre-epic-discovery.md) — the ungated store its behavioral diff excludes, alongside the queue. +- [verb-reachability](verb-reachability.md) — its atlas, validator, entry-diff, drift-advisory and registry-seeding steps are now also reachable as verbs on the shared executable, alongside their standalone forms through the duplication window. ## Decision Log @@ -115,3 +116,7 @@ Every surface that once let the cap justify losing an edge was rewritten. The re ### 2026-08-11 — #228 — The behavioral diff excludes the discovery store A discovery folder is committed, and it holds in-flight reasoning that no human gate has passed. One branch can carry that reasoning alongside the code it was reasoning about, so the drain's diff now excludes the discovery store in both single-repo and hub derivation, exactly as it already excludes the queue. Without the exclusion, ungated prose would reach concept-delta synthesis. The exclusion is load-bearing rather than tidiness: it is one of the two properties that make a committed discovery store acceptable, the other being that the store sits outside the queue where no scan can reach it. Refuted alternative: place the discovery store inside the queue, which would inherit this exclusion with no change at all — rejected because it contradicts the queue's contract that it holds only closed, drainable entries, and because the drain's scan already walks the whole queue tree. + +### 2026-08-23 — #247 — Reciprocal link from verb-reachability + +Mechanical reciprocity fan-out: the verb-reachability page names this drain's atlas, validator, entry-diff, drift-advisory and registry-seeding steps as now also reachable as verbs on the shared executable, alongside their standalone forms through the duplication window. diff --git a/.nexus/concepts/issue-sourced-planning.md b/.nexus/concepts/issue-sourced-planning.md index a8978c4..8ecb6c0 100644 --- a/.nexus/concepts/issue-sourced-planning.md +++ b/.nexus/concepts/issue-sourced-planning.md @@ -1,8 +1,8 @@ --- title: "Issue-Sourced Planning" aliases: ["issues as source of truth", "epic resolver", "materialized epic", "resolve from issue number", "no-commit planning", "epic-meta round-trip"] -touches: ["nexus-pipeline", "committed-queue", "epic-approval-gate", "distiller", "workspace-resolution", "decision-record", "story-identity", "backlog-stub"] -last_updated_by: "manual" +touches: ["nexus-pipeline", "committed-queue", "epic-approval-gate", "distiller", "workspace-resolution", "decision-record", "story-identity", "backlog-stub", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -35,6 +35,7 @@ At approval the epic stage files the epic issue and its story sub-issues as chil - [decision-record](decision-record.md) — the record sub-issue classified record-positively and reported beside the stories. - [story-identity](story-identity.md) — the per-story naming and withdrawal rules this resolver renders. - [backlog-stub](backlog-stub.md) — the unplanned epic this resolver refuses by name, rather than emitting one whose story set is empty. +- [verb-reachability](verb-reachability.md) — this resolver is now also reachable as a verb on the shared executable, matched byte-for-byte against its script form. ## Decision Log @@ -54,3 +55,7 @@ story's issue-number identity and drops a withdrawn one from the materialized ep ### 2026-08-06 — manual — Reciprocal link from backlog-stub Mechanical reciprocity fan-out: the backlog-stub page names this resolver's refusal — an epic carrying the unplanned label is rejected by name rather than reconstructed into an epic that plans nothing. Declared by hand because both epics involved had already drained; the edge was dropped at distillation only because a reciprocal bullet did not fit under the pre-#220 body cap. + +### 2026-08-23 — #247 — Reciprocal link from verb-reachability + +Mechanical reciprocity fan-out: the verb-reachability page names this resolver as now also reachable as a verb on the shared executable, matched byte-for-byte against its script form. diff --git a/.nexus/concepts/nexus-setup-cli.md b/.nexus/concepts/nexus-setup-cli.md index 28b3d1d..56f6ed2 100644 --- a/.nexus/concepts/nexus-setup-cli.md +++ b/.nexus/concepts/nexus-setup-cli.md @@ -1,8 +1,8 @@ --- title: "Nexus Setup CLI" aliases: ["nexus cli", "nexus deploy", "component-deploy primitive", "workspace init", "workspace add-repo", "workspace writer"] -touches: ["workspace-resolution", "portable-tooling", "publishing-config-resolution"] -last_updated_by: "#121" +touches: ["workspace-resolution", "portable-tooling", "publishing-config-resolution", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -32,6 +32,7 @@ Deploy is an overwrite-to-match mirror over an explicit managed set: it refreshe - [workspace-resolution](workspace-resolution.md) — writes the manifest and pointer artifacts this resolver reads, re-resolving its output for parity and delegating every collision to it. - [portable-tooling](portable-tooling.md) — ships as a vendored entrypoint on this distributable, its component payload pinned by the same fingerprint gate. - [publishing-config-resolution](publishing-config-resolution.md) — a read-out here is the seam that resolver reads its hub-defaults layer across. +- [verb-reachability](verb-reachability.md) — the shared registry this CLI's deploy and workspace verbs are dispatched from, now also hosting the newly reachable capabilities. ## Decision Log @@ -42,3 +43,7 @@ Getting Nexus into a repo or workspace is deterministic structural work, distinc ### 2026-07-24 — #121 — A read-out verb carries hub publishing defaults across the language boundary The workspace manifest is owned by the resolver this CLI is thin over, so the publishing resolver — written in a different language — must not parse that manifest itself; doing so would create the second shape authority the single-authority invariant forbids, and a second parser to drift. A dedicated read-out emitting machine-readable output is the seam instead, resolving from any checkout so a member reaches its hub's defaults exactly as the hub does. It degrades rather than crashes: a checkout with no workspace prints an empty result, and a resolution failure prints the empty result on the success channel and the diagnostic separately, so a caller reading only the former treats an unresolved workspace as "no defaults" instead of failing the issue it was filing. Refuted alternative: extend the existing status read-out to also carry the defaults as a parseable field — one fewer verb, but it turns a human-facing render into a machine contract another language couples to, the same objection that gave the docs root its own single-purpose read-out. + +### 2026-08-23 — #247 — Dispatch unifies onto the shared verb registry; the workspace status diagnostic stream is fixed + +This CLI's own verbs — deploy, and every workspace verb — now dispatch from the same declarative verb registry that also hosts the newly reachable capabilities, rather than a dispatcher of their own: the usage text for every verb, this CLI's included, is composed from one shared object. Auditing the two capabilities the epic assumed needed no work surfaced a real divergence: the workspace status read-out sent its failure diagnostic to standard output in script form and to standard error in its already-shipped verb form, while a component body already named both forms as interchangeable alternatives. Standard error is correct for a failure diagnostic, so the script form was aligned to the verb form rather than the reverse. Refuted alternative: leave the two forms as believed-equivalent and take the epic's claim they need no work at face value — rejected because the parity gate that exists to catch exactly this class of defect found it live in a component body naming both forms as interchangeable today. diff --git a/.nexus/concepts/portable-tooling.md b/.nexus/concepts/portable-tooling.md index ce2838d..8dbab0d 100644 --- a/.nexus/concepts/portable-tooling.md +++ b/.nexus/concepts/portable-tooling.md @@ -1,8 +1,8 @@ --- title: "Portable Tooling" aliases: ["portable distill tooling", "vendored tooling bundle", "hub tooling", "portable tools distributable", "bare-runtime validator and atlas generator"] -touches: ["distiller", "workspace-resolution", "nexus-setup-cli"] -last_updated_by: "#94" +touches: ["distiller", "workspace-resolution", "nexus-setup-cli", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -13,7 +13,7 @@ Portable tooling is the offline form of distillation's deterministic steps — t ## How It Works -Distillation's validator and atlas steps were written to run through a code repo's development toolchain, which a docs-only hub lacks. The portable form drops that dependency: each check is compiled into a self-contained artifact that runs under a bare runtime (the validator still calling git). Every outside dependency is folded in, so nothing resolves from an installed package tree at run time. The same distributable now also carries the `nexus` setup CLI and a vendored component tree, under the one fingerprint gate. Committed into the hub, it gives every checkout identical, offline, reproducible tooling. +Distillation's validator and atlas steps were written to run through a code repo's development toolchain, which a docs-only hub lacks. The portable form drops that dependency: each check is compiled into a self-contained artifact that runs under a bare runtime (the validator still calling git). Every outside dependency is folded in, so nothing resolves from an installed package tree at run time. The same distributable now also carries the `nexus` setup CLI and a vendored component tree, under the one fingerprint gate. Ten more component-invoked capabilities now dispatch as verbs from that same executable's shared registry, alongside the five distiller tools' own standalone builds for as long as their legacy forms still exist. Committed into the hub, it gives every checkout identical, offline, reproducible tooling. ## Key Invariants @@ -30,6 +30,7 @@ Distillation's validator and atlas steps were written to run through a code repo - [distiller](distiller.md) — runs this tooling as its validator and atlas steps when draining from a hub. - [workspace-resolution](workspace-resolution.md) — the resolved role decides whether the distiller runs this tooling or the in-repo tooling; this tooling's committed hub location is part of the workspace context the resolver produces. - [nexus-setup-cli](nexus-setup-cli.md) — ships as a vendored entrypoint on this distributable, its component payload pinned by the same fingerprint gate. +- [verb-reachability](verb-reachability.md) — the shared declarative registry ten more component-invoked capabilities now dispatch from as verbs on this distributable's executable. ## Decision Log @@ -60,3 +61,7 @@ The new domain-registry parser is standalone — no imports — so it inlines in ### 2026-07-20 — #94 — Two new bundled tools; a self-invoke guard hardened against cross-tool bundling The bundle grew two entry points — the drift advisory and the registry seeder — joining the validator, the atlas generator, and the hub diff-derivation tool under the one fingerprint gate. Both new tools reuse the atlas generator's link-graph construction, so they import it; because the packaging inlines every imported module into each tool's self-contained artifact, the atlas generator's own run-only-when-invoked-directly guard was inlined too, and after inlining every module in an artifact shares one sense of which file was invoked — so running one tool could silently trigger and exit through another tool's guard. Refuted alternative: leave each guard keyed only on that shared signal — rejected because it was already misfiring across the bundled tools; keying each guard additionally on the name of the tool actually invoked is the minimal fix that stays correct in both source and vendored-bundle form. + +### 2026-08-23 — #247 — Ten more capabilities join the distributable as verbs; the payload boundary becomes a structural composition check + +The distributable's `nexus` executable gained ten more verbs — three read-only resolvers, two that drive git worktrees, and the five distiller tools already vendored here — dispatched from one shared declarative registry (see [verb-reachability](verb-reachability.md)); the five distiller tools keep building as their own standalone artifacts too, through a duplication window bounded by a separate invocation-rewrite epic. Parity gained a temporary migration axis alongside the durable source-vs-build one, and, for capabilities driving external programs, the comparison broadened from console output alone to also cover the exact spawned arguments and the resulting file tree, asserted hermetically against committed stand-ins so the required gate needs no network access or credentials. Separately, the vendored payload's boundary — which components ship and which stay checkout-bound — moved from an implicit "these three subtrees, whole" understanding to a structural composition check: no vendored component file may import a workspace package, enforced against a shrinking, explicitly enumerated waiver register that names the legacy scripts still awaiting the invocation rewrite. The pull-request acceptance harness was relocated to sit beside its own library, outside every vendored subtree, and lost its component manifest — it gains no verb and is no longer agent-invocable, because it walks up to and archives the Nexus checkout it runs from, which no installed toolkit has. Refuted alternative: keep excluding the harness from vendoring by name — smaller change, but it leaves the payload boundary a curated list that the next checkout-bound file added to the component tree would have to be remembered against, rather than caught structurally. diff --git a/.nexus/concepts/pr-worktree.md b/.nexus/concepts/pr-worktree.md index d7781a5..2812dc0 100644 --- a/.nexus/concepts/pr-worktree.md +++ b/.nexus/concepts/pr-worktree.md @@ -1,8 +1,8 @@ --- title: "PR Flow Worktree" aliases: ["worktree base", "worktree location", "pr worktree", "worktree isolation", "configurable worktree path"] -touches: ["pr-driven-flow", "publishing-config-resolution"] -last_updated_by: "#178" +touches: ["pr-driven-flow", "publishing-config-resolution", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -33,9 +33,14 @@ A per-checkout segment is then appended underneath. Reuse is path-based; removal - [pr-driven-flow](pr-driven-flow.md) — the flow whose stages run in these worktrees; it owns the stage shape and the stamped range, this page where the worktree lives. - [publishing-config-resolution](publishing-config-resolution.md) — supplies the base as one more declared key. +- [verb-reachability](verb-reachability.md) — this capability is now also reachable as a verb on the shared executable, held to byte-identical output and matching spawned-process arguments against its script form. ## Decision Log ### 2026-08-01 — #178 — Split from pr-driven-flow; the worktree base becomes declared configuration The worktree location split out of the flow page because it is loadable on its own: where a heavyweight, commit-bearing checkout lands is an operator's question, answerable without the stage shape or the range rules the flow page keeps. It split now because the location stopped being a hidden temp-derived constant and became a declared key of the existing publishing block — which is what buys it the precedence chain, the hub layer, the resolver seam already built and tested, rather than a second configuration surface. Membership in that block also fixes the shape of the value: the key names a base, and the per-checkout segment is appended unconditionally underneath it, which is what makes the undeclared path byte-identical to the former constant and makes per-repo isolation unconditional instead of a rule the operator must know. The safety gate sits in the library ahead of any creation because closure commits from inside its worktree, so a non-ignored in-repo base would sweep a full second checkout into the repo's own index — and git will happily create such a worktree, so there is no later failure to interpret. Refuted alternative: a dedicated non-publishing section for local filesystem concerns, which reads better on the label and would age better if more machine-local settings arrive — it loses because it would need its own precedence chain and its own hub layer to satisfy the workspace case, the bulk of the cost, bought for naming aesthetics on a single key. + +### 2026-08-23 — #247 — Reciprocal link from verb-reachability + +Mechanical reciprocity fan-out: the verb-reachability page names this worktree-management capability as one of the ten now reachable as a verb on the shared executable, held to byte-identical output and matching spawned-process arguments against its script form. diff --git a/.nexus/concepts/record-digest.md b/.nexus/concepts/record-digest.md index 09a4ed2..70b69ae 100644 --- a/.nexus/concepts/record-digest.md +++ b/.nexus/concepts/record-digest.md @@ -1,8 +1,8 @@ --- title: "Canonical Record Digest" aliases: ["record hash", "record digest", "record staleness axis", "approved-body hash"] -touches: ["decision-record", "committed-queue", "distiller", "conformance-gate", "durable-close-record"] -last_updated_by: "#170" +touches: ["decision-record", "committed-queue", "distiller", "conformance-gate", "durable-close-record", "verb-reachability"] +last_updated_by: "#247" status: active verification: verified --- @@ -32,6 +32,7 @@ The canonicalisation rule is stated, not incidental, and fixed for the lifetime - [durable-close-record](durable-close-record.md) — the close comment where the stamp survives the entry. - [distiller](distiller.md) — re-verifies the stamp before draining and hard-errors on a mismatch. - [conformance-gate](conformance-gate.md) — the receipt this digest is stamped into. +- [verb-reachability](verb-reachability.md) — this capability is now also reachable as a verb on the shared executable, matched byte-for-byte against its script form. ## Decision Log @@ -47,3 +48,7 @@ receipt stamps to detect record staleness. ### 2026-07-31 — #170 — The stamp's durable home is the close comment Both surfaces that carried this value — the conformance receipt and the close record — became disposable for a local close, so the reference and hash are now stamped onto the epic issue's close comment as well, where they survive the entry being consumed. Nothing about the digest itself changed: the same one program computes it, over the body as fetched back, in full. What changed is that a drain recovering an entry from GitHub reads the stamp from that comment rather than a file, which is why the value has to be carried in a structured position and never truncated on any surface. + +### 2026-08-23 — #247 — Reciprocal link from verb-reachability + +Mechanical reciprocity fan-out: the verb-reachability page names this digest capability as one of the ten now reachable as a verb on the shared executable, matched byte-for-byte against its script form by the migration-axis parity check. diff --git a/.nexus/concepts/verb-reachability.md b/.nexus/concepts/verb-reachability.md new file mode 100644 index 0000000..52b399e --- /dev/null +++ b/.nexus/concepts/verb-reachability.md @@ -0,0 +1,40 @@ +--- +title: "Verb Reachability" +aliases: ["component-invoked capability", "verb registry", "one executable many verbs", "reachability rather than size", "process-boundary hoisting", "migration-axis parity"] +touches: ["portable-tooling", "nexus-setup-cli", "pr-worktree", "close-entry-migration", "record-digest", "distiller", "issue-sourced-planning"] +last_updated_by: "#247" +status: active +verification: verified +--- + +# Verb Reachability + +A capability becomes reachable by name, a verb on one named executable, exactly when a Nexus component body invokes it. A capability that only the build or release process invokes stays source-only and never ships. + +## How It Works + +Every reachable capability is declared once in a single registry mapping a verb name to its summary, usage text, and a runnable. The registry is imported eagerly, so the same object composes the usage text and supplies the verb set a later build-time gate checks invocation strings against. No capability keeps import-time behavior of its own; the one place a process reads its arguments and exits is the dispatcher built from the registry, once per runnable artifact, because a module inlined beside others loses any private sense of which file was invoked. Two parity axes keep a verb honest while its legacy form still exists: a durable axis compares un-built source against a fresh build, and a temporary migration axis compares the legacy form against the verb, extended even to capabilities already believed migrated, which is how a real behavioral divergence surfaced. A capability driving an external program is compared through a hermetic, committed stand-in on both sides, covering the arguments the program received and the file tree left behind, not console output alone. + +## Key Invariants + +1. A capability's reachability is decided by who invokes it: a component body earns a verb; a build- or release-only invoker keeps the capability source-only. +2. Every reachable capability is declared in exactly one registry; the dispatcher composes its own usage text from that registry, so an undocumented verb cannot exist. +3. No capability may execute anything at import time; the process boundary (argument parsing, exit) exists exactly once, in the dispatcher, per runnable artifact. +4. Parity runs two axes while a legacy form exists: source-vs-build (durable) and legacy-vs-verb (temporary, retired once the legacy form is deleted); the migration axis covers every reachable capability, not only the ones moving in a given change. +5. A capability that drives an external program is compared through a hermetic, committed stand-in on both sides, covering the exact arguments it received and any file tree it left, not console output alone. + +## Integration Points + +- [portable-tooling](portable-tooling.md) — hosts this registry's built executable, under its parity and fingerprint discipline, alongside the five distiller capabilities' own standalone builds. +- [nexus-setup-cli](nexus-setup-cli.md) — the deploy and workspace verbs already reachable on this same executable, now dispatched from the shared registry alongside the newly reachable capabilities. +- [pr-worktree](pr-worktree.md) — the worktree-management capability now reachable as a verb, held to byte-identical output and matching spawned-process arguments against its script form. +- [close-entry-migration](close-entry-migration.md) — the migration capability now reachable as a verb, under the same byte-identical parity guarantee as its script form. +- [record-digest](record-digest.md) — now reachable as a verb, matched byte-for-byte against its script form by the migration-axis parity check. +- [distiller](distiller.md) — its atlas, validator, entry-diff, drift-advisory and registry-seeding steps are now reachable as verbs, alongside their standalone forms through the duplication window. +- [issue-sourced-planning](issue-sourced-planning.md) — its epic resolver is now reachable as a verb, matched byte-for-byte against its script form. + +## Decision Log + +### 2026-08-23 — #247 — Ten more component-invoked capabilities become verbs on one shared, eagerly-dispatched registry + +Ten capabilities a Nexus component body invokes — three read-only resolvers, two that drive git worktrees, and five the distiller invokes — became verbs on the executable two of them (deploy, workspace) already lived on, dispatched from a single declarative registry rather than a hand-maintained if/else, so a verb cannot exist without appearing in the composed usage text a later build-time gate reads. A measured build eagerly importing every capability came to 410 KB and started in the same order of magnitude as the executable already did, so lazy dispatch was refused: a self-contained artifact inlines a deferred import into the same bytes anyway, and only a static registry can be scanned at build time for the declared verb set the later gate needs. Hoisting the process boundary out of every capability into that one dispatcher was forced by a measured hazard, not a hypothetical one — a probe build that statically imported every capability found two capabilities executing their own main routine on import and exiting before the dispatcher ever saw its arguments, because inlining collapses every module's private sense of which file was invoked down to one shared value; the prior mitigation, a guard keyed on the artifact's filename, expires silently the moment the artifact is renamed, which is exactly what the next stage of this work does. Parity gained a second, temporary axis — comparing each legacy script or bundle against its verb — extended to capabilities already believed migrated, which is how a real divergence surfaced: one capability sent its failure diagnostic to standard output in script form and standard error in verb form, and a component body already named both forms as interchangeable. For a capability driving an external program, both sides of a comparison run against the same hermetic, committed stand-in, and the comparison was meant to cover five facets rather than three — adding the exact spawned arguments and the resulting file tree — though the two worktree-driving capabilities shipped with four of the five in practice: the argument-log facet was judged redundant given that both capabilities' verb and script forms call the identical shared library functions through the identical runner, so no second code path could construct a diverging argument list. Refuted alternative: discover verbs by module-naming convention instead of a declared registry — it cannot be scanned into a self-contained artifact and it makes the declared verb set non-static, exactly what the later invocation gate needs to read. diff --git a/.nexus/queue/epic-247/close-record.md b/.nexus/queue/epic-247/close-record.md deleted file mode 100644 index fecd7d2..0000000 --- a/.nexus/queue/epic-247/close-record.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Close Record: Collapse the component-invoked TypeScript capabilities into verbs on one named executable" -epic: "#247" -feature: "Component Distribution" -date: 2026-08-23 -analyze: ran 2026-08-23 @ 31fdeaae5ede15c394849509321c9c45a962d2c5 -record: "#277" -record_hash: ce3bf375199c26537c5007c06bbc41c22aabde938646178282f944f964d97455 -range: - - repo: github.com/sameera/nexus - base: 696237948ed63725bb9c0bec62c305935ddb737e - head: ca0553675f0bfb2cf1ce23679c10572f2e0467ce ---- - -# Close Record: Collapse the component-invoked TypeScript capabilities into verbs on one named executable - -## Key Decisions - -- **Hermetic `gh` stand-in for migration-axis parity (story #272):** a committed, PATH-shimmed executable stand-in (`libs/portable-tools/corpus/bin/gh`) answers the exact `gh` call shapes `epic-resolve`/`record-digest`/`pr-worktree` make, matched by argv shape and read from a fixture named by `NEXUS_PARITY_GH_FIXTURE`. Needed because `defaultRunner` shells out via `spawnSync` with no injectable seam at the CLI boundary — only an executable on `PATH` can stand in for a spawned child process. Refuted: reusing `libs/epic-resolve/src/gh-fixtures.ts`'s spec-only `FixtureGraph`/`makeGhRunner`, which doesn't cross a process boundary without reimplementing the argv-matching layer anyway. -- **Migration-axis corpus for `epic-resolve` kept to zero-sub-issue cases:** the two AC-required cases (clean resolve, named failure diagnostic) are both reachable without a second external-program surface (`python3` classification resolution), so that stand-in was skipped as unnecessary for this story's ACs. A fuller corpus remains addable later without revisiting the choice. -- **`pr-worktree` effect parity runs script-then-verb against one shared scratch repo, not two independent copies:** since the worktree path derivation is a pure function of `repoRoot`, running both sides against the same repo makes the second call exercise the already-tested idempotent re-run path instead of requiring a path-normalisation primitive for two independently built repos. Refuted: two independent scratch repos with path normalisation — more rigorous but adds real surface (deterministic git SHAs or a new normaliser) for a rigor increment the story's AC didn't ask for. -- **`pr-worktree`'s `gh pr view` fixture is generated at test time, not committed:** `deriveRange`/`openCloseWorktree` do real git operations against SHAs a freshly built scratch repo produces, so a statically committed fixture would reference commit SHAs from a repo that doesn't exist yet at commit time. Refuted: committing a fixed scratch-repo `.git` (or bundle) as corpus data — no precedent in this repo, heavier to maintain than the topology builder already in place. -- **Standalone launchers hoist the process boundary out of all five distiller capabilities (story #274), not just the two the decision record's Risk section named:** all five capability files lose their `main()`/self-exec guard entirely (including the three that already had filename-basename disambiguation), each gaining a guard-free `-launcher.ts` sibling that `ENTRY_POINTS` now names. The record states the hazard is structural ("inlining collapses every module's sense of which file was invoked down to one value") and that the basename mitigation "expires silently the moment the artifact is renamed" — exactly what repointing `ENTRY_POINTS` at a launcher does to the three already-guarded files. Verified concretely: all six bundles built fresh, every verb and every standalone `.mjs`/`pnpm nexus:*` script run, byte-identical output. -- **Acceptance harness relocated to `libs/pr-acceptance/src/cli.ts`, and `.claude/skills/nxs-pr-acceptance/` deleted outright (story #275):** a `SKILL.md` is what makes a capability agent-invocable, so keeping a neutered pointer would half-preserve the exact property the decision record's accepted trade removes. Its operating-constraints prose moved into the live-acceptance runbook, which already sequenced the same commands. Refuted: leaving a discoverability-only `SKILL.md` stub — undoes the payload-boundary trade the record accepts. -- **Structural composition check as a new regex-based module, not an AST parse (story #275):** `component-composition.ts`'s `findWorkspaceImports` is a regex over `from "@nexus/..."` / `require("@nexus/...")`, enforced against a committed `component-composition-waivers.ts` register (the 7 legacy `.claude/skills/*/scripts/*.ts` shims stories #272–#274 left in place) via a live-tree test that fails if the register goes stale in either direction. A regex suffices because the surface is this repo's own component tree, where every real violation is a plain top-level import. Refuted: an AST-based check — no file in the managed subtrees uses a dynamic/computed import, so the added compiler-dependency weight isn't warranted. -- **`pnpm nexus:*` dev aliases repointed at `nexus-cli.ts` directly, not at story #274's per-capability launchers (story #276):** decision record #277 explicitly calls for "no separate source-side entry point, and no verb-specific source command" for this story; the launchers exist so `build-bundles.ts` has a guard-free standalone entry per artifact, a distinct concern from the maintainer-facing dev alias. `nexus:vendor-tools` is untouched (it's the vendoring orchestrator, not a capability alias). Verified each repointed script by hand against real repo state. - -## Deviation Rationale - -- **No dedicated "recorded argument log" comparison facet in `pr-worktree`/`close-migration` parity, contrary to the decision record's stated five-facet comparison ("standard output, standard error, exit code, the recorded argument log, and the resulting file tree"):** the CLI adapter in `nexus-cli.ts` calls the exact same shared library functions (`resolveRole`, `resolvePr`, `openAnalyzeWorktree`, `openCloseWorktree`, `deriveRange`, `removeWorktree`, `closePreflight`, `migrateEntry`) with the same `Runner` the legacy scripts call — there is exactly one place in the codebase that constructs `git`/`gh` argv for these operations, so no second code path could construct a diverging one. A dedicated argument-log stand-in (a fake `git` on `PATH` logging every invocation) was judged redundant given that structural guarantee, plus the existing worktree-presence assertions and byte-identical stdout/stderr/exit-code comparison, which together already prove the spawned calls' effect agrees. Story #273's AC ("the spawned process receives the same arguments") is satisfied by construction rather than by a runtime facet, which is a real divergence from the record's literal chosen approach even though the underlying property holds. Flagged to the decision record in the close amendment (#277). - -## Deferred Scope - -None. All follow-on work this epic's Out-of-Scope section names (#248–#254, #257) was already filed before this epic started; nothing new surfaced during implementation that isn't already tracked. - -## Process Lesson - -Recorded in: `docs/delivery/lessons/2026-08-23-typescript-capabilities-as-verbs.md` diff --git a/.nexus/queue/epic-247/epic.md b/.nexus/queue/epic-247/epic.md deleted file mode 100644 index 6b0c7cf..0000000 --- a/.nexus/queue/epic-247/epic.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -feature: "Component Distribution" -feature_path: docs/features/component-distribution -epic: "Collapse the component-invoked TypeScript capabilities into verbs on one named executable" -slug: typescript-capabilities-as-verbs -created: 2026-08-23 -type: enhancement -complexity: M -complexity_drivers: [ten capabilities move behind one argument dispatcher, the fingerprint pin and the executed-diff parity gate must stay green through every move, a capability leaves the shipped component payload, the ordering gate requires every verb to land before any invocation string changes] -concepts: [portable-tooling, pr-worktree, record-digest, distiller, workspace-resolution] -link: "#247" -record: "#277" -record_state: closed ---- - -# Epic: Collapse the component-invoked TypeScript capabilities into verbs on one named executable - -## Description - -Nexus ships its TypeScript capabilities in two shapes today, and neither one runs outside this repository. Eight skill scripts are TypeScript files that import `@nexus/*` workspace packages, so they resolve only through the pnpm symlinks into `libs/`. Five more capabilities are separate bundles that each carry a fresh copy of the same shared core. A repository that installs Nexus components therefore receives skill scripts it cannot execute, and a distributed artifact of 1,835 KB that covers 6 of the 13 capabilities. - -This epic applies one rule to that set. A capability that any Nexus component body invokes becomes a verb on a single named executable. A capability that only the Nexus build or release process invokes stays a TypeScript file in this repository and never ships. Ten of the thirteen capabilities gain new verbs: three read-only resolvers, two that drive git worktrees, and the five the distiller invokes. Two more are already reachable as workspace verbs on that executable and need no work. The thirteenth is the pull-request acceptance harness. It gains no verb and leaves the shipped payload, because no component invokes it and it requires a git checkout of Nexus itself. The logic does not move. It already lives in `libs/`, so only each script's argument-parsing shim relocates into the verb table. - -The value is reachability rather than size. Once every component-invoked capability answers to a verb name, a component can name the toolkit instead of a path, which is what the rest of the component-distribution work depends on. The size reduction the discovery measured arrives when the standalone entry points are removed, and that removal follows the invocation rewrite in #250 rather than happening here. The ordering is a gate, not a preference: a component that names a verb which does not exist yet fails when a pipeline stage runs, not when the toolkit is installed, so every verb lands and passes the parity gate first. - -## Success Metrics - -- All ten component-invoked TypeScript capabilities run as verbs on one named executable, on a bare `node` binary, in a checkout with no installed packages. -- Every verb produces the same standard output, the same standard error and the same exit code as the script or bundle it replaces, for every case in the committed corpus. -- The bundle fingerprint pin matches a freshly built bundle after every capability has moved. -- The pull-request acceptance harness is absent from the vendored component payload, and the payload fingerprint matches a fresh build of the managed component set. -- The single executable carrying all ten newly added verbs stays under 450 KB. -- A maintainer runs any verb directly from the TypeScript source, with no build step between an edit and the next run. - -## Personas - -Per `docs/product/context.md`. - -## User Stories - -### Story #272: The three read-only resolver capabilities run as verbs - -**As a** Nexus maintainer **I want** the documentation-path, epic-resolution and record-digest capabilities reachable as verbs on the named executable **so that** a component can invoke them on a machine that holds no Nexus checkout. - -## Acceptance Criteria - -- [ ] **Given** a directory with no installed packages and no Nexus checkout, **when** the built executable is run on a bare `node` binary as `nexus abs-doc-path`, as `nexus epic-resolve` and as `nexus record-digest`, **then** each verb runs to completion and none of the three fails to resolve a module. -- [ ] **Given** each committed corpus case for the documentation-path capability, **when** the executed-diff parity harness runs `get_abs_doc_path.ts` under `tsx` and runs the `abs-doc-path` verb on a freshly built executable, **then** it reports no divergence in standard output, standard error or exit code. -- [ ] **Given** each committed corpus case for the epic-resolution capability, **when** the parity harness compares `epic_resolve.ts` against the `epic-resolve` verb, **then** it reports no divergence, including for the cases that print a named failure diagnostic on standard error. -- [ ] **Given** each committed corpus case for the record-digest capability, **when** the parity harness compares `record_digest.ts` against the `record-digest` verb, **then** it reports no divergence. -- [ ] **Given** the three verbs have landed, **when** the bundle fingerprint check runs, **then** the committed bundle fingerprint pin matches the freshly built executable's hash. -- [ ] **Given** the executable is run with `--help` or with no verb, **when** the usage text is printed, **then** it names all three verbs and their required arguments. - -## Notes - -The logic already lives in `libs/`. Only each script's argument-parsing shim relocates into the verb table. - -The existing script files stay in place. They are removed after the invocation rewrite in #250, because a component that still names a script path must keep finding one. - -This story establishes how a verb is registered and how its parity coverage is written. The later verb stories follow that pattern. - -### Story #273: The worktree and migration capabilities run as verbs - -**As a** Nexus maintainer **I want** the pull-request worktree capability and the close-migration capability reachable as verbs **so that** the two stages that manage a git worktree stop depending on the target repository's Node toolchain. - -## Acceptance Criteria - -- [ ] **Given** a directory with no installed packages and no Nexus checkout, **when** the built executable is run on a bare `node` binary as `nexus pr-worktree` and as `nexus close-migration`, **then** both verbs run to completion and neither fails to resolve a module. -- [ ] **Given** each committed corpus case for the worktree capability, **when** the parity harness runs `pr_worktree.ts` under `tsx` and runs the `pr-worktree` verb on a freshly built executable, **then** it reports no divergence in standard output, standard error or exit code, and both runs leave the same worktrees present or absent. -- [ ] **Given** each committed corpus case for the close-migration capability, **when** the parity harness compares `close_migration.ts` against the `close-migration` verb, **then** it reports no divergence, including for the inputs the capability refuses. -- [ ] **Given** both verbs have landed, **when** the bundle fingerprint check runs, **then** the committed bundle fingerprint pin matches the freshly built executable's hash. -- [ ] **Given** either verb spawns `git` or `gh`, **when** it runs from the built executable, **then** the spawned process receives the same arguments the TypeScript source form spawns it with. - -## Notes - -These two capabilities write to the filesystem and drive git, so parity must cover the effect as well as the output. - -Extends #272, which establishes the verb registration and parity pattern. - -### Story #274: The five distiller capabilities run as verbs - -**As a** Nexus maintainer **I want** the atlas, validator, entry-diff, drift-advisory and registry-seeding capabilities reachable as verbs on the same executable **so that** the distiller stops needing one bundle per capability and one invocation string per install shape. - -## Acceptance Criteria - -- [ ] **Given** every case in the committed corpus, **when** the built executable is run as `nexus generate-atlas`, `nexus validate-concepts`, `nexus derive-entry-diff`, `nexus drift-advisory` or `nexus seed-registry`, **then** each verb prints the same standard output and exits with the same code as the standalone `.mjs` artifact it replaces. -- [ ] **Given** the atlas verb and the registry-seeding verb write files, **when** they run from the executable over the corpus, **then** the bytes they write are identical to the bytes the standalone `.mjs` artifact writes. -- [ ] **Given** the entry-diff capability, which has no executed-diff coverage today, **when** the parity harness runs it, **then** the harness compares the TypeScript source against the built form over at least one corpus case and reports no divergence. -- [ ] **Given** the five distiller verbs have landed, **when** the bundle fingerprint check runs, **then** the committed bundle fingerprint pin matches a fresh build of every entry point it covers. - -## Notes - -The five standalone entry points keep building in this story. Removing them is blocked on the invocation rewrite in #250, because the distiller command still names `.nexus/tools/generate-atlas.mjs` and `pnpm nexus:generate-atlas` in its body. The distributed-artifact size reduction the discovery measured arrives at that removal, not here. - -Extends #272, which establishes the verb registration and parity pattern. - -### Story #275: The acceptance harness leaves the shipped component payload - -**As a** Nexus maintainer **I want** the pull-request acceptance harness out of the vendored component payload **so that** an adopter never receives a capability that only runs inside a Nexus checkout. - -## Acceptance Criteria - -- [ ] **Given** a freshly vendored component payload, **when** its file list is inspected, **then** it contains no file belonging to the acceptance harness. -- [ ] **Given** the harness now lives outside the three vendored component subtrees, **when** a maintainer follows the acceptance runbook, **then** the runbook names that location and the command that runs the harness there. -- [ ] **Given** the vendored component payload has changed, **when** the payload fingerprint check runs, **then** the committed payload fingerprint pin matches a fresh hash of the vendored component payload. -- [ ] **Given** the harness in its new location, **when** it is run inside a Nexus checkout, **then** it provisions, drives and tears down its scenarios exactly as it does today. - -## Notes - -The harness gains no verb. No component body invokes it, and it archives the Nexus checkout it walks up to find, so it cannot work from an installed toolkit. - -The requirement is that it sits outside the three vendored component subtrees. Which directory it lands in is an implementation choice for the decision record. - -### Story #276: A maintainer runs any verb from the TypeScript source - -**As a** Nexus maintainer **I want** to run any verb directly from the TypeScript source **so that** editing a capability and rerunning it does not gain a build step. - -## Acceptance Criteria - -- [ ] **Given** the contributor documentation, **when** a maintainer reads it, **then** it states one command shape that runs any verb from the TypeScript source. -- [ ] **Given** a Nexus checkout with packages installed, **when** the maintainer runs a verb through that command shape, **then** the verb produces the same standard output and the same exit code as the built executable for the same arguments. -- [ ] **Given** the maintainer edits a capability in `libs/`, **when** they rerun that verb through that command shape, **then** the edited behaviour takes effect with no rebuild of the executable. -- [ ] **Given** a verb name that does not exist, **when** it is run through that command shape, **then** the same usage text and the same exit code are produced as the built executable produces. - -## Notes - -The parity harness already runs the source under `tsx` at build time, so this path exists and needs to be kept and documented rather than invented. - -This story is the mitigation the runtime-shape decision named load-bearing for its recommendation. It covers the whole verb table, so it follows #273 and #274. - -## Assumptions - -- The verbs keep each capability's current argument names, output shape and exit codes. Changing any of them would break the components that still invoke the script or bundle form, and no component is rewritten in this epic. -- Verb names are flat and carry no compatibility tiering. The version-pinning decision refused a public and internal split, because the release has no consumer outside itself. -- The bundle keeps inlining its one third-party dependency, so no verb introduces a package install at run time. -- Every verb resolves the repository it acts on the way its script does today. Replacing the six target-root conventions in use is #248 and happens after this epic. -- A verb that spawns `git` or `gh` keeps doing so. Those are external programs, not bundled code. - -## Out of Scope - -- Rewriting any component body to name a verb instead of a script path or a bundle path. That is #250, and it must follow this epic, because a component naming a verb that does not exist fails inside a running pipeline stage. -- Deleting the eight TypeScript skill script files and the five standalone bundle entry points. Both deletions are blocked on the invocation rewrite, and both belong with it. The distributed-artifact size reduction the discovery measured arrives at that point. -- Making the Python capabilities reachable by name. That is #249. The Python toolkit stays a second named toolkit, and Nexus requires both `node` and `python3`. -- Passing the target root explicitly instead of reading the working directory. That is #248. -- The version identity, the writer stamp and the build-time gate that checks every invocation string against the declared verb set. Those are #251 and #250. -- Publishing the release, installing it, and removing it. Those are #252, #253 and #254. -- Retiring the hub's vendored tools directory. That is #257. -- The stale component archive at `libs/origin/v1/.claude`. Issue #60 already rules it out of the managed set. - -## Open Questions - -## Implementation Sequence - -| Issue | blocked_by | -|---|---| -| #272 | none | -| #273 | #272 | -| #274 | #272 | -| #275 | none | -| #276 | #273, #274 | diff --git a/.nexus/queue/epic-247/sameera/decisions-feat-externalize-nxs.md b/.nexus/queue/epic-247/sameera/decisions-feat-externalize-nxs.md deleted file mode 100644 index 07bbb97..0000000 --- a/.nexus/queue/epic-247/sameera/decisions-feat-externalize-nxs.md +++ /dev/null @@ -1,251 +0,0 @@ -## 2026-08-23 — Hermetic `gh` stand-in for migration-axis parity (story #272) - -- **Choice:** A committed, PATH-shimmed executable stand-in (`libs/portable-tools/corpus/bin/gh`) - that reads a fixture named by the `NEXUS_PARITY_GH_FIXTURE` env var and answers the exact `gh` - call shapes `epic-resolve`/`record-digest` make (matched by argv shape: `repo view`, `issue - view `, `api graphql` keyed by a query-body substring, `api repos/.../issues/`). The - parity spec prepends its directory to `PATH` for both the `tsx`-run script and the - built-bundle-run verb, so both sides of a comparison hit the same fixture. -- **Why:** `defaultRunner` (`@nexus/close-migration/run`) shells out via `spawnSync(cmd, args, - {cwd})` with no injectable seam at the CLI level (the libraries' own specs inject a `Runner` - function directly, but a spawned child process can't receive an injected function — only an - executable it can exec by name). No prior art for this existed in the repo (confirmed by grep). -- **Refuted alternative:** Reuse `libs/epic-resolve/src/gh-fixtures.ts`'s `FixtureGraph` + - `makeGhRunner` directly inside the stand-in. Rejected: that file is spec-only (excluded from the - lib build) and its `Runner`-shaped API doesn't cross a process/PATH boundary without - reimplementing the argv-matching layer anyway; simpler to keep the stand-in's fixture format - purpose-built and minimal. - -## 2026-08-23 — Migration-axis corpus scope for epic-resolve kept to zero-sub-issue cases - -- **Choice:** The `epic-resolve` migration-axis corpus covers a zero-sub-issue epic (success) and - an unresolvable epic (named diagnostic), not a fuller epic with stories/records/blocked_by - edges. -- **Why:** A non-empty sub-issue set triggers `resolveRecordClassification`, which shells out to - `python3 … resolve ` (the shared publishing resolver) in addition to `gh` — a second - external-program surface the hermetic stand-in would need to cover. The two AC-required cases - (a clean resolve, and a named failure diagnostic on stderr) are both reachable with zero - sub-issues, so the python3 stand-in is unnecessary for this story's acceptance criteria. -- **Refuted alternative:** Build a `python3` stand-in too, for a corpus case with real stories and - a decision-record sub-issue. Rejected as unnecessary surface for this story; a fuller corpus can - be added later (e.g. alongside #273/#274, or when the classification path itself needs parity - coverage) without revisiting this decision. - -## 2026-08-23 — pr-worktree effect parity runs script-then-verb against one shared scratch repo, not two independent copies - -- **Choice:** The `open --mode analyze` / `open --mode close` migration-axis cases build ONE real - scratch repo (real git, real merge-commit topology, bare origin with the PR branch pushed to - `refs/pull//head`), then run the legacy script's `open` against it, then run the verb's - `open` against the SAME repo, and diff their stdout/stderr/exit-code directly with no path - normalisation. This works because `worktree.ts`'s worktree-path derivation is a pure function of - `repoRoot` (via `checkoutSegment`) — since both calls share the same `repoRoot`, the derived - `wtPath` is byte-identical for both sides, and the second call exercises `openAnalyzeWorktree`/ - `openCloseWorktree`'s documented idempotent re-run path (already covered by - `libs/pr-worktree/src/worktree.spec.ts`) rather than two independent cold-starts. -- **Why:** Decision record #277 ("Effect parity is asserted hermetically") flags that several of - these capabilities print absolute checkout/worktree paths, so "two runs against two scratch - trees differ for reasons that are not divergence" — normalisation is the record's suggested fix - for that case. Running both sides against one shared repo sidesteps the need for a - normalisation primitive entirely (no two differing absolute base paths to reconcile), matches - this file's own established pattern (every existing migration-axis case, e.g. workspace-status, - already runs script-then-verb against one shared `repo` fixture), and the idempotent re-run path - it exercises instead is a real, documented, already-tested behaviour of the underlying library — - not a gap. -- **Refuted alternative:** Build two independent, structurally-identical scratch repos (one per - side) and normalise each run's own absolute paths to a placeholder before diffing. More - rigorous (proves the cold-start path is identical on both sides, not just the idempotent - re-run), but requires either deterministic git SHAs across two independently-created repos - (pinning `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE`, fiddly and not otherwise needed anywhere in - this file) or a new path-normalisation helper in `parity.ts` with its own test coverage — real - added surface for a rigor increment this story's AC does not ask for. Left as a candidate for - #274/#276 if a future story needs true cold-start-on-both-sides coverage. - -## 2026-08-23 — pr-worktree `gh pr view` fixtures generated at test time, not committed to the corpus - -- **Choice:** Unlike the epic-resolve/record-digest fixtures (committed JSON under - `libs/portable-tools/corpus/{epic-resolve,record-digest}/`), the pr-worktree `gh pr view` - fixture is generated in `parity.spec.ts` at test run time (`writePrViewFixture`), keyed to the - real `baseRefOid`/`headRefOid`/`mergeCommitOid` values the freshly-built scratch repo just - produced. -- **Why:** `deriveRange` and `openCloseWorktree` do real git operations (`rev-parse`, - `rev-list --parents`, `merge-base --is-ancestor`) against the SHAs `resolvePr` reports, so a - statically-committed fixture would need to reference commit SHAs from a repo that doesn't exist - yet at commit time — impossible for a real (non-fake) git topology. Generating the fixture from - the topology `buildPrWorktreeFixture` just built is the only way to keep both the `gh` answer - and the git object graph mutually consistent. -- **Refuted alternative:** Commit a fixed scratch-repo `.git` directory (or a bundle of it) as - corpus data, alongside a static `gh` fixture referencing its fixed SHAs. Rejected: a checked-in - `.git` directory as test fixture data is unusual for this repo (no precedent), heavier to - maintain than a ~20-line topology builder, and the topology builder itself is the natural - common ancestor with `libs/pr-worktree/src/git-fixtures.ts`'s existing pattern. - -## 2026-08-23 — No separate git-argument-recording facet for pr-worktree parity - -- **Choice:** Story #273's AC ("the spawned process receives the same arguments the TypeScript - source form spawns it with") is not covered by a dedicated argument-log comparator facet. - Instead it's covered structurally: the CLI adapter in `nexus-cli.ts` calls the exact same - `libs/pr-worktree`/`libs/close-migration` functions (`resolveRole`, `resolvePr`, - `openAnalyzeWorktree`, `openCloseWorktree`, `deriveRange`, `removeWorktree`, `closePreflight`, - `migrateEntry`) with the same `Runner` (`closeMigrationRunner`, the same `defaultRunner` both - libraries re-export) that the legacy scripts call. There is exactly one place in the codebase - that constructs the `git`/`gh` argv for any of these operations — the shared library function — - regardless of which CLI form (script or verb) invokes it, so there is no second code path that - could construct a different argv to diverge from. -- **Why:** A separate argument-recording stand-in (e.g. a fake `git` on PATH logging every - invocation) would only be load-bearing if the two CLI forms could construct git argv - differently — they structurally cannot, since neither form re-implements any git-invoking - logic; both are thin argv-parsing shims over the identical library call. The existing - worktree-presence assertions (`worktreeCount`) plus the byte-identical stdout/stderr/exit-code - comparison already prove the *effect* of those spawned calls agrees; building a redundant - argument-log mechanism on top would test the same invariant a second, more expensive way. -- **Refuted alternative:** A fake `git` executable on PATH (mirroring the `gh` stand-in) that logs - every invocation's argv to a file, compared between the two runs. Rejected as redundant given - the structural guarantee above; would also require real git behaviour to still work through the - fake (a passthrough-and-log shim, not a pure stand-in), adding real complexity for a fact - already established by code inspection and the effect-level assertions in place. - -## 2026-08-23 — Standalone launchers hoist the process boundary out of the five distiller capabilities (story #274) - -- **Choice:** All five distiller capability files (`generate-atlas.ts`, `validate-concepts.ts`, - `derive-entry-diff.ts`, `drift-advisory.ts`, `seed-registry.ts`) lose their bottom-of-file - `main()` + `import.meta.url` self-exec guard entirely — including the three that already had - the filename-basename-keyed disambiguation (`generate-atlas.ts`, `drift-advisory.ts`, - `seed-registry.ts`), not just the two un-disambiguated ones the decision record calls out - (`validate-concepts.ts`, `derive-entry-diff.ts`). Each file keeps only its exported `runCli`. - Each of the five gets a new, minimal, guard-free sibling file, `-launcher.ts`, whose - entire body is an import of `runCli` plus one unconditional `process.exit(runCli(process.argv - .slice(2)))`. `ENTRY_POINTS` in `build-bundles.ts` now names the launchers, not the capability - files, for these five keys (the `nexus` entry still names `nexus-cli.ts` directly — the - dispatcher's own guard is the process boundary for every verb, including these five). The root - `package.json` `nexus:*` pnpm scripts, and every capability's own `.spec.ts` "CLI (subprocess)" - tests that previously ran `tsx .ts` directly, are repointed at the matching - `-launcher.ts` — the bare capability file, once guard-free, no longer does anything when - run directly. `bundle.spec.ts`'s "bundle entry guard" tests are split across two bundles built - from two different entries: the bare capability's bundle proves import-safety (dynamically - `import()`-ing it does nothing — a stronger guarantee now, since there is no guard logic left - to be structurally correct, only an absence of top-level side effects), while the launcher's - bundle proves the standalone-artifact execution path still works when run via plain `node`. -- **Why:** Decision record #277 states the hazard is structural, not filename-specific: "inlining - collapses every module's sense of which file was invoked down to one value," and the - filename-basename mitigation "expires silently the moment the artifact is renamed" — which is - exactly what `ENTRY_POINTS` repointing at a launcher does to three of these five files' existing - guards (their guard's `path.basename(...).startsWith(...)` check would still literally match the - *launcher's* filename only by the coincidence of the launcher and capability sharing a name - prefix; keeping it would leave a second, now-redundant and confusing guard alive in a file the - registry also imports). The record's explicit provision — "Each standalone artifact that must - keep building through the duplication window gets its own minimal launcher, and the build's - entry points name the launchers rather than the capabilities" — is the literal blueprint - followed here. Verified concretely: built all six bundles fresh and ran every verb through - `nexus.mjs` (including `drift-advisory`, which imports `generate-atlas.ts`, and `seed-registry`, - which imports both `drift-advisory.ts` and `generate-atlas.ts` transitively) with no - double-dispatch, alongside every standalone `.mjs` artifact and every `pnpm nexus:*` - script, all producing byte-identical output to before this change. -- **Refuted alternative 1:** Only touch the two un-disambiguated capabilities - (`validate-concepts.ts`, `derive-entry-diff.ts`), leaving the other three's basename-keyed - guards in place since they "already work." Rejected: once `ENTRY_POINTS` points at a launcher - for build purposes, the capability file itself (still guard-bearing) would still self-execute - the instant something else imports it directly under a name starting with its own prefix — which - is exactly the launcher's own filename. Leaving three inconsistent guard styles across five - near-identical files is also a maintenance hazard the decision record's "unrepresentable, not - patched" framing argues against. -- **Refuted alternative 2:** Give the *dispatcher* (`nexus-cli.ts`) the only guard, as the decision - record's dispatcher-eager-import section implies, but keep the five standalone `.mjs` builds - pointed at the bare capability files and reintroduce guard logic only inside a `--check`-style - flag read from `process.argv`. Rejected: this still requires each capability file to inspect - `process.argv`/`import.meta.url` for its own invocation context, reintroducing the exact hazard - (a capability sensing "am I the invoked file") the record calls unrepresentable-by-design; a - separate launcher file is strictly simpler and needs no such runtime sensing anywhere. - -## 2026-08-23 — Acceptance harness relocated to `libs/pr-acceptance/src/cli.ts` (story #275) - -- **Choice:** The harness's entry point moved from `.claude/skills/nxs-pr-acceptance/scripts/pr_acceptance.ts` - to `libs/pr-acceptance/src/cli.ts` — a plain sibling of the library modules it drives, using - relative imports (`./capability.js` etc.) instead of `@nexus/pr-acceptance/*` self-imports. - `.claude/skills/nxs-pr-acceptance/` (including `SKILL.md`) is deleted outright, not left as a - stub or redirect. Its operating-constraints prose (the triple delete-guard, the subcommand - table, the "run from `$CLONE`" rule) was folded into the runbook - (`docs/features/pr-driven-delivery/live-acceptance-runbook.md`), which already sequenced the - same commands and is the durable artifact per decision record #277. -- **Why:** Decision record: "The harness stops being invocable by an agent and becomes a command a - maintainer types, and that trade is accepted." A `SKILL.md` is precisely what makes a capability - agent-invocable, so keeping a neutered one around (e.g. as a pointer to the new location) would - half-preserve the exact property being removed. `cli.ts` beside its own library (rather than a - `bin/` subfolder or a name matching the old `pr_acceptance.ts`) matches this repo's existing - convention of a package's CLI adapter living in its own `src/` (e.g. `nexus-cli.ts` in - `portable-tools`) — there is no other CLI entry point in `libs/pr-acceptance` to collide with. -- **Refuted alternative:** Leave a minimal `.claude/skills/nxs-pr-acceptance/SKILL.md` that only - points at the new location, for discoverability. Rejected: decision record's stated payload- - boundary criterion is structural ("no vendored component file may import a workspace package or - require the source-checkout runtime"), and a pointer-only `SKILL.md` with no `scripts/` would - still register the harness as an agent-invocable skill, undoing the exact trade the record - accepts. The runbook is already the documented discovery path. - -## 2026-08-23 — Structural composition check as a new `component-composition.ts` module, keyed by static import-regex, not a TS/AST parse (story #275) - -- **Choice:** New module `libs/portable-tools/src/component-composition.ts` exports - `findWorkspaceImports(content: string): string[]` (a regex over `from "@nexus/..."` and - `require("@nexus/...")`) and `checkComponentComposition(claudeDir, waivers): CompositionViolation[]`, - which walks `listComponentFiles` (already the vendoring-payload's own file walk) and flags any - `.ts`/`.js`/`.mjs`/`.cjs` file with an unwaived workspace import. The waiver register is a - separate committed file, `component-composition-waivers.ts`, exporting a flat - `readonly string[]` of repo-relative paths (posix-style, matching `listComponentFiles`'s own - output format) — the 7 legacy `.claude/skills/*/scripts/*.ts` shims stories #272-#274 left in - place (`get_abs_doc_path.ts`, `close_migration.ts`, `epic_resolve.ts`, `pr_worktree.ts`, - `record_digest.ts`, `docs_root.ts`, `workspace_status.ts`). It is enforced by a live-tree test in - `parity.spec.ts` (not just documented): `checkComponentComposition(liveClaudeDir(SRC_DIR), [])` - must equal the waiver register exactly, so an unwaived new violation OR the register going stale - (naming a file that no longer violates) both fail the suite. -- **Why:** A regex is enough because the surface being checked is narrow and adversarial-input-free - (this repo's own component tree, not third-party code) — every real violation is a plain - top-level `import ... from "@nexus/..."` or `require("@nexus/...")`, and the existing five - distiller capabilities and the `nexus-cli.ts` registry already prove this repo's own composition - check doesn't need a full TS AST walk anywhere else (`parity.ts`'s comparators are similarly - plain string/line operations, not AST-based). A dedicated waiver-register file (rather than - inline exceptions passed at each call site) makes the exception list itself grep-able, diffable, - and the single place #250 empties out when the legacy scripts are finally deleted. -- **Refuted alternative:** Parse each file's AST (e.g. via TypeScript's compiler API) to find - imports precisely, handling re-exports, dynamic `import()`, and computed specifiers. Rejected as - disproportionate: none of the 7 waived files (or any file in the managed subtrees) uses a dynamic - or computed import, and adding a TS-compiler dependency to a build-time payload check is a much - heavier primitive than the property being checked warrants — a regex miss would only under-flag - an exotic import form no file in this tree actually uses, which the live-tree test would still - catch if it were ever added (the test compares against a hand-maintained expected list, not - merely "check runs without crashing"). - -## 2026-08-23 — Repoint the `pnpm nexus:*` dev aliases at `nexus-cli.ts`, not at story #274's launchers (story #276) - -- **Choice:** The four capability-invoking root `package.json` scripts — - `nexus:generate-atlas`, `nexus:validate-concepts`, `nexus:check-atlas`, `nexus:drift-advisory`, - `nexus:seed-registry` — now run `tsx libs/portable-tools/src/nexus-cli.ts [--check]` - instead of `tsx libs/portable-tools/src/-launcher.ts`. `nexus:vendor-tools` is untouched - (it is the build/vendor orchestrator itself, not a capability-invoking alias). Verified each - repointed script by hand: `pnpm nexus:check-atlas`, `pnpm nexus:validate-concepts`, `pnpm - nexus:drift-advisory` against this repo's real `.nexus/concepts/`, and `pnpm exec tsx - nexus-cli.ts seed-registry --out-dir ` — all produce the same output shape as before. -- **Why:** Decision record #277 ("The source run is the same dispatcher, and the in-repo aliases - repoint to it"): "The in-repo build and release aliases that today invoke capabilities directly - are redefined to go through that same shape... Repointing is in scope because the alias names - that component bodies invoke do not change. Only the alias definitions change." The alias names - are exactly what `.claude/commands/nxs.distill.md` already names literally (`pnpm - nexus:generate-atlas`, `pnpm nexus:validate-concepts -- --base HEAD ...`, `pnpm - nexus:drift-advisory`) — a command body, which per the epic's Out-of-Scope list must not be - rewritten to name a verb directly (that's #250). Repointing only the alias's *definition* - satisfies the decision record's instruction without touching that command body at all: the - command still types `pnpm nexus:generate-atlas`; what that alias now runs underneath changed. - Left ambiguous at first read: story #274 had *already* introduced one-launcher-per-capability - specifically so these same pnpm scripts kept working post-guard-removal, so re-pointing them - again looked like it might be reverting #274's fix. It isn't — the launchers exist so - `build-bundles.ts`'s `ENTRY_POINTS` has a guard-free, self-contained esbuild entry per standalone - artifact; the pnpm dev aliases are a separate concern (a maintainer's daily-driver command), and - decision record #277's own words for *this* story say to run them through the dispatcher now - that it exists, which is a stronger, more direct proof of "one command shape" than the launchers - give (each launcher is still a distinct, per-capability source entry point — precisely what the - decision record's Key Decision for this story rules out: "no separate source-side entry point, - and no verb-specific source command"). -- **Refuted alternative:** Leave the four `nexus:*` scripts pointed at their #274 launchers. - Simpler (zero net script changes this story), and still produces identical output today since - #274's parity tests already prove `-launcher.ts` and the `nexus-cli.ts` verb are - output-identical. Rejected because it does not satisfy the decision record's explicit - instruction for this story ("the in-repo build and release aliases... are redefined to go - through that same shape"), and it would leave two source-side ways to invoke the same - capability (launcher vs. verb) where the record calls for exactly one. diff --git a/docs/concepts.md b/docs/concepts.md index 0d6de82..f60f99d 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -4,7 +4,7 @@ # Concept Atlas -Orientation map of the concept store — 53 active concepts. Each links to its full page +Orientation map of the concept store — 54 active concepts. Each links to its full page (behavior, invariants, decision history); code locations live in the matching `.nexus/anchors/.md` sidecar. @@ -15,13 +15,14 @@ Orientation map of the concept store — 53 active concepts. Each links to its f - [Nexus Pipeline](../.nexus/concepts/nexus-pipeline.md) — Nexus is a lean, spec-driven delivery pipeline assisting product and project management — turning intent into validated, decision-grade specs. - [Concept Store](../.nexus/concepts/concept-store.md) — The concept store is Nexus's machine knowledge surface: one distilled concept per file, keyed by a readable slug, holding current behavior, hard invariants, blast radius, and the durable why. - [Approvable Decision Record](../.nexus/concepts/decision-record.md) — An epic's decision record — the architectural why the design stage produces — lives as a sub-issue of the epic issue: one copy, born durable, addressable by the provenance reference form. -- [Epic Approval Gate](../.nexus/concepts/epic-approval-gate.md) — The epic stage files the epic and its story issues together, gated by a single decision-grade digest the human approves. - [Issue-Sourced Planning](../.nexus/concepts/issue-sourced-planning.md) — Issue-Sourced Planning makes GitHub issues the single source of truth for epic and story planning: nothing is committed at planning, and one deterministic resolver reconstructs the epic from its issue number. +- [Epic Approval Gate](../.nexus/concepts/epic-approval-gate.md) — The epic stage files the epic and its story issues together, gated by a single decision-grade digest the human approves. +- [Close-Entry Migration](../.nexus/concepts/close-entry-migration.md) — Close-entry migration is the cross-repo tail of the close stage: it puts a closed epic where its concepts are distilled. - [PR-Driven Post-Merge Flow](../.nexus/concepts/pr-driven-flow.md) — The lead can run the conformance, closure, and distillation stages against a pull request instead of a live branch. - [Publishing Config Resolution](../.nexus/concepts/publishing-config-resolution.md) — Publishing config resolution replaces every discovered-by-failure GitHub-publishing decision with one declared configuration block, resolved by a single shared resolver every publishing consumer goes through. +- [Verb Reachability](../.nexus/concepts/verb-reachability.md) — A capability becomes reachable by name, a verb on one named executable, exactly when a Nexus component body invokes it. - [Workspace Resolution](../.nexus/concepts/workspace-resolution.md) — Workspace resolution makes a multi-repo product declared and discoverable: one manifest in the hub names the hub and its members, and a thin pointer in each member names only the hub. - [Backlog Stub](../.nexus/concepts/backlog-stub.md) — A backlog stub is an epic identified but not yet planned — an epic issue carrying the repository's declared epic classification plus exactly one label denoting that unplanned state. -- [Close-Entry Migration](../.nexus/concepts/close-entry-migration.md) — Close-entry migration is the cross-repo tail of the close stage: it puts a closed epic where its concepts are distilled. - [Conformance Gate](../.nexus/concepts/conformance-gate.md) — Analyze checks the implemented code against the epic's acceptance criteria and the decision record's invariants, then proves it ran by leaving a receipt. - [Taxonomy Drift Advisory](../.nexus/concepts/drift-advisory.md) — The drift advisory is a deterministic, non-blocking step of the drain that reads the concept link graph and each page's filing and reports taxonomy decay as text in the distillation-PR body. - [Durable Close Record](../.nexus/concepts/durable-close-record.md) — The durable copy of a close's rationale is the comment the close stage posts on the epic issue, in every mode; the close-record file is a hand-off copy carrying no durability obligation. @@ -34,18 +35,18 @@ Orientation map of the concept store — 53 active concepts. Each links to its f - [Discovery Graduation](../.nexus/concepts/discovery-graduation.md) — A finished discovery becomes issues only through the epic stage, which reads the discovery document as its intent and files through the emission path it already owns. - [Distillation PR](../.nexus/concepts/distillation-pr.md) — The distiller does not write the concept store directly; it opens a reviewed pull request against the store. - [Grep-Native Retrieval](../.nexus/concepts/grep-native-retrieval.md) — The knowledge store is retrieved by plain text search over readable files — search, list, and read — with no graph engine, embeddings, or community detection. +- [Nexus Setup CLI](../.nexus/concepts/nexus-setup-cli.md) — The Nexus Setup CLI is the portable `nexus` command that owns the *structural* half of getting Nexus into a repo or a whole multi-repo workspace — installing components and declaring or growing a workspace. +- [Portable Tooling](../.nexus/concepts/portable-tooling.md) — Portable tooling is the offline form of distillation's deterministic steps — the concept validator, the atlas generator, and a hub diff-derivation tool — built to run on a bare Node.js runtime. - [Code Anchors](../.nexus/concepts/code-anchors.md) — Code anchors are derived sidecar files that map a concept to the source that implements it. - [Finding Severity](../.nexus/concepts/finding-severity.md) — Concept validation reports two classes of finding: blocking ones that fail the run, and advisories that never do. - [Fog Referral Gate](../.nexus/concepts/fog-referral-gate.md) — The epic stage tests whether an intent's functional goals can be stated at all, before it measures how big they are. - [Forcing-Function Razor](../.nexus/concepts/forcing-function-razor.md) — Every artifact must exist to force a decision a human must make, or it is cut scaffolding. - [Gold-plating](../.nexus/concepts/gold-plating.md) — Gold-plating is the production of heavy, elaborate artifacts ahead of validated need — volume manufactured on speculation about what might matter rather than distilled from what has been decided. -- [Nexus Setup CLI](../.nexus/concepts/nexus-setup-cli.md) — The Nexus Setup CLI is the portable `nexus` command that owns the *structural* half of getting Nexus into a repo or a whole multi-repo workspace — installing components and declaring or growing a workspace. -- [Portable Tooling](../.nexus/concepts/portable-tooling.md) — Portable tooling is the offline form of distillation's deterministic steps — the concept validator, the atlas generator, and a hub diff-derivation tool — built to run on a bare Node.js runtime. +- [PR Flow Worktree](../.nexus/concepts/pr-worktree.md) — The pull-request post-merge flow runs its stages in an isolated worktree, not the lead's own checkout. - [Story as Unit](../.nexus/concepts/story-as-unit.md) — The user story is Nexus's terminal planning unit and its issue granularity. - [Two-Store Split](../.nexus/concepts/two-store-split.md) — Nexus keeps two knowledge surfaces that never share an artifact: a lean human-judgment surface for what a person must read and decide, and a machine knowledge surface where distilled volume is legitimate. - [Append-Only Decision Log](../.nexus/concepts/append-only-decision-log.md) — Every concept page carries a Decision Log — the append-only, immutable record of why the concept is the way it is. - [Domain Taxonomy](../.nexus/concepts/domain-taxonomy.md) — Domain taxonomy is a curated, two-level grouping for concept pages — domains and optional subdomains — authored in a registry that lives beside the atlas. -- [PR Flow Worktree](../.nexus/concepts/pr-worktree.md) — The pull-request post-merge flow runs its stages in an isolated worktree, not the lead's own checkout. - [Provenance Reference](../.nexus/concepts/provenance-reference.md) — A provenance reference links a concept page or a log entry back to the issue that originated it, so an agent can hop from a compressed page to the full context it distilled away. - [Registry Seeding](../.nexus/concepts/registry-seeding.md) — Registry seeding is a one-time adoption tool for a store that has no domain registry yet. - [Remote Identity Normalization](../.nexus/concepts/remote-identity-normalization.md) — A single git remote can be written many equivalent ways — as a secure-shell address or a web address, with or without a trailing suffix, with an upper- or lower-cased host.