diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 5b6b469d9..ce33692d7 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -126,8 +126,15 @@ deno task check # Run tests deno task test -# Run CLI E2E tests (expensive — run before merge) +# Discover available CLI E2E suites and gates +deno task e2e:cli suites +deno task e2e:cli gates scaffold.runtime + +# Run the full CLI E2E smoke before merge deno task e2e:cli + +# Debug the explicit full suite with readable output +deno task e2e:cli run scaffold.runtime --cleanup --format pretty ``` ### Working with the harness diff --git a/.llm/harness/debt/arch-debt.md b/.llm/harness/debt/arch-debt.md index ba3844e13..7a757155b 100644 --- a/.llm/harness/debt/arch-debt.md +++ b/.llm/harness/debt/arch-debt.md @@ -16,6 +16,11 @@ Seeded from feature slices, kernel adapters, template assets, binary edges, and executable CLI fitness gates now cover the former monoliths. - **Gate:** F-1, F-5, F-6, F-7 +- **Wave 1 closure evidence:** `feat-package-quality-wave1-contracts--contracts` slice 10 split + `packages/runtime-config/mod.ts` into `src/domain/types.ts`, `src/application/loader.ts`, + `src/application/watcher.ts`, and `src/diagnostics/summary.ts`; `deno check`, `deno doc --lint`, + `deno publish --dry-run --allow-dirty`, `deno test --allow-all`, `deno lint`, and + `deno fmt --check` pass for the package. ## packages/config — AP-1 / doctrine verdict Refactor (schema.ts 945 LOC) @@ -28,6 +33,52 @@ Seeded from maintainer command graph; `deno task arch:check` enforces public/maintainer isolation. - **Gate:** F-1, F-5, F-10 +## packages/config — AP-16 root helpers.ts + +- **Reason:** Root `helpers.ts` held saga authoring input types behind a generic helper name. +- **Owner:** Wave 1 contracts and schemas. +- **Target:** S1 alpha package-quality wave. +- **Linked plan:** `.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md` +- **Created:** 2026-06-06 +- **Status:** closed 2026-06-06 — slice 11 renamed `helpers.ts` to + `src/domain/saga-inputs.ts` and kept the public `defineSagas`/input type exports stable through + `src/public/mod.ts`. +- **Gate:** F-11, AP-16, `deno check mod.ts` + +## packages/config/src/domain/mod.ts — justified domain barrel + +- **Reason:** `src/domain/mod.ts` is a sub-barrel, but it intentionally curates the domain schema + surface consumed by `src/public/mod.ts` and future docs/reference generation. +- **Owner:** Wave 1 contracts and schemas. +- **Target:** Revisit when generated reference tooling can crawl individual schema modules. +- **Linked plan:** `.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md` +- **Created:** 2026-06-06 +- **Status:** open, DEBT_ACCEPTED — slice 17 added `arch:barrel-ok` justification in the file. +- **Gate:** F-18, `Select-String -Path src/domain/mod.ts -Pattern 'arch:barrel-ok'` + +## packages/contracts — AP-16 helpers directory + +- **Reason:** Root `helpers/` held query and transform helpers behind a generic folder name. +- **Owner:** Wave 1 contracts and schemas. +- **Target:** S1 alpha package-quality wave. +- **Linked plan:** `.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md` +- **Created:** 2026-06-06 +- **Status:** closed 2026-06-06 — slices 19 and 20 moved `paginated-query.ts` and + `transform.ts` into `src/application/` as role-named modules and removed the `helpers/` + directory. +- **Gate:** F-11, AP-16, `deno check mod.ts` + +## packages/contracts/crud — accepted root subpath layout + +- **Reason:** `crud/` remains at the package root to preserve the established `./crud` subpath + export and avoid broad downstream import churn during S1. +- **Owner:** Wave 1 contracts and schemas. +- **Target:** Revisit when subpath exports can move without consumer breakage. +- **Linked plan:** `.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md` +- **Created:** 2026-06-06 +- **Status:** open, DEBT_ACCEPTED — locked decision L8 keeps `contracts/crud/` at package root. +- **Gate:** F-5/F-6 remain green for `@netscript/contracts`; consumer validation in slices 25-27. + ## packages/cron — AP-17 / doctrine verdict Refactor - **Reason:** `interfaces/` should become `ports/`; adapter classes should be named by technology. diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/commits.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/commits.md new file mode 100644 index 000000000..5fdc5e370 --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/commits.md @@ -0,0 +1,42 @@ +# Commits: Wave 1 — Contracts & schemas + +Append every commit created during the run immediately after creating it. + +Format: + +```md +- : +``` + +## Log + +- 187f862: plan(wave1): research + plan & design for config, contracts, runtime-config +- d0be0ad: refactor(runtime-config): scaffold domain types +- 31b1464: refactor(runtime-config): extract loader application module +- 617e7d4: refactor(runtime-config): extract watcher application module +- 23e1480: feat(runtime-config): add structured config summary +- f59f9ef: refactor(runtime-config): expose thin public barrel +- f413fde: chore(runtime-config): add package tasks and publish config +- 45a276b: docs(runtime-config): add package README +- df8d087: docs(runtime-config): scaffold package docs +- 7b83766: test(runtime-config): cover loader accessors and summary +- a8a7caf: test(runtime-config): complete package gate sweep +- 2b638df: refactor(config): rename saga input helpers +- d60f43c: docs(config): export saga group input and document config types +- fbcfee5: docs(config): fix merge subpath public types +- 0ac2d8b: docs(config): harden plugin schema public docs +- 43e51b4: docs(config): add recipes and advanced guide +- 3c10baa: docs(config): justify domain barrel +- 68f6d6d: test(config): complete package gate sweep +- aab8d2f: refactor(contracts): move paginated query helper +- 5062e76: refactor(contracts): move transform helpers +- df7a919: docs(contracts): export subpath schema types +- ec09969: docs(contracts): add getting started guide +- eb545ca: docs(contracts): add advanced extending guide +- b066da6: test(contracts): complete package gate sweep +- f4cbd3c: test(workspace): validate cli consumer +- cdff7f0: test(workspace): validate plugin consumers +- ad0b232: test(workspace): verify final package publish dry runs +- 936209a: docs(agents): clarify full cli e2e command +- f6bf743: test(cli): restore full scaffold runtime e2e +- b96ba0d: docs(agents): port durable claude guidance diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/context-pack.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/context-pack.md new file mode 100644 index 000000000..04d14b2d6 --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/context-pack.md @@ -0,0 +1,87 @@ +# Context Pack: Wave 1 — Contracts & schemas + +| Field | Value | +|-------|-------| +| Run ID | `feat-package-quality-wave1-contracts--contracts` | +| Branch | `feat/package-quality-wave1-contracts` | +| Base | `feat/package-quality` (Wave 0 `shared` + Wave 0b harness/docs merged) | +| Phase | **IMPL-EVAL complete → `FAIL_FIX`** (returns to generator for one fix) | +| Units | `@netscript/config`, `@netscript/contracts`, `@netscript/runtime-config` | +| Archetype | 1 — Small Contract (all three) | +| Scope overlay | none (package wave) | + +## Goal + +Bring the 3 units to the S1 alpha bar: `deno publish --dry-run` with **0 slow-types**, `deno doc --lint` clean, README ≥ 150 LOC, `/docs` per STANDARDS § 7, archetype gate matrix green per unit. **S1 STOPS at publish-clean dry-run — do NOT publish.** + +## Status + +- [x] `research.md` — re-baselined with REAL dry-run numbers (0 slow types all three) +- [x] `plan.md` — locked decisions, open-decision sweep, risk register, gate set selected +- [x] `worklog.md` — Design checkpoint with public surface, domain vocabulary, 27 commit slices +- [x] `drift.md` — re-baseline drift logged +- [x] `commits.md` — scaffolded (no commits yet) +- [x] `plan-eval.md` — **PLAN-EVAL PASS** (Arch-1 gate set adjusted: added F-14 Console-log, F-17 Abstract-derived co-location) +- [x] Slice 1 — `runtime-config` domain types scaffolded; `deno doc --lint src/domain/types.ts` passed. +- [x] Slice 2 — `runtime-config` loader extracted; `deno check src/application/loader.ts` passed. +- [x] Slice 3 — `runtime-config` watcher extracted with no `console.*` in `src/`; `deno check src/application/watcher.ts` passed. +- [x] Slice 4 — `runtime-config` structured summary added; `deno check src/diagnostics/summary.ts` passed. +- [x] Slice 5 — `runtime-config` root barrel rewritten; `deno doc --lint mod.ts` passed. +- [x] Slice 6 — `runtime-config` `deno.json` metadata/tasks/publish config added; `deno publish --dry-run --allow-dirty` passed. +- [x] Slice 7 — `runtime-config` README added; line-count gate returned 346 lines. +- [x] Slice 8 — `runtime-config` docs scaffold added; docs file-list gate returned 8 files. +- [x] Slice 9 — `runtime-config` tests added; `deno test --allow-all` passed with 8 tests. +- [x] Slice 10 — `runtime-config` gate sweep passed after lint/format/example fixes; current README line count is 339. +- [x] Slice 11 — `config/helpers.ts` renamed to `src/domain/saga-inputs.ts`; `deno check mod.ts` passed. +- [x] Slices 12-13 — `SagaGroupInput` exported and remaining `types.ts` JSDoc added; `deno doc --lint mod.ts` passed. +- [x] Slice 14 — config merge subpath private-type-ref fixed; `deno doc --lint src/merge/mod.ts` passed. +- [x] Slice 15 — config plugin schemas documented and Zod internal public type leak removed; `deno doc --lint src/schema/plugins/mod.ts` passed. +- [x] Slice 16 — config docs recipes and advanced page added; docs file-list gate returned 7 files. +- [x] Slice 17 — config domain barrel marked with `arch:barrel-ok`; grep gate passed. +- [x] Slice 18 — config gate sweep passed after static cleanup, type split, and domain schema grouping; publish dry run remained at 0 slow types. +- [x] Slice 19 — contracts paginated-query helper moved to `src/application`; `deno check mod.ts` passed. +- [x] Slice 20 — contracts transform helper moved to `src/application`; `deno check mod.ts` and `deno check transform.ts` passed; `helpers/` removed. +- [x] Slice 21 — contracts subpaths exported schema/procedure public types; `deno doc --lint crud.ts query.ts transform.ts` passed. +- [x] Slice 22 — contracts `docs/getting-started.md` added and linked; docs file-list gate passed. +- [x] Slice 23 — contracts `docs/advanced/extending.md` added and linked; docs file-list gate passed. +- [x] Slice 24 — contracts gate sweep passed; helpers AP-16 debt closed and root `crud/` accepted debt recorded. +- [x] Slice 25 — `packages/cli` consumer `deno check` passed with 0 errors. +- [x] Slice 26 — `plugins/sagas` and `plugins/workers` consumer `deno check` passed with 0 errors. +- [x] Slice 27 — final publish dry run passed for `runtime-config`, `config`, and `contracts` with 0 slow-type errors. + +## Key findings (re-baselined) + +| Unit | Slow types | `deno doc --lint` | README LOC | `/docs` | Tests | +|------|-----------:|------------------:|-----------:|---------|-------| +| `@netscript/config` | **0** | 33 errors | 255 ✓ | partial | exists | +| `@netscript/contracts` | **0** | 21 errors | 424 ✓ | partial | exists | +| `@netscript/runtime-config` | **0** | 34 errors | **0** ✗ | **missing** ✗ | **missing** ✗ | + +## Locked decisions (8) + +1. All three stay Archetype 1. +2. `runtime-config/mod.ts` splits into `src/domain/`, `src/application/`, `src/diagnostics/`. +3. `config/helpers.ts` → `src/domain/saga-inputs.ts`. +4. `contracts/helpers/` → `src/application/` by role. +5. `runtime-config` console usage → structured diagnostics (return values). +6. Fix `private-type-ref` by exporting referenced types. +7. Remove Zod internal (`z.ZodType`) from public signatures. +8. Keep `crud/` at package root (subpath export stability). + +## Commit slices + +27 slices ordered by dependency: runtime-config (1–10), config (11–18), contracts (19–24), cross-cutting (25–27). + +## Operating reminders + +- Implementation has completed through slice 27. +- **IMPL-EVAL verdict: `FAIL_FIX`** (`evaluate.md`) was addressed before the post-review CLI E2E + pass. The config `./paths` doc-lint gap was fixed and committed before this handoff. +- Post-review CLI E2E gap is closed: bare `deno task e2e:cli` now runs the full + `scaffold.runtime` merge-readiness suite with cleanup instead of the narrow plugin scaffold + smoke. The suite scaffolds a project, initializes/generates/seeds the DB, launches Aspire, + adds plugins, validates generated checks, exercises worker/saga/trigger runtime endpoints, checks + plugin doctor, verifies OTEL trace linkage, and cleans up apphost/Docker resources. +- Full CLI E2E evidence: `deno task e2e:cli` returned PASS with `passed=41 failed=0 skipped=0`. + `docker ps` was empty afterward, and `aspire ps` reported no running apphost. +- Do not self-evaluate in the implementation session. Hand off to a separate IMPL-EVAL session. diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/drift.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/drift.md new file mode 100644 index 000000000..7246075e9 --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/drift.md @@ -0,0 +1,103 @@ +# Drift Log: Wave 1 — Contracts & schemas + +Drift is append-only. Record facts that diverge from the plan, RFC, doctrine, or current-state documentation. + +## 2026-06-06 — Research re-baseline + +- **What:** The canonical audit (`.llm/tmp/run/copilot-evaluate-every-package-jsr-release--package-jsr-alpha-release/`) claimed slow-type counts of 35, 30, and 1 for config, contracts, and runtime-config. Re-derivation against `feat/package-quality` shows **0 slow types for all three**. +- **Source:** `deno publish --dry-run --allow-dirty` per package. +- **Expected:** Slow types exist and need fixing. +- **Actual:** Workspace `compilerOptions.isolatedDeclarations: true` already enforces explicit types; all slow types were eliminated in prior waves. +- **Severity:** minor +- **Action:** accept — plan updated to reflect real state; slices for slow-type fixes removed. +- **Evidence:** `research.md` findings #1, #12, #21. + +## 2026-06-06 — `runtime-config` archetype confirmation + +- **What:** Stale audit suggested `runtime-config` might pull toward Archetype 2 (Integration) due to `@std/path` and `Deno.*` usage. +- **Source:** `docs/architecture/doctrine/06-archetypes.md` decision tree. +- **Expected:** Might need ports/adapters. +- **Actual:** Single file-system edge, no second backend planned. Value is still "clarity of types." Archetype 1 confirmed. +- **Severity:** minor +- **Action:** accept — locked in plan.md L1. +- **Evidence:** `plan.md` § Archetype. + +## 2026-06-05 — PLAN-EVAL gate-set adjustment + +- **What:** The plan's selected Archetype-1 fitness gate set omitted **F-14 (Console-log lint)** and **F-17 (Abstract-derived co-location)**, both marked `required` for Arch 1 in `gates/archetype-gate-matrix.md`. +- **Source:** PLAN-EVAL checklist walk vs. `.llm/harness/gates/archetype-gate-matrix.md`. +- **Expected:** Gate set lists every `required` Arch-1 gate. +- **Actual:** F-14 and F-17 missing; F-14 is materially relevant because L5 removes `runtime-config` console usage and F-14 is its proving gate. +- **Severity:** minor +- **Action:** adjust — added F-14 (mapped to L5) and F-17 (PENDING_SCRIPT, no violation) to `plan.md` §Fitness Gates + Validation Plan and `worklog.md` gate table. Plan-Gate "Gate set selected" box now satisfied; verdict PASS. +- **Evidence:** `plan-eval.md`, `plan.md` §Fitness Gates. + +## 2026-06-06 — Config slices 12-13 gate dependency + +- **What:** Slice 12 exports `SagaGroupInput` and names `deno doc --lint mod.ts` as its gate, but + that gate still fails on the known 32 `types.ts` property JSDoc errors assigned to slice 13. +- **Source:** `deno doc --lint mod.ts` after exporting `SagaGroupInput`. +- **Expected:** Slice 12 gate passes after the private-type-ref export fix. +- **Actual:** The private-type-ref issue is fixed, but doc-lint remains blocked until slice 13 adds + the missing JSDoc. The only honest green gate is after slices 12 and 13 are both present. +- **Severity:** minor +- **Action:** combine the implementation evidence for slices 12 and 13, then continue with slice + 14 in order. +- **Evidence:** `worklog.md` progress rows for slices 12 and 13. + +## 2026-06-06 — Config merge subpath type exports + +- **What:** Slice 14 planned to export `DatabaseEntry`, `ServiceContributionEntry`, and + `AppContributionEntry`, but `mergePartialConfig()` also publicly names `NetScriptConfig`. +- **Source:** `deno doc --lint src/merge/mod.ts`. +- **Expected:** Exporting the three contribution entry types is enough. +- **Actual:** Exporting `NetScriptConfig` makes its referenced config section types public through + the `./merge` subpath too, so the subpath must export the config type family used by + `NetScriptConfig`. +- **Severity:** minor +- **Action:** export the referenced config types from `src/merge/mod.ts`; keep runtime behavior + unchanged. +- **Evidence:** `deno doc --lint src/merge/mod.ts` passed after expanding the type exports. + +## 2026-06-06 — Config plugin schema public annotation + +- **What:** Slice 15 planned to remove `z.ZodType` public annotations by using inferred schema + types. +- **Source:** `deno doc --lint src/schema/plugins/mod.ts`. +- **Expected:** Removing explicit `z.ZodType` annotations is enough. +- **Actual:** Doc-lint requires explicit types for exported schema constants, and + `ReturnType>` still leaks private Zod internals. A local + `PluginSettingsSchema` parse/safeParse contract keeps the public surface documented without + naming Zod internals. +- **Severity:** minor +- **Action:** expose schema constants as `PluginSettingsSchema`; runtime values remain Zod + schemas. +- **Evidence:** `deno doc --lint src/schema/plugins/mod.ts` passed. + +## 2026-06-06 — Config sweep static and cardinality cleanup + +- **What:** Slice 18's full sweep surfaced several pre-existing static-gate issues not itemized as + standalone config slices: stale workspace test expectations, unversioned `@std/assert` imports in + tests, `console.` in public examples, explicit `z.ZodType` annotations in exported domain schema + constants, `types.ts` over 500 LOC, and `src/domain` over the 12-child cardinality limit. +- **Source:** `deno test --allow-all`, `deno lint`, manual F-1/F-14/F-16 scans. +- **Expected:** Slice 18 would mostly verify already-completed fixes. +- **Actual:** The sweep required cleanup to make the package satisfy the whole Archetype-1 gate set. +- **Severity:** minor +- **Action:** fixed in slice 18 by updating workspace tests/imports/examples, using inferred schema + constant types, splitting public config types into section/root modules, and grouping schema files + under `src/domain/schemas/`. +- **Evidence:** Config slice 18 sweep in `worklog.md`. + +## 2026-06-06 — Contracts subpath transitive schema type export + +- **What:** Slice 21 named `ContractSchema`, `ContractObjectSchema`, and `BaseContractProcedure` + as required public exports, but once those types were exported through `./crud` and `./query`, + `ContractSchema` publicly referenced `ContractParseResult`. +- **Source:** `deno doc --lint crud.ts query.ts transform.ts`. +- **Expected:** The three planned exports were sufficient. +- **Actual:** The transitive parse-result type also had to be public on the affected subpaths. +- **Severity:** minor +- **Action:** export `ContractParseResult` from `crud.ts` and `query.ts` with the schema type + contracts. +- **Evidence:** `deno doc --lint crud.ts query.ts transform.ts` passed. diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/evaluate.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/evaluate.md new file mode 100644 index 000000000..41ecfb265 --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/evaluate.md @@ -0,0 +1,104 @@ +# Evaluation: Wave 1 — Contracts & schemas (`@netscript/config`, `@netscript/contracts`, `@netscript/runtime-config`) + +Allowed result values: `PASS`, `FAIL`, `N/A`, `PENDING_SCRIPT`, `DEBT_ACCEPTED`, `NOT_RUN`. + +## Metadata + +| Field | Value | +| -------------- | ------------------------------------------------- | +| Run ID | `feat-package-quality-wave1-contracts--contracts` | +| Target | `@netscript/config`, `@netscript/contracts`, `@netscript/runtime-config` | +| Archetype | 1 — Small Contract (all three) | +| Scope overlays | none | +| Evaluator | IMPL-EVAL, separate session, 2026-06-06 | + +## Process Verification + +| Check | Result | Evidence | +| -------------------------------------- | ------ | ------------------------------------------------------------------------ | +| Plan-Gate passed before implementation | PASS | `plan-eval.md` = `PASS` (adjusted, F-14/F-17 added) before slice 1 | +| Design section exists in worklog | PASS | `worklog.md` § Design (public surface, vocabulary, ports, constants, 27 slices, contributor path) | +| Commit slices match design plan | PASS | `commits.md` lists 27 slices in design order; runtime-config (1–10) → config (11–18) → contracts (19–24) → cross-cutting (25–27) | +| Each slice has a passing gate | PASS | `worklog.md` Progress Log records a named gate per slice | +| No speculative seams (unused files) | PASS | All `src/` files reachable from a barrel/subpath; no template-only files found | +| Constants used for finite vocabularies | PASS | `RUNTIME_CONFIG_TOPICS`, `DEFAULT_DEBOUNCE_MS`, `POINTER_FILE_NAME`, contracts `DEFAULT_PAGINATION_*` | + +## Static Gates + +Independently re-run by the evaluator with Deno 2.8.2. The sandbox **cannot reach JSR +(`jsr.io`) or npm**, so any gate that must resolve `@std/*`, `zod`, or `@orpc/*` (type-check, +test, publish dry-run) could not be re-run here and relies on generator evidence. + +| Gate | Command or check | Result | Evidence | Notes | +| --------------- | ---------------- | ------------- | -------- | ----- | +| Format | `deno fmt --check packages/{runtime-config,config,contracts}` | PASS | `Checked 18/41/29 files`, no diffs | offline | +| Lint | `deno lint packages/{runtime-config,config,contracts}` | PASS | `Checked 8/32/20 files`, 0 problems | offline | +| Doc lint (root) | `deno doc --lint mod.ts` per package | PASS | runtime-config `Checked 1 file`; config root clean; contracts `Checked 4 files` | offline | +| **Doc lint (subpaths)** | `deno doc --lint ` | **FAIL** | `@netscript/config` `./paths` (`src/paths/mod.ts`) → **28 `missing-jsdoc` errors** | see Findings | +| Narrow typecheck | `deno check mod.ts` | NOT_RUN | JSR/npm blocked in sandbox; generator reports PASS (worklog slices 18/24/10) | env limitation | +| Publish dry-run | `deno publish --dry-run --allow-dirty` | NOT_RUN | JSR/npm blocked; generator reports 0 slow types all three (worklog slice 27) | env limitation; PLAN-EVAL flagged F-6 for IMPL re-run | +| Tests | `deno test --allow-all` | NOT_RUN | JSR/npm blocked; generator reports 8/10/4 passed | env limitation | + +## Fitness Gates + +| Gate | Function | Result | Evidence | Violations | +| ---- | ----------------------- | ------------- | -------- | ---------- | +| F-1 | File-size lint | PASS | No non-test `.ts` > 500 LOC across all three packages (evaluator scan) | none | +| F-5 | Public surface audit | **FAIL** | `./paths` subpath doc-lint = 28 errors (root + merge + schema/plugins + contracts subpaths clean) | config `./paths` | +| F-6 | JSR publishability | NOT_RUN | JSR/npm blocked; generator evidence only | — | +| F-7 | Doc-score gate | **FAIL** | READMEs 339/255/424 ✓ and docs 8/7/7 files ✓, but doc-lint not clean on `config` `./paths` | config `./paths` JSDoc | +| F-8 | Workspace lib check | PASS | Root `deno.json` `lib` includes `deno.ns`, `deno.unstable` | none | +| F-10 | Test-shape audit | PARTIAL | No test file > 500 LOC; suites present all three; run NOT_RUN (env) | — | +| F-11 | Forbidden-folder lint | PASS | No `utils/helpers/common/lib/interfaces` dirs in any target package (evaluator scan) | none | +| F-12 | Naming-convention lint | PASS | `deno lint` clean; no `I*`/`*_T`/`*Impl` exports | none | +| F-14 | Console-log lint | PASS | `grep console.` over `runtime-config/{mod.ts,src}`, `config/src`, `contracts/src` = 0 | none | +| F-15 | Re-export-upstream lint | PASS | `deno lint` clean; no `export * from 'npm:'/'jsr:'` outside `@netscript`/`@std` | none | +| F-16 | Folder-cardinality lint | PASS | No `src/` dir > 12 immediate children (evaluator scan) | none | +| F-17 | Abstract-derived audit | PASS | No abstract/derived class pairs (type + factory surfaces) | none | +| F-18 | Sub-barrel lint | PASS | `config/src/domain/mod.ts` + `contracts/src/public/mod.ts` carry `arch:barrel-ok` | none (debt-accepted) | + +## Consumer Gates + +| Consumer | Validation | Result | Evidence | +| ----------------- | -------------- | ------- | -------- | +| `packages/cli` | `deno check` | NOT_RUN | JSR/npm blocked in sandbox; generator reports 0 errors (slice 25) | +| `plugins/sagas` | `deno check` | NOT_RUN | JSR/npm blocked; generator reports 0 errors (slice 26) | +| `plugins/workers` | `deno check` | NOT_RUN | JSR/npm blocked; generator reports 0 errors (slice 26) | + +## Anti-Pattern Check + +| AP | Status | Evidence | Notes | +| ----- | ------------- | -------- | ----- | +| AP-1 | CLEAR | `runtime-config/mod.ts` split into domain/application/diagnostics; no file > 500 LOC | | +| AP-13 | CLEAR | F-14 console scan = 0 in published src | console removed per L5 | +| AP-16 | CLEAR | No `helpers.ts`/`helpers/` remain in config or contracts | renames landed | +| AP-22 | DEBT_ACCEPTED | `config/src/domain/mod.ts` justified `arch:barrel-ok`; entry in `debt/arch-debt.md` | | + +## Arch-Debt Delta + +| Metric | Count | Evidence | +| --------------------- | ----- | -------- | +| New entries | 2 | `contracts` root `crud/` layout; `config/src/domain/mod.ts` sub-barrel | +| Resolved entries | 3 | runtime-config single-file; config AP-16; contracts AP-16 | +| Deepened violations | 0 | — | +| Unrecorded violations | 0 | The `./paths` doc-lint gap is incomplete scope, not undocumented debt | + +## Findings + +| Severity | Finding | Evidence | Required action | +| -------- | ------- | -------- | --------------- | +| Medium | `@netscript/config` publishes the `./paths` subpath (`exports["./paths"] → ./src/paths/mod.ts`), but `deno doc --lint src/paths/mod.ts` reports **28 `missing-jsdoc` errors** on the exported `PathConstants`/`PathFiles`/`Permissions` members. The config doc-lint sweep (worklog slice 18) only ran `mod.ts src/merge/mod.ts src/schema/plugins/mod.ts` and never linted `./paths`. The plan's acceptance bar (`plan.md` § Goal) and the generator's own handoff note require doc-lint clean on **every** entrypoint, root **and all subpaths**. F-5/F-7 therefore fail on this subpath. | `deno doc --lint packages/config/src/paths/mod.ts` → `error: Found 28 documentation lint errors.` | fix — add JSDoc to the exported members of `src/paths/mod.ts` (and any transitively named public types), then re-run `deno doc --lint` on all four config entrypoints | +| Low (env) | F-6 publish dry-run, F-10 test runs, and consumer `deno check` could not be independently re-verified: the evaluator sandbox cannot reach `jsr.io`/`registry.npmjs.org` to resolve `@std/*`, `zod`, `@orpc/*`. PLAN-EVAL already flagged F-6 for IMPL-time confirmation. | `curl https://jsr.io/@std/path/meta.json` → HTTP 000; `deno check` → `JSR package manifest ... failed to load` | confirm F-6/F-10/consumer gates on a network-enabled runner (or accept generator evidence) before publish | + +## Lessons for Promotion + +| Lesson | Pattern | Applies to | Confidence | +| ------ | ------- | ---------- | ---------- | +| Doc-lint sweeps must enumerate **every** `exports` entry from `deno.json`, not only the subpaths a slice happened to touch. A subpath that no slice edited (here `./paths`) still ships and still gates on F-5/F-7. | Derive the doc-lint target list from `deno.json` `exports` keys, not from the slice diff. | Archetype 1 (Small Contract), any multi-subpath package | high | + +## Verdict + +| Field | Value | +| --------- | ----- | +| Verdict | `FAIL_FIX` | +| Rationale | The plan is sound and 3 packages are largely at the S1 bar (fmt, lint, root + merge + schema/plugins + all contracts subpaths doc-lint clean; F-1/F-8/F-11/F-12/F-14/F-15/F-16/F-17/F-18 pass; READMEs and docs meet thresholds). One required gate fails: the published `@netscript/config` `./paths` subpath has 28 `missing-jsdoc` doc-lint errors that the config sweep never exercised, so F-5/F-7 are not clean on all config entrypoints as the approved plan requires. This is an implementation/docs completeness gap with a valid plan — `FAIL_FIX`, not `FAIL_RESCOPE` or `FAIL_DEBT`. Fix: document the `./paths` exports and re-run doc-lint over all four config entrypoints. (F-6/F-10/consumer gates remain on generator evidence only due to the sandbox JSR/npm block; re-confirm on a networked runner before publish.) | diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan-eval.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan-eval.md new file mode 100644 index 000000000..fb3967064 --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan-eval.md @@ -0,0 +1,49 @@ +# PLAN-EVAL Verdict + +## Run + +| Field | Value | +|-------|-------| +| Run ID | `feat-package-quality-wave1-contracts--contracts` | +| Branch | `feat/package-quality-wave1-contracts` | +| Evaluator | Copilot (separate session) | +| Date | 2026-06-05 | + +## Verdict: ✅ `PASS` (adjusted) + +## Checklist + +| Plan-Gate box | Result | Evidence | +|---|---|---| +| Research present & current (re-baselined vs `feat/package-quality`) | PASS | `research.md` findings #1–#30 with verification commands | +| Decisions locked (L1–L8 w/ rationale) | PASS | `plan.md` §Locked Decisions | +| Open-decision sweep (4 defer + 1 must-resolve, resolved) | PASS | `plan.md` §Open-Decision Sweep | +| Commit slices (27, ordered, gate+files each) | PASS | `worklog.md` §Commit Slices | +| Risk register | PASS | `plan.md` §Risk Register | +| Gate set selected | **PASS after adjustment** | Added F-14, F-17 per evaluator | +| Deferred scope explicit | PASS | `worklog.md` §Deferred Scope | +| jsr-audit surface scan | PASS | `research.md` §jsr-audit surface scan | + +## Adjustments applied + +The selected Archetype-1 gate set omitted two gates the matrix marks `required`: + +1. **F-14 Console-log lint** — directly material; it's the proving gate for **L5** (`runtime-config` console → return-value diagnostics). Added to plan.md §Fitness Gates and Validation Plan. +2. **F-17 Abstract-derived co-location** — added as `PENDING_SCRIPT`, no violation (all three are type-only + factory surfaces). + +## Spot-checks confirmed + +- `config/helpers.ts` — matches finding #8 +- `contracts/helpers/{paginated-query,transform}.ts` — matches finding #17 +- `contracts/crud/` at root — matches finding #18 +- `runtime-config/mod.ts` = 415 LOC with `console.*` at 334–405 — matches finding #25, #29 +- `runtime-config` no README/docs/tests — matches findings #23, #24, #26 +- `config/src/domain/mod.ts` barrel — matches finding #11 + +## Caveat for IMPL-EVAL + +The `deno publish --dry-run` = **0 slow types** re-baseline could not be independently re-run in the evaluator sandbox. All *structural* findings the plan rests on were verified; IMPL-EVAL must still confirm F-6 (0 slow types on all three) at the implement gate. + +## Implementation may begin + +No slice should be committed beyond this point without the F-14/F-17 additions in the gate sweep. diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md new file mode 100644 index 000000000..5f248843b --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/plan.md @@ -0,0 +1,202 @@ +# Plan: Wave 1 — Contracts & schemas + +## Run Metadata + +| Field | Value | +| -------------- | ----------------------------------- | +| Run ID | `feat-package-quality-wave1-contracts--contracts` | +| Branch | `feat/package-quality-wave1-contracts` | +| Phase | `plan` | +| Target | `@netscript/config`, `@netscript/contracts`, `@netscript/runtime-config` | +| Archetype | 1 — Small Contract (all three) | +| Scope overlays | none | + +## Archetype + +**Archetype 1 — Small Contract** for all three units. + +- `@netscript/config`: publishes types, Zod schemas, and small loading invariants. The `@std/fs`/`@std/path` usage is for config loading, not integration adapters. No port/adapter split is justified (only one loading strategy). Value = clarity of types. +- `@netscript/contracts`: publishes contract primitives, schema factories, and narrow helpers. The oRPC usage is a dependency, not an adapter surface we own. No builder DSL of our own yet. Value = stable vocabulary across package boundaries. +- `@netscript/runtime-config`: publishes runtime override types and a loader. The `Deno.watchFs` usage is a file-system edge, not an adapter. No second store backend is planned. Value = hot-reloadable config contract. + +If `@netscript/contracts` grows a NetScript-defined fluent builder (e.g. `defineContract().route().input().output()`), it escalates to Archetype 4. That is out of scope for this wave. + +## Current Doctrine Verdict + +From `docs/architecture/doctrine/10-codebase-verdict-and-handoff.md`: + +| Package | Verdict | Headline action | +|---------|---------|-----------------| +| `@netscript/config` | Refactor | Split `schema.ts` (already done in current tree); rename `helpers.ts` | +| `@netscript/contracts` | Keep | Confirm version-axis shape; `crud/` folder review; rename `helpers/` | +| `@netscript/runtime-config` | Refactor | Split single-file `mod.ts`; add subpaths if exports grow | + +## Axioms in Play + +| Axiom | Why it matters | +|-------|---------------| +| A1 | Public types first — every exported interface must be documented before implementation touches it. | +| A2 | Simple over easy — the surface is small, named, and predictable. No kitchen-sink exports. | +| A6 | Helpers must be justified — `helpers.ts` and `helpers/` are generic names that hide real concerns. | +| A8 | One concern per folder — `runtime-config/mod.ts` mixes domain, application, and presentation. | +| A9 | Archetype drives shape — all three are Small Contract; no `presentation/` or `state/` needed. | +| A14 | Tests and gates preserve doctrine — every package gets tests and a passing dry-run gate. | + +## Goal + +Bring all three packages to S1 alpha bar: + +- `deno publish --dry-run` with **0 slow-types** and **0 portability errors** ✓ (already true) +- `deno doc --lint` **clean** on every entrypoint (root + all subpaths) +- **README ≥ 150 LOC** per STANDARDS § 6 (all three) +- **`/docs` per STANDARDS § 7** (all three — expand partial, create missing) +- **Archetype gate matrix green** per unit (F-1 through F-18 applicable to Archetype 1) +- **Tests** for every public function (runtime-config missing; config/contracts need expansion) + +## Scope + +- `@netscript/runtime-config`: + - Split `mod.ts` into `src/domain/`, `src/application/`, `src/diagnostics/` (doctrine-aligned) + - Add JSDoc to all exported symbols (34 doc-lint errors) + - Remove `console.*` from application code (AP-13) — introduce structured diagnostics + - Add `deno.json` standard tasks, description, publish config + - Write README.md from scratch (≥ 150 LOC) + - Scaffold `/docs` per STANDARDS § 7 + - Add tests (`tests/`) + +- `@netscript/config`: + - Rename `helpers.ts` → `src/domain/saga-inputs.ts` (AP-16) + - Fix `private-type-ref` on `SagaGroupInput` (export from public surface) + - Add JSDoc to `types.ts` interface properties (32 doc-lint errors) + - Fix `private-type-ref` in `src/merge/mod.ts` (export `DatabaseEntry`, `ServiceContributionEntry`, `AppContributionEntry`) + - Fix `missing-jsdoc` in `src/schema/plugins/mod.ts` + - Fix Zod internal type leak (`z.ZodType` in public schema exports) + - Expand `/docs` with `recipes/` and `advanced/` + +- `@netscript/contracts`: + - Rename `helpers/` → fold into `src/application/` by role (AP-16) + - Fix `private-type-ref` on subpaths — export `ContractSchema`, `ContractObjectSchema`, `BaseContractProcedure` from public surface so subpaths can reference them + - Add `getting-started.md` to `/docs` + - Add `advanced/` to `/docs` + +## Non-Scope + +- **JSR publish / OIDC / version bumps** — S1 stops at dry-run clean. +- **Aspire integration** — no runtime behavior changes. +- **Docs site generation** — out of scope; we ship on-disk docs only. +- **Back-compat aliases** — path imports are kept; publish rewrites to `jsr:`. +- **Breaking API changes** — all renames are internal file moves; public exports stay stable. + +## Hidden Scope + +- Downstream import updates: `packages/cli` imports `@netscript/config` types and `@netscript/runtime-config` types. File moves inside packages do not affect these imports (public surface unchanged). +- `plugins/sagas` and `plugins/workers` import `@netscript/contracts` — public surface unchanged. +- The `config/helpers.ts` rename requires updating `src/public/mod.ts` and `mod.ts` re-exports, but not downstream consumers (they import `defineSagas` from `@netscript/config` root). + +## Locked Decisions + +| ID | Decision | Rationale | +|----|----------|-----------| +| L1 | All three units stay **Archetype 1** | None has ports/adapters worth naming; value is type clarity, not runtime behavior. | +| L2 | `runtime-config/mod.ts` **splits into doctrine layout** | 415 LOC mixing domain/application/presentation violates A8. Split into `src/domain/types.ts`, `src/application/loader.ts`, `src/application/watcher.ts`, `src/diagnostics/summary.ts`. | +| L3 | `config/helpers.ts` → **`src/domain/saga-inputs.ts`** | AP-16 violation. The file contains saga definition input types — pure domain vocabulary. No downstream breakage (same exports via `mod.ts`). | +| L4 | `contracts/helpers/` → **`src/application/` by role** | AP-16 violation. `paginated-query.ts` → `src/application/paginated-query.ts`. `transform.ts` → `src/application/transform-helpers.ts`. | +| L5 | `runtime-config` console usage → **structured diagnostics functions** | AP-13 violation. Replace `console.log` in `watchRuntimeConfig` and `logRuntimeConfigSummary` with return-value diagnostics. Callers (CLI binaries) decide how to emit. | +| L6 | Fix `private-type-ref` by **exporting the referenced types**, not by hiding them | `deno doc --lint` requires public types for public signatures. `ContractSchema`, `ContractObjectSchema`, `BaseContractProcedure` become public exports. | +| L7 | Zod internal leak (`z.ZodType`) → **use inferred schema types** | Instead of `export const schema: z.ZodType`, export `export type Schema = z.infer` and `export const schema`. This removes the Zod internal from the public signature. | +| L8 | Keep `crud/` at package root | Moving it under `src/` would break the `./crud` subpath export mapping. The inconsistency is acceptable for a subpath concern; debt entry if we want to unify later. | + +## Open-Decision Sweep + +| Decision | Status | Notes | +|----------|--------|-------| +| Should `runtime-config` expose `./testing` subpath with in-memory config fixtures? | safe to defer | No consumer needs it today. Add when a test package requests it. | +| Should `config` export `NetScriptConfig` from root (currently only types)? | safe to defer | CLI imports `NetScriptConfig` via `@netscript/config` already. No change needed. | +| Should `contracts` add `./testing` subpath with mock contract builders? | safe to defer | No plugin test uses it today. | +| Should `runtime-config` add a `./watching` subpath for the watcher only? | safe to defer | Only CLI binaries use the watcher. Keep it on root for now. | +| Should `config/src/domain/mod.ts` (sub-barrel) be eliminated? | must resolve now | AP-22 violation. `src/domain/mod.ts` is a pure re-export barrel inside `src/`. Resolution: keep it as the curated public domain surface, add `// arch:barrel-ok ` comment. It is referenced by `src/public/mod.ts`. | + +## Risk Register + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|------------| +| Downstream import breakage from file moves | Low | Medium | Public surface (`mod.ts`, subpath exports) unchanged. Verify with `deno check` on CLI + plugins after each unit's slices. | +| `deno doc --lint` still fails after JSDoc additions due to nested private-type-ref | Medium | Low | Run `deno doc --lint` after every slice. The linter is the gate. | +| `runtime-config` watcher tests are flaky (file system timing) | Medium | Low | Use `Deno.makeTempDir` + manual file writes. Mock `Deno.watchFs` if needed. Test the loader logic, not the OS watcher. | +| Zod v4 internal type changes break schema/plugins exports | Low | High | Mitigated by L7 (remove `z.ZodType` from public signatures). | +| Scope creep — tempted to redesign `runtime-config` API | Medium | High | Locked: split files only, no API changes. The public function signatures stay identical. | + +## Anti-Patterns to Resolve or Avoid + +| AP | Status | Plan | +|----|--------|------| +| AP-1 (Monolithic file) | existing — `runtime-config/mod.ts` | Resolve: split into domain/application/diagnostics files. | +| AP-13 (console.log in published code) | existing — `runtime-config` | Resolve: replace with return-value diagnostics. | +| AP-14 (Re-export upstream) | risk — `contracts` exports oRPC types | Avoid: do not re-export `@orpc/server` symbols. Only export NetScript-owned types. | +| AP-16 (utils/helpers folders) | existing — `config/helpers.ts`, `contracts/helpers/` | Resolve: rename to role-named folders/files. | +| AP-22 (Useless re-export barrel) | existing — `config/src/domain/mod.ts` | Resolve: add `// arch:barrel-ok` justification; it genuinely aggregates 12 schema files into one import for `src/public/mod.ts`. | + +## Fitness Gates + +From `gates/archetype-gate-matrix.md` for Archetype 1: + +| Gate | Required | Expected evidence | +|------|----------|-------------------| +| F-1 File-size lint | yes | Manual: all files < 500 LOC; `runtime-config` split proves this. | +| F-5 Public surface audit | yes | `deno doc --lint` clean on all entrypoints. | +| F-6 JSR publishability | yes | `deno publish --dry-run --allow-dirty` = 0 slow types, 0 errors. | +| F-7 Doc-score gate | yes | `deno doc --lint` clean + README ≥ 150 LOC + `/docs` per STANDARDS § 7. | +| F-8 Workspace lib check | yes | `compilerOptions.lib` includes `"deno.unstable"` where needed (config already has `--unstable-kv` in check task). | +| F-10 Test-shape audit | yes | No test file > 500 LOC; tests exist for all three packages. | +| F-11 Forbidden-folder lint | yes | No `utils/`, `helpers/`, `common/`, `lib/`, `interfaces/` under `src/`. | +| F-12 Naming-convention lint | yes | No `I*` prefixes, `*_T` suffixes, `*Impl` classes. | +| F-14 Console-log lint | yes | No `console.*` in published non-presentation code. Proves L5 (`runtime-config` console → return-value diagnostics). `grep -rn "console\." packages/*/mod.ts packages/*/src` returns 0 after slices 3–5. | +| F-15 Re-export-upstream lint | yes | No `export * from 'npm:...'` or `jsr:...` except `@netscript/*` and `@std/*`. | +| F-16 Folder-cardinality lint | yes | ≤ 12 immediate children per directory; ≤ 4 nesting levels from `src/`. | +| F-14 Console-log lint | yes | `grep -r "console\." packages/runtime-config/src/` = 0 hits after slice 5. Proving gate for L5. | +| F-17 Abstract-derived co-location | yes | `PENDING_SCRIPT` — no violation (all three are type-only + factory surfaces). | +| F-17 Abstract-derived co-location | yes | No abstract/derived class pairs in any of the three Small Contracts (type-only + factory surfaces). Manual review; `PENDING_SCRIPT` with no detected violation. | +| F-18 Sub-barrel lint | yes | `config/src/domain/mod.ts` justified with `arch:barrel-ok`. | +| Static gates | yes | `deno check` passes; `deno lint` passes; `deno fmt --check` passes. | +| Consumer import validation | yes | `deno check` on CLI + plugins after changes. | + +## Arch-Debt Implications + +| Entry | Action | Notes | +|-------|--------|-------| +| `packages/runtime-config` — doctrine verdict Refactor | **close** | Splitting mod.ts resolves the single-file concern. | +| `packages/config` — AP-16 (`helpers.ts`) | **close** | Rename to `src/domain/saga-inputs.ts` resolves. | +| `packages/contracts` — AP-16 (`helpers/`) | **close** | Rename to `src/application/` files resolves. | +| `packages/contracts/crud/` at root (not under `src/`) | **create** | Subpath export concern at root is inconsistent with `src/` layout. Accept for now; unify in a future wave when subpath structure is revisited across all packages. | +| `packages/config/src/domain/mod.ts` — sub-barrel | **create** | Justified barrel with `arch:barrel-ok` comment. Close when `generate-reference.ts` can crawl individual schema files. | + +## Validation Plan + +| Order | Gate | Command or check | Expected result | +|-------|------|-----------------|-----------------| +| 1 | F-6 | `cd packages/ && deno publish --dry-run --allow-dirty` | Success, 0 slow types | +| 2 | F-5 / F-7 | `cd packages/ && deno doc --lint mod.ts` | 0 errors | +| 3 | F-5 / F-7 | `cd packages/ && deno doc --lint ` | 0 errors per subpath | +| 4 | F-7 | `wc -l README.md` | ≥ 150 | +| 5 | F-7 | `find docs -type f` | Matches STANDARDS § 7 structure | +| 6 | F-10 | `deno test --allow-all` | Passes | +| 6.5 | F-14 | `grep -rn "console\." mod.ts src/` | 0 matches (runtime-config) | +| 7 | Static | `deno check mod.ts` | 0 errors | +| 8 | Static | `deno lint` | 0 errors | +| 9 | Static | `deno fmt --check` | 0 errors | +| 10 | Consumer | `cd packages/cli && deno check` | 0 errors (after all three units) | +| 11 | F-14 | `grep -r "console\." packages/runtime-config/src/` | 0 hits | +| 12 | F-17 | Visual inspection of `src/` folders | No abstract-derived violations | + +## Dependencies + +- `@std/path` — runtime-config, config +- `@std/fs` — config +- `zod@^4.3.6` — config, contracts +- `@orpc/server@^1.13.5` — contracts (dev-only, not re-exported) + +## Drift Watch + +- If `deno doc --lint` adds new error categories, log in `drift.md`. +- If downstream packages fail `deno check` after file moves, log in `drift.md` as `significant`. +- If any file exceeds 500 LOC during slicing, log as AP-1 finding. diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/research.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/research.md new file mode 100644 index 000000000..f016ade11 --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/research.md @@ -0,0 +1,132 @@ +# Research — feat-package-quality-wave1-contracts--contracts + +> **Re-baselined against `feat/package-quality` @ 76fbeb7.** +> The canonical audit under `.llm/tmp/run/copilot-evaluate-every-package-jsr-release--package-jsr-alpha-release/` +> predates the plugin-platform merge; all counts below are re-derived from the current tree. + +## Re-baseline + +- Carried-in canonical audit: `plan_runtime-config.md`, `plan_config.md`, `plan_contracts.md`. +- What changed vs the carried-in version: + - `runtime-config`: stale audit claimed 1 slow type; **actual dry-run = 0 slow types**. + - `config`: stale audit claimed 35 slow types; **actual dry-run = 0 slow types** (workspace `isolatedDeclarations` already enforced). + - `contracts`: stale audit claimed 30 slow types; **actual dry-run = 0 slow types**. + - All three packages now have `version: 0.0.1-alpha.0` and scoped names. + - `config` README exists (was flagged missing in stale audit); `runtime-config` README still missing. + +## Findings + +### `@netscript/config` + +| # | Finding | How to verify | +|---|---------|---------------| +| 1 | `deno publish --dry-run` **SUCCESS** — 0 slow types, 0 portability errors. | `cd packages/config && deno publish --dry-run --allow-dirty` | +| 2 | 1 `unanalyzable-dynamic-import` warning on `loader.ts:93` (non-blocking). | same dry-run output | +| 3 | `deno doc --lint mod.ts` = **33 errors**: 1 `private-type-ref` (`SagaGroupInput` in `helpers.ts:57`) + 32 `missing-jsdoc` on `types.ts` interface properties. | `deno doc --lint mod.ts` | +| 4 | `deno doc --lint src/merge/mod.ts` = **multiple errors**: `private-type-ref` (`NetScriptConfig`, `DatabaseEntry`, `ServiceContributionEntry`, `AppContributionEntry`) + `missing-jsdoc` on `PartialConfig` properties. | `deno doc --lint src/merge/mod.ts` | +| 5 | `deno doc --lint src/schema/plugins/mod.ts` = **many `missing-jsdoc`** + `private-type-ref` on `z.ZodType` (Zod internals leak through public schema constants). | `deno doc --lint src/schema/plugins/mod.ts` | +| 6 | README = **255 LOC** ✓ (≥ 150). Covers overview, quickstart, API glance, install, license. Missing some STANDARDS § 6 sections (mental model, common recipes, observability, stability, compatibility, contributing). | `wc -l README.md` | +| 7 | `/docs` partially present: `README.md`, `architecture.md`, `concepts.md`, `getting-started.md`. **Missing**: `recipes/`, `reference/` (beyond stub), `advanced/`. | `find docs -type f` | +| 8 | `helpers.ts` (68 LOC) at package root — **generic folder name** (AP-16). Contains `SagaDefinitionInput`, `SagaGroupInput`, `SagasConfigInput`, `defineSagas`. This is domain vocabulary, not generic helpers. | `cat helpers.ts` | +| 9 | `types.ts` = 500 LOC — at F-1 boundary but acceptable for a type-only file with 1:1 field JSDoc. | `wc -l types.ts` | +| 10 | Subpath exports: `./merge`, `./paths`, `./schema/plugins` — all resolve and are imported by CLI. | `deno.json` + `grep -r "@netscript/config/merge"` | +| 11 | Published file list = 29 files (includes root `*.ts`, `src/**/*.ts`, docs). Clean — no test files leak. | dry-run output | + +### `@netscript/contracts` + +| # | Finding | How to verify | +|---|---------|---------------| +| 12 | `deno publish --dry-run` **SUCCESS** — 0 slow types, 0 portability errors. | `cd packages/contracts && deno publish --dry-run --allow-dirty` | +| 13 | `deno doc --lint mod.ts` = **1 error** (checked 1 file — root mod.ts is just `export * from './src/public/mod.ts'`). | `deno doc --lint mod.ts` | +| 14 | `deno doc --lint crud.ts query.ts transform.ts` = **21 errors**: all `private-type-ref` — `ContractSchema`, `ContractObjectSchema`, `BaseContractProcedure` are referenced in public signatures but not exported from the subpaths that use them. | `deno doc --lint crud.ts query.ts transform.ts` | +| 15 | README = **424 LOC** ✓. Strong overview, quickstart, subpath explanation. Missing some STANDARDS § 6 sections. | `wc -l README.md` | +| 16 | `/docs` partially present: `README.md`, `architecture.md`, `concepts.md`, `recipes/paginated-contract.md`, `reference/README.md`. **Missing**: `getting-started.md`, `advanced/`. | `find docs -type f` | +| 17 | `helpers/` folder — **generic name** (AP-16). Contains `paginated-query.ts` (264 LOC) and `transform.ts` (182 LOC). These are application-layer concerns, not generic helpers. | `ls helpers/` | +| 18 | `crud/` folder at root (not under `src/`) — contains `create-crud-contract.ts` (7.77 KB). Acceptable for a subpath concern but inconsistent with `src/` layout. | `ls crud/` | +| 19 | Subpath exports: `./crud`, `./query`, `./transform` — all resolve. Consumed by `plugins/sagas`, `plugins/workers`. | `deno.json` + grep | +| 20 | Published file list = 20 files. Clean. | dry-run output | + +### `@netscript/runtime-config` + +| # | Finding | How to verify | +|---|---------|---------------| +| 21 | `deno publish --dry-run` **SUCCESS** — 0 slow types, 0 portability errors. | `cd packages/runtime-config && deno publish --dry-run --allow-dirty` | +| 22 | `deno doc --lint mod.ts` = **34 errors**: all `missing-jsdoc` on interface properties (`JobOverride`, `SagaOverride`, `TriggerOverride`, `FeatureFlag`, `RuntimeTask`, `RuntimeConfig`). | `deno doc --lint mod.ts` | +| 23 | **README MISSING** — 0 LOC. | `ls README.md` fails | +| 24 | **`/docs` MISSING entirely**. | `ls docs/` fails | +| 25 | `mod.ts` = **415 LOC** — single file mixing domain types, application logic (loader), presentation (console.log in watcher/summary), and side effects (`Deno.watchFs`, `setTimeout`). F-1 file-size gate: 415 < 500, so not a hard violation, but the **doctrine verdict is "Refactor — Split single-file mod.ts"**. | `wc -l mod.ts` + doctrine/10-codebase-verdict-and-handoff.md | +| 26 | **No tests** — `tests/` directory does not exist. | `ls tests/` fails | +| 27 | **No `deno.json` tasks** — missing `check`, `test`, `lint`, `fmt`, `publish:dry-run`. | `cat deno.json` | +| 28 | **No `description` field** in `deno.json`. | `cat deno.json` | +| 29 | `console.log` / `console.warn` / `console.error` used in `watchRuntimeConfig` and `logRuntimeConfigSummary` — AP-13 violation (console in non-presentation code). | `grep -n "console\." mod.ts` | +| 30 | Published file list = 2 files (`deno.json`, `mod.ts`). Minimal but correct. | dry-run output | + +## jsr-audit surface scan (Plan-Gate input) + +### `@netscript/config` + +| Check | Status | Notes | +|-------|--------|-------| +| Required metadata | ✓ | name, version, exports, license present | +| Scoped package name | ✓ | `@netscript/config` | +| Package description | ✓ | ≤ 250 chars | +| Valid exports | ✓ | 4 entrypoints, all exist | +| No slow types | ✓ | 0 slow types | +| Clean file list | ✓ | No test files, no IDE configs | +| ESM only | ✓ | No CJS | +| Module documentation | ⚠ | `@module` present on root, missing on some subpaths | +| Symbol documentation | ✗ | 33+ `missing-jsdoc` errors | + +**Surface risks:** +- `private-type-ref` on `SagaGroupInput` blocks clean `deno doc --lint`. +- Zod internal type (`z.ZodType`) leaks through public schema exports in `schema/plugins` — this is a slow-type risk if Zod changes internals. +- Dynamic import in `loader.ts` produces publish warning (non-blocking but noisy). + +### `@netscript/contracts` + +| Check | Status | Notes | +|-------|--------|-------| +| Required metadata | ✓ | name, version, exports, license present | +| Scoped package name | ✓ | `@netscript/contracts` | +| Package description | ✓ | ≤ 250 chars | +| Valid exports | ✓ | 4 entrypoints, all exist | +| No slow types | ✓ | 0 slow types | +| Clean file list | ✓ | No test files leak | +| ESM only | ✓ | No CJS | +| Module documentation | ✓ | `@module` on all entrypoints | +| Symbol documentation | ✗ | 21 `private-type-ref` errors on subpaths | + +**Surface risks:** +- `private-type-ref` is the dominant issue: `ContractSchema`, `ContractObjectSchema`, `BaseContractProcedure` are used in public function signatures but not re-exported from the subpaths that consume them. Consumers cannot name these types. +- `helpers/` folder name is AP-16 (generic folder vocab). + +### `@netscript/runtime-config` + +| Check | Status | Notes | +|-------|--------|-------| +| Required metadata | ⚠ | name, version present; **description missing** | +| Scoped package name | ✓ | `@netscript/runtime-config` | +| Package description | ✗ | Missing | +| Valid exports | ✓ | 1 entrypoint exists | +| No slow types | ✓ | 0 slow types | +| Clean file list | ✓ | Minimal (2 files) | +| ESM only | ✓ | No CJS | +| Module documentation | ✓ | `@module` present | +| Symbol documentation | ✗ | 34 `missing-jsdoc` errors | + +**Surface risks:** +- Single 415-line file is a maintenance liability; no separation of domain/application/presentation. +- Console usage in watcher violates AP-13. +- Missing README and docs entirely — JSR doc score will be severely impacted. +- No tests means no fitness function protecting the contract. + +## Open questions (all closed by this research) + +| Question | Answer | Evidence | +|----------|--------|----------| +| `runtime-config`: does ~13.4 KB `mod.ts` exceed F-1? | **No** — 415 LOC, under 500 cap. But doctrine verdict still says "Refactor — split single-file mod.ts" because it mixes concerns. | `wc -l mod.ts` + doctrine/10 | +| `config/helpers.ts` and `contracts/helpers/`: rename or debt? | **Rename now** — both are small moves with no downstream breakage. `config/helpers.ts` → `src/domain/saga-inputs.ts`. `contracts/helpers/` → fold into `src/application/` by role. | grep shows imports are intra-package only | +| STANDARDS § 7 `/docs` target for each unit? | **All three need expansion**. `config` needs recipes/ + advanced/. `contracts` needs getting-started.md + advanced/. `runtime-config` needs full docs from scratch. | `find docs -type f` per package | +| Real slow-type counts? | **All three = 0 slow types**. The stale audit counts (35, 30, 1) are obsolete. | `deno publish --dry-run` per package | +| Archetype for config/runtime-config — pulled toward Integration? | **No — both stay Archetype 1**. `config` uses `@std/fs`/`@std/path` for loading, but the package value is still "clarity of types." `runtime-config` uses `@std/path` and `Deno.*` for file watching, but the surface is still a small contract (load + accessors). Neither has ports/adapters worth naming. | doctrine/06-archetypes.md § "How to choose" | +| `contracts` archetype — 1 or 4? | **Archetype 1 for this wave**. Doctrine table says 4 (DSL/Builder), but the package has no NetScript-defined fluent builder. It exports schema factories and type aliases. The DSL is oRPC's, not ours. If a builder API is added later, archetype escalates to 4. | `cat mod.ts` + `cat src/public/mod.ts` | diff --git a/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/worklog.md b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/worklog.md new file mode 100644 index 000000000..4865ad40d --- /dev/null +++ b/.llm/tmp/run/feat-package-quality-wave1-contracts--contracts/worklog.md @@ -0,0 +1,382 @@ +# Worklog: Wave 1 — Contracts & schemas + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------ | +| Run ID | `feat-package-quality-wave1-contracts--contracts` | +| Branch | `feat/package-quality-wave1-contracts` | +| Archetype | 1 — Small Contract | +| Scope overlays | none | + +## Design + +### Public Surface + +#### `@netscript/config` + +Root (`mod.ts`): +- `defineConfig(config)` — authoring entrypoint +- `defineConfigAsync(factory)` — async authoring entrypoint +- `loadConfig(options?)` — runtime loader +- `initConfig()` — cached init +- `getConfig()` — cached accessor +- `isConfigLoaded()`, `clearConfigCache()` — cache management +- `getEnv(name)`, `hasEnv(name)`, `resolveEnv(def)`, `getMode()`, `isDev()`, `isProd()`, `isTest()` — environment helpers +- `discoverWorkspace()`, `findWorkspaceRoot()`, `findMember()`, `getMemberEntrypoint()` — workspace discovery +- `inspectConfig(config)` — diagnostics +- `defineSagas(config)` — saga config authoring (from renamed `saga-inputs.ts`) +- Types: `NetScriptConfig`, `NetScriptConfigInput`, `AppConfig`, `AspireConfig`, `DatabaseConfig`, `DatabasesConfig`, `DeployConfig`, `EnvDef`, `GatewayConfig`, `LoadConfigOptions`, `LoggingConfig`, `PathsConfig`, `PermissionConfig`, `PermissionValue`, `ResolvedEnvType`, `RuntimeConfigPathEntry`, `RuntimeConfigSection`, `SagaDefinition`, `SagaGroup`, `SagasConfig`, `SagaRetentionConfig`, `SagaRetryConfig`, `SagaScalingConfig`, `SagaStoreProvider`, `SagaTimeoutConfig`, `SagaTransportProvider`, `SdkConfig`, `ServiceConfig`, `TriggerDefinitionConfig`, `TriggerGroup`, `TriggerRetentionConfig`, `TriggerScalingConfig`, `TriggersConfig`, `WebhookConfig`, `WindowsDeployConfig`, `WorkspaceMap`, `WorkspaceMember`, `WorkspaceMemberType`, `SagaDefinitionInput`, `SagaGroupInput`, `SagasConfigInput`, `InspectionReport` + +Subpaths: +- `./merge` — `mergePartialConfig()`, `PartialConfig` +- `./paths` — `SCAFFOLD_DIRS`, `SCAFFOLD_FILES`, `PERMISSIONS` +- `./schema/plugins` — `installedVersionSchema`, `pluginEntrySchema`, `backgroundProcessorSchema`, `serviceSchema`, and inferred types + +#### `@netscript/contracts` + +Root (`mod.ts`): +- `baseContract` — oRPC base contract primitive +- `boundedString()`, `nonNegativeInt()`, `nonNegativeNumber()`, `paginationLimit()`, `paginationOffset()`, `positiveInt()`, `positiveNumber()`, `stringToInt()`, `stringToNumber()` — Zod helper factories +- `COMMON_ERROR_CODES`, `DEFAULT_INTEGER_MAX`, `DEFAULT_PAGINATION_LIMIT`, `DEFAULT_PAGINATION_LIMIT_MAX`, `DEFAULT_PAGINATION_OFFSET` — constants +- `getResourceType()`, `notFound()` — error helpers +- `SuccessSchema`, `ForbiddenErrorSchema`, `NotFoundErrorSchema`, `RateLimitErrorSchema`, `ServiceUnavailableErrorSchema`, `UnauthorizedErrorSchema`, `ValidationErrorSchema`, `OffsetPaginationInputSchema`, `OffsetPaginationMetaSchema`, `OffsetPaginationQuerySchema`, `CursorPaginationInputSchema`, `CursorPaginationMetaSchema`, `CursorPaginationQuerySchema` — schemas +- `inspectContracts()` — diagnostics +- Types: `BaseContract`, `BaseContractOutputBuilder`, `BaseContractProcedure`, `BaseContractRouteBuilder`, `BaseContractRouteOptions`, `BoundedStringSchemaOptions`, `DefaultedIntegerSchemaOptions`, `IntegerSchemaOptions`, `StringSchemaOptions`, `ContractDefaultableSchema`, `ContractNumberSchema`, `ContractObjectSchema`, `ContractParseResult`, `ContractSchema`, `ContractStringSchema`, `CursorPaginationInput`, `CursorPaginationMeta`, `CursorPaginationQuery`, `ErrorResult`, `ForbiddenError`, `NotFoundError`, `NotFoundOptions`, `OffsetPaginationInput`, `OffsetPaginationMeta`, `OffsetPaginationQuery`, `OkResult`, `RateLimitError`, `Result`, `ServiceUnavailableError`, `SuccessResponse`, `UnauthorizedError`, `ValidationError`, `ContractsInspectionTarget`, `InspectionReport`, `InspectionStatus` + +Subpaths: +- `./crud` — `createCrudContract()`, `createReadOnlyContract()`, `createListOnlyContract()`, `CrudContractOptions`, `CrudContractOperation` +- `./query` — `paginatedQuery()`, `FilterOperatorSchema`, `FilterConditionSchema`, `FiltersSchema`, `SearchInputSchema`, `PaginationInputSchema`, `OffsetPaginationInputSchema`, `CursorPaginationInputSchema`, `PaginationOutputSchema`, `CursorPaginationOutputSchema`, `createPaginatedOutput()`, `createCursorPaginatedOutput()` +- `./transform` — `createTransformer()`, `TransformFn` + +#### `@netscript/runtime-config` + +Root (`mod.ts`): +- `loadRuntimeConfig()` — load overrides from disk +- `isFeatureEnabled(config, flagId, defaultValue?)` — feature flag check +- `getJobOverride(config, jobId)` — job override accessor +- `getSagaOverride(config, sagaId)` — saga override accessor +- `getTriggerOverride(config, triggerId)` — trigger override accessor +- `getRuntimeTask(config, taskId)` — task definition accessor +- `watchRuntimeConfig(onChange, options?)` — hot-reload watcher +- `summarizeRuntimeConfig(config, prefix?)` — structured summary (replaces console-based `logRuntimeConfigSummary`) +- Types: `JobOverride`, `SagaOverride`, `TriggerOverride`, `FeatureFlag`, `RuntimeTask`, `RuntimeConfig`, `RuntimeConfigSummary` + +### Domain Vocabulary + +- `NetScriptConfig` — fully validated project configuration +- `NetScriptConfigInput` — authoring form accepted by `defineConfig` +- `PartialConfig` — plugin contribution fragment +- `RuntimeConfig` — hot-reloadable runtime overrides (jobs, sagas, triggers, features, tasks) +- `JobOverride` / `SagaOverride` / `TriggerOverride` — per-ID runtime override shapes +- `FeatureFlag` — boolean + rollout percentage flag +- `RuntimeTask` — task definition with runtime and entrypoint +- `ContractSchema` / `ContractObjectSchema` — branded Zod schema types +- `BaseContractProcedure` — oRPC procedure shape +- `Result` — discriminated union for expected failures +- `CrudContractOperation` — named CRUD operation type + +### Ports + +None for this wave. All three packages are Small Contract; they do not own ports/adapters. + +`runtime-config` uses `Deno.readTextFile`, `Deno.watchFs`, and `Deno.env.get` at the file-system edge. These are platform primitives, not ports. If a second backend (e.g. Redis-backed runtime config) is added later, a `RuntimeConfigStorePort` would be introduced. + +### Constants + +- `RUNTIME_CONFIG_TOPICS` — `['jobs', 'sagas', 'triggers', 'features', 'tasks']` as const array +- `DEFAULT_DEBOUNCE_MS` — `300` (watcher debounce) +- `POINTER_FILE_NAME` — `'current'` +- `COMMON_ERROR_CODES` — from contracts domain +- `DEFAULT_PAGINATION_LIMIT` / `DEFAULT_PAGINATION_LIMIT_MAX` / `DEFAULT_PAGINATION_OFFSET` — from contracts domain + +### Commit Slices + +Ordered by dependency (runtime-config first — it has no downstream consumers in this wave; then config; then contracts). Each slice names what it proves, the gate that proves it, and the files it touches. + +#### `@netscript/runtime-config` (slices 1–10) + +| # | Slice | Gate | Files | +|---|-------|------|-------| +| 1 | Scaffold `src/domain/types.ts` with JSDoc | `deno doc --lint src/domain/types.ts` | `src/domain/types.ts` (new) | +| 2 | Scaffold `src/application/loader.ts` (extract from mod.ts) | `deno check src/application/loader.ts` | `src/application/loader.ts` (new) | +| 3 | Scaffold `src/application/watcher.ts` (extract watch logic, remove console) | `deno check src/application/watcher.ts` | `src/application/watcher.ts` (new) | +| 4 | Scaffold `src/diagnostics/summary.ts` (replace console-based summary) | `deno check src/diagnostics/summary.ts` | `src/diagnostics/summary.ts` (new) | +| 5 | Rewrite root `mod.ts` as thin barrel | `deno doc --lint mod.ts` | `mod.ts` (rewrite) | +| 6 | Add `deno.json` standard tasks + description + publish config | `deno publish --dry-run` | `deno.json` (update) | +| 7 | Write README.md (≥ 150 LOC) | `wc -l README.md` | `README.md` (new) | +| 8 | Scaffold `/docs` per STANDARDS § 7 | `find docs -type f` | `docs/README.md`, `docs/architecture.md`, `docs/concepts.md`, `docs/getting-started.md`, `docs/recipes/basic-usage.md`, `docs/recipes/testing.md`, `docs/advanced/extending.md` (new) | +| 9 | Add tests for loader, accessors, summary | `deno test --allow-all` | `tests/loader_test.ts`, `tests/accessors_test.ts`, `tests/summary_test.ts` (new) | +| 10 | Runtime-config gate sweep | All F-* gates | Run full validation plan | + +#### `@netscript/config` (slices 11–18) + +| # | Slice | Gate | Files | +|---|-------|------|-------| +| 11 | Rename `helpers.ts` → `src/domain/saga-inputs.ts`, update imports | `deno check mod.ts` | `helpers.ts` → `src/domain/saga-inputs.ts`, `src/public/mod.ts`, `mod.ts` | +| 12 | Fix `private-type-ref` on `SagaGroupInput` | `deno doc --lint mod.ts` | `src/domain/saga-inputs.ts`, `src/public/mod.ts` | +| 13 | Add JSDoc to `types.ts` interface properties | `deno doc --lint mod.ts` | `types.ts` | +| 14 | Fix `private-type-ref` in `src/merge/mod.ts` | `deno doc --lint src/merge/mod.ts` | `src/merge/mod.ts` | +| 15 | Fix `missing-jsdoc` in `src/schema/plugins/mod.ts` + Zod internal leak | `deno doc --lint src/schema/plugins/mod.ts` | `src/schema/plugins/mod.ts` | +| 16 | Expand `/docs` with `recipes/` and `advanced/` | `find docs -type f` | `docs/recipes/merge-config.md`, `docs/recipes/plugin-schemas.md`, `docs/advanced/extending.md` (new) | +| 17 | Add `arch:barrel-ok` to `src/domain/mod.ts` | `grep arch:barrel-ok src/domain/mod.ts` | `src/domain/mod.ts` | +| 18 | Config gate sweep | All F-* gates | Run full validation plan | + +#### `@netscript/contracts` (slices 19–24) + +| # | Slice | Gate | Files | +|---|-------|------|-------| +| 19 | Move `helpers/paginated-query.ts` → `src/application/paginated-query.ts` | `deno check mod.ts` | `helpers/paginated-query.ts` → `src/application/paginated-query.ts`, `query.ts`, `src/public/mod.ts` | +| 20 | Move `helpers/transform.ts` → `src/application/transform-helpers.ts` | `deno check mod.ts` | `helpers/transform.ts` → `src/application/transform-helpers.ts`, `transform.ts`, `src/public/mod.ts` | +| 21 | Export `ContractSchema`, `ContractObjectSchema`, `BaseContractProcedure` from public surface | `deno doc --lint crud.ts query.ts transform.ts` | `src/public/mod.ts`, `src/application/contract-primitives.ts` | +| 22 | Add `getting-started.md` to `/docs` | `find docs -type f` | `docs/getting-started.md` (new) | +| 23 | Add `advanced/extending.md` to `/docs` | `find docs -type f` | `docs/advanced/extending.md` (new) | +| 24 | Contracts gate sweep | All F-* gates | Run full validation plan | + +#### Cross-cutting (slices 25–27) + +| # | Slice | Gate | Files | +|---|-------|------|-------| +| 25 | Consumer validation: `deno check` on CLI | `cd packages/cli && deno check` | No file changes; validation only | +| 26 | Consumer validation: `deno check` on plugins | `cd plugins/sagas && deno check` + `cd plugins/workers && deno check` | No file changes; validation only | +| 27 | Final workspace gate: `deno publish --dry-run` all three | Per-package dry-run | Validation only | + +**Total slices: 27** (< 30 ✓) + +### Deferred Scope + +- `./testing` subpath for any package — no consumer needs it today. +- `runtime-config` watcher integration test with real `Deno.watchFs` — too flaky for CI; unit-test the loader logic only. +- `contracts` `./builders` subpath — only if a NetScript-defined fluent builder is added later. +- Auto-generated `reference/` pages — future `.llm/tools/generate-reference.ts` handles this. +- `config` dynamic import rewrite — the `unanalyzable-dynamic-import` warning is non-blocking and would require a significant loader refactor. + +### Contributor Path + +A new developer adding a runtime config topic (e.g. `webhooks`): + +1. Open `packages/runtime-config/src/domain/types.ts` — add `WebhookOverride` interface. +2. Open `packages/runtime-config/src/application/loader.ts` — add the topic file read in `loadRuntimeConfig`. +3. Open `packages/runtime-config/mod.ts` — export the new type and accessor. +4. Add test in `tests/loader_test.ts` — assert the new topic loads. +5. Run `deno test --allow-all` and `deno doc --lint mod.ts`. + +A new developer adding a config schema section: + +1. Open `packages/config/src/domain/` — create `
-schema.ts` with Zod schema. +2. Open `packages/config/src/domain/mod.ts` — re-export the new schema. +3. Open `packages/config/types.ts` — add the corresponding TypeScript interface. +4. Open `packages/config/src/public/mod.ts` — export the schema and type. +5. Run `deno doc --lint mod.ts` and `deno test --allow-all`. + +## Progress Log + +| Time | Slice | Step | Notes | +|------|-------|------|-------| +| — | — | Research | Re-baselined all three units against current tree. Real slow-type count = 0 for all. | +| — | — | Plan & Design | Locked 8 decisions, 27 commit slices, archetype = 1 for all three. | +| — | — | PLAN-EVAL | `PASS` (adjusted). Evaluator added F-14 and F-17 to the gate set. | +| 2026-06-06 | 1 | Implement | Added `packages/runtime-config/src/domain/types.ts` with documented runtime override types and finite runtime/topic constants. | +| 2026-06-06 | 1 | Gate | `deno doc --lint src/domain/types.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 2 | Implement | Added `packages/runtime-config/src/application/loader.ts` with the runtime directory resolver, pointer parsing, config loader, and accessor functions extracted from `mod.ts`. | +| 2026-06-06 | 2 | Gate | `deno check src/application/loader.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 3 | Implement | Added `packages/runtime-config/src/application/watcher.ts` with debounced reloads and no package-level console emission. | +| 2026-06-06 | 3 | Gate | `deno check src/application/watcher.ts` passed; `Get-ChildItem src -Recurse -File \| Select-String -Pattern 'console\\.'` returned 0 matches. | +| 2026-06-06 | 4 | Implement | Added `packages/runtime-config/src/diagnostics/summary.ts` with `summarizeRuntimeConfig()` and typed structured summary data. | +| 2026-06-06 | 4 | Gate | `deno check src/diagnostics/summary.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 5 | Implement | Rewrote `packages/runtime-config/mod.ts` as a documented thin barrel over domain, application, and diagnostics modules. | +| 2026-06-06 | 5 | Gate | `deno doc --lint mod.ts` passed; `Get-ChildItem src -Recurse -File \| Select-String -Pattern 'console\\.'` returned 0 matches. | +| 2026-06-06 | 6 | Implement | Added runtime-config package description, license, standard local tasks, and publish include/exclude config to `deno.json`. | +| 2026-06-06 | 6 | Gate | `deno publish --dry-run --allow-dirty` passed with 0 slow-type errors and published `deno.json`, `mod.ts`, and `src/**/*.ts`. | +| 2026-06-06 | 7 | Implement | Added `packages/runtime-config/README.md` covering install, mental model, file layout, APIs, permissions, examples, diagnostics, and stability. | +| 2026-06-06 | 7 | Gate | `(Get-Content README.md).Count` returned 346 lines, satisfying the README >= 150 LOC gate. | +| 2026-06-06 | 8 | Implement | Added runtime-config docs pages for overview, architecture, concepts, getting started, basic usage, testing, reference index, and extending. | +| 2026-06-06 | 8 | Gate | `Get-ChildItem docs -Recurse -File` listed 8 docs files under README, architecture, concepts, getting-started, recipes, reference, and advanced. | +| 2026-06-06 | 9 | Implement | Added runtime-config tests for loader defaults/topic loading/plain pointers, accessors, feature flag fallback, and structured summaries. | +| 2026-06-06 | 9 | Gate | `deno test --allow-all` passed: 8 tests, 0 failed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 10 | Implement | Ran runtime-config gate sweep and fixed lint/format findings from the new tests/docs plus removed `console.` from the root module example. | +| 2026-06-06 | 10 | Gate | Runtime-config sweep passed: `deno check`, `deno doc --lint`, `deno publish --dry-run --allow-dirty`, `deno test --allow-all`, `deno lint`, `deno fmt --check`, README/docs gates, and manual F-1/F-8/F-11/F-12/F-14/F-15/F-16/F-17/F-18 scans. | +| 2026-06-06 | 11 | Implement | Renamed `packages/config/helpers.ts` to `src/domain/saga-inputs.ts` and updated `src/public/mod.ts` exports without changing the root API. | +| 2026-06-06 | 11 | Gate | `deno check mod.ts` passed; helper-name scans found no remaining `helpers.ts` file or `src/helpers` directory in `packages/config`. | +| 2026-06-06 | 12 | Implement | Exported `SagaGroupInput` from `packages/config/src/public/mod.ts` and the root `mod.ts`, fixing the private-type-ref path for saga group inputs. | +| 2026-06-06 | 12 | Gate | `deno doc --lint mod.ts` initially cleared the private-type-ref but remained blocked by the known slice 13 `types.ts` JSDoc errors; see `drift.md`. | +| 2026-06-06 | 13 | Implement | Added JSDoc to the remaining exported `types.ts` interface properties in `SdkConfig`, `NetScriptConfig`, and `NetScriptConfigInput`. | +| 2026-06-06 | 13 | Gate | `deno doc --lint mod.ts` passed after slices 12 and 13 were both present; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 14 | Implement | Exported merge subpath contribution entry types, `NetScriptConfig`, and the referenced config type family; documented `PartialConfig` properties. | +| 2026-06-06 | 14 | Gate | `deno doc --lint src/merge/mod.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 15 | Implement | Added JSDoc to plugin schema interfaces and replaced public `z.ZodType` annotations with a local `PluginSettingsSchema` parse/safeParse contract. | +| 2026-06-06 | 15 | Gate | `deno doc --lint src/schema/plugins/mod.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 16 | Implement | Added config docs recipes for merge contributions and plugin schemas plus `docs/advanced/extending.md`. | +| 2026-06-06 | 16 | Gate | `Get-ChildItem docs -Recurse -File` listed 7 config docs files including recipes and advanced pages. | +| 2026-06-06 | 17 | Implement | Added `arch:barrel-ok` justification to `packages/config/src/domain/mod.ts` and recorded accepted sub-barrel debt. | +| 2026-06-06 | 17 | Gate | `Select-String -Path src/domain/mod.ts -Pattern 'arch:barrel-ok'` found the marker. | +| 2026-06-06 | 18 | Implement | Ran the config gate sweep and fixed sweep findings: stale workspace-member test expectations, unversioned test imports, `console.` examples, explicit public `z.ZodType` annotations, `types.ts` LOC, and `src/domain` folder cardinality by splitting config types and grouping schemas under `src/domain/schemas/`. | +| 2026-06-06 | 18 | Gate | Config sweep passed: `deno check`, `deno doc --lint`, `deno publish --dry-run --allow-dirty`, `deno test --allow-all`, `deno lint`, `deno fmt --check`, README/docs gates, and manual F-1/F-11/F-12/F-14/F-15/F-16/F-17/F-18 scans. Publish dry run reported the known non-failing `unanalyzable-dynamic-import` warning for `loader.ts`. | +| 2026-06-06 | 19 | Implement | Moved `packages/contracts/helpers/paginated-query.ts` to `src/application/paginated-query.ts` and updated `query.ts` to re-export the new role-based module. | +| 2026-06-06 | 19 | Gate | `deno check mod.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 20 | Implement | Moved `packages/contracts/helpers/transform.ts` to `src/application/transform-helpers.ts`, updated `transform.ts`, and removed the empty `helpers/` directory. | +| 2026-06-06 | 20 | Gate | `deno check mod.ts` passed; extra `deno check transform.ts` passed for the moved subpath; helper-directory scan found no `helpers/` directory under `packages/contracts`. | +| 2026-06-06 | 21 | Implement | Exported `ContractSchema`, `ContractObjectSchema`, `BaseContractProcedure`, and transitive `ContractParseResult` from the `./crud` and/or `./query` subpath barrels so public signatures have complete docs. | +| 2026-06-06 | 21 | Gate | `deno doc --lint crud.ts query.ts transform.ts` passed; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 22 | Implement | Added `packages/contracts/docs/getting-started.md` and linked it from the contracts docs index. | +| 2026-06-06 | 22 | Gate | `Get-ChildItem docs -Recurse -File` listed 6 contracts docs files including `docs/getting-started.md`. | +| 2026-06-06 | 23 | Implement | Added `packages/contracts/docs/advanced/extending.md` and linked it from the contracts docs index. | +| 2026-06-06 | 23 | Gate | `Get-ChildItem docs -Recurse -File` listed 7 contracts docs files including `docs/advanced/extending.md`. | +| 2026-06-06 | 24 | Implement | Ran the contracts gate sweep, removed a stale lint ignore, normalized package formatting, removed the stale `helpers/**/*.ts` publish include, marked `src/public/mod.ts` with `arch:barrel-ok`, and updated architecture debt for the closed helpers directory plus accepted root `crud/` layout. | +| 2026-06-06 | 24 | Gate | Contracts sweep passed: `deno check`, `deno doc --lint`, `deno publish --dry-run --allow-dirty`, `deno test --allow-all`, `deno lint`, `deno fmt --check`, README/docs gates, and manual F-1/F-11/F-12/F-14/F-15/F-16/F-17/F-18 scans. | +| 2026-06-06 | 25 | Gate | `cd packages/cli && deno check` passed with 0 errors; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 26 | Gate | `cd plugins/sagas && deno check` and `cd plugins/workers && deno check` both passed with 0 errors; Deno reported unrelated workspace-config warnings from `examples/playground/deno.json`. | +| 2026-06-06 | 27 | Gate | Final publish dry run passed for `packages/runtime-config`, `packages/config`, and `packages/contracts`: all three completed `deno publish --dry-run --allow-dirty` with 0 slow-type errors. `packages/config` retained the known non-failing `unanalyzable-dynamic-import` warning for `loader.ts`. | + +## Decisions + +| Decision | Reason | Source | +|----------|--------|--------| +| Archetype 1 for all three | Value is type clarity, not runtime behavior | doctrine/06-archetypes.md | +| Split `runtime-config/mod.ts` | Mixes domain/application/presentation in one file | A8, doctrine/10 verdict | +| Rename `helpers.ts` / `helpers/` | Generic folder names hide real concerns | AP-16 | +| Export referenced types for `private-type-ref` | `deno doc --lint` requires public types for public signatures | F-5 | +| Remove Zod internal from public signatures | Prevents breakage on Zod version bumps | jsr-audit rubric | +| Replace console with return-value diagnostics | AP-13 violation; callers should control output | doctrine/09 | + +## Drift + +| Drift | Severity | Logged in drift.md | +|-------|----------|-------------------| +| Config slices 12-13 gate dependency | minor | yes | +| Config merge subpath type exports | minor | yes | +| Config plugin schema public annotation | minor | yes | +| Config sweep static/cardinality cleanup | minor | yes | +| Contracts subpath transitive schema type export | minor | yes | +| Contracts root `crud/` layout accepted debt | minor | arch-debt.md | + +## Gate Results + +### Static Gates + +| Gate | Command or check | Result | Notes | +|------|-----------------|--------|-------| +| `deno check` | `deno check mod.ts` | NOT_RUN | Will run per slice | +| `deno lint` | `deno lint` | NOT_RUN | Will run per slice | +| `deno fmt --check` | `deno fmt --check` | NOT_RUN | Will run per slice | + +### Fitness Gates + +| Gate | Result | Evidence | Notes | +|------|--------|----------|-------| +| F-1 File-size lint | PENDING_SCRIPT | Manual check: all files < 500 LOC | `runtime-config` split addresses this | +| F-5 Public surface audit | PENDING_SCRIPT | `deno doc --lint` per entrypoint | Will run after each slice | +| F-6 JSR publishability | PASS | `deno publish --dry-run` = 0 slow types | Already true for all three | +| F-7 Doc-score gate | PENDING_SCRIPT | README + docs + JSDoc | In progress | +| F-10 Test-shape audit | PENDING_SCRIPT | Tests exist / will be added | `runtime-config` needs tests | +| F-11 Forbidden-folder lint | PENDING_SCRIPT | No `helpers/` after renames | Will verify after slices 11, 19, 20 | +| F-12 Naming-convention lint | PENDING_SCRIPT | Manual review | No violations detected | +| F-14 Console-log lint | PENDING_SCRIPT | `grep -rn "console\." mod.ts src/` after slices 3–5 | runtime-config console usage removed by L5 | +| F-15 Re-export-upstream lint | PENDING_SCRIPT | No upstream re-exports | Will verify | +| F-16 Folder-cardinality lint | PENDING_SCRIPT | ≤ 12 children per dir | Will verify | +| F-17 Abstract-derived co-location | PENDING_SCRIPT | No abstract/derived class pairs | Type-only/factory surfaces; no violation | +| F-18 Sub-barrel lint | PENDING_SCRIPT | `arch:barrel-ok` on domain mod.ts | Slice 17 | + +### Consumer Gates + +| Consumer | Result | Evidence | Notes | +|----------|--------|----------|-------| +| `packages/cli` | PASS | `deno check` | Slice 25, 0 errors | +| `plugins/sagas` | PASS | `deno check` | Slice 26, 0 errors | +| `plugins/workers` | PASS | `deno check` | Slice 26, 0 errors | + +### Runtime-config Slice 10 Sweep + +| Gate | Result | Evidence | +|------|--------|----------| +| Static check | PASS | `deno check mod.ts` | +| Lint | PASS | `deno lint` | +| Format | PASS | `deno fmt --check` | +| F-5 / F-7 doc lint | PASS | `deno doc --lint mod.ts` | +| F-6 publishability | PASS | `deno publish --dry-run --allow-dirty` = 0 slow-type errors | +| F-7 README | PASS | `(Get-Content README.md).Count` = 339 | +| F-7 docs | PASS | `Get-ChildItem docs -Recurse -File` = 8 docs files | +| F-10 tests | PASS | `deno test --allow-all` = 8 passed, 0 failed | +| F-1 file size | PASS | No `.ts` or `.md` file > 500 LOC | +| F-8 workspace lib | PASS | Root `deno.json` includes `deno.ns` and `deno.unstable` | +| F-11 forbidden folders | PASS | No `utils`, `helpers`, `common`, `lib`, or `interfaces` under `src/` | +| F-12 naming | PASS | No exported `I*`, `*_T`, or `*Impl` declarations | +| F-14 console | PASS | No `console.` in `mod.ts` or `src/` | +| F-15 upstream re-export | PASS | No upstream `npm:`, `jsr:`, `@std`, or `@orpc` re-exports | +| F-16 cardinality | PASS | No `src/` directory has more than 12 immediate children | +| F-17 abstract-derived | PASS | No abstract classes or `extends` relationships in `src/` | +| F-18 sub-barrel | PASS | No `src/**/mod.ts` sub-barrels | + +### Config Slice 18 Sweep + +| Gate | Result | Evidence | +|------|--------|----------| +| Static check | PASS | `deno check mod.ts` | +| Lint | PASS | `deno lint` | +| Format | PASS | `deno fmt --check` | +| F-5 / F-7 doc lint | PASS | `deno doc --lint mod.ts src/merge/mod.ts src/schema/plugins/mod.ts` | +| F-6 publishability | PASS | `deno publish --dry-run --allow-dirty` = 0 slow-type errors; known `unanalyzable-dynamic-import` warning only | +| F-7 README | PASS | `(Get-Content README.md).Count` = 255 | +| F-7 docs | PASS | `Get-ChildItem docs -Recurse -File` = 7 docs files | +| F-10 tests | PASS | `deno test --allow-all` = 10 passed, 0 failed | +| F-1 file size | PASS | No non-test `.ts` file > 500 LOC; `types.ts` split into section/root type modules | +| F-11 forbidden folders | PASS | No `utils`, `helpers`, `common`, `lib`, or `interfaces` under `src/` | +| F-12 naming | PASS | No exported `I*`, `*_T`, or `*Impl` declarations by case-sensitive scan | +| F-14 console/Zod public annotation scan | PASS | No `console.` or `z.ZodType` in `mod.ts` or `src/` | +| F-15 upstream re-export | PASS | No upstream `npm:`, `jsr:`, `@std`, or `@orpc` re-exports | +| F-16 cardinality | PASS | No `src/` directory has more than 12 immediate children after grouping domain schemas | +| F-17 abstract-derived | PASS | No abstract classes or class `extends` relationships in `src/` | +| F-18 sub-barrel | PASS | `src/domain/mod.ts` has `arch:barrel-ok` | + +### Contracts Slice 24 Sweep + +| Gate | Result | Evidence | +|------|--------|----------| +| Static check | PASS | `deno check mod.ts crud.ts query.ts transform.ts` | +| Lint | PASS | `deno lint` | +| Format | PASS | `deno fmt --check` | +| F-5 / F-7 doc lint | PASS | `deno doc --lint mod.ts crud.ts query.ts transform.ts` | +| F-6 publishability | PASS | `deno publish --dry-run --allow-dirty` = 0 slow-type errors | +| F-7 README | PASS | `(Get-Content README.md).Count` = 424 | +| F-7 docs | PASS | `Get-ChildItem docs -Recurse -File` = 7 docs files | +| F-10 tests | PASS | `deno test --allow-all` = 4 passed, 0 failed | +| F-1 file size | PASS | No non-test `.ts` file > 500 LOC | +| F-11 forbidden folders | PASS | No `utils`, `helpers`, `common`, `lib`, or `interfaces` directory under the package | +| F-12 naming | PASS | No exported `I*`, `*_T`, or `*Impl` declarations by case-sensitive scan | +| F-14 console | PASS | No `console.` in published TypeScript sources | +| F-15 upstream re-export | PASS | No upstream `npm:`, `jsr:`, `@std`, or `@orpc` re-exports | +| F-16 cardinality | PASS | No `src/` directory has more than 12 immediate children | +| F-17 abstract-derived | PASS | No abstract classes or class `extends` relationships in `src/`, `crud/`, or `schemas/` | +| F-18 sub-barrel | PASS | `src/public/mod.ts` has `arch:barrel-ok`; root `crud/` layout recorded as accepted debt | + +### Final Slice 27 Publish Sweep + +| Package | Result | Evidence | +|---------|--------|----------| +| `@netscript/runtime-config` | PASS | `deno publish --dry-run --allow-dirty` completed with 0 slow-type errors | +| `@netscript/config` | PASS | `deno publish --dry-run --allow-dirty` completed with 0 slow-type errors; known dynamic-import warning only | +| `@netscript/contracts` | PASS | `deno publish --dry-run --allow-dirty` completed with 0 slow-type errors | + +## Handoff Notes + +- Evaluator should inspect `runtime-config` split first — it is the largest structural change. +- Verify `deno doc --lint` is clean on ALL entrypoints (root + every subpath) before approving. +- Check that no `helpers/` folder remains after slices 11, 19, 20. +- Confirm `runtime-config` has tests before final gate. +- Implementation completed through slice 27. Hand off to a separate IMPL-EVAL session; do not use + this implementation session as the evaluator. + +## Post-review CLI E2E Fix + +Reviewer feedback found that the bare CLI E2E gate still exercised only a narrow plugin scaffold +suite. The runner now maps bare `deno task e2e:cli` to the full `scaffold.runtime` suite with +cleanup, and the documentation/tooling has been updated to make that merge-readiness contract +explicit. + +| Gate | Result | Evidence | +|------|--------|----------| +| Unit/static follow-up | PASS | `deno check --unstable-kv packages/cli/e2e/mod.ts packages/cli/e2e/cli.ts packages/telemetry/src/instrumentation/queue.ts packages/queue/adapters/redis.adapter.ts packages/kv/core/auto-detect.ts plugins/workers/bin/runtime.ts plugins/workers/worker/job-execution.ts plugins/workers/worker/worker.ts plugins/triggers/src/runtime/trigger-runtime-processor.ts` | +| Focused tests | PASS | `deno test --allow-all packages/cli/e2e/tests packages/cli/src/maintainer/features/sync/plugin/copy-official-plugin-copy_test.ts` = 21 passed | +| KV auto-detect tests | PASS | `deno test --allow-env packages/kv/tests/auto-detect_test.ts` = 2 passed | +| Full CLI E2E | PASS | `deno task e2e:cli` = `passed=41 failed=0 skipped=0` | +| Cleanup check | PASS | `docker ps` returned no containers; `aspire ps` reported no running apphost | diff --git a/.llm/tools/README.md b/.llm/tools/README.md new file mode 100644 index 000000000..fc14e4a2a --- /dev/null +++ b/.llm/tools/README.md @@ -0,0 +1,43 @@ +# .llm/tools README + +Reusable Deno utilities for MCP/Codex sessions live here. Keep one-off scripts in `.llm/tmp/`. + +## Start Here + +- Use `deno task e2e:cli` for the full CLI merge-readiness gate. +- Use `.llm/tools/scaffold-e2e-test.ts` only as a legacy comparison smoke when debugging parity with + the previous repo checkout. +- Use `.llm/tools/parse-deno-check-errors.ts` to summarize noisy generated-project type-check logs. +- Use `tools/fitness/*.ts` for doctrine and package-readiness gates; do not duplicate those scripts + under `.llm/tools`. + +## Common Commands + +```powershell +# Full CLI E2E merge gate +deno task e2e:cli + +# Explicit full suite with readable output +deno task e2e:cli run scaffold.runtime --cleanup --format pretty + +# Parse a saved deno-check log +deno run --allow-read .llm/tools/parse-deno-check-errors.ts --input .llm/tmp/check.log --pretty + +# Scan text without PowerShell quoting issues +deno run --allow-read .llm/tools/find-lines.ts --root packages/cli --contains "scaffold.runtime" +``` + +## Tool Index + +| Tool | Use | +| ---------------------------- | --------------------------------------------------------------------- | +| `find-lines.ts` | Substring or regex scans across one or more roots. | +| `find-import-patterns.ts` | Import alias and relative-path debt scans. | +| `find-symbol-usages.ts` | Symbol-boundary usage scans for refactors. | +| `list-exports.ts` | Export and re-export inventory for package surfaces. | +| `compare-export-surface.ts` | Compare actual exports against an expected symbol list. | +| `parse-deno-check-errors.ts` | Group Deno/TypeScript errors by kind, message, and path. | +| `git-commit-paths.ts` | Commit/push selected paths without Windows shell quoting issues. | +| `scaffold-e2e-test.ts` | Legacy full scaffold smoke for CLI/plugin/DB/Aspire parity debugging. | + +See `.llm/tools/entry.md` for examples and selection notes. diff --git a/.llm/tools/compare-export-surface.ts b/.llm/tools/compare-export-surface.ts new file mode 100644 index 000000000..83e7845d7 --- /dev/null +++ b/.llm/tools/compare-export-surface.ts @@ -0,0 +1,192 @@ +/** + * MCP-friendly export surface diff helper. + * + * Use this to compare the current exported symbol set against an expected contract list from an RFC, + * WI doc, or migration note. Scan a package tree, then compare against explicit expected names. + * + * Examples: + * - deno run --allow-read .llm/tools/compare-export-surface.ts --root packages/fresh --expect definePage --expect definePartial + * - deno run --allow-read .llm/tools/compare-export-surface.ts --root packages/fresh --expect-file .llm/temp/fresh-contract.txt + * - deno run --allow-read .llm/tools/compare-export-surface.ts --root packages/fresh --expect definePage --fail-on-missing + */ + +interface Options { + roots: string[]; + expected: Set; + expectFile?: string; + ignoreDirs: Set; + showActual: boolean; + failOnMissing: boolean; +} + +const DEFAULT_IGNORE_DIRS = ['.git', '_fresh', 'node_modules', 'dist', 'coverage']; +const EXPORT_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cts']); + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-read .llm/tools/compare-export-surface.ts [options]', + '', + 'Options:', + ' --root Root path to scan. Repeatable. Default: .', + ' --expect Expected exported symbol. Repeatable.', + ' --expect-file File with one expected symbol per line; blank lines and # comments ignored.', + ' --ignore Directory name to skip. Repeatable.', + ' --show-actual Print the full actual symbol set.', + ' --fail-on-missing Exit non-zero when any expected symbol is missing.', + ' --help Show this help.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) throw new Error(`Missing value for ${flag}`); + return value; +} + +function parseArgs(args: string[]): Options | null { + const roots: string[] = []; + const expected = new Set(); + const ignoreDirs = new Set(DEFAULT_IGNORE_DIRS); + let expectFile: string | undefined; + let showActual = false; + let failOnMissing = false; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + switch (arg) { + case '--root': + roots.push(requireValue(args, index, arg)); + index++; + break; + case '--expect': + expected.add(requireValue(args, index, arg)); + index++; + break; + case '--expect-file': + expectFile = requireValue(args, index, arg); + index++; + break; + case '--ignore': + ignoreDirs.add(requireValue(args, index, arg)); + index++; + break; + case '--show-actual': + showActual = true; + break; + case '--fail-on-missing': + failOnMissing = true; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (roots.length === 0) roots.push('.'); + return { roots, expected, expectFile, ignoreDirs, showActual, failOnMissing }; +} + +function fileExtension(path: string): string { + const dotIndex = path.lastIndexOf('.'); + return dotIndex === -1 ? '' : path.slice(dotIndex); +} + +function addNamedExport(raw: string, names: Set): void { + for (const part of raw.split(',')) { + const trimmed = part.trim(); + if (!trimmed) continue; + const aliasMatch = trimmed.match(/\bas\s+([A-Za-z_$][\w$]*)$/); + names.add(aliasMatch?.[1] ?? trimmed); + } +} + +function collectNamesFromLine(line: string, names: Set): void { + const directPatterns: RegExp[] = [ + /^\s*export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+const\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+let\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+var\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+class\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+interface\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+type\s+([A-Za-z_$][\w$]*)/, + /^\s*export\s+enum\s+([A-Za-z_$][\w$]*)/, + ]; + + for (const pattern of directPatterns) { + const match = line.match(pattern); + if (match) names.add(match[1]); + } + + const namedMatch = line.match(/^\s*export\s+\{([^}]+)\}/); + if (namedMatch) addNamedExport(namedMatch[1], names); +} + +async function collectExportNames( + path: string, + options: Options, + names: Set, +): Promise { + const info = await Deno.stat(path); + + if (info.isDirectory) { + for await (const entry of Deno.readDir(path)) { + if (entry.isDirectory && options.ignoreDirs.has(entry.name)) continue; + await collectExportNames(`${path}/${entry.name}`, options, names); + } + return; + } + + if (!EXPORT_EXTS.has(fileExtension(path))) return; + + const text = await Deno.readTextFile(path); + for (const line of text.split(/\r?\n/)) { + collectNamesFromLine(line, names); + } +} + +async function loadExpectedFromFile(path: string, expected: Set): Promise { + const text = await Deno.readTextFile(path); + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + expected.add(trimmed); + } +} + +const options = parseArgs(Deno.args); +if (options) { + if (options.expectFile) { + await loadExpectedFromFile(options.expectFile, options.expected); + } + + if (options.expected.size === 0) { + throw new Error('Provide at least one expected symbol via --expect or --expect-file.'); + } + + const actual = new Set(); + for (const root of options.roots) { + await collectExportNames(root, options, actual); + } + + const actualList = [...actual].sort(); + const expectedList = [...options.expected].sort(); + const present = expectedList.filter((name) => actual.has(name)); + const missing = expectedList.filter((name) => !actual.has(name)); + const extra = actualList.filter((name) => !options.expected.has(name)); + + console.log(`expected: ${expectedList.join(', ')}`); + console.log(`present: ${present.length > 0 ? present.join(', ') : '-'}`); + console.log(`missing: ${missing.length > 0 ? missing.join(', ') : '-'}`); + console.log(`extra-count: ${extra.length}`); + + if (options.showActual) { + console.log(`actual: ${actualList.join(', ')}`); + } + + if (options.failOnMissing && missing.length > 0) { + Deno.exit(1); + } +} diff --git a/.llm/tools/entry.md b/.llm/tools/entry.md new file mode 100644 index 000000000..a817ea605 --- /dev/null +++ b/.llm/tools/entry.md @@ -0,0 +1,100 @@ +# .llm/tools entry + +Reusable MCP-friendly Deno tools live here. This directory is for agent utilities that are useful +across sessions; temporary investigation scripts belong under `.llm/tmp/`. + +## Rules + +- Prefer these tools over brittle PowerShell `Select-String`, `findstr`, or ad hoc quoting-heavy + pipelines when scanning this repo from a Windows MCP session. +- Keep tool permissions explicit. Most scanners only need `--allow-read`. +- Do not duplicate doctrine/package fitness gates here. The current repo keeps those under + `tools/fitness/`. +- The maintained full CLI E2E gate is `deno task e2e:cli`; the legacy scaffold tool is diagnostic + only. + +## Full CLI E2E + +### `deno task e2e:cli` + +- Purpose: run the full merge-readiness CLI E2E suite. +- Coverage: scaffolds a project, adds the official plugins, initializes/generates/seeds the DB, + type-checks generated workspaces, starts Aspire, waits runtime resources, exercises HTTP behavior, + validates OTEL behavior, and cleans up. +- Example command: + - `deno task e2e:cli` + +### `packages/cli/e2e` + +- Purpose: product-grade CLI E2E validation package with suite/gate listing and targeted execution. +- Example commands: + - `deno task e2e:cli suites` + - `deno task e2e:cli gates scaffold.runtime` + - `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` + - `deno task e2e:cli gate scaffold.runtime generated.deno-check --cleanup --format pretty` +- Notes: `scaffold.plugins` is not the full suite. It is a narrower plugin scaffold and diagnostic + smoke. Use `scaffold.runtime` for merge readiness. + +### `scaffold-e2e-test.ts` + +- Purpose: legacy generated-project scaffold smoke kept as a comparison harness while the maintained + `packages/cli/e2e` runner covers the merge gate. +- Why MCP-friendly: emits step-level output and a streaming log under `.llm/tmp/scaffold-e2e-test/`. +- Example commands: + - `deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts --format pretty --cleanup` + - `deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts --dry-run --format pretty` +- Notes: use this only for parity debugging with the previous repo checkout. For PR readiness, use + `deno task e2e:cli`. + +## Diagnostics + +### `parse-deno-check-errors.ts` + +- Purpose: parse `deno check` / `deno task check*` output into structured JSON grouped by error kind + and affected paths. +- Example commands: + - `deno run --allow-read .llm/tools/parse-deno-check-errors.ts --input .llm/tmp/check.log --pretty` + - `deno run --allow-run --allow-read .llm/tools/parse-deno-check-errors.ts --pretty --cwd . -- deno task check` + +### `find-lines.ts` + +- Purpose: scan roots for substring or regex matches and print `path:line: text`. +- Example command: + - `deno run --allow-read .llm/tools/find-lines.ts --root packages/cli --regex "scaffold\\.(runtime|plugins)"` + +### `find-import-patterns.ts` + +- Purpose: scan imports and CSS `@import` lines for legacy aliases and relative-path debt. +- Example command: + - `deno run --allow-read .llm/tools/find-import-patterns.ts --root packages/cli` + +### `find-symbol-usages.ts` + +- Purpose: scan for symbol usages with symbol-boundary matching for refactor prep. +- Example command: + - `deno run --allow-read .llm/tools/find-symbol-usages.ts --root packages/cli --symbol copyOfficialPlugin` + +### `list-exports.ts` + +- Purpose: inventory exported symbols and re-exports across a package or app surface. +- Example command: + - `deno run --allow-read .llm/tools/list-exports.ts --root packages/contracts` + +### `compare-export-surface.ts` + +- Purpose: compare actual exported symbols against an expected contract list. +- Example command: + - `deno run --allow-read .llm/tools/compare-export-surface.ts --root packages/contracts --expect contract` + +### `git-commit-paths.ts` + +- Purpose: run `git commit` with explicit message/path inputs without Windows shell quoting issues. +- Example commands: + - `deno run --allow-run .llm/tools/git-commit-paths.ts --message chore-sync --all-staged --dry-run` + - `deno run --allow-read --allow-run .llm/tools/git-commit-paths.ts --message-file .llm/tmp/commit-message.txt --path-file .llm/tmp/commit-paths.txt --push` + +## Non-ported Old Tools + +The previous checkout also had playground dashboard, benchmark comparison, and helper-generation +scripts. They were not ported because they target older app surfaces or one-off investigations. Add +new versions only when a current task needs them. diff --git a/.llm/tools/find-import-patterns.ts b/.llm/tools/find-import-patterns.ts new file mode 100644 index 000000000..f8fb482e4 --- /dev/null +++ b/.llm/tools/find-import-patterns.ts @@ -0,0 +1,195 @@ +/** + * MCP-friendly reusable import-pattern scanner. + * + * Use this when checking for alias migration leftovers, brittle deep relative imports, or legacy + * alias usage. This is more reliable than PowerShell text search on Windows because it avoids shell + * quoting problems around paths like `(dashboard)` or `[id]`. + * + * Examples: + * - deno run --allow-read .llm/tools/find-import-patterns.ts --root packages/cli/src + * - deno run --allow-read .llm/tools/find-import-patterns.ts --root packages/cli/src --legacy-alias @old/ + * - deno run --allow-read .llm/tools/find-import-patterns.ts --root packages/cli/src --include-sibling + */ + +interface Options { + roots: string[]; + legacyAliases: string[]; + includeSibling: boolean; + exts: Set; + ignoreDirs: Set; + summary: boolean; +} + +interface Stats { + scannedFiles: number; + matchedFiles: number; + matchedLines: number; +} + +const DEFAULT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cts', '.css']; +const DEFAULT_IGNORE_DIRS = ['.git', '_fresh', 'node_modules', 'dist', 'coverage']; +const DEFAULT_LEGACY_ALIASES = ['@/']; + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-read .llm/tools/find-import-patterns.ts [options]', + '', + 'Options:', + ' --root Root path to scan. Repeatable. Default: .', + ' --legacy-alias Alias prefix to treat as legacy. Repeatable. Default: @/', + ' --include-sibling Also report ./ sibling-relative imports.', + ' --ext Extensions to scan, e.g. .ts,.tsx,.css', + ' --ignore Directory name to skip. Repeatable.', + ' --no-summary Omit summary output.', + ' --help Show this help.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) throw new Error(`Missing value for ${flag}`); + return value; +} + +function normalizeExt(ext: string): string { + return ext.startsWith('.') ? ext : `.${ext}`; +} + +function parseArgs(args: string[]): Options | null { + const roots: string[] = []; + const legacyAliases = [...DEFAULT_LEGACY_ALIASES]; + const exts = new Set(DEFAULT_EXTS); + const ignoreDirs = new Set(DEFAULT_IGNORE_DIRS); + let includeSibling = false; + let summary = true; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + + switch (arg) { + case '--root': + roots.push(requireValue(args, index, arg)); + index++; + break; + case '--legacy-alias': + legacyAliases.push(requireValue(args, index, arg)); + index++; + break; + case '--include-sibling': + includeSibling = true; + break; + case '--ext': { + exts.clear(); + for (const ext of requireValue(args, index, arg).split(',')) { + exts.add(normalizeExt(ext.trim())); + } + index++; + break; + } + case '--ignore': + ignoreDirs.add(requireValue(args, index, arg)); + index++; + break; + case '--no-summary': + summary = false; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (roots.length === 0) roots.push('.'); + + return { roots, legacyAliases, includeSibling, exts, ignoreDirs, summary }; +} + +function shouldScanFile(path: string, exts: Set): boolean { + const dotIndex = path.lastIndexOf('.'); + if (dotIndex === -1) return false; + return exts.has(path.slice(dotIndex)); +} + +function extractSpecifier(line: string): { source: 'import' | 'css'; specifier: string } | null { + const importMatch = line.match(/(?:\bfrom\s+|^\s*import\s+)['"]([^'"]+)['"]/); + if (importMatch) { + return { source: 'import', specifier: importMatch[1] }; + } + + const cssMatch = line.match(/@import\s+['"]([^'"]+)['"]/); + if (cssMatch) { + return { source: 'css', specifier: cssMatch[1] }; + } + + return null; +} + +function classifySpecifier(specifier: string, options: Options): string | null { + if (options.legacyAliases.some((alias) => specifier.startsWith(alias))) { + return 'legacy-alias'; + } + if (specifier.startsWith('../')) { + return 'parent-relative'; + } + if (options.includeSibling && specifier.startsWith('./')) { + return 'sibling-relative'; + } + return null; +} + +async function scanPath(path: string, options: Options, stats: Stats): Promise { + const info = await Deno.stat(path); + + if (info.isDirectory) { + for await (const entry of Deno.readDir(path)) { + if (entry.isDirectory && options.ignoreDirs.has(entry.name)) continue; + await scanPath(`${path}/${entry.name}`, options, stats); + } + return; + } + + if (!shouldScanFile(path, options.exts)) return; + + const text = await Deno.readTextFile(path); + stats.scannedFiles++; + + const lines = text.split(/\r?\n/); + let fileMatched = false; + + for (let index = 0; index < lines.length; index++) { + const extracted = extractSpecifier(lines[index]); + if (!extracted) continue; + + const category = classifySpecifier(extracted.specifier, options); + if (!category) continue; + + fileMatched = true; + stats.matchedLines++; + console.log( + `${path}:${index + 1}: [${extracted.source}:${category}] ${extracted.specifier} :: ${ + lines[index].trim() + }`, + ); + } + + if (fileMatched) stats.matchedFiles++; +} + +const options = parseArgs(Deno.args); + +if (options) { + const stats: Stats = { scannedFiles: 0, matchedFiles: 0, matchedLines: 0 }; + + for (const root of options.roots) { + await scanPath(root, options, stats); + } + + if (options.summary) { + console.error( + `summary: scanned ${stats.scannedFiles} file(s), matched ${stats.matchedFiles} file(s), ${stats.matchedLines} line(s)`, + ); + } +} diff --git a/.llm/tools/find-lines.ts b/.llm/tools/find-lines.ts new file mode 100644 index 000000000..8f8af52f8 --- /dev/null +++ b/.llm/tools/find-lines.ts @@ -0,0 +1,186 @@ +/** + * MCP-friendly reusable Deno line scanner. + * + * Prefer this over PowerShell `Select-String`, `findstr`, or complex shell pipelines when scanning + * this repo from an MCP session. It is more reliable on Windows paths and usually more performant + * because one Deno process can walk the tree once and handle all matching in-process. + * + * Examples: + * - deno run --allow-read .llm/tools/find-lines.ts --root packages/cli/src --contains @netscript/ + * - deno run --allow-read .llm/tools/find-lines.ts --root packages/fresh --regex define(ListPage|DetailPage|FormPage) + * - deno run --allow-read .llm/tools/find-lines.ts --root packages/cli/src --contains ../ --ext .ts,.tsx + */ + +interface Options { + roots: string[]; + contains: string[]; + regexes: RegExp[]; + exts: Set; + ignoreDirs: Set; + summary: boolean; +} + +interface ScanStats { + scannedFiles: number; + matchedFiles: number; + matchedLines: number; +} + +const DEFAULT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cts', '.css', '.json', '.md']; +const DEFAULT_IGNORE_DIRS = ['.git', '_fresh', 'node_modules', 'dist', 'coverage']; + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-read .llm/tools/find-lines.ts [options]', + '', + 'Options:', + ' --root Root path to scan. Repeatable. Default: .', + ' --contains Substring to match. Repeatable.', + ' --regex JavaScript regex source to match. Repeatable.', + ' --ext Extensions to scan, e.g. .ts,.tsx,.css', + ' --ignore Directory name to skip. Repeatable.', + ' --no-summary Omit summary output.', + ' --help Show this help.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) { + throw new Error(`Missing value for ${flag}`); + } + return value; +} + +function normalizeExt(ext: string): string { + return ext === '*' || ext.startsWith('.') ? ext : `.${ext}`; +} + +function parseArgs(args: string[]): Options | null { + const roots: string[] = []; + const contains: string[] = []; + const regexes: RegExp[] = []; + const exts = new Set(DEFAULT_EXTS); + const ignoreDirs = new Set(DEFAULT_IGNORE_DIRS); + let summary = true; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + + switch (arg) { + case '--root': + roots.push(requireValue(args, index, arg)); + index++; + break; + case '--contains': + contains.push(requireValue(args, index, arg)); + index++; + break; + case '--regex': + regexes.push(new RegExp(requireValue(args, index, arg))); + index++; + break; + case '--ext': { + const value = requireValue(args, index, arg); + exts.clear(); + for (const ext of value.split(',')) { + const normalized = normalizeExt(ext.trim()); + if (normalized) exts.add(normalized); + } + index++; + break; + } + case '--ignore': + ignoreDirs.add(requireValue(args, index, arg)); + index++; + break; + case '--no-summary': + summary = false; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (roots.length === 0) roots.push('.'); + + if (contains.length === 0 && regexes.length === 0) { + throw new Error('Provide at least one --contains or --regex matcher.'); + } + + return { roots, contains, regexes, exts, ignoreDirs, summary }; +} + +function shouldScanFile(path: string, exts: Set): boolean { + if (exts.has('*')) return true; + + const lastDot = path.lastIndexOf('.'); + if (lastDot === -1) return false; + + return exts.has(path.slice(lastDot)); +} + +function lineMatches(line: string, options: Options): boolean { + return options.contains.some((value) => line.includes(value)) || + options.regexes.some((pattern) => pattern.test(line)); +} + +async function scanPath(path: string, options: Options, stats: ScanStats): Promise { + const fileInfo = await Deno.stat(path); + + if (fileInfo.isDirectory) { + for await (const entry of Deno.readDir(path)) { + if (entry.isDirectory && options.ignoreDirs.has(entry.name)) continue; + await scanPath(`${path}/${entry.name}`, options, stats); + } + return; + } + + if (!shouldScanFile(path, options.exts)) return; + + let text: string; + try { + text = await Deno.readTextFile(path); + } catch (error: unknown) { + if (error instanceof Deno.errors.InvalidData) return; + throw error; + } + + stats.scannedFiles++; + + const lines = text.split(/\r?\n/); + let fileMatched = false; + + for (let index = 0; index < lines.length; index++) { + const line = lines[index]; + if (!lineMatches(line, options)) continue; + + fileMatched = true; + stats.matchedLines++; + console.log(`${path}:${index + 1}: ${line.trim()}`); + } + + if (fileMatched) { + stats.matchedFiles++; + } +} + +const options = parseArgs(Deno.args); + +if (options) { + const stats: ScanStats = { scannedFiles: 0, matchedFiles: 0, matchedLines: 0 }; + + for (const root of options.roots) { + await scanPath(root, options, stats); + } + + if (options.summary) { + console.error( + `summary: scanned ${stats.scannedFiles} file(s), matched ${stats.matchedFiles} file(s), ${stats.matchedLines} line(s)`, + ); + } +} diff --git a/.llm/tools/find-symbol-usages.ts b/.llm/tools/find-symbol-usages.ts new file mode 100644 index 000000000..a21df589d --- /dev/null +++ b/.llm/tools/find-symbol-usages.ts @@ -0,0 +1,195 @@ +/** + * MCP-friendly reusable symbol usage scanner. + * + * Prefer this over brittle shell grep/Select-String pipelines when you want word-boundary matching + * for symbols during refactors, API audits, or migration work. + * + * Examples: + * - deno run --allow-read .llm/tools/find-symbol-usages.ts --root packages/fresh --symbol definePage + * - deno run --allow-read .llm/tools/find-symbol-usages.ts --root packages/cli/src --symbol defineCommand + * - deno run --allow-read .llm/tools/find-symbol-usages.ts --root packages/fresh --symbol createListPage --substring + */ + +interface Options { + roots: string[]; + symbols: string[]; + exts: Set; + ignoreDirs: Set; + caseSensitive: boolean; + substring: boolean; + summary: boolean; +} + +interface Stats { + scannedFiles: number; + matchedFiles: number; + matchedLines: number; +} + +const DEFAULT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cts', '.css', '.md']; +const DEFAULT_IGNORE_DIRS = ['.git', '_fresh', 'node_modules', 'dist', 'coverage']; + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-read .llm/tools/find-symbol-usages.ts [options]', + '', + 'Options:', + ' --root Root path to scan. Repeatable. Default: .', + ' --symbol Symbol to match. Repeatable. Required.', + ' --ext Extensions to scan, e.g. .ts,.tsx,.md', + ' --ignore Directory name to skip. Repeatable.', + ' --case-sensitive Make matching case-sensitive.', + ' --substring Match substrings instead of symbol boundaries.', + ' --no-summary Omit summary output.', + ' --help Show this help.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) throw new Error(`Missing value for ${flag}`); + return value; +} + +function normalizeExt(ext: string): string { + return ext.startsWith('.') ? ext : `.${ext}`; +} + +function parseArgs(args: string[]): Options | null { + const roots: string[] = []; + const symbols: string[] = []; + const exts = new Set(DEFAULT_EXTS); + const ignoreDirs = new Set(DEFAULT_IGNORE_DIRS); + let caseSensitive = false; + let substring = false; + let summary = true; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + switch (arg) { + case '--root': + roots.push(requireValue(args, index, arg)); + index++; + break; + case '--symbol': + symbols.push(requireValue(args, index, arg)); + index++; + break; + case '--ext': { + exts.clear(); + for (const ext of requireValue(args, index, arg).split(',')) { + exts.add(normalizeExt(ext.trim())); + } + index++; + break; + } + case '--ignore': + ignoreDirs.add(requireValue(args, index, arg)); + index++; + break; + case '--case-sensitive': + caseSensitive = true; + break; + case '--substring': + substring = true; + break; + case '--no-summary': + summary = false; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (roots.length === 0) roots.push('.'); + if (symbols.length === 0) throw new Error('Provide at least one --symbol value.'); + + return { roots, symbols, exts, ignoreDirs, caseSensitive, substring, summary }; +} + +function shouldScanFile(path: string, exts: Set): boolean { + const dotIndex = path.lastIndexOf('.'); + if (dotIndex === -1) return false; + return exts.has(path.slice(dotIndex)); +} + +function escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function createPatterns(options: Options): Array<{ symbol: string; pattern: RegExp }> { + const flags = options.caseSensitive ? '' : 'i'; + + return options.symbols.map((symbol) => { + const escaped = escapeRegex(symbol); + const source = options.substring + ? escaped + : `(^|[^A-Za-z0-9_$])(${escaped})(?=$|[^A-Za-z0-9_$])`; + + return { symbol, pattern: new RegExp(source, flags) }; + }); +} + +async function scanPath( + path: string, + options: Options, + patterns: Array<{ symbol: string; pattern: RegExp }>, + stats: Stats, +): Promise { + const info = await Deno.stat(path); + + if (info.isDirectory) { + for await (const entry of Deno.readDir(path)) { + if (entry.isDirectory && options.ignoreDirs.has(entry.name)) continue; + await scanPath(`${path}/${entry.name}`, options, patterns, stats); + } + return; + } + + if (!shouldScanFile(path, options.exts)) return; + + let text: string; + try { + text = await Deno.readTextFile(path); + } catch (error: unknown) { + if (error instanceof Deno.errors.InvalidData) return; + throw error; + } + + stats.scannedFiles++; + + let fileMatched = false; + const lines = text.split(/\r?\n/); + for (let index = 0; index < lines.length; index++) { + const matchedSymbols = patterns.filter(({ pattern }) => pattern.test(lines[index])).map(( + { symbol }, + ) => symbol); + if (matchedSymbols.length === 0) continue; + + fileMatched = true; + stats.matchedLines++; + console.log(`${path}:${index + 1}: [${matchedSymbols.join(',')}] ${lines[index].trim()}`); + } + + if (fileMatched) stats.matchedFiles++; +} + +const options = parseArgs(Deno.args); +if (options) { + const patterns = createPatterns(options); + const stats: Stats = { scannedFiles: 0, matchedFiles: 0, matchedLines: 0 }; + + for (const root of options.roots) { + await scanPath(root, options, patterns, stats); + } + + if (options.summary) { + console.error( + `summary: scanned ${stats.scannedFiles} file(s), matched ${stats.matchedFiles} file(s), ${stats.matchedLines} line(s)`, + ); + } +} diff --git a/.llm/tools/git-commit-paths.ts b/.llm/tools/git-commit-paths.ts new file mode 100644 index 000000000..b7534db8b --- /dev/null +++ b/.llm/tools/git-commit-paths.ts @@ -0,0 +1,194 @@ +/** + * MCP-friendly git commit/push helper for Windows/MCP sessions. + * + * Use this instead of PowerShell-heavy git commands when commit messages contain `:` or when file + * paths contain characters like `(`, `)`, and `[]`. Deno passes arguments directly to git, so the + * shell does not get a chance to mangle them. + * + * Examples: + * - deno run --allow-run .llm/tools/git-commit-paths.ts --message chore-sync --all-staged --push --dry-run + * - deno run --allow-read --allow-run .llm/tools/git-commit-paths.ts --message-file .llm/temp/commit-message.txt --path-file .llm/temp/commit-paths.txt + */ + +interface Options { + cwd: string; + message?: string; + messageFile?: string; + paths: string[]; + pathFiles: string[]; + push: boolean; + branch?: string; + allStaged: boolean; + dryRun: boolean; +} + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-run .llm/tools/git-commit-paths.ts --message [options]', + '', + 'Options:', + ' --message Commit message.', + ' --message-file Read commit message from a file.', + ' --path Path to include with git commit --only. Repeatable.', + ' --path-file Read newline-separated commit paths from a file. Repeatable.', + ' --all-staged Commit staged changes instead of selecting paths.', + ' --push Push to origin after a successful commit.', + ' --branch Push branch override. Default: current branch.', + ' --cwd Repo root/current working directory. Default: current directory.', + ' --dry-run Print commands without executing them.', + ' --help Show this help.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) { + throw new Error(`Missing value for ${flag}`); + } + return value; +} + +function parseArgs(args: string[]): Options | null { + const options: Options = { + cwd: Deno.cwd(), + paths: [], + pathFiles: [], + push: false, + allStaged: false, + dryRun: false, + }; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + switch (arg) { + case '--message': + options.message = requireValue(args, index, arg); + index++; + break; + case '--message-file': + options.messageFile = requireValue(args, index, arg); + index++; + break; + case '--path': + options.paths.push(requireValue(args, index, arg)); + index++; + break; + case '--path-file': + options.pathFiles.push(requireValue(args, index, arg)); + index++; + break; + case '--push': + options.push = true; + break; + case '--branch': + options.branch = requireValue(args, index, arg); + index++; + break; + case '--cwd': + options.cwd = requireValue(args, index, arg); + index++; + break; + case '--all-staged': + options.allStaged = true; + break; + case '--dry-run': + options.dryRun = true; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (options.message && options.messageFile) { + throw new Error('Use either --message or --message-file, not both.'); + } + + if (!options.message && !options.messageFile) { + throw new Error('Provide --message or --message-file .'); + } + + if (!options.allStaged && options.paths.length === 0 && options.pathFiles.length === 0) { + throw new Error('Provide at least one --path, --path-file, or use --all-staged.'); + } + + if (options.allStaged && (options.paths.length > 0 || options.pathFiles.length > 0)) { + throw new Error('Use either --all-staged or explicit commit paths, not both.'); + } + + return options; +} + +async function resolveMessage(options: Options): Promise { + if (options.message) return options.message; + const text = await Deno.readTextFile(options.messageFile!); + const message = text.trim(); + if (!message) throw new Error('Commit message file is empty.'); + return message; +} + +async function resolvePaths(options: Options): Promise { + const paths = [...options.paths]; + for (const pathFile of options.pathFiles) { + const text = await Deno.readTextFile(pathFile); + for (const line of text.split(/\r?\n/)) { + const value = line.trim(); + if (value) paths.push(value); + } + } + return paths; +} + +async function runGit(args: string[], cwd: string, dryRun: boolean): Promise { + console.error(`git ${args.join(' ')}`); + if (dryRun) return; + + const command = new Deno.Command('git', { + args, + cwd, + stdout: 'inherit', + stderr: 'inherit', + }); + const { code } = await command.output(); + if (code !== 0) { + throw new Error(`git ${args[0]} failed with exit code ${code}`); + } +} + +async function currentBranch(cwd: string): Promise { + const command = new Deno.Command('git', { + args: ['branch', '--show-current'], + cwd, + stdout: 'piped', + stderr: 'inherit', + }); + const { code, stdout } = await command.output(); + if (code !== 0) { + throw new Error('Unable to determine current branch.'); + } + const branch = new TextDecoder().decode(stdout).trim(); + if (!branch) { + throw new Error('Current branch is empty.'); + } + return branch; +} + +const options = parseArgs(Deno.args); +if (options) { + const message = await resolveMessage(options); + const paths = options.allStaged ? [] : await resolvePaths(options); + const commitArgs = ['commit', '-m', message]; + if (!options.allStaged) { + commitArgs.push('--only', '--', ...paths); + } + + await runGit(commitArgs, options.cwd, options.dryRun); + + if (options.push) { + const branch = options.branch ?? await currentBranch(options.cwd); + await runGit(['push', 'origin', branch], options.cwd, options.dryRun); + } +} diff --git a/.llm/tools/list-exports.ts b/.llm/tools/list-exports.ts new file mode 100644 index 000000000..b7e636213 --- /dev/null +++ b/.llm/tools/list-exports.ts @@ -0,0 +1,192 @@ +/** + * MCP-friendly reusable export inventory tool. + * + * Use this to inventory a package or app surface without brittle shell pipelines. It prints + * `path:line: kind:name :: source line` for common export forms so future sessions can quickly map + * public APIs and migration surfaces. + * + * Examples: + * - deno run --allow-read .llm/tools/list-exports.ts --root packages/fresh + * - deno run --allow-read .llm/tools/list-exports.ts --root packages/fresh --name define + * - deno run --allow-read .llm/tools/list-exports.ts --root packages/cli/src --name handler + */ + +interface Options { + roots: string[]; + nameFilters: string[]; + exts: Set; + ignoreDirs: Set; + summary: boolean; +} + +interface Stats { + scannedFiles: number; + matchedFiles: number; + matchedLines: number; +} + +interface ExportMatch { + kind: string; + name: string; +} + +const DEFAULT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cts']; +const DEFAULT_IGNORE_DIRS = ['.git', '_fresh', 'node_modules', 'dist', 'coverage']; + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-read .llm/tools/list-exports.ts [options]', + '', + 'Options:', + ' --root Root path to scan. Repeatable. Default: .', + ' --name Filter by exported symbol or line content. Repeatable.', + ' --ext Extensions to scan, e.g. .ts,.tsx', + ' --ignore Directory name to skip. Repeatable.', + ' --no-summary Omit summary output.', + ' --help Show this help.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) throw new Error(`Missing value for ${flag}`); + return value; +} + +function normalizeExt(ext: string): string { + return ext.startsWith('.') ? ext : `.${ext}`; +} + +function parseArgs(args: string[]): Options | null { + const roots: string[] = []; + const nameFilters: string[] = []; + const exts = new Set(DEFAULT_EXTS); + const ignoreDirs = new Set(DEFAULT_IGNORE_DIRS); + let summary = true; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + switch (arg) { + case '--root': + roots.push(requireValue(args, index, arg)); + index++; + break; + case '--name': + nameFilters.push(requireValue(args, index, arg)); + index++; + break; + case '--ext': { + exts.clear(); + for (const ext of requireValue(args, index, arg).split(',')) { + exts.add(normalizeExt(ext.trim())); + } + index++; + break; + } + case '--ignore': + ignoreDirs.add(requireValue(args, index, arg)); + index++; + break; + case '--no-summary': + summary = false; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (roots.length === 0) roots.push('.'); + return { roots, nameFilters, exts, ignoreDirs, summary }; +} + +function shouldScanFile(path: string, exts: Set): boolean { + const dotIndex = path.lastIndexOf('.'); + if (dotIndex === -1) return false; + return exts.has(path.slice(dotIndex)); +} + +function parseExport(line: string): ExportMatch | null { + const patterns: Array<[RegExp, string]> = [ + [/^\s*export\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/, 'default-function'], + [/^\s*export\s+default\s+class\s+([A-Za-z_$][\w$]*)/, 'default-class'], + [/^\s*export\s+default\b/, 'default'], + [/^\s*export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/, 'function'], + [/^\s*export\s+const\s+([A-Za-z_$][\w$]*)/, 'const'], + [/^\s*export\s+let\s+([A-Za-z_$][\w$]*)/, 'let'], + [/^\s*export\s+var\s+([A-Za-z_$][\w$]*)/, 'var'], + [/^\s*export\s+class\s+([A-Za-z_$][\w$]*)/, 'class'], + [/^\s*export\s+interface\s+([A-Za-z_$][\w$]*)/, 'interface'], + [/^\s*export\s+type\s+([A-Za-z_$][\w$]*)/, 'type'], + [/^\s*export\s+enum\s+([A-Za-z_$][\w$]*)/, 'enum'], + [/^\s*export\s+\*\s+from\s+['"]([^'"]+)['"]/, 'star-reexport'], + [/^\s*export\s+\{([^}]+)\}(?:\s+from\s+['"]([^'"]+)['"])?/, 'named-export'], + ]; + + for (const [pattern, kind] of patterns) { + const match = line.match(pattern); + if (!match) continue; + + const rawName = match[1] ?? 'default'; + return { kind, name: rawName.replace(/\s+/g, ' ').trim() }; + } + + return null; +} + +function matchesFilter(line: string, parsed: ExportMatch, filters: string[]): boolean { + if (filters.length === 0) return true; + const haystacks = [parsed.name, parsed.kind, line]; + return filters.some((filter) => haystacks.some((value) => value.includes(filter))); +} + +async function scanPath(path: string, options: Options, stats: Stats): Promise { + const info = await Deno.stat(path); + + if (info.isDirectory) { + for await (const entry of Deno.readDir(path)) { + if (entry.isDirectory && options.ignoreDirs.has(entry.name)) continue; + await scanPath(`${path}/${entry.name}`, options, stats); + } + return; + } + + if (!shouldScanFile(path, options.exts)) return; + + const text = await Deno.readTextFile(path); + stats.scannedFiles++; + + const lines = text.split(/\r?\n/); + let fileMatched = false; + + for (let index = 0; index < lines.length; index++) { + const parsed = parseExport(lines[index]); + if (!parsed) continue; + if (!matchesFilter(lines[index], parsed, options.nameFilters)) continue; + + fileMatched = true; + stats.matchedLines++; + console.log(`${path}:${index + 1}: ${parsed.kind}:${parsed.name} :: ${lines[index].trim()}`); + } + + if (fileMatched) stats.matchedFiles++; +} + +const options = parseArgs(Deno.args); + +if (options) { + const stats: Stats = { scannedFiles: 0, matchedFiles: 0, matchedLines: 0 }; + + for (const root of options.roots) { + await scanPath(root, options, stats); + } + + if (options.summary) { + console.error( + `summary: scanned ${stats.scannedFiles} file(s), matched ${stats.matchedFiles} file(s), ${stats.matchedLines} line(s)`, + ); + } +} diff --git a/.llm/tools/parse-deno-check-errors.ts b/.llm/tools/parse-deno-check-errors.ts new file mode 100644 index 000000000..7b45e7936 --- /dev/null +++ b/.llm/tools/parse-deno-check-errors.ts @@ -0,0 +1,349 @@ +/** + * MCP-friendly structured parser for `deno check` / `deno task check*` failures. + * + * Use this when raw terminal output is too noisy to reason about efficiently. The script strips + * ANSI codes, parses Deno/TypeScript-style error blocks, deduplicates repeated occurrences, and + * emits grouped JSON by error kind with affected files and locations. + * + * Examples: + * - deno run --allow-read .llm/tools/parse-deno-check-errors.ts --input .llm/tmp/check-packages.log + * - deno run --allow-run --allow-read .llm/tools/parse-deno-check-errors.ts --cwd . -- deno task check:packages + * - deno run --allow-run --allow-read .llm/tools/parse-deno-check-errors.ts --pretty --cwd packages/fresh -- deno check ./route/contract.ts + */ + +interface SourceData { + text: string; + command?: string[]; + exitCode?: number; +} + +interface Location { + path: string; + line: number; + column: number; +} + +interface ParsedOccurrence { + kind: string; + message: string; + location?: Location; +} + +interface GroupedPath { + path: string; + count: number; + locations: Array<{ line: number; column: number }>; +} + +interface ErrorGroup { + kind: string; + message: string; + count: number; + paths: GroupedPath[]; +} + +interface OutputReport { + source: { + mode: 'file' | 'stdin' | 'command'; + input?: string; + command?: string[]; + cwd?: string; + exitCode?: number; + }; + summary: { + totalOccurrences: number; + uniqueOccurrences: number; + uniqueKinds: number; + uniquePaths: number; + }; + groups: ErrorGroup[]; +} + +interface Options { + input?: string; + cwd?: string; + pretty: boolean; + command?: string[]; +} + +const ANSI_PATTERN = /\u001b\[[0-9;]*m/g; +const ERROR_HEADER = /^([A-Z]+[0-9]+)\s+\[ERROR\]:\s+(.*)$/; +const LOCATION_LINE = /^\s*at\s+(?:file:\/\/)?(.+?):(\d+):(\d+)\s*$/; + +function printHelp(): void { + console.log([ + 'Usage:', + ' deno run --allow-read .llm/tools/parse-deno-check-errors.ts [options]', + ' deno run --allow-run --allow-read .llm/tools/parse-deno-check-errors.ts [options] -- deno task check:packages', + '', + 'Options:', + ' --input Parse a saved log file instead of stdin/command output.', + ' --cwd Working directory used when running a command after `--`.', + ' --pretty Pretty-print JSON output.', + ' --help Show this help.', + '', + 'Notes:', + ' - If `--input` is omitted and a command is not provided after `--`, the script reads stdin.', + ' - The JSON groups by error kind + message, then lists affected paths and locations.', + ].join('\n')); +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value) { + throw new Error(`Missing value for ${flag}`); + } + return value; +} + +function parseArgs(args: string[]): Options | null { + const separatorIndex = args.indexOf('--'); + const optionArgs = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args; + const command = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : undefined; + + let input: string | undefined; + let cwd: string | undefined; + let pretty = false; + + for (let index = 0; index < optionArgs.length; index++) { + const arg = optionArgs[index]; + switch (arg) { + case '--input': + input = requireValue(optionArgs, index, arg); + index++; + break; + case '--cwd': + cwd = requireValue(optionArgs, index, arg); + index++; + break; + case '--pretty': + pretty = true; + break; + case '--help': + printHelp(); + return null; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + return { + input, + cwd, + pretty, + command: command && command.length > 0 ? command : undefined, + }; +} + +async function readStdinText(): Promise { + const chunks: Uint8Array[] = []; + const reader = Deno.stdin.readable.getReader(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + if (value) chunks.push(value); + } + } finally { + reader.releaseLock(); + } + + return new TextDecoder().decode(concatChunks(chunks)); +} + +function concatChunks(chunks: Uint8Array[]): Uint8Array { + const length = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const output = new Uint8Array(length); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.length; + } + return output; +} + +async function loadSource(options: Options): Promise { + if (options.input) { + return { + text: await Deno.readTextFile(options.input), + }; + } + + if (options.command) { + const [name, ...commandArgs] = options.command; + const result = await new Deno.Command(name, { + args: commandArgs, + cwd: options.cwd, + stdout: 'piped', + stderr: 'piped', + }).output(); + + return { + text: new TextDecoder().decode(result.stdout) + new TextDecoder().decode(result.stderr), + command: options.command, + exitCode: result.code, + }; + } + + return { + text: await readStdinText(), + }; +} + +function stripAnsi(text: string): string { + return text.replaceAll(ANSI_PATTERN, ''); +} + +function normalizePath(path: string): string { + return path.replace(/\\/g, '/'); +} + +function parseOccurrences(text: string): ParsedOccurrence[] { + const clean = stripAnsi(text); + const lines = clean.split(/\r?\n/); + const occurrences: ParsedOccurrence[] = []; + + for (let index = 0; index < lines.length; index++) { + const headerMatch = lines[index].match(ERROR_HEADER); + if (!headerMatch) continue; + + const [, kind, message] = headerMatch; + let location: Location | undefined; + + for (let scan = index + 1; scan < lines.length; scan++) { + const nextLine = lines[scan]; + if (ERROR_HEADER.test(nextLine)) break; + const locationMatch = nextLine.match(LOCATION_LINE); + if (locationMatch) { + location = { + path: normalizePath(locationMatch[1]), + line: Number(locationMatch[2]), + column: Number(locationMatch[3]), + }; + break; + } + if (nextLine.startsWith('Found ') || nextLine.startsWith('error: ')) break; + } + + occurrences.push({ kind, message, location }); + } + + return occurrences; +} + +function dedupeOccurrences(occurrences: ParsedOccurrence[]): ParsedOccurrence[] { + const seen = new Set(); + const unique: ParsedOccurrence[] = []; + + for (const occurrence of occurrences) { + const key = JSON.stringify({ + kind: occurrence.kind, + message: occurrence.message, + location: occurrence.location ?? null, + }); + if (seen.has(key)) continue; + seen.add(key); + unique.push(occurrence); + } + + return unique; +} + +function groupOccurrences(occurrences: ParsedOccurrence[]): ErrorGroup[] { + const groups = new Map< + string, + { kind: string; message: string; occurrences: ParsedOccurrence[] } + >(); + + for (const occurrence of occurrences) { + const key = `${occurrence.kind}::${occurrence.message}`; + const current = groups.get(key); + if (current) { + current.occurrences.push(occurrence); + } else { + groups.set(key, { + kind: occurrence.kind, + message: occurrence.message, + occurrences: [occurrence], + }); + } + } + + return [...groups.values()] + .map((group): ErrorGroup => { + const byPath = new Map(); + + for (const occurrence of group.occurrences) { + const path = occurrence.location?.path ?? '(unknown)'; + const current = byPath.get(path) ?? { path, count: 0, locations: [] }; + current.count += 1; + + if (occurrence.location) { + const hasLocation = current.locations.some((location) => + location.line === occurrence.location!.line && + location.column === occurrence.location!.column + ); + if (!hasLocation) { + current.locations.push({ + line: occurrence.location.line, + column: occurrence.location.column, + }); + } + } + + byPath.set(path, current); + } + + return { + kind: group.kind, + message: group.message, + count: group.occurrences.length, + paths: [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path)), + }; + }) + .sort((left, right) => { + if (right.count !== left.count) return right.count - left.count; + if (left.kind !== right.kind) return left.kind.localeCompare(right.kind); + return left.message.localeCompare(right.message); + }); +} + +async function main(): Promise { + const options = parseArgs(Deno.args); + if (!options) return; + + const source = await loadSource(options); + const occurrences = parseOccurrences(source.text); + const uniqueOccurrences = dedupeOccurrences(occurrences); + const groups = groupOccurrences(uniqueOccurrences); + const uniquePaths = new Set( + uniqueOccurrences.flatMap((occurrence) => + occurrence.location?.path ? [occurrence.location.path] : [] + ), + ); + + const report: OutputReport = { + source: { + mode: options.input ? 'file' : options.command ? 'command' : 'stdin', + input: options.input, + command: source.command, + cwd: options.cwd, + exitCode: source.exitCode, + }, + summary: { + totalOccurrences: occurrences.length, + uniqueOccurrences: uniqueOccurrences.length, + uniqueKinds: new Set(uniqueOccurrences.map((occurrence) => occurrence.kind)).size, + uniquePaths: uniquePaths.size, + }, + groups, + }; + + console.log(JSON.stringify(report, null, options.pretty ? 2 : undefined)); + + if (options.command && source.exitCode && source.exitCode !== 0) { + Deno.exit(source.exitCode); + } +} + +await main(); diff --git a/.llm/tools/scaffold-e2e-test.ts b/.llm/tools/scaffold-e2e-test.ts new file mode 100644 index 000000000..e4b048241 --- /dev/null +++ b/.llm/tools/scaffold-e2e-test.ts @@ -0,0 +1,1403 @@ +/** + * MCP-friendly full scaffold E2E smoke for CLI/plugin/DB/Aspire changes. + * + * This tool creates a fresh generated NetScript project, adds the official plugin suite in local + * contributor mode, runs the DB init/generate/seed workflow, starts Aspire, calls real plugin + * endpoints, triggers a worker job, and emits machine-readable step results. + * + * Examples: + * - deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts + * - deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts --format pretty --cleanup + * - deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts --repo . --name plugin-smoke-manual --strict-telemetry + */ + +import { delay } from 'jsr:@std/async@1/delay'; +import { ensureDir } from 'jsr:@std/fs@1/ensure-dir'; +import { exists } from 'jsr:@std/fs@1/exists'; +import { dirname, fromFileUrl, join, resolve } from 'jsr:@std/path@1'; +import { Command } from 'jsr:@cliffy/command@1.0.0'; + +type OutputFormat = 'ndjson' | 'json' | 'pretty'; +type StepKind = 'command' | 'http' | 'tcp' | 'sleep' | 'summary'; +type StepStatus = 'passed' | 'failed' | 'warning' | 'skipped'; + +interface Options { + repo: string; + cli: string; + smokeRoot: string; + name: string; + db: string; + source: 'auto' | 'starter' | 'local'; + samples: boolean; + cleanup: boolean; + dryRun: boolean; + skipTelemetry: boolean; + strictTelemetry: boolean; + format: OutputFormat; + report?: string; + logFile: string; + progressIntervalMs: number; + commandTimeoutMs: number; + httpTimeoutMs: number; + workersUrl: string; + sagasUrl: string; + triggersUrl: string; +} + +interface CommandOptionValues { + repo?: string; + cli?: string; + smokeRoot?: string; + name?: string; + db?: string; + source?: string; + samples?: boolean; + noSamples?: boolean; + cleanup?: boolean; + dryRun?: boolean; + skipTelemetry?: boolean; + strictTelemetry?: boolean; + format?: string; + report?: string; + logFile?: string; + progressIntervalMs?: number; + commandTimeoutMs?: number; + httpTimeoutMs?: number; + workersUrl?: string; + sagasUrl?: string; + triggersUrl?: string; +} + +interface CommandDetails { + command: string[]; + cwd: string; + exitCode?: number; + stdoutTail?: string; + stderrTail?: string; + timedOut?: boolean; +} + +interface HttpDetails { + method: string; + url: string; + statusCode?: number; + ok?: boolean; + bodyPreview?: string; + jsonSummary?: unknown; + timedOut?: boolean; +} + +interface TcpDetails { + host: string; + port: number; + connected?: boolean; + timedOut?: boolean; +} + +interface StepResult { + id: string; + title: string; + kind: StepKind; + status: StepStatus; + critical: boolean; + durationMs: number; + details?: CommandDetails | HttpDetails | TcpDetails | Record; + error?: string; +} + +interface Report { + ok: boolean; + startedAspire: boolean; + stoppedAspire: boolean; + project: { + repo: string; + smokeRoot: string; + name: string; + root: string; + appHost: string; + stopCommand: string[]; + logFile: string; + }; + options: { + db: string; + source: string; + samples: boolean; + cleanup: boolean; + dryRun: boolean; + skipTelemetry: boolean; + strictTelemetry: boolean; + format: OutputFormat; + progressIntervalMs: number; + }; + summary: { + total: number; + passed: number; + failed: number; + warnings: number; + skipped: number; + durationMs: number; + }; + steps: StepResult[]; +} + +class SmokeFailure extends Error { + constructor(readonly step: StepResult) { + super(`Smoke step failed: ${step.id} (${step.title})`); + } +} + +const decoder = new TextDecoder(); +function inferRepoRoot(): string { + const toolPath = fromFileUrl(import.meta.url); + return resolve(dirname(toolPath), '..', '..'); +} + +function timestampName(date = new Date()): string { + const pad = (value: number) => String(value).padStart(2, '0'); + return [ + date.getFullYear(), + pad(date.getMonth() + 1), + pad(date.getDate()), + '-', + pad(date.getHours()), + pad(date.getMinutes()), + pad(date.getSeconds()), + ].join(''); +} + +function ensurePositiveInt(value: number, flag: string): number { + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`${flag} must be a positive integer, got "${value}"`); + } + return value; +} + +function parseSourceMode(raw: string): Options['source'] { + if (raw === 'auto' || raw === 'starter' || raw === 'local') { + return raw; + } + throw new Error(`--source must be one of auto, starter, local; got "${raw}"`); +} + +function parseFormat(raw: string): OutputFormat { + if (raw === 'ndjson' || raw === 'json' || raw === 'pretty') { + return raw; + } + throw new Error(`--format must be one of ndjson, json, pretty; got "${raw}"`); +} + +function defaultOptions(): Options { + const repo = inferRepoRoot(); + return { + repo, + cli: join(repo, 'packages', 'cli', 'bin', 'netscript-dev.ts'), + smokeRoot: join(repo, '.llm', 'tmp', 'manual-scaffold-smoke'), + name: `plugin-smoke-${timestampName()}`, + db: 'postgres', + source: 'local', + samples: true, + cleanup: false, + dryRun: false, + skipTelemetry: false, + strictTelemetry: false, + format: 'ndjson', + logFile: join(repo, '.llm', 'tmp', 'scaffold-e2e-test', `plugin-smoke-${timestampName()}.log`), + progressIntervalMs: 15_000, + commandTimeoutMs: 900_000, + httpTimeoutMs: 30_000, + workersUrl: 'http://localhost:8091', + sagasUrl: 'http://localhost:8092', + triggersUrl: 'http://localhost:8093', + }; +} + +function normalizeCommandOptions(raw: CommandOptionValues): Options { + const defaults = defaultOptions(); + const repo = raw.repo ? resolve(raw.repo) : defaults.repo; + const cli = raw.cli ? resolve(raw.cli) : join(repo, 'packages', 'cli', 'bin', 'netscript-dev.ts'); + const smokeRoot = raw.smokeRoot + ? resolve(raw.smokeRoot) + : join(repo, '.llm', 'tmp', 'manual-scaffold-smoke'); + const name = raw.name ?? defaults.name; + + return { + repo, + cli, + smokeRoot, + name, + db: raw.db ?? defaults.db, + source: parseSourceMode(raw.source ?? defaults.source), + samples: !(raw.samples === false || raw.noSamples === true), + cleanup: raw.cleanup === true, + dryRun: raw.dryRun === true, + skipTelemetry: raw.skipTelemetry === true, + strictTelemetry: raw.strictTelemetry === true, + format: parseFormat(raw.format ?? defaults.format), + report: raw.report ? resolve(raw.report) : undefined, + logFile: raw.logFile + ? resolve(raw.logFile) + : join(repo, '.llm', 'tmp', 'scaffold-e2e-test', `${name}.log`), + progressIntervalMs: ensurePositiveInt( + raw.progressIntervalMs ?? defaults.progressIntervalMs, + '--progress-interval-ms', + ), + commandTimeoutMs: ensurePositiveInt( + raw.commandTimeoutMs ?? defaults.commandTimeoutMs, + '--command-timeout-ms', + ), + httpTimeoutMs: ensurePositiveInt( + raw.httpTimeoutMs ?? defaults.httpTimeoutMs, + '--http-timeout-ms', + ), + workersUrl: (raw.workersUrl ?? defaults.workersUrl).replace(/\/+$/, ''), + sagasUrl: (raw.sagasUrl ?? defaults.sagasUrl).replace(/\/+$/, ''), + triggersUrl: (raw.triggersUrl ?? defaults.triggersUrl).replace(/\/+$/, ''), + }; +} + +async function parseCliOptions(args: string[]): Promise { + let parsed: Options | null = null; + await new Command() + .name('scaffold-e2e-test') + .version('1.0.0') + .description('Run the full generated-project scaffold smoke for CLI/plugin/DB/Aspire changes.') + .option('--repo ', 'NetScript repo root. Default: inferred from tool path.') + .option( + '--cli ', + 'CLI entrypoint. Default: /packages/cli/bin/netscript-dev.ts.', + ) + .option( + '--smoke-root ', + 'Parent directory for generated projects. Default: /.llm/tmp/manual-scaffold-smoke.', + ) + .option('--name ', 'Generated project name. Default: plugin-smoke-.') + .option('--db ', 'DB engine/key for plugin DB provisioning.', { + default: 'postgres', + }) + .option('--source ', 'Compatibility metadata; netscript-dev uses local sources.', { + default: 'local', + }) + .option('--samples', 'Include official sample tasks/jobs/sagas/triggers.', { + default: true, + }) + .option('--no-samples', 'Skip official sample tasks/jobs/sagas/triggers.') + .option('--cleanup', 'Stop the generated Aspire AppHost before exit.', { + default: false, + }) + .option('--dry-run', 'Emit the planned steps without executing commands or HTTP calls.', { + default: false, + }) + .option('--skip-telemetry', 'Skip OTLP port and aspire otel checks.', { + default: false, + }) + .option('--strict-telemetry', 'Treat telemetry check failures as fatal.', { + default: false, + }) + .option('--format ', 'Output mode: ndjson, json, pretty.', { + default: 'ndjson', + }) + .option('--report ', 'Also write the final JSON report to this path.') + .option( + '--log-file ', + 'Write a streaming progress log. Default: /.llm/tmp/scaffold-e2e-test/.log.', + ) + .option( + '--progress-interval-ms ', + 'Heartbeat interval for long-running commands.', + { + default: 15_000, + }, + ) + .option('--command-timeout-ms ', 'Per-command timeout in milliseconds.', { + default: 900_000, + }) + .option('--http-timeout-ms ', 'Per-request timeout in milliseconds.', { + default: 30_000, + }) + .option('--workers-url ', 'Workers API base URL.', { + default: 'http://localhost:8091', + }) + .option('--sagas-url ', 'Sagas API base URL.', { + default: 'http://localhost:8092', + }) + .option('--triggers-url ', 'Triggers API base URL.', { + default: 'http://localhost:8093', + }) + .example( + 'Default NDJSON run', + 'deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts', + ) + .example( + 'Pretty dry-run plan', + 'deno run --allow-read --allow-write --allow-run --allow-net --allow-env .llm/tools/scaffold-e2e-test.ts --dry-run --format pretty', + ) + .action((options: CommandOptionValues) => { + parsed = normalizeCommandOptions(options); + }) + .parse(args); + + if (parsed === null) { + return null; + } + return parsed; +} + +function tail(text: string, maxLength = 8_000): string { + return text.length <= maxLength ? text : text.slice(text.length - maxLength); +} + +function unknownToMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function summarizeJson(value: unknown): unknown { + if (Array.isArray(value)) { + return { + type: 'array', + length: value.length, + first: value.length > 0 ? summarizeJson(value[0]) : undefined, + }; + } + + if (value !== null && typeof value === 'object') { + const record = value as Record; + const keys = Object.keys(record); + const summary: Record = { type: 'object', keys: keys.slice(0, 20) }; + for (const key of ['success', 'status', 'service', 'total', 'message', 'id', 'triggered']) { + if (key in record) { + summary[key] = record[key]; + } + } + if ('data' in record && record.data !== null && typeof record.data === 'object') { + const data = record.data as Record; + summary.dataKeys = Object.keys(data).slice(0, 20); + for (const key of ['total', 'message', 'jobsCreated', 'tasksCreated', 'jobId', 'triggered']) { + if (key in data) { + summary[`data.${key}`] = data[key]; + } + } + } + return summary; + } + + return value; +} + +function createBaseStep( + id: string, + title: string, + kind: StepKind, + critical: boolean, + startedAt: number, +): Omit { + return { + id, + title, + kind, + critical, + durationMs: Math.round(performance.now() - startedAt), + }; +} + +function formatDuration(durationMs: number): string { + if (durationMs < 1_000) { + return `${durationMs}ms`; + } + const seconds = Math.round(durationMs / 1_000); + if (seconds < 60) { + return `${seconds}s`; + } + const minutes = Math.floor(seconds / 60); + const rest = seconds % 60; + return `${minutes}m ${String(rest).padStart(2, '0')}s`; +} + +function statusLabel(status: StepStatus): string { + switch (status) { + case 'passed': + return 'PASS'; + case 'failed': + return 'FAIL'; + case 'warning': + return 'WARN'; + case 'skipped': + return 'SKIP'; + } +} + +function compactDetails(details: StepResult['details']): Record | undefined { + if (!details) { + return undefined; + } + const record = details as Record; + if ('command' in record) { + return { + cwd: record.cwd, + command: Array.isArray(record.command) ? record.command.join(' ') : record.command, + exitCode: record.exitCode, + timedOut: record.timedOut, + }; + } + if ('url' in record) { + return { + method: record.method, + url: record.url, + statusCode: record.statusCode, + ok: record.ok, + timedOut: record.timedOut, + }; + } + return record; +} + +function prettyStepExtra(step: StepResult): string { + const details = step.details as Record | undefined; + if (!details) { + return ''; + } + if (typeof details.exitCode === 'number') { + return ` (exit ${details.exitCode})`; + } + if (typeof details.statusCode === 'number') { + return ` (HTTP ${details.statusCode})`; + } + if (details.connected === true) { + return ' (connected)'; + } + return ''; +} + +function printPrettySummary(report: Report): void { + const rows = [ + ['status', report.ok ? 'passed' : 'failed'], + ['duration', formatDuration(report.summary.durationMs)], + ['passed', String(report.summary.passed)], + ['failed', String(report.summary.failed)], + ['warnings', String(report.summary.warnings)], + ['skipped', String(report.summary.skipped)], + ['project', report.project.root], + ['log', report.project.logFile], + ]; + const labelWidth = Math.max(...rows.map(([label]) => label.length)); + + console.log('\nSummary'); + for (const [label, value] of rows) { + console.log(` ${label.padEnd(labelWidth)} ${value}`); + } + + const failed = report.steps.filter((step) => step.status === 'failed'); + if (failed.length > 0) { + console.log('\nFailures'); + for (const step of failed) { + console.log(` ${step.id} - ${step.error ?? step.title}`); + } + } +} + +async function withTimeout( + timeoutMs: number, + action: (signal: AbortSignal) => Promise, +): Promise<{ value?: T; timedOut: boolean }> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + return { value: await action(controller.signal), timedOut: false }; + } catch (error: unknown) { + if (controller.signal.aborted) { + return { timedOut: true }; + } + throw error; + } finally { + clearTimeout(timeout); + } +} + +class SmokeRunner { + readonly #options: Options; + readonly #steps: StepResult[] = []; + #startedAspire = false; + #stoppedAspire = false; + + constructor(options: Options) { + this.#options = options; + } + + #appendLog(message: string): void { + const line = `[${new Date().toISOString()}] ${message}\n`; + Deno.writeTextFileSync(this.#options.logFile, line, { append: true, create: true }); + } + + #initializeLog(): void { + Deno.mkdirSync(dirname(this.#options.logFile), { recursive: true }); + Deno.writeTextFileSync( + this.#options.logFile, + [ + `NetScript scaffold E2E smoke`, + `started=${new Date().toISOString()}`, + `repo=${this.#options.repo}`, + `project=${this.projectRoot}`, + `db=${this.#options.db}`, + `source=${this.#options.source}`, + `samples=${this.#options.samples}`, + '', + ].join('\n'), + ); + } + + #emitStart(step: { + id: string; + title: string; + kind: StepKind; + critical: boolean; + details?: Record; + }): void { + if (this.#options.format === 'pretty') { + console.log(`\n[RUN ] ${step.id} - ${step.title}`); + const details = step.details; + if (details?.command) { + console.log(` cwd: ${details.cwd}`); + console.log(` cmd: ${details.command}`); + } else if (details?.url) { + console.log(` ${details.method} ${details.url}`); + } + } else if (this.#options.format === 'ndjson') { + console.log(JSON.stringify({ event: 'step-start', step })); + } + + this.#appendLog(`START ${step.id} (${step.kind}, critical=${step.critical}) ${step.title}`); + if (step.details) { + this.#appendLog(`DETAIL ${step.id} ${JSON.stringify(step.details)}`); + } + } + + #emitProgress(id: string, title: string, elapsedMs: number): void { + const message = `${id} still running after ${formatDuration(elapsedMs)} - ${title}`; + if (this.#options.format === 'pretty') { + console.log(`[....] ${message}`); + } else if (this.#options.format === 'ndjson') { + console.log(JSON.stringify({ event: 'step-progress', id, title, elapsedMs })); + } + this.#appendLog(`PROGRESS ${message}`); + } + + #emitEnd(step: StepResult): void { + const label = statusLabel(step.status); + if (this.#options.format === 'pretty') { + const suffix = step.error ? ` - ${step.error}` : ''; + console.log( + `[${label}] ${step.id} ${formatDuration(step.durationMs)}${prettyStepExtra(step)}${suffix}`, + ); + const details = compactDetails(step.details); + if (details && (step.status === 'failed' || step.status === 'warning')) { + console.log(` ${JSON.stringify(details)}`); + } + } else if (this.#options.format === 'ndjson') { + console.log(JSON.stringify({ event: 'step', step })); + } + + this.#appendLog( + `END ${label} ${step.id} ${formatDuration(step.durationMs)} ${step.error ?? ''}`.trimEnd(), + ); + if (step.details) { + this.#appendLog(`RESULT ${step.id} ${JSON.stringify(step.details, null, 2)}`); + } + } + + get projectRoot(): string { + return join(this.#options.smokeRoot, this.#options.name); + } + + get appHost(): string { + return join(this.projectRoot, 'aspire', 'apphost.ts'); + } + + get stopCommand(): string[] { + return ['aspire', 'stop', '--apphost', this.appHost, '--non-interactive', '--nologo']; + } + + async run(): Promise { + const startedAt = performance.now(); + this.#initializeLog(); + + try { + await this.#preflight(); + await this.#scaffoldProject(); + await this.#addPlugins(); + await this.#validateGeneratedProject(); + await this.#runDbWorkflow(); + await this.#typeCheckGeneratedProject(); + await this.#startAspire(); + await this.#waitForResources(); + await this.#inspectRuntime(); + await this.#exerciseApis(); + await this.#runPluginVerifier(); + await this.#checkTelemetry(); + } catch (error: unknown) { + if (!(error instanceof SmokeFailure)) { + this.#record({ + id: 'unexpected-error', + title: 'Unexpected tool error', + kind: 'summary', + status: 'failed', + critical: true, + durationMs: 0, + error: unknownToMessage(error), + }); + } + } finally { + if (this.#options.cleanup && this.#startedAspire && !this.#options.dryRun) { + await this.#cleanupAspire(); + } + } + + const report = this.#buildReport(Math.round(performance.now() - startedAt)); + this.#appendLog( + `SUMMARY ok=${report.ok} passed=${report.summary.passed} failed=${report.summary.failed} warnings=${report.summary.warnings} skipped=${report.summary.skipped}`, + ); + if (this.#options.report) { + await ensureDir(dirname(this.#options.report)); + await Deno.writeTextFile(this.#options.report, `${JSON.stringify(report, null, 2)}\n`); + } + return report; + } + + #record(step: StepResult): StepResult { + this.#steps.push(step); + this.#emitEnd(step); + if (step.critical && step.status === 'failed') { + throw new SmokeFailure(step); + } + return step; + } + + #commandArgs(...args: string[]): string[] { + return ['run', '-A', this.#options.cli, ...args]; + } + + async #preflight(): Promise { + await this.#ensureExists('preflight-repo', 'Repo root exists', this.#options.repo); + await this.#ensureExists('preflight-cli', 'CLI entrypoint exists', this.#options.cli); + await this.#runCommand({ + id: 'preflight-deno-version', + title: 'Deno is available', + cwd: this.#options.repo, + command: ['deno', '--version'], + }); + await this.#runCommand({ + id: 'preflight-aspire-version', + title: 'Aspire CLI is available', + cwd: this.#options.repo, + command: ['aspire', '--version'], + }); + await this.#runCommand({ + id: 'preflight-no-running-apphost', + title: 'List running Aspire AppHosts', + cwd: this.#options.repo, + command: ['aspire', 'ps', '--format', 'Json'], + critical: false, + }); + } + + async #ensureExists(id: string, title: string, path: string): Promise { + const startedAt = performance.now(); + this.#emitStart({ + id, + title, + kind: 'summary', + critical: true, + details: { path }, + }); + const pathExists = await exists(path); + this.#record({ + ...createBaseStep(id, title, 'summary', true, startedAt), + status: pathExists ? 'passed' : 'failed', + details: { path }, + error: pathExists ? undefined : `Path does not exist: ${path}`, + }); + } + + async #scaffoldProject(): Promise { + if (!this.#options.dryRun) { + await ensureDir(this.#options.smokeRoot); + } + await this.#runCommand({ + id: 'init-project', + title: 'Scaffold fresh DB-less local project', + cwd: this.#options.repo, + command: [ + 'deno', + ...this.#commandArgs( + 'init', + this.#options.name, + '--ci', + '--path', + this.#options.smokeRoot, + '--db', + 'none', + '--service', + '--service-name', + 'users', + '--service-port', + '3001', + '--app-name', + 'web', + '--editor', + 'none', + '--no-git', + ), + ], + }); + } + + async #addPlugins(): Promise { + const sampleFlag = this.#options.samples ? '--samples' : '--no-samples'; + const plugins: Array<{ id: string; kind: string; name: string; args: string[] }> = [ + { + id: 'plugin-add-workers', + kind: 'worker', + name: 'workers', + args: ['--db', this.#options.db, sampleFlag], + }, + { id: 'plugin-add-sagas', kind: 'saga', name: 'sagas', args: [sampleFlag] }, + { id: 'plugin-add-triggers', kind: 'trigger', name: 'triggers', args: [sampleFlag] }, + { id: 'plugin-add-streams', kind: 'stream', name: 'streams', args: [sampleFlag] }, + ]; + + for (const plugin of plugins) { + await this.#runCommand({ + id: plugin.id, + title: `Add ${plugin.name} official plugin`, + cwd: this.projectRoot, + command: [ + 'deno', + ...this.#commandArgs( + 'plugin', + 'add', + plugin.kind, + '--name', + plugin.name, + ...plugin.args, + '--project-root', + '.', + ), + ], + }); + } + } + + async #validateGeneratedProject(): Promise { + await this.#runCommand({ + id: 'plugin-list', + title: 'List configured plugins', + cwd: this.projectRoot, + command: ['deno', ...this.#commandArgs('plugin', 'list', '--project-root', '.')], + }); + } + + async #runDbWorkflow(): Promise { + await this.#runCommand({ + id: 'aspire-restore', + title: 'Restore Aspire TypeScript SDK modules', + cwd: join(this.projectRoot, 'aspire'), + command: ['aspire', 'restore'], + }); + await this.#runCommand({ + id: 'db-init', + title: 'Create and apply initial migration', + cwd: this.projectRoot, + command: [ + 'deno', + ...this.#commandArgs( + 'db', + 'init', + '--db', + this.#options.db, + '--name', + 'init', + '--project-root', + '.', + ), + ], + }); + await this.#runCommand({ + id: 'db-generate', + title: 'Generate Prisma client and Zod schemas', + cwd: this.projectRoot, + command: [ + 'deno', + ...this.#commandArgs('db', 'generate', '--db', this.#options.db, '--project-root', '.'), + ], + }); + await this.#runCommand({ + id: 'db-seed', + title: 'Seed database', + cwd: this.projectRoot, + command: [ + 'deno', + ...this.#commandArgs('db', 'seed', '--db', this.#options.db, '--project-root', '.'), + ], + }); + await this.#runCommand({ + id: 'db-status-after-init', + title: 'Check DB migration status after init', + cwd: this.projectRoot, + command: [ + 'deno', + ...this.#commandArgs('db', 'status', '--db', this.#options.db, '--project-root', '.'), + ], + }); + } + + async #typeCheckGeneratedProject(): Promise { + await this.#runCommand({ + id: 'deno-check-generated-workspaces', + title: 'Type-check generated packages/plugins/background/services/database', + cwd: this.projectRoot, + command: [ + 'deno', + 'check', + '--unstable-kv', + './packages', + './plugins', + './workers', + './sagas', + './triggers', + './services', + './database', + ], + }); + } + + async #startAspire(): Promise { + await this.#runCommand({ + id: 'aspire-start', + title: 'Start generated Aspire AppHost', + cwd: this.projectRoot, + command: [ + 'aspire', + 'start', + '--apphost', + this.appHost, + '--isolated', + '--non-interactive', + '--nologo', + ], + }); + this.#startedAspire = !this.#options.dryRun; + } + + async #waitForResources(): Promise { + const resources = [ + 'postgres', + 'garnet', + 'workers-api', + 'workers', + 'sagas-api', + 'sagas', + 'triggers-api', + 'triggers', + ]; + + for (const resource of resources) { + await this.#runCommand({ + id: `aspire-wait-${resource}`, + title: `Wait for Aspire resource ${resource}`, + cwd: this.projectRoot, + command: [ + 'aspire', + 'wait', + resource, + '--apphost', + this.appHost, + '--non-interactive', + '--nologo', + ], + }); + } + } + + async #inspectRuntime(): Promise { + await this.#runCommand({ + id: 'aspire-describe', + title: 'Describe generated Aspire topology', + cwd: this.projectRoot, + command: ['aspire', 'describe', '--apphost', this.appHost], + }); + await this.#runCommand({ + id: 'aspire-logs-workers', + title: 'Read workers background logs', + cwd: this.projectRoot, + command: ['aspire', 'logs', 'workers', '--apphost', this.appHost, '-n', '120'], + }); + await this.#runCommand({ + id: 'aspire-logs-workers-api', + title: 'Read workers API logs', + cwd: this.projectRoot, + command: ['aspire', 'logs', 'workers-api', '--apphost', this.appHost, '-n', '120'], + }); + } + + async #exerciseApis(): Promise { + await this.#httpGet( + 'http-workers-health', + 'Workers API health', + `${this.#options.workersUrl}/health`, + ); + await this.#httpGet( + 'http-sagas-live', + 'Sagas API liveness', + `${this.#options.sagasUrl}/health/live`, + ); + await this.#httpGet( + 'http-sagas-ready', + 'Sagas API readiness', + `${this.#options.sagasUrl}/health/ready`, + ); + await this.#httpGet( + 'http-triggers-health', + 'Triggers API health', + `${this.#options.triggersUrl}/health`, + ); + + await this.#httpGet( + 'http-workers-jobs', + 'List worker jobs', + `${this.#options.workersUrl}/api/v1/workers/jobs`, + ); + await this.#httpGet( + 'http-workers-tasks', + 'List worker tasks', + `${this.#options.workersUrl}/api/v1/workers/tasks`, + ); + await this.#httpPost( + 'http-workers-seed', + 'Seed worker demo data through API', + `${this.#options.workersUrl}/api/v1/workers/seed`, + ); + await this.#httpPost( + 'http-workers-trigger-health-job', + 'Trigger workers-plugin-health-check job', + `${this.#options.workersUrl}/api/v1/workers/jobs/workers-plugin-health-check/trigger`, + ); + await this.#sleep('wait-worker-execution', 'Wait for worker execution to complete', 8_000); + await this.#httpGet( + 'http-workers-executions', + 'List recent worker executions', + `${this.#options.workersUrl}/api/v1/workers/executions?limit=10`, + ); + + await this.#httpGet( + 'http-sagas-list', + 'List saga definitions', + `${this.#options.sagasUrl}/api/v1/sagas/sagas`, + ); + await this.#httpGet( + 'http-sagas-instances', + 'List saga instances', + `${this.#options.sagasUrl}/api/v1/sagas/instances`, + ); + await this.#httpGet( + 'http-triggers-list', + 'List trigger definitions', + `${this.#options.triggersUrl}/api/v1/triggers/triggers`, + ); + await this.#httpGet( + 'http-triggers-events', + 'List trigger events', + `${this.#options.triggersUrl}/api/v1/triggers/events`, + ); + } + + async #runPluginVerifier(): Promise { + await this.#runCommand({ + id: 'workers-plugin-verifier', + title: 'Run plugin-owned workers verifier', + cwd: this.projectRoot, + command: [ + 'deno', + 'run', + '--allow-net', + '--allow-env', + 'plugins/workers/verify-plugin.ts', + '--seed', + ], + }); + } + + async #checkTelemetry(): Promise { + if (this.#options.skipTelemetry) { + this.#record({ + id: 'telemetry-skipped', + title: 'Telemetry checks skipped', + kind: 'summary', + status: 'skipped', + critical: false, + durationMs: 0, + }); + return; + } + + const critical = this.#options.strictTelemetry; + await this.#tcpCheck({ + id: 'telemetry-otlp-port', + title: 'OTLP HTTP collector accepts TCP connections', + host: 'localhost', + port: 4318, + critical, + }); + await this.#runCommand({ + id: 'telemetry-otel-logs', + title: 'Query Aspire structured logs for workers', + cwd: this.projectRoot, + command: [ + 'aspire', + 'otel', + 'logs', + 'workers', + '--apphost', + this.appHost, + '--non-interactive', + '--nologo', + '--limit', + '20', + '--format', + 'Json', + ], + critical, + }); + await this.#runCommand({ + id: 'telemetry-otel-traces', + title: 'Query Aspire traces for workers', + cwd: this.projectRoot, + command: [ + 'aspire', + 'otel', + 'traces', + 'workers', + '--apphost', + this.appHost, + '--non-interactive', + '--nologo', + '--limit', + '20', + '--format', + 'Json', + ], + critical, + }); + } + + async #cleanupAspire(): Promise { + await this.#runCommand({ + id: 'aspire-stop', + title: 'Stop generated Aspire AppHost', + cwd: this.projectRoot, + command: this.stopCommand, + critical: false, + }); + this.#stoppedAspire = true; + } + + async #runCommand(options: { + id: string; + title: string; + cwd: string; + command: string[]; + critical?: boolean; + }): Promise { + const critical = options.critical ?? true; + const startedAt = performance.now(); + const commandDetails = { + command: options.command.join(' '), + cwd: options.cwd, + }; + this.#emitStart({ + id: options.id, + title: options.title, + kind: 'command', + critical, + details: commandDetails, + }); + + if (this.#options.dryRun) { + this.#record({ + ...createBaseStep(options.id, options.title, 'command', critical, startedAt), + status: 'skipped', + details: { command: options.command, cwd: options.cwd }, + }); + return; + } + + const heartbeat = setInterval(() => { + this.#emitProgress(options.id, options.title, Math.round(performance.now() - startedAt)); + }, this.#options.progressIntervalMs); + + try { + const [command, ...args] = options.command; + const result = await withTimeout( + this.#options.commandTimeoutMs, + (signal) => + new Deno.Command(command, { + args, + cwd: options.cwd, + stdout: 'piped', + stderr: 'piped', + signal, + }).output(), + ); + + if (result.timedOut || !result.value) { + this.#record({ + ...createBaseStep(options.id, options.title, 'command', critical, startedAt), + status: critical ? 'failed' : 'warning', + details: { command: options.command, cwd: options.cwd, timedOut: true }, + error: `Command timed out after ${this.#options.commandTimeoutMs}ms`, + }); + return; + } + + const output = result.value; + const passed = output.code === 0; + this.#record({ + ...createBaseStep(options.id, options.title, 'command', critical, startedAt), + status: passed ? 'passed' : critical ? 'failed' : 'warning', + details: { + command: options.command, + cwd: options.cwd, + exitCode: output.code, + stdoutTail: tail(decoder.decode(output.stdout)), + stderrTail: tail(decoder.decode(output.stderr)), + }, + error: passed ? undefined : `Command exited with code ${output.code}`, + }); + } catch (error: unknown) { + this.#record({ + ...createBaseStep(options.id, options.title, 'command', critical, startedAt), + status: critical ? 'failed' : 'warning', + details: { command: options.command, cwd: options.cwd }, + error: unknownToMessage(error), + }); + } finally { + clearInterval(heartbeat); + } + } + + async #httpGet(id: string, title: string, url: string): Promise { + await this.#httpRequest(id, title, 'GET', url); + } + + async #httpPost(id: string, title: string, url: string): Promise { + await this.#httpRequest(id, title, 'POST', url); + } + + async #httpRequest(id: string, title: string, method: string, url: string): Promise { + const startedAt = performance.now(); + const critical = true; + this.#emitStart({ + id, + title, + kind: 'http', + critical, + details: { method, url }, + }); + + if (this.#options.dryRun) { + this.#record({ + ...createBaseStep(id, title, 'http', critical, startedAt), + status: 'skipped', + details: { method, url }, + }); + return; + } + + try { + const result = await withTimeout(this.#options.httpTimeoutMs, async (signal) => { + const response = await fetch(url, { method, signal }); + const body = await response.text(); + return { response, body }; + }); + + if (result.timedOut || !result.value) { + this.#record({ + ...createBaseStep(id, title, 'http', critical, startedAt), + status: 'failed', + details: { method, url, timedOut: true }, + error: `HTTP request timed out after ${this.#options.httpTimeoutMs}ms`, + }); + return; + } + + const { response, body } = result.value; + let jsonSummary: unknown; + try { + jsonSummary = summarizeJson(JSON.parse(body)); + } catch { + jsonSummary = undefined; + } + + this.#record({ + ...createBaseStep(id, title, 'http', critical, startedAt), + status: response.ok ? 'passed' : 'failed', + details: { + method, + url, + statusCode: response.status, + ok: response.ok, + bodyPreview: tail(body, 2_000), + jsonSummary, + }, + error: response.ok ? undefined : `HTTP ${response.status} from ${url}`, + }); + } catch (error: unknown) { + this.#record({ + ...createBaseStep(id, title, 'http', critical, startedAt), + status: 'failed', + details: { method, url }, + error: unknownToMessage(error), + }); + } + } + + async #tcpCheck(options: { + id: string; + title: string; + host: string; + port: number; + critical: boolean; + }): Promise { + const startedAt = performance.now(); + this.#emitStart({ + id: options.id, + title: options.title, + kind: 'tcp', + critical: options.critical, + details: { host: options.host, port: options.port }, + }); + + if (this.#options.dryRun) { + this.#record({ + ...createBaseStep(options.id, options.title, 'tcp', options.critical, startedAt), + status: 'skipped', + details: { host: options.host, port: options.port }, + }); + return; + } + + try { + const result = await Promise.race([ + Deno.connect({ hostname: options.host, port: options.port, transport: 'tcp' }), + delay(this.#options.httpTimeoutMs).then(() => null), + ]); + + if (result === null) { + this.#record({ + ...createBaseStep(options.id, options.title, 'tcp', options.critical, startedAt), + status: options.critical ? 'failed' : 'warning', + details: { host: options.host, port: options.port, connected: false, timedOut: true }, + error: `TCP check timed out after ${this.#options.httpTimeoutMs}ms`, + }); + return; + } + + result.close(); + this.#record({ + ...createBaseStep(options.id, options.title, 'tcp', options.critical, startedAt), + status: 'passed', + details: { host: options.host, port: options.port, connected: true }, + }); + } catch (error: unknown) { + this.#record({ + ...createBaseStep(options.id, options.title, 'tcp', options.critical, startedAt), + status: options.critical ? 'failed' : 'warning', + details: { host: options.host, port: options.port, connected: false }, + error: unknownToMessage(error), + }); + } + } + + async #sleep(id: string, title: string, durationMs: number): Promise { + const startedAt = performance.now(); + this.#emitStart({ + id, + title, + kind: 'sleep', + critical: true, + details: { durationMs }, + }); + if (!this.#options.dryRun) { + await delay(durationMs); + } + this.#record({ + ...createBaseStep(id, title, 'sleep', true, startedAt), + status: this.#options.dryRun ? 'skipped' : 'passed', + details: { durationMs }, + }); + } + + #buildReport(durationMs: number): Report { + const failed = this.#steps.filter((step) => step.status === 'failed').length; + const warnings = this.#steps.filter((step) => step.status === 'warning').length; + const skipped = this.#steps.filter((step) => step.status === 'skipped').length; + const passed = this.#steps.filter((step) => step.status === 'passed').length; + const criticalFailed = this.#steps.some((step) => step.critical && step.status === 'failed'); + + return { + ok: !criticalFailed, + startedAspire: this.#startedAspire, + stoppedAspire: this.#stoppedAspire, + project: { + repo: this.#options.repo, + smokeRoot: this.#options.smokeRoot, + name: this.#options.name, + root: this.projectRoot, + appHost: this.appHost, + stopCommand: this.stopCommand, + logFile: this.#options.logFile, + }, + options: { + db: this.#options.db, + source: this.#options.source, + samples: this.#options.samples, + cleanup: this.#options.cleanup, + dryRun: this.#options.dryRun, + skipTelemetry: this.#options.skipTelemetry, + strictTelemetry: this.#options.strictTelemetry, + format: this.#options.format, + progressIntervalMs: this.#options.progressIntervalMs, + }, + summary: { + total: this.#steps.length, + passed, + failed, + warnings, + skipped, + durationMs, + }, + steps: this.#steps, + }; + } +} + +async function main(): Promise { + const options = await parseCliOptions(Deno.args); + if (!options) { + return; + } + + const runner = new SmokeRunner(options); + const report = await runner.run(); + + if (options.format === 'ndjson') { + console.log(JSON.stringify({ event: 'summary', report })); + } else if (options.format === 'pretty') { + printPrettySummary(report); + } else { + console.log(JSON.stringify(report)); + } + + if (!report.ok) { + Deno.exit(1); + } +} + +await main(); diff --git a/AGENTS.md b/AGENTS.md index 11cc999c3..ec0ea5b89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,69 @@ # AGENTS.md -Use `.llm/harness/` for harnessed work. Use `.agents/skills/netscript-doctrine` for package and -plugin architecture decisions, and follow `.agents/rules/*.mdc` where present. +NetScript package and plugin architecture is governed by the Architecture Doctrine under +`docs/architecture/doctrine/`. Use `.agents/skills/netscript-doctrine` for package and plugin +architecture decisions, gates, and debt entries. Use `.agents/skills/netscript-harness` and +`.llm/harness/` for harnessed work. Follow `.agents/rules/*.mdc` where present. + +When the user says `use harness`, activate the harness workflow. The evaluator must be a separate +session from the implementation session. + +## Operating Rules + +1. Doctrine first for `packages/` and `plugins/`: identify archetype, public surface, gates, and + debt before changing framework code. +2. Contract first: define the schema/type contract, then implementation, then tests. +3. Wrap, do not reinvent: prefer Web Platform APIs, `Deno.*`, `@std/*`, and upstream APIs before + local abstractions. +4. Research before writing: check relevant `.agents/` skills, `.llm/harness/` context, doctrine, + repo docs, and existing code before implementing. +5. Drift is explicit: if implementation reality diverges from plan, docs, or doctrine, record it in + the harness run drift/worklog artifacts. +6. Do not delete lock files or caches, and do not run `deno cache --reload`, without approval. + +## Read Order + +Read only what the task needs. + +1. `AGENTS.md` +2. relevant `.agents/skills/*/SKILL.md` +3. relevant doctrine files under `docs/architecture/doctrine/` +4. relevant harness workflow/archetype/gate files under `.llm/harness/` +5. relevant run artifacts under `.llm/tmp/run/` +6. relevant package/plugin docs and README files +7. focused code + +For internal `@netscript/*` package APIs, prefer `deno doc ` and +`deno doc --filter ` before broad implementation reads. + +## Tooling + +Preferred order: + +1. Relevant MCP server or repo-native tool. +2. Reusable Deno scripts in `.llm/tools/` or `tools/`. +3. Focused shell commands. +4. Web search only when repo context is insufficient or freshness is required. + +Use checked-in Deno scripts instead of complex one-off PowerShell when the logic is reusable. +Temporary scratch/output belongs in `.llm/tmp/`; reusable helper scripts belong in `.llm/tools/` or +`tools/` depending on whether they are harness/agent utilities or product-facing repo tooling. + +## Validation + +Run the smallest validation that proves the change. For targeted `deno check` commands that touch +workspace code, include `--unstable-kv`. + +Common commands: + +```powershell +deno task check +deno task test +deno task lint +deno task fmt +deno task publish:dry-run +deno task arch:check +``` Before a branch is considered ready to merge, run the full CLI E2E test suite: @@ -9,5 +71,9 @@ Before a branch is considered ready to merge, run the full CLI E2E test suite: deno task e2e:cli ``` +The bare task runs the `scaffold.runtime` merge-readiness suite with cleanup enabled. For debugging, +use `deno task e2e:cli suites`, `deno task e2e:cli gates scaffold.runtime`, or +`deno task e2e:cli run scaffold.runtime --cleanup --format pretty`. + This gate is expensive. Do not run it for every intermediate implementation loop; run it during the evaluator/merge-readiness pass or when explicitly requested. diff --git a/deno.lock b/deno.lock index 879de9bcf..263125bc2 100644 --- a/deno.lock +++ b/deno.lock @@ -2,11 +2,15 @@ "version": "5", "specifiers": { "jsr:@cliffy/ansi@1.0.1": "1.0.1", + "jsr:@cliffy/command@1.0.0": "1.0.0", "jsr:@cliffy/command@^1.0.0-rc.7": "1.0.1", + "jsr:@cliffy/flags@1.0.0": "1.0.0", "jsr:@cliffy/flags@1.0.1": "1.0.1", + "jsr:@cliffy/internal@1.0.0": "1.0.0", "jsr:@cliffy/internal@1.0.1": "1.0.1", "jsr:@cliffy/keycode@1.0.1": "1.0.1", "jsr:@cliffy/prompt@1": "1.0.1", + "jsr:@cliffy/table@1.0.0": "1.0.0", "jsr:@cliffy/table@1.0.1": "1.0.1", "jsr:@david/console-static-text@0.3": "0.3.4", "jsr:@david/dax@0.43": "0.43.2", @@ -99,6 +103,7 @@ "npm:es-toolkit@^1.30.0": "1.46.1", "npm:hono@4": "4.12.23", "npm:hono@^4.10.6": "4.12.23", + "npm:ioredis@5.4.1": "5.4.1", "npm:ioredis@^5.4.1": "5.11.1", "npm:ioredis@^5.6.1": "5.11.1", "npm:json-canon@^1.0.1": "1.0.1", @@ -122,28 +127,48 @@ "@cliffy/ansi@1.0.1": { "integrity": "46be51d0993a916dbed68564a6630dc1a742ebb0247744e04bc17e85d72f5bed", "dependencies": [ - "jsr:@cliffy/internal", + "jsr:@cliffy/internal@1.0.1", "jsr:@std/encoding", "jsr:@std/io@~0.225.3" ] }, + "@cliffy/command@1.0.0": { + "integrity": "c52a241ea68857fcdaff4f3173eb404f8017d7bc35553b6f533c592b89dde7d2", + "dependencies": [ + "jsr:@cliffy/flags@1.0.0", + "jsr:@cliffy/internal@1.0.0", + "jsr:@cliffy/table@1.0.0", + "jsr:@std/fmt@^1.0.9", + "jsr:@std/text@^1.0.17" + ] + }, "@cliffy/command@1.0.1": { "integrity": "0172d9c7d8aeb26b8f4f44ffa833247e4fbab707e258ee271e10a2a2f87b531d", "dependencies": [ - "jsr:@cliffy/flags", - "jsr:@cliffy/internal", - "jsr:@cliffy/table", + "jsr:@cliffy/flags@1.0.1", + "jsr:@cliffy/internal@1.0.1", + "jsr:@cliffy/table@1.0.1", "jsr:@std/fmt@^1.0.9", "jsr:@std/text@^1.0.17" ] }, + "@cliffy/flags@1.0.0": { + "integrity": "8b57698adc644da8f90422d58976362d41a4ebca39c312ca1c101585d0148feb", + "dependencies": [ + "jsr:@cliffy/internal@1.0.0", + "jsr:@std/text@^1.0.17" + ] + }, "@cliffy/flags@1.0.1": { "integrity": "468dcf65acb33b51ecb2e232c30603209c528b1d1bd9e065921b57fcb342cb01", "dependencies": [ - "jsr:@cliffy/internal", + "jsr:@cliffy/internal@1.0.1", "jsr:@std/text@^1.0.17" ] }, + "@cliffy/internal@1.0.0": { + "integrity": "1e17ccbcd5420093c0a93e5b3827bbdc9abac5195bacf187edc44665e54bdde6" + }, "@cliffy/internal@1.0.1": { "integrity": "9e2bba59ad559b790f09c57219c727a69f0179ebabc07f1bf9db25232b606760" }, @@ -154,7 +179,7 @@ "integrity": "717265ab9a2ad2a3c06d2a090bb3068f1a5a5efd6ea3c6da9ea09274f74830cd", "dependencies": [ "jsr:@cliffy/ansi", - "jsr:@cliffy/internal", + "jsr:@cliffy/internal@1.0.1", "jsr:@cliffy/keycode", "jsr:@std/assert@^1.0.19", "jsr:@std/fmt@^1.0.9", @@ -163,6 +188,12 @@ "jsr:@std/text@^1.0.17" ] }, + "@cliffy/table@1.0.0": { + "integrity": "3fdaa9e1ef1ea62022108adabd826932bdea8dd05497079896febcd41322907f", + "dependencies": [ + "jsr:@std/fmt@^1.0.9" + ] + }, "@cliffy/table@1.0.1": { "integrity": "2b1baa3ef9e16ecb7f3fdcf1deb9f5fa5ffa308ea0e68478224a4c2d44d58166", "dependencies": [ @@ -1440,6 +1471,20 @@ "standard-as-callback" ] }, + "ioredis@5.4.1": { + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dependencies": [ + "@ioredis/commands", + "cluster-key-slot", + "debug", + "denque", + "lodash.defaults", + "lodash.isarguments", + "redis-errors", + "redis-parser", + "standard-as-callback" + ] + }, "is-docker@3.0.0": { "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "bin": true @@ -1533,9 +1578,15 @@ "scripts": true, "bin": true }, + "lodash.defaults@4.2.0": { + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "lodash.includes@4.3.0": { "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "lodash.isarguments@3.1.0": { + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "lodash.isboolean@3.0.3": { "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, diff --git a/examples/playground/.gitignore b/examples/playground/.gitignore deleted file mode 100644 index 24c9f11c9..000000000 --- a/examples/playground/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Dependencies -node_modules/ - -# Build artifacts -.deploy/ -*.exe - -# IDE -.idea/ - -# Aspire -dotnet/**/bin/ -dotnet/**/obj/ -.aspire/ - -# Generated (regenerated by netscript generate) -dotnet/AppHost/appsettings.json - -# OS -.DS_Store -Thumbs.db - -# Environment -.env -.env.local diff --git a/examples/playground/README.md b/examples/playground/README.md deleted file mode 100644 index fc7ce8415..000000000 --- a/examples/playground/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Playground - -A NetScript application scaffolded with `netscript init`. - -## Quick Start - -```bash -# Start the Fresh app directly (no Aspire orchestration) -deno task --cwd apps/dashboard dev -``` - -## Project Structure - -``` -playground/ -├── apps/dashboard/ # Fresh frontend (defineFreshApp) -├── contracts/ # Shared oRPC contracts (versioned) -├── plugins/ # Plugin registry and implementations -├── appsettings.json # NetScript infrastructure config (Services/Databases/…) -├── deno.json # Workspace root configuration -└── netscript.config.ts # NetScript framework configuration -``` - -## Commands - -Use `packages/cli/bin/netscript-dev.ts` for local contributor workflows; -`packages/cli/bin/netscript.ts` mirrors the published public CLI surface. - -| Command | Description | -| ------------------------------------------------------ | ------------------------------------- | -| `deno task --cwd apps/dashboard dev` | Start the Fresh app (Vite dev server) | -| `deno task check` | Type-check all workspace members | -| `deno task lint` | Run linter | -| `deno task fmt` | Format code | -| `deno task test` | Run tests | -| `deno run -A packages/cli/bin/netscript-dev.ts --help` | Show local contributor CLI commands | -| `deno run -A packages/cli/bin/netscript.ts --help` | Show public CLI commands | - -## Learn More - -- [NetScript Documentation](https://github.com/rickylabs/netscript) -- [Deno Manual](https://docs.deno.com) -- [Fresh](https://fresh.deno.dev) -- [oRPC](https://orpc.unnoq.com) diff --git a/examples/playground/apps/dashboard/.generated/manifest.ts b/examples/playground/apps/dashboard/.generated/manifest.ts deleted file mode 100644 index 0431c7155..000000000 --- a/examples/playground/apps/dashboard/.generated/manifest.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This file is seeded by netscript init and regenerated by @netscript/fresh/vite. - -export const routePatterns = { - $route: '/', - health: { - $route: '/health', - }, - examples: { - $route: '/examples', - telemetry: { - $route: '/examples/telemetry', - }, - }, -} as const; diff --git a/examples/playground/apps/dashboard/.generated/routes.ts b/examples/playground/apps/dashboard/.generated/routes.ts deleted file mode 100644 index c87519991..000000000 --- a/examples/playground/apps/dashboard/.generated/routes.ts +++ /dev/null @@ -1,27 +0,0 @@ -// This file is seeded by netscript init and regenerated by @netscript/fresh/vite. - -import { createRouteReference } from '@netscript/fresh/route'; -import { routePatterns } from './manifest.ts'; - -export const routes = { - $route: createRouteReference(routePatterns.$route, { - id: 'home', - kind: 'page', - }), - health: { - $route: createRouteReference(routePatterns.health.$route, { - id: 'health', - kind: 'page', - }), - }, - examples: { - $route: createRouteReference(routePatterns.examples.$route, { - id: 'examples', - kind: 'page', - }), - telemetry: createRouteReference(routePatterns.examples.telemetry.$route, { - id: 'examples.telemetry', - kind: 'page', - }), - }, -} as const; diff --git a/examples/playground/apps/dashboard/assets/components/actions.css b/examples/playground/apps/dashboard/assets/components/actions.css deleted file mode 100644 index b59e74e1e..000000000 --- a/examples/playground/apps/dashboard/assets/components/actions.css +++ /dev/null @@ -1,52 +0,0 @@ -.ns-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--ns-space-2); - font-family: var(--ns-font-sans); - font-size: var(--ns-text-sm); - font-weight: 500; - line-height: 1; - border: 1px solid transparent; - border-radius: var(--ns-radius-md); - padding: var(--ns-space-2) var(--ns-space-4); - cursor: pointer; - transition: - background var(--ns-ease-normal), - border-color var(--ns-ease-normal), - color var(--ns-ease-normal), - box-shadow var(--ns-ease-normal); - white-space: nowrap; - text-decoration: none; -} - -.ns-btn:focus-visible { - outline: 2px solid var(--ns-ring); - outline-offset: 2px; -} - -.ns-btn--primary { - background: var(--ns-primary); - color: var(--ns-primary-fg); - border-color: var(--ns-primary); -} - -.ns-btn--primary:hover { - background: var(--ns-primary-hover); -} - -.ns-btn--ghost { - background: transparent; - color: var(--ns-muted-fg); -} - -.ns-btn--ghost:hover { - background: rgba(235, 228, 210, 0.04); - color: var(--ns-fg); -} - -.ns-btn--sm { - font-size: var(--ns-text-xs); - padding: var(--ns-space-1-5) var(--ns-space-3); - border-radius: var(--ns-radius-sm); -} diff --git a/examples/playground/apps/dashboard/assets/components/feedback.css b/examples/playground/apps/dashboard/assets/components/feedback.css deleted file mode 100644 index ecde11f9d..000000000 --- a/examples/playground/apps/dashboard/assets/components/feedback.css +++ /dev/null @@ -1,35 +0,0 @@ -.ns-inline-notice { - display: flex; - align-items: flex-start; - gap: var(--ns-space-3); - border: 1px solid var(--ns-secondary-border); - color: var(--ns-fg); - border-radius: var(--ns-radius-md); - padding: var(--ns-space-3) var(--ns-space-4); - background: color-mix(in srgb, var(--ns-surface) 92%, transparent); -} - -.ns-inline-notice__body { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: var(--ns-space-1); -} - -.ns-inline-notice__title { - font-size: var(--ns-text-sm); - font-weight: 600; - line-height: 1.4; - color: var(--ns-fg); -} - -.ns-inline-notice__description { - font-size: var(--ns-text-sm); - line-height: 1.6; - color: color-mix(in srgb, var(--ns-fg) 92%, var(--ns-muted-fg)); -} - -.ns-inline-notice--info { - border-color: var(--ns-secondary-border); -} diff --git a/examples/playground/apps/dashboard/assets/components/forms.css b/examples/playground/apps/dashboard/assets/components/forms.css deleted file mode 100644 index 3e1f0e064..000000000 --- a/examples/playground/apps/dashboard/assets/components/forms.css +++ /dev/null @@ -1,26 +0,0 @@ -.ns-input, -.ns-select, -.ns-textarea { - width: 100%; - font-family: var(--ns-font-sans); - font-size: var(--ns-text-sm); - color: var(--ns-fg); - background: var(--ns-surface); - border: 1px solid var(--ns-input-border); - border-radius: var(--ns-radius-md); - padding: var(--ns-space-2) var(--ns-space-3); - transition: border-color var(--ns-ease-normal), box-shadow var(--ns-ease-normal); -} - -.ns-input::placeholder, -.ns-textarea::placeholder { - color: var(--ns-muted-fg); -} - -.ns-input:focus, -.ns-select:focus, -.ns-textarea:focus { - outline: none; - border-color: var(--ns-ring); - box-shadow: 0 0 0 2px rgba(200, 149, 108, 0.15); -} diff --git a/examples/playground/apps/dashboard/assets/components/surfaces.css b/examples/playground/apps/dashboard/assets/components/surfaces.css deleted file mode 100644 index ba4aabc63..000000000 --- a/examples/playground/apps/dashboard/assets/components/surfaces.css +++ /dev/null @@ -1,59 +0,0 @@ -.ns-card { - border-radius: var(--ns-radius-xl); - border: 1px solid var(--ns-border-hover); - overflow: hidden; - background: var(--ns-card); - color: var(--ns-card-fg); - box-shadow: var(--ns-shadow-xs); -} - -.ns-card__header { - display: flex; - flex-direction: column; - gap: var(--ns-space-2); - padding: var(--ns-space-5) var(--ns-space-6); - border-bottom: 1px solid var(--ns-border); -} - -.ns-card__title { - font-size: var(--ns-text-sm); - font-weight: 600; - line-height: 1.35; -} - -.ns-card__description { - font-size: var(--ns-text-sm); - line-height: 1.55; - color: var(--ns-muted-fg); -} - -.ns-card__body { - padding: var(--ns-space-5) var(--ns-space-6); -} - -.ns-card__footer { - padding: var(--ns-space-4) var(--ns-space-6); - border-top: 1px solid var(--ns-border); - background: color-mix(in srgb, var(--ns-card) 78%, var(--ns-surface)); -} - -.ns-badge { - display: inline-flex; - align-items: center; - gap: var(--ns-space-1); - font-family: var(--ns-font-mono); - font-size: 0.67rem; - font-weight: 500; - line-height: 1.2; - text-transform: uppercase; - letter-spacing: 0.08em; - padding: var(--ns-space-0-5) var(--ns-space-2); - border-radius: var(--ns-radius-sm); - white-space: nowrap; -} - -.ns-badge--primary { - color: var(--ns-primary); - background: var(--ns-primary-subtle); - border: 1px solid var(--ns-primary-border); -} diff --git a/examples/playground/apps/dashboard/assets/layouts.css b/examples/playground/apps/dashboard/assets/layouts.css deleted file mode 100644 index 4ede96dc8..000000000 --- a/examples/playground/apps/dashboard/assets/layouts.css +++ /dev/null @@ -1,106 +0,0 @@ -/* Layout primitives sourced from the playground design-system seed. */ - -.ns-shell { - width: min(var(--ns-shell-max, 1060px), calc(100% - var(--ns-space-8))); - margin-inline: auto; -} - -.ns-shell--wide { - --ns-shell-max: 1280px; -} - -.ns-shell--narrow { - --ns-shell-max: 720px; -} - -.ns-stack { - display: flex; - flex-direction: column; - gap: var(--ns-stack-gap, var(--ns-space-4)); -} - -.ns-stack--xs { - --ns-stack-gap: var(--ns-space-1); -} - -.ns-stack--sm { - --ns-stack-gap: var(--ns-space-2); -} - -.ns-stack--md { - --ns-stack-gap: var(--ns-space-4); -} - -.ns-stack--lg { - --ns-stack-gap: var(--ns-space-6); -} - -.ns-stack--xl { - --ns-stack-gap: var(--ns-space-8); -} - -.ns-stack--2xl { - --ns-stack-gap: var(--ns-space-12); -} - -.ns-grid { - display: grid; - gap: var(--ns-grid-gap, var(--ns-space-5)); - grid-template-columns: repeat(auto-fit, minmax(min(var(--ns-grid-min, 280px), 100%), 1fr)); -} - -.ns-section { - display: flex; - flex-direction: column; - gap: var(--ns-section-gap, var(--ns-space-4)); - padding-block: var(--ns-section-block, var(--ns-space-6)); -} - -.ns-section--lg { - --ns-section-block: var(--ns-space-8); - --ns-section-gap: var(--ns-space-5); -} - -.ns-topbar { - position: sticky; - top: 0; - z-index: var(--ns-z-sticky); - height: 52px; - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--ns-space-4); - padding-inline: var(--ns-space-8); - border-bottom: 1px solid var(--ns-border); - background: color-mix(in srgb, var(--ns-bg) 84%, transparent); - backdrop-filter: blur(10px); -} - -.ns-skip-link { - position: absolute; - left: var(--ns-space-4); - top: var(--ns-space-3); - z-index: var(--ns-z-toast); - transform: translateY(-200%); - border: 1px solid var(--ns-ring); - border-radius: var(--ns-radius-sm); - background: var(--ns-bg); - color: var(--ns-fg); - padding: var(--ns-space-2) var(--ns-space-3); - font-size: var(--ns-text-sm); - transition: transform var(--ns-ease-fast); -} - -.ns-skip-link:focus-visible { - transform: translateY(0); -} - -@media (max-width: 640px) { - .ns-topbar { - padding-inline: var(--ns-space-4); - } - - .ns-shell { - width: calc(100% - var(--ns-space-6)); - } -} diff --git a/examples/playground/apps/dashboard/assets/styles.css b/examples/playground/apps/dashboard/assets/styles.css deleted file mode 100644 index 130cfdd31..000000000 --- a/examples/playground/apps/dashboard/assets/styles.css +++ /dev/null @@ -1,91 +0,0 @@ -@import 'tailwindcss'; -@import './tokens.css'; -@import './layouts.css'; -@import './components/actions.css'; -@import './components/forms.css'; -@import './components/surfaces.css'; -@import './components/feedback.css'; - -@theme { - --color-ns-bg: var(--ns-bg); - --color-ns-fg: var(--ns-fg); - --color-ns-surface: var(--ns-surface); - --color-ns-surface-raised: var(--ns-surface-raised); - --color-ns-overlay: var(--ns-overlay); - --color-ns-card: var(--ns-card); - --color-ns-card-fg: var(--ns-card-fg); - --color-ns-primary: var(--ns-primary); - --color-ns-primary-fg: var(--ns-primary-fg); - --color-ns-primary-hover: var(--ns-primary-hover); - --color-ns-primary-subtle: var(--ns-primary-subtle); - --color-ns-secondary: var(--ns-secondary); - --color-ns-secondary-fg: var(--ns-secondary-fg); - --color-ns-secondary-hover: var(--ns-secondary-hover); - --color-ns-secondary-subtle: var(--ns-secondary-subtle); - --color-ns-muted: var(--ns-muted); - --color-ns-muted-fg: var(--ns-muted-fg); - --color-ns-accent: var(--ns-accent); - --color-ns-accent-fg: var(--ns-accent-fg); - --color-ns-accent-subtle: var(--ns-accent-subtle); - --color-ns-success: var(--ns-success); - --color-ns-success-fg: var(--ns-success-fg); - --color-ns-success-subtle: var(--ns-success-subtle); - --color-ns-warning: var(--ns-warning); - --color-ns-warning-fg: var(--ns-warning-fg); - --color-ns-warning-subtle: var(--ns-warning-subtle); - --color-ns-destructive: var(--ns-destructive); - --color-ns-destructive-fg: var(--ns-destructive-fg); - --color-ns-destructive-subtle: var(--ns-destructive-subtle); - --color-ns-border: var(--ns-border); - --color-ns-border-hover: var(--ns-border-hover); - --color-ns-border-strong: var(--ns-border-strong); - --color-ns-input: var(--ns-input-border); - --color-ns-ring: var(--ns-ring); - --font-sans: var(--ns-font-sans); - --font-mono: var(--ns-font-mono); - --radius-xs: 2px; - --radius-sm: 4px; - --radius-md: 6px; - --radius-lg: 8px; - --radius-xl: 12px; - --radius-2xl: 16px; - --shadow-xs: var(--ns-shadow-xs); - --shadow-sm: var(--ns-shadow-sm); - --shadow-md: var(--ns-shadow-md); - --shadow-lg: var(--ns-shadow-lg); -} - -@layer base { - html { - @apply bg-ns-bg text-ns-fg; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - color-scheme: dark; - min-height: 100%; - } - - html[data-theme='light'] { - color-scheme: light; - } - - body { - @apply bg-ns-bg text-ns-fg font-sans; - min-height: 100vh; - -webkit-tap-highlight-color: rgba(200, 149, 108, 0.18); - transition: background-color 0.2s ease, color 0.2s ease; - } - - a, - button { - touch-action: manipulation; - } - - h1, - h2, - h3, - h4 { - @apply tracking-tight leading-tight; - text-wrap: balance; - scroll-margin-top: 5rem; - } -} diff --git a/examples/playground/apps/dashboard/assets/tokens.css b/examples/playground/apps/dashboard/assets/tokens.css deleted file mode 100644 index a8e797205..000000000 --- a/examples/playground/apps/dashboard/assets/tokens.css +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Initial token seed for @netscript/fresh-ui. - * - * Current state: - * - checked-in CSS custom properties - * - copied into consumer apps as owned source - * - * Planned direction: - * - Style Dictionary becomes the canonical token source - * - this file becomes a generated artifact - */ - -:root { - color-scheme: dark; - - --ns-gray-1: #fafaf9; - --ns-gray-2: #f1efe9; - --ns-gray-3: #e2ddd3; - --ns-gray-4: #cbc5b8; - --ns-gray-5: #b5b3ad; - --ns-gray-6: #87847d; - --ns-gray-7: #65615b; - --ns-gray-8: #4a4844; - --ns-gray-9: #2c2b28; - --ns-gray-10: #1e1e1c; - --ns-gray-11: #171716; - --ns-gray-12: #111110; - - --ns-copper-1: #f8efe8; - --ns-copper-2: #efd7c3; - --ns-copper-3: #e4bd9d; - --ns-copper-4: #dca97e; - --ns-copper-5: #d4a76a; - --ns-copper-6: #c8956c; - --ns-copper-7: #b07d52; - --ns-copper-8: #8f6340; - - --ns-teal-1: #eff9f7; - --ns-teal-2: #cde9e4; - --ns-teal-3: #9fd3cb; - --ns-teal-4: #7cc8be; - --ns-teal-5: #5ba8a0; - --ns-teal-6: #4a918a; - --ns-teal-7: #3a7570; - - --ns-slate-1: #f3f5f8; - --ns-slate-2: #dde1e8; - --ns-slate-3: #bcc4d0; - --ns-slate-4: #a8aeb8; - --ns-slate-5: #9098a6; - --ns-slate-6: #777f8d; - --ns-slate-7: #5f6773; - - --ns-red-4: #eb6b6b; - --ns-red-5: #e04545; - --ns-red-6: #c73636; - --ns-red-7: #a02d2d; - - --ns-amber-4: #f0c56b; - --ns-amber-5: #e0ae4f; - --ns-amber-6: #c89438; - - --ns-bg: var(--ns-gray-12); - --ns-fg: var(--ns-gray-1); - --ns-surface: var(--ns-gray-11); - --ns-surface-raised: var(--ns-gray-10); - --ns-overlay: rgba(17, 17, 16, 0.8); - - --ns-card: var(--ns-gray-10); - --ns-card-fg: var(--ns-gray-1); - - --ns-primary: var(--ns-copper-6); - --ns-primary-fg: var(--ns-gray-12); - --ns-primary-hover: var(--ns-copper-7); - --ns-primary-subtle: rgba(200, 149, 108, 0.08); - --ns-primary-border: rgba(200, 149, 108, 0.18); - - --ns-secondary: var(--ns-slate-5); - --ns-secondary-fg: var(--ns-gray-12); - --ns-secondary-hover: var(--ns-slate-6); - --ns-secondary-subtle: rgba(144, 152, 166, 0.07); - --ns-secondary-border: rgba(144, 152, 166, 0.14); - - --ns-muted: var(--ns-gray-9); - --ns-muted-fg: var(--ns-gray-5); - - --ns-accent: var(--ns-copper-6); - --ns-accent-fg: var(--ns-gray-12); - --ns-accent-subtle: rgba(200, 149, 108, 0.08); - --ns-accent-border: rgba(200, 149, 108, 0.18); - - --ns-success: var(--ns-teal-5); - --ns-success-fg: var(--ns-gray-12); - --ns-success-subtle: rgba(91, 168, 160, 0.07); - --ns-success-border: rgba(91, 168, 160, 0.15); - - --ns-warning: var(--ns-amber-5); - --ns-warning-fg: var(--ns-gray-12); - --ns-warning-subtle: rgba(224, 174, 79, 0.08); - --ns-warning-border: rgba(224, 174, 79, 0.16); - - --ns-destructive: var(--ns-red-5); - --ns-destructive-fg: #ffffff; - --ns-destructive-subtle: rgba(224, 69, 69, 0.08); - --ns-destructive-border: rgba(224, 69, 69, 0.16); - - --ns-border: rgba(235, 228, 210, 0.06); - --ns-border-hover: rgba(235, 228, 210, 0.12); - --ns-border-strong: rgba(235, 228, 210, 0.18); - --ns-input-border: rgba(235, 228, 210, 0.1); - --ns-ring: var(--ns-copper-6); - - --ns-font-sans: 'DM Sans', 'Segoe UI', system-ui, -apple-system, sans-serif; - --ns-font-mono: 'DM Mono', ui-monospace, 'Cascadia Code', 'Fira Code', monospace; - - --ns-text-xs: 0.75rem; - --ns-text-sm: 0.875rem; - --ns-text-base: 1rem; - --ns-text-lg: 1.125rem; - --ns-text-xl: 1.25rem; - --ns-text-2xl: 1.5rem; - --ns-text-3xl: 1.875rem; - --ns-text-4xl: 2.25rem; - - --ns-leading-tight: 1.1; - --ns-leading-snug: 1.3; - --ns-leading-normal: 1.5; - --ns-leading-relaxed: 1.7; - - --ns-tracking-tight: -0.02em; - --ns-tracking-normal: 0; - --ns-tracking-wide: 0.12em; - - --ns-space-0: 0; - --ns-space-px: 1px; - --ns-space-0-5: 0.125rem; - --ns-space-1: 0.25rem; - --ns-space-1-5: 0.375rem; - --ns-space-2: 0.5rem; - --ns-space-3: 0.75rem; - --ns-space-4: 1rem; - --ns-space-5: 1.25rem; - --ns-space-6: 1.5rem; - --ns-space-8: 2rem; - --ns-space-10: 2.5rem; - --ns-space-12: 3rem; - --ns-space-16: 4rem; - --ns-space-20: 5rem; - - --ns-radius-sm: 4px; - --ns-radius-md: 6px; - --ns-radius-lg: 8px; - --ns-radius-xl: 12px; - --ns-radius-2xl: 16px; - --ns-radius-full: 9999px; - - --ns-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.18); - --ns-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.1); - --ns-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.08); - --ns-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.12), 0 4px 6px rgba(0, 0, 0, 0.06); - --ns-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.06); - - --ns-ease-fast: 100ms ease; - --ns-ease-normal: 150ms ease; - --ns-ease-slow: 250ms ease; - --ns-ease-spring: 300ms cubic-bezier(0.34, 1.56, 0.64, 1); - - --ns-z-base: 0; - --ns-z-dropdown: 10; - --ns-z-sticky: 20; - --ns-z-overlay: 30; - --ns-z-modal: 40; - --ns-z-toast: 50; -} - -[data-theme='light'] { - color-scheme: light; - - --ns-bg: var(--ns-gray-1); - --ns-fg: var(--ns-gray-12); - --ns-surface: var(--ns-gray-2); - --ns-surface-raised: #ffffff; - --ns-overlay: rgba(0, 0, 0, 0.4); - - --ns-card: #ffffff; - --ns-card-fg: var(--ns-gray-12); - - --ns-primary-subtle: rgba(200, 149, 108, 0.1); - --ns-primary-border: rgba(200, 149, 108, 0.25); - --ns-secondary-subtle: rgba(144, 152, 166, 0.1); - --ns-secondary-border: rgba(144, 152, 166, 0.2); - - --ns-muted: var(--ns-gray-3); - --ns-muted-fg: var(--ns-gray-7); - - --ns-success-subtle: rgba(91, 168, 160, 0.1); - --ns-success-border: rgba(91, 168, 160, 0.2); - --ns-warning-subtle: rgba(224, 174, 79, 0.1); - --ns-warning-border: rgba(224, 174, 79, 0.2); - --ns-destructive-subtle: rgba(224, 69, 69, 0.1); - --ns-destructive-border: rgba(224, 69, 69, 0.2); - - --ns-border: rgba(0, 0, 0, 0.08); - --ns-border-hover: rgba(0, 0, 0, 0.14); - --ns-border-strong: rgba(0, 0, 0, 0.22); - --ns-input-border: var(--ns-gray-4); - - --ns-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05); - --ns-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); - --ns-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.04); - --ns-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.06), 0 4px 6px rgba(0, 0, 0, 0.03); -} diff --git a/examples/playground/apps/dashboard/client.ts b/examples/playground/apps/dashboard/client.ts deleted file mode 100644 index 555d7abb4..000000000 --- a/examples/playground/apps/dashboard/client.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Import CSS files here for hot module reloading to work. -import './assets/styles.css'; diff --git a/examples/playground/apps/dashboard/components/ui/button.tsx b/examples/playground/apps/dashboard/components/ui/button.tsx deleted file mode 100644 index cbb9ce01b..000000000 --- a/examples/playground/apps/dashboard/components/ui/button.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { cn } from '@netscript/fresh-ui'; -import type { ComponentChildren, JSX, VNode } from 'preact'; - -interface SharedButtonProps { - children: ComponentChildren; - variant?: ButtonVariant; - size?: ButtonSize; - disabled?: boolean; - loading?: boolean; - class?: string; - icon?: ComponentChildren; - iconPosition?: 'left' | 'right'; -} - -interface FreshAnchorNavigationAttributes extends JSX.AnchorHTMLAttributes { - 'f-client-nav'?: boolean; -} - -export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'; -export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'; -export type ButtonElementType = 'button' | 'submit' | 'reset'; - -type ButtonAsButtonProps = - & SharedButtonProps - & Omit, 'children' | 'class' | 'type'> - & { - type?: ButtonElementType; - href?: never; - clientNav?: never; - }; - -type ButtonAsLinkProps = - & SharedButtonProps - & Omit, 'children' | 'class' | 'href' | 'type'> - & { - type: 'link'; - href: string; - clientNav?: boolean; - }; - -type ButtonProps = ButtonAsButtonProps | ButtonAsLinkProps; - -const VARIANT_CLASSES: Record, string> = { - primary: 'ns-btn--primary', - secondary: 'ns-btn--secondary', - outline: 'ns-btn--outline', - ghost: 'ns-btn--ghost', - destructive: 'ns-btn--destructive', -}; - -const SIZE_CLASSES: Record, string | undefined> = { - sm: 'ns-btn--sm', - md: undefined, - lg: 'ns-btn--lg', - icon: 'ns-btn--icon', -}; - -export function Button(props: ButtonProps): VNode { - const { - children, - variant = 'primary', - size = 'md', - disabled = false, - loading = false, - class: className, - icon, - iconPosition = 'left', - } = props; - const classes = cn('ns-btn', VARIANT_CLASSES[variant], SIZE_CLASSES[size], className); - const leadingIcon = loading - ?