diff --git a/.gitignore b/.gitignore index c24f3d2..03462e7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,14 @@ hooks/dist/ # Animation assets animation/ *.gif + +# spec2rtl v1.0 M1 +core/dist/ +core/node_modules/ +bench/RESULTS.md.tmp +.spec2rtl/ +*.tsbuildinfo +events.log + +# Hermes scratch — local-only plans, scratch, never commit +.hermes/ diff --git a/M1.1_RELEASE_REPORT.md b/M1.1_RELEASE_REPORT.md new file mode 100644 index 0000000..cf401f5 --- /dev/null +++ b/M1.1_RELEASE_REPORT.md @@ -0,0 +1,234 @@ +# M1.1 Release Report — `spec2rtl-cc` `v1.0.1-m1` + +**Substrate stabilisation. No new features. The substrate is now frozen.** + +This is the gate report. It records the architectural changes, the verification results, the known remaining debt, and whether the substrate is ready to freeze before `compile-context` features land on top. + +--- + +## 1. Architecture changes + +The M1 substrate shipped a deterministic compiler and a markdown-based runtime. The `.hermes/reviews/` documents (ARCHITECTURE_REVIEW, SUBSTRATE_RISKS, STABLE_API, SCHEMA_RECOMMENDATIONS) flagged four MUST-fixes and several deferred items. v1.0.1-m1 lands the four MUST-fixes and the **Option B architectural correction** that the user explicitly asked for before T10's commit. + +### 1.1 The Option B correction (T10) + +The original v1.0.1-m1 draft plan called for `compile()` to take an optional `artefact_store` field, hash the body's bytes, and write them out — making `compile()` an `async` function because of the filesystem write. Architecture review (Option B memo) rejected this. v1.0.1-m1 ships Option B instead. + +**Substrate as it ships:** + +``` +compile() computes. ← pure, sync, deterministic, zero I/O +ArtefactStore persists. ← async, content-addressed, generic +CLI composes both. ← compile → bridge → store.record +``` + +**Why this matters:** + +- `compile()` is bytewise deterministic and side-effect-free. It can run in memory, in a serverless function, in a thread, or in a test. No `await` for `compile()` itself. +- `ArtefactStore` is generic. It carries `{ type, ref, body }` envelopes where `body` is opaque bytes. The same store persists context packages, AST snapshots, lint reports, synthesis reports, timing reports, waveforms, benchmarks, and verification results — without the store ever knowing what any of those are. +- Plugin authors who later want a remote artefact store (S3, OCI, internal HTTP) implement the same `ArtefactStore` API. The compiler never has to grow a network code path. + +**Internal changes for Option B:** + +| File | Change | +|---|---| +| `core/src/compiler.ts` | `compile()` reverted to sync; `CompileInputs.artefact_store` and `CompileOutput.artefact_sha` removed. The compiler's function body has **zero** I/O calls. | +| `core/src/artefact.ts` | Redesigned around an opaque `Artefact` envelope (`type`, `ref`, `body`). The store never inspects bytes; only the producer's metadata shows up in the manifest. | +| `core/src/cli/artefact-bridge.ts` | New. Owns the `CompileOutput → Artefact` serialisation, including the decision that `task_id` is *not* in the body (preserving the content-addressing invariant). | +| `core/src/cli/index.ts` | Composes `compile()` (sync) with `store.record(artefact)` (async). The compile step stays synchronous; only the persistence step is async. | +| `core/test/artefact.test.ts` | All 9 tests verify the store against opaque bytes (one test uses a 5-byte non-JSON buffer to assert the store really doesn't inspect). | + +### 1.2 The four MUST-fixes from the architecture review + +| Risk | What changed | Where | +|---|---|---| +| **R-4** Cache key correctness | The `input_hash` previously dropped `description`, `exemplars_per_agent`, and `soft_token_margin` from the digest input. The full profile object is now canonicalised into the hash. | `core/src/compiler.ts:221` | +| **R-5** Open entity kinds | `Entity` is now `{ kind, id, fields, extensions? }`. First-class kinds ship with `isX` narrowing helpers. Plugins can register new kinds via `KindRegistry.register(r)`. | `core/src/entities.ts`, `core/src/kind-registry.ts` | +| **R-3** Artefact store | See Option B above. | `core/src/artefact.ts`, `core/src/cli/artefact-bridge.ts` | +| **R-2** AST layer depth | `ModuleDecl` exposes ports (with direction + width), parameters (name + default), clock signals, reset signals, reset polarity. `Instantiation` exposes parameter overrides. The compiler renders these into the `ast_slice` layer. | `core/src/sv-scanner.ts`, `core/src/ast.ts`, `core/src/compiler.ts` (`renderAst`) | + +### 1.3 SCHEMA-B (SignalRef unification) + +Was two near-identical types: `Signal` (in entities) and `ContractSignal` (in contracts). Now one type, `SignalRef`, in `core/src/types.ts`. The contract layer aliases it; the contract parser delegates validation to `isSignalRef`. Roughly 20 LOC of duplicate validation gone. + +### 1.4 Fixture migration + +`core/bench/uart/entities.json` rewritten in open shape. The strict `addEntity()` validation (in `core/src/entities.ts`) was already enforced since T3; the fixture file was the last piece. + +--- + +## 2. Final verification results + +### 2.1 Verification gauntlet (run from `/tmp/.hermes-verify-spec2rtl-m11.sh`) + +| # | Check | Result | +|---|---|---| +| 1 | `npm run build:core` exits clean | **PASS** | +| 2 | `npm run test:core` reports tests passing | **PASS** (106/106) | +| 3a | Ablation reports `Verdict: PASS` | **PASS** | +| 3b | Variance = 0.00% across 10 reruns | **PASS** | +| 3c | Manifest count = 1 after 10 reruns (content-addressing invariant) | **PASS** | +| 4 | `git diff --stat 90f8149 HEAD -- commands/ spec2rtl/ hooks/ bin/install.js` is empty | **PASS** | +| 5 | `compile()` is I/O-free (no `node:fs` imports, no I/O calls in body) | **PASS** | +| 6 | `package.json` version is `1.0.1-m1` | **PASS** | + +**8/8 gates green.** This is *ad-hoc* verifier-script evidence (not a CI suite); a CI suite lands in v1.0.2 once M2's runtime is in scope. + +### 2.2 Test inventory + +``` +Test Files 13 passed (13) +Tests 106 passed (106) +``` + +Breakdown by file: + +| File | Tests | Subject | +|---|---|---| +| `signal-ref.test.ts` | 11 | Shared `SignalRef` type | +| `contract.test.ts` | 5 | Contract YAML parser (UART + AXI4-Lite) | +| `contract-signal-alias.test.ts` | 4 | `ContractSignal` aliases `SignalRef` | +| `entities.test.ts` | 18 | Open-shape entity model + 11 narrowing helpers | +| `kind-registry.test.ts` | 12 | `KindRegistry` plugin surface | +| `profile.test.ts` | 4 | Profile YAML loader | +| `hash.test.ts` | 4 | Content-addressing digest helper | +| `ast.test.ts` | 15 | AST layer (ports, parameters, clocks, reset polarity) | +| `compiler-cache-key.test.ts` | 5 | Cache key now includes the full profile | +| `determinism.test.ts` | 3 | 10-run determinism (content_hash, layers, tokens) | +| `compiler.test.ts` | 13 | 5-layer pipeline + token budget + AST rendering + bridge | +| `artefact.test.ts` | 9 | Generic artefact store + opaque-body tests | +| `events.test.ts` | 3 | Append-only `events.log` | + +### 2.3 Ablation evidence (`core/bench/RESULTS.md`) + +``` +Question 1 — compile() determinism (cache key invariance) + Unique content_hashes across 10 reruns: 1 + Variance: 0.00% (PASS criterion ≤ 2%) + Token counts: min=614 median=614 max=614 + Verdict: PASS ✅ + +Question 2 — ArtefactStore content-addressing invariant + Manifest files at /.spec2rtl/artefacts/manifests/ after 10 reruns: 1 + Criterion: exactly 1 (identical bodies → identical sha → no duplicate writes) + Verdict: PASS ✅ + +Overall: PASS ✅ +``` + +### 2.4 Architectural invariant check (the Option B invariant, verified by source inspection) + +The `compile()` function body — 1,593 characters — has: +- 0 references to `node:fs` imports +- 0 calls to filesystem functions (`readFileSync`, `writeFileSync`, `existsSync`, `mkdirSync`, `readdirSync`, `appendFileSync`, `readFile`, `writeFile`) +- 0 network calls +- 0 `await` statements + +`compile()` is mathematically I/O-free. The compiler can run entirely in memory. + +### 2.5 v0.1 untouched + +```bash +git diff --stat 90f8149 HEAD -- commands/ spec2rtl/ hooks/ bin/install.js +# → empty +``` + +The v0.1 plugin markdown (every `commands/s2r/*.md` and `spec2rtl/agents/*.md`) is byte-identical to its state at the v0.1 release commit. Zero regressions in user-facing behaviour. + +--- + +## 3. Remaining known technical debt + +v1.0.1-m1 fixes the four MUSTs from the architecture review. Six SHOULD/CAN items from the same review remain — all deliberate, all parked to v1.0.2+: + +| Item | Risk | Why deferred | +|---|---|---| +| **R-6:** `validateContractSemantics` (sva_template references unwired signals) | SHOULD | Awaiting closed-union-via-registry stabilisation. The validated contract surface in v1.0.1-m1 is shape-only. | +| **R-7:** Yargs migration for the CLI | SHOULD | The CLI ships one subcommand. Yargs lands when M2 adds the second (likely `run`). | +| **R-8:** EventsLog accepts unvalidated shapes (`append` doesn't check) | SHOULD | Compile event shape is FROZEN; runtime events land in M2 and bring their own validator. | +| **R-1 entity-store index** | CAN | The current linear scans (`findEntities`, `findRelations`) are fine at expected M1 scale (≤ 100 entities, ≤ 1000 relations). Index lands when M2 ships the executor and the boundary matters. | +| **R-9 tree-sitter swap** | CAN | The regex scanner is bounded by what the M1 ablation exercises. Real SV in M2 (AXI shims, FIFOs, FSMs) will stress it. The port-list / parameter / clock-domain extractions (T7) are forward-compatible: they map cleanly onto tree-sitter. | +| **R-11 tokeniser interface** | CAN | The `0.25 tokens/char` heuristic is good enough for content-comparison determinism, which is what M1 cares about. Real cost-tracking lands in M3. | + +Also parked (with explicit reasons in the architecture review): + +- v0.1 → v1.0 migration tool (schema is now stable in M1.1; migrator lives in M2). +- `graph.s2r` storage format (entities flow through `entities.json`; the YAML layer lands in v1.0.2 after a milestone of real use). +- New specialist agents, dashboard, EDA adapters (M2+). +- Knowledge engine / RAG / embeddings (post-v1.0). + +The latter list is **non-goals**, not debt. They are deliberately out of scope. + +--- + +## 4. Is the substrate ready to freeze? + +**Yes.** Here's why, and the corresponding invariants. + +The substrate is **frozen** as of v1.0.1-m1. The next minor release (v1.0.2) cannot break any of: + +| Invariant | How it's enforced | +|---|---| +| 1. The engineering graph contains no execution state. | Typed `Entity` with no `status`/`retry`/`outputs[]` fields. Runtime concerns live in `events.log` and (post-M2) in the runtime store. | +| 2. `compile()` produces byte-identical output for byte-identical inputs. | `core/test/determinism.test.ts` — 10-run assertion. Ablation — variance 0.00%. Hash collisions are part of the type-test surface. | +| 3. Contracts are facts, not rules. | Contract schema has `interfaces` + `invariants`. No `behaviour_required`, no `must_match_this_output`. The contract parser doesn't accept those fields. | +| 4. The engineering graph is a graph, not a tree. | Entities have `id: string`. Relationships are flat `from`/`rel`/`to`. Hierarchies are typed edges; there is no `parent_id` field. | +| 5. `compile()` performs zero I/O. | Source inspection: no `node:fs` import, no I/O call in the function body. The compiler runs entirely in memory. | +| 6. Extensions are plugins, not core edits. | `KindRegistry.register(r)` accepts new kinds. `Artefact` envelope is opaque. The CLI is the seam between `compile()` and the store; neither side knows the other's contract details. | + +These six are the architectural commitment that v1.0.1-m1 carries forward. They are the answer to *"is the substrate ready to freeze"*: the work above is invariant-tested, the work the architecture review recommended is shipped or deliberately parked, and the public surface (CLI args, contract YAML, profile YAML, event log shape) is byte-stable. + +**Recommendation: tag this commit as `v1.0.1-m1`.** Do not begin M2 runtime work until the user gives the explicit go-ahead. + +--- + +## 5. Commits shipped in v1.0.1-m1 + +``` +bc046ff feat(v1.0.1-m1): Option B — compile() pure; ArtefactStore generic +2a05cc8 feat(v1.0.1-m1): bench fixture open-shape; ablation → dual gate +3c1a3f7 chore(v1.0.1-m1): bump version 1.0.1-m1 + substrate stabilisation README +``` + +Plus the M1 work that the architecture review took as given: + +``` +7c5d5fe fix(v1.0.1-m1): pin vitest root so npm run test:core works from repo root +e22ca9e feat(v1.0.1-m1): AST layer exposes ports, parameters, clocks, reset polarity +c669763 feat(v1.0.1-m1): AST slice renderer surfaces ports, parameters, clocks, reset +eabdcb9 refactor(v1.0.1-m1): renderContracts delegates to renderSignalRef +c0f6ea6 fix(v1.0.1-m1): full profile in cache key (correctness fix, R-4) +3af9b73 feat(v1.0.1-m1): open-shape Entity with narrowing helpers +78050b5 refactor(v1.0.1-m1): ContractSignal now aliases SignalRef +e22ca9e feat(v1.0.1-m1): +``` + +(Prior M1 commits listed in `git log 90f8149..HEAD`.) + +--- + +## 6. What `compile-context` features look like next (NOT shipped here) + +The substrate is frozen. The *next* milestone does *not* touch `compile()`, the contract parser, the AST layer, or the entity model. It adds: + +- Runtime executor (a real DAG runner, not just compile). +- EDA feedback loop (Yosys / Verilator adapters writing to the same `ArtefactStore`). +- v0.1 → v1.0 migration tool. +- `graph.s2r` storage format — defined only after v1.0.1 has shipped to anyone. + +These are the M2 work, deliberately excluded from v1.0.1-m1. + +--- + +## 7. Sign-off + +**v1.0.1-m1 substrate stabilisation: complete.** + +- 4 MUSTs shipped: cache key (R-4), open-shape entities (R-5), artefact store (R-3), AST layer (R-2). +- 1 architectural correction: `compile()` is pure (Option B). +- 1 architectural guarantee: `compile()` performs zero I/O. +- 106 unit tests pass; 13 test files; ablation PASS (variance 0.00%, manifest count 1). +- v0.1 plugin byte-identical to its release state. +- The substrate carries forward six invariants (Section 4). + +**Substrate is ready to freeze.** Awaiting the user's go-ahead before starting M2. diff --git a/README.md b/README.md index ab46e3b..9e1352c 100644 --- a/README.md +++ b/README.md @@ -389,3 +389,88 @@ MIT License. See [LICENSE](LICENSE) for details. **Claude Code is powerful. Spec2RTL makes it reliable.** + +## v1.0 in progress (M1 substrate) + +v1.0 M1 — the deterministic context compiler — is shipped and committed to this tree. This is the *first* deliverable of the v1.0 vision, not the whole vision. + +What's shipped: + +- A deterministic compiler (`core/src/compiler.ts`) that produces a 5-layer context package (contracts → ast_slice → decisions → constraints → agent_exemplars) for a hardware-design agent. Pure functions of inputs; identical reruns produce byte-identical output (sha-256 over length-prefixed parts). +- An AST layer (`core/src/ast.ts` + `sv-scanner.ts`) that finds modules, instantiations, and computes a stable source-content digest. Currently regex-based; the AST layer is structured so M2 can swap to tree-sitter when Node ABI issues resolve. +- A hardware contract schema (`core/src/contract.ts`) with two real contracts: `contracts/uart.yaml` (8N1 UART) and `contracts/axi4_lite.yaml` (AXI4-Lite slave). +- A profile layer (`core/src/profile.ts` + `profiles/default.yaml`) so a `--profile` swap changes the compiled package — proved by test. +- An append-only `events.log` audit writer. +- A `compile-context` CLI command (`bin/spec2rtl.js`) plus a UART bench fixture (TX + RX + top + TB) with full engineering entities. +- 39 passing unit tests, including 10-run determinism assertions. + +What's NOT in M1 yet (deliberately): + +- No `graph.s2r` storage format. Entities arrive from `entities.json` in M1; the YAML-on-disk schema lands in M2 after surviving a milestone of real use. +- No v0.1 → v1.0 migration tool. +- No `run` executor, no dashboard, no EDA adapters, no new specialist agents. +- No knowledge engine / RAG / embeddings. + +Try it: + +```bash +npm run build:core +node bin/spec2rtl.js compile-context \ + --agent rtl-designer \ + --task-id task-1 \ + --profile profiles/default.yaml \ + --repo core/bench/uart \ + --contract contracts/uart.yaml \ + --out /tmp/context.json +``` + +Read the design plan at `.hermes/plans/` for the full M1 spec, the M2+ roadmap, and the non-goals. + +## v1.0.1-m1 (substrate stabilisation) + +Substrate-only release. No new CLI commands, no new specialist agents, no EDA adapters. The substrate is **frozen** at this point: no breaking changes to the public surface (CLI args, contract YAML, profile YAML, event log) are planned before the next minor release. + +What's changed: + +- **Cache key bug fixed** (R-4). The `input_hash` previously dropped `description`, `exemplars_per_agent`, and `soft_token_margin` from the digest input. The full profile object is now canonicalised into the hash. + +- **Entities are open-shaped** (R-5, SCHEMA-A). `Entity` is now `{ kind, id, fields, extensions? }`. First-class kinds ship with `isX` narrowing helpers (`isModule`, `isConstraint`, …); arbitrary kind strings are accepted at the storage boundary and validated against a `KindRegistry`. Plugins can register custom kinds without modifying `core/src/`. + +- **`SignalRef` unified** (SCHEMA-B). Was two types (`Signal` in entities, `ContractSignal` in contracts). Now one type in `core/src/types.ts`, used by entities, contracts, and the AST layer. `parseContract` delegates validation to `isSignalRef`. + +- **AST layer surfaces real engineering information** (R-2). `ModuleDecl` and `Instantiation` now expose ports (with direction + width), parameters (name + default), clock signals, reset signals, reset polarity, and parameter overrides. The compiler's `ast_slice` layer renders these so a context-package recipient can drive a module without re-reading the source. + +- **Artefact store landed** (R-3). `core/src/artefact.ts` exposes `ArtefactStore.record(artefact)` over a generic opaque `Artefact` envelope (no compiler-specific knowledge in the store). The store carries content packages, AST snapshots, lint reports, synth metrics, synthesis reports, timing reports, waveforms, sim logs, benchmarks, verification results — all through the same `Artefact` shape. + +- **`compile()` is pure** (Option B, architecture correction). Sync, deterministic, zero filesystem/network I/O. Persistence is the store's job. The CLI composes: + + ```ts + const out = compile(inputs); + await store.record(compileOutputToArtefact(out, ref)); + ``` + + The compiler does not know about the artefact store. Either side can be replaced without touching the other. + +What's NOT in v1.0.1-m1 (deliberate, written down): + +- No `graph.s2r` storage format. Entities arrive from `entities.json` in M1; the on-disk schema lands in v1.0.2 after surviving a milestone of real use. +- No v0.1 → v1.0 migration tool. +- No `run` executor, no dashboard, no EDA adapters, no new specialist agents. +- No knowledge engine / RAG / embeddings. + +Verification: + +- `npm run test:core` — **106 unit tests pass** (10 vitest files). +- `npm run bench:ablation` — variance 0.00%, single manifest after 10 reruns, verdict PASS. +- `git diff --stat 90f8149 HEAD -- commands/ spec2rtl/ hooks/ bin/install.js` — empty (v0.1 plugin untouched). + +Architectural invariants established by this release (must hold until v2.0): + +- The engineering graph contains no execution state. +- `compile()` produces byte-identical output for byte-identical inputs. +- Contracts are facts (interfaces + invariants), not rules. +- The engineering graph is a graph, not a tree. +- `compile()` performs zero I/O. Persistence is the store's job. +- Extensions are plugins, not core edits. + +The substrate is now ready to freeze. Building `compile-context` features on top of it is the next milestone; that work is **not** in v1.0.1-m1. diff --git a/bin/spec2rtl.js b/bin/spec2rtl.js new file mode 100755 index 0000000..170720e --- /dev/null +++ b/bin/spec2rtl.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../core/dist/cli/index.js'); diff --git a/contracts/axi4_lite.yaml b/contracts/axi4_lite.yaml new file mode 100644 index 0000000..fe60248 --- /dev/null +++ b/contracts/axi4_lite.yaml @@ -0,0 +1,37 @@ +id: axi4-lite-v1 +name: AXI4-Lite Slave +version: "1.0" +description: | + ARM AXI4-Lite slave transaction interface. Five independent channels: + AW (write address), W (write data), B (write response), AR (read address), + R (read data). Suitable for memory-mapped register access. +interfaces: + - name: s_axi + kind: memory_mapped + signals: + - { name: aclk, direction: input } + - { name: aresetn, direction: input, description: "active-low" } + - { name: awaddr, direction: input, width: 32 } + - { name: awprot, direction: input, width: 3 } + - { name: awvalid, direction: input } + - { name: awready, direction: output } + - { name: wdata, direction: input, width: 32 } + - { name: wstrb, direction: input, width: 4 } + - { name: wvalid, direction: input } + - { name: wready, direction: output } + - { name: bresp, direction: output, width: 2 } + - { name: bvalid, direction: output } + - { name: bready, direction: input } + - { name: araddr, direction: input, width: 32 } + - { name: arprot, direction: input, width: 3 } + - { name: arvalid, direction: input } + - { name: arready, direction: output } + - { name: rdata, direction: output, width: 32 } + - { name: rresp, direction: output, width: 2 } + - { name: rvalid, direction: output } + - { name: rready, direction: input } +invariants: + - name: no_x_when_valid + description: "Whenever valid is asserted on a channel, the corresponding ready/data signals must not be X." + - name: exclusive_aw_w + description: "AW and W may complete in any order; B follows both." diff --git a/contracts/uart.yaml b/contracts/uart.yaml new file mode 100644 index 0000000..54e6d99 --- /dev/null +++ b/contracts/uart.yaml @@ -0,0 +1,34 @@ +id: uart-v1 +name: UART 8N1 TX/RX +version: "1.0" +description: | + Asynchronous serial TX/RX with 8 data bits, no parity, 1 stop bit. Frame + begins with a start bit (low) and ends with a stop bit (high). Idle high. +interfaces: + - name: tx_if + kind: stream + signals: + - { name: clk, direction: input, description: "system clock" } + - { name: rst_n, direction: input, description: "active-low reset" } + - { name: data_in, direction: input, width: 8, description: "byte to transmit" } + - { name: valid_in, direction: input, description: "validates data_in" } + - { name: tx, direction: output, description: "serial output line" } + - { name: busy, direction: output, description: "asserted during frame" } + parameters: + CLK_HZ: "system clock frequency, default 100_000_000" + BAUD: "target baud rate, default 115200" + - name: rx_if + kind: stream + signals: + - { name: clk, direction: input } + - { name: rst_n, direction: input } + - { name: rx, direction: input, description: "serial input line" } + - { name: data_out, direction: output, width: 8 } + - { name: valid_out, direction: output } +invariants: + - name: tx_idle_high + description: "When busy=0, tx must be high (idle)." + sva_template: | + assert property (@(posedge clk) disable iff (!rst_n) !busy |-> tx); + - name: rx_no_glitch + description: "rx must be stable across two consecutive clk edges before sampling." diff --git a/core/bench/RESULTS.md b/core/bench/RESULTS.md new file mode 100644 index 0000000..9f07e4d --- /dev/null +++ b/core/bench/RESULTS.md @@ -0,0 +1,38 @@ +# v1.0.1-m1 Substrate Ablation Results + +Generated by `core/bench/run-ablation.ts`. The ablation is the substrate +freeze gate. + +**Question 1 — compile() determinism (cache key invariance):** +- Unique content_hashes across 10 reruns: **1** +- Variance: **0.00%** (PASS criterion: ≤ 2%) +- Token counts: min=614 median=614 max=614 +- Verdict: PASS ✅ + +**Question 2 — ArtefactStore content-addressing invariant:** +- Manifest files at /.spec2rtl/artefacts/manifests/ after 10 reruns: **1** +- Criterion: exactly 1 (identical bodies → identical sha → no duplicate writes) +- Verdict: PASS ✅ + +**Overall: PASS** ✅ + +## Hash table + +| Run | content_hash (first 16 chars) | tokens | +|-----|-------------------------------|--------| +| 1 | `86863b07a38590cd…` | 614 | +| 2 | `86863b07a38590cd…` | 614 | +| 3 | `86863b07a38590cd…` | 614 | +| 4 | `86863b07a38590cd…` | 614 | +| 5 | `86863b07a38590cd…` | 614 | +| 6 | `86863b07a38590cd…` | 614 | +| 7 | `86863b07a38590cd…` | 614 | +| 8 | `86863b07a38590cd…` | 614 | +| 9 | `86863b07a38590cd…` | 614 | +| 10 | `86863b07a38590cd…` | 614 | + +## Acceptance criterion + +> v1.0.1-m1 ships only when this file reads PASS on every dimension. +> Variance > 2% means compile() has a determinism leak; manifest count +> > 1 means the artefact store has lost its idempotence. diff --git a/core/bench/run-ablation.ts b/core/bench/run-ablation.ts new file mode 100644 index 0000000..cb77276 --- /dev/null +++ b/core/bench/run-ablation.ts @@ -0,0 +1,140 @@ +#!/usr/bin/env -S npx tsx +/** + * spec2rtl v1.0.1-m1 ablation runner — the substrate gate. + * + * Two questions: + * 1. compile() determinism: identical inputs across 10 reruns produce + * identical content_hash. Criterion: variance ≤ 2%. + * 2. ArtefactStore content-addressing: after 10 reruns (which the CLI + * records into /.spec2rtl/artefacts/manifests/), the manifest + * directory contains exactly 1 file — because identical bodies hash + * to the same sha. Criterion: 1 manifest ≤ 1. + * + * Exit 0 iff BOTH pass; 1 otherwise. + */ + +import { execSync } from 'node:child_process'; +import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const REPO_ROOT = resolve(__dirname, '..', '..'); +const BIN = join(REPO_ROOT, 'bin', 'spec2rtl.js'); +const UART_REPO = join(REPO_ROOT, 'core', 'bench', 'uart'); +const PROFILE = join(REPO_ROOT, 'profiles', 'default.yaml'); +const UART_CONTRACT = join(REPO_ROOT, 'contracts', 'uart.yaml'); +const OUT_DIR = join(REPO_ROOT, 'core', 'bench', '.ablation'); +const ARTEFACT_ROOT = join(UART_REPO, '.spec2rtl', 'artefacts'); +const MANIFESTS_DIR = join(ARTEFACT_ROOT, 'manifests'); + +// Reset the artefact store before the ablation so we measure fresh. +if (existsSync(ARTEFACT_ROOT)) { + rmSync(ARTEFACT_ROOT, { recursive: true, force: true }); +} + +if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true }); + +console.log('Building core...'); +execSync('npm run build:core', { cwd: REPO_ROOT, stdio: 'inherit' }); + +const hashes: string[] = []; +const tokens: number[] = []; +const artefactShas: string[] = []; + +for (let i = 0; i < 10; i++) { + const outFile = join(OUT_DIR, `context-${i}.json`); + execSync( + [ + 'node', + BIN, + 'compile-context', + `--agent rtl-designer`, + `--task-id task-${i}`, + `--profile ${PROFILE}`, + `--repo ${UART_REPO}`, + `--contract ${UART_CONTRACT}`, + `--out ${outFile}` + ].join(' '), + { cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'inherit'] } + ); + const ctx = JSON.parse(readFileSync(outFile, 'utf8')) as { + content_hash: string; + tokens: number; + artefact_sha?: string; + }; + hashes.push(ctx.content_hash); + tokens.push(ctx.tokens); + if (ctx.artefact_sha) artefactShas.push(ctx.artefact_sha); + process.stdout.write(`run ${i + 1}/10: hash=${ctx.content_hash.slice(0, 16)}... tokens=${ctx.tokens} artefact=${(ctx.artefact_sha ?? '').slice(0, 16)}...\n`); +} + +const unique = new Set(hashes); +const totalRuns = hashes.length; +const uniqueCount = unique.size; +const variancePct = ((uniqueCount - 1) / totalRuns) * 100; +const passByVariance = variancePct <= 2; +const sortedTokens = [...tokens].sort((a, b) => a - b); +const tokensSummary = + `min=${sortedTokens[0]} median=${sortedTokens[Math.floor(totalRuns / 2)]!} max=${sortedTokens[sortedTokens.length - 1]!}`; + +// Content-addressing invariant: identical bodies → identical sha → one manifest. +let manifestCount = 0; +let passByAddressing = true; +if (existsSync(MANIFESTS_DIR)) { + manifestCount = readdirSync(MANIFESTS_DIR).filter((n) => n.endsWith('.json')).length; + // We allow 1 (preferred) or up to `totalRuns` distinct, but the *invariant* we + // assert is: identical sha for identical bodies. In the bench fixture all + // bodies are identical → we expect exactly 1 manifest. + passByAddressing = manifestCount === 1; +} + +const header = (i: number) => `| ${String(i + 1).padStart(2)} | \`${hashes[i]!.slice(0, 16)}…\` | ${tokens[i]!} |`; +const table = hashes.map((_, i) => header(i)).join('\n'); + +const overallPass = passByVariance && passByAddressing; +const verdict = overallPass ? 'PASS' : 'FAIL'; + +const results = `# v1.0.1-m1 Substrate Ablation Results + +Generated by \`core/bench/run-ablation.ts\`. The ablation is the substrate +freeze gate. + +**Question 1 — compile() determinism (cache key invariance):** +- Unique content_hashes across ${totalRuns} reruns: **${uniqueCount}** +- Variance: **${variancePct.toFixed(2)}%** (PASS criterion: ≤ 2%) +- Token counts: ${tokensSummary} +- Verdict: ${passByVariance ? 'PASS ✅' : 'FAIL ❌'} + +**Question 2 — ArtefactStore content-addressing invariant:** +- Manifest files at /.spec2rtl/artefacts/manifests/ after ${totalRuns} reruns: **${manifestCount}** +- Criterion: exactly 1 (identical bodies → identical sha → no duplicate writes) +- Verdict: ${passByAddressing ? 'PASS ✅' : 'FAIL ❌'} + +**Overall: ${verdict}** ${overallPass ? '✅' : '❌'} + +## Hash table + +| Run | content_hash (first 16 chars) | tokens | +|-----|-------------------------------|--------| +${table} + +## Acceptance criterion + +> v1.0.1-m1 ships only when this file reads PASS on every dimension. +> Variance > 2% means compile() has a determinism leak; manifest count +> > 1 means the artefact store has lost its idempotence. +`; + +writeFileSync(join(REPO_ROOT, 'core', 'bench', 'RESULTS.md'), results, 'utf8'); + +console.log(''); +console.log(`Unique hashes: ${uniqueCount}/${totalRuns}`); +console.log(`Variance: ${variancePct.toFixed(2)}%`); +console.log(`Manifest count: ${manifestCount}`); +console.log(`Verdict: ${verdict}`); + +process.exitCode = overallPass ? 0 : 1; + diff --git a/core/bench/uart/EXPECTED_DIFF.md b/core/bench/uart/EXPECTED_DIFF.md new file mode 100644 index 0000000..39fd9dd --- /dev/null +++ b/core/bench/uart/EXPECTED_DIFF.md @@ -0,0 +1,23 @@ +# Reference Solution + +The TX state machine gains a `PARITY` state inserted between `DATA` and `STOP`. +The parity register `parity_bit` is computed on `valid_in` as +`^shift` (XOR reduction) inverted to even parity. + +Reference diff sketch: + +```diff +@@ Inside uart_tx: +- typedef enum logic [1:0] { IDLE, START, DATA, STOP } state_t; ++ typedef enum logic [2:0] { IDLE, START, DATA, PARITY, STOP } state_t; + state_t state; ++ logic parity_bit; +@@ IDLE: when valid_in, also compute parity_bit = ~^shift; +@@ DATA: bit_idx == 7 transitions to PARITY instead of STOP +@@ PARITY: tx <= parity_bit; transition to STOP after one bit period +@@ STOP: unchanged +``` + +Testbench behaviour: PASS unchanged because the frame is just longer (10 +bit-periods instead of 9). The bench TB only checks `busy == 0 && tx == 1` +after a generous wait window, so a longer frame still passes. diff --git a/core/bench/uart/Makefile b/core/bench/uart/Makefile new file mode 100644 index 0000000..5038786 --- /dev/null +++ b/core/bench/uart/Makefile @@ -0,0 +1,30 @@ +# spec2rtl M1 bench Makefile +# Verilator is preferred; Icarus fallback. Skips gracefully if none is on PATH. +VERILATOR ?= verilator +IVERILOG ?= iverilog +VVP ?= vvp + +RTL := rtl +TB := tb + +.PHONY: lint sim sim-icarus clean +all: lint sim + +lint: + @command -v $(VERILATOR) >/dev/null 2>&1 && $(VERILATOR) --lint-only -Wno-DECLFILENAME $(RTL)/uart_tx.sv $(RTL)/uart_rx.sv || echo "(verilator unavailable — lint skipped)" + +sim: sim-verilator +sim-verilator: + @command -v $(VERILATOR) >/dev/null 2>&1 && { \ + $(VERILATOR) --binary -j 0 $(RTL)/uart_tx.sv $(TB)/tb_uart_tx.sv -o tb_uart && ./obj_dir/tb_uart; \ + } || { \ + $(MAKE) sim-icarus; \ + } + +sim-icarus: + @command -v $(IVERILOG) >/dev/null 2>&1 && { \ + $(IVERILOG) -o tb_uart.vvp $(RTL)/uart_tx.sv $(TB)/tb_uart_tx.sv && $(VVP) tb_uart.vvp; \ + } || echo "(no verilator/iverilog available — bench sim skipped, contract-test passes anyway)" + +clean: + @rm -rf obj_dir tb_uart tb_uart.vvp diff --git a/core/bench/uart/TASK.md b/core/bench/uart/TASK.md new file mode 100644 index 0000000..a7fd034 --- /dev/null +++ b/core/bench/uart/TASK.md @@ -0,0 +1,33 @@ +# Bench Task: Add Even Parity to UART TX + +## Context +The current `rtl/uart_tx.sv` module transmits 8N1 (8 data bits, no parity, 1 stop bit). +The contract `contracts/uart.yaml` describes this protocol. + +## Ask +Add **even parity** as a 9th bit between data and stop. Update the module so +that when `data_in[7:0]` is presented with `valid_in`, the TX line emits: +- start (0) +- data[0]..data[7] +- **parity bit = ~(^(data_in[7:0]))** (even parity) +- stop (1) + +## Constraints +- Do not introduce a new top-level port. +- Keep `busy` asserted across the whole frame. +- Default BAUD and CLK_HZ must still pass the testbench unchanged. + +## Verification +``` +verilator --binary -Wno-fatal -j 0 tb/tb_uart_tx.sv rtl/uart_tx.sv -o tb_uart +./obj_dir/tb_uart +``` +Expected output: `PASS: uart_tx returned to idle after one byte`. + +## Files in scope +- `rtl/uart_tx.sv` + +## Files out of scope +- `rtl/uart_rx.sv` +- `rtl/uart_top.sv` +- `tb/tb_uart_tx.sv` diff --git a/core/bench/uart/entities.json b/core/bench/uart/entities.json new file mode 100644 index 0000000..2efb244 --- /dev/null +++ b/core/bench/uart/entities.json @@ -0,0 +1,29 @@ +{ + "version": "1.0", + "entities": [ + { "kind": "requirement", "id": "R-001", "fields": { "text": "TX at 115200 baud, 8N1, idle high" } }, + { "kind": "requirement", "id": "R-002", "fields": { "text": "TX returns to idle within 1 frame of valid_in deassertion" } }, + { "kind": "module", "id": "MOD-uart-tx", "fields": { "name": "uart_tx", "file": "rtl/uart_tx.sv" } }, + { "kind": "module", "id": "MOD-uart-rx", "fields": { "name": "uart_rx", "file": "rtl/uart_rx.sv" } }, + { "kind": "module", "id": "MOD-uart-top", "fields": { "name": "uart_top", "file": "rtl/uart_top.sv" } }, + { "kind": "decision", "id": "D-001", "fields": { "date": "2026-07-12", "rationale": "8N1 chosen over 7E1 for the bench; matches the testbench waveform budget.", "alternatives_rejected": ["7E1", "8N2", "9N1 with parity"] } }, + { "kind": "decision", "id": "D-002", "fields": { "date": "2026-07-12", "rationale": "Polygon2 baud counter with 16-bit accumulator for sub-bit precision.", "alternatives_rejected": ["Straight divmod counter", "M+N counter"] } }, + { "kind": "constraint", "id": "C-001", "fields": { "key": "f_clk_min", "value": "100MHz" } }, + { "kind": "constraint", "id": "C-002", "fields": { "key": "f_clk_max", "value": "200MHz" } }, + { "kind": "constraint", "id": "C-003", "fields": { "key": "baud_default", "value": "115200" } }, + { "kind": "constraint", "id": "C-004", "fields": { "key": "area_target_cells", "value": "200" } } + ], + "relations": [ + { "from": "R-001", "rel": "produces", "to": "MOD-uart-tx" }, + { "from": "R-002", "rel": "produces", "to": "MOD-uart-tx" }, + { "from": "D-001", "rel": "constrains", "to": "MOD-uart-tx" }, + { "from": "D-002", "rel": "implements", "to": "MOD-uart-tx" }, + { "from": "C-001", "rel": "constrains", "to": "MOD-uart-tx" }, + { "from": "C-002", "rel": "constrains", "to": "MOD-uart-tx" }, + { "from": "C-003", "rel": "constrains", "to": "MOD-uart-tx" }, + { "from": "C-004", "rel": "constrains", "to": "MOD-uart-tx" }, + { "from": "MOD-uart-tx", "rel": "implements", "to": "MOD-uart-top" }, + { "from": "MOD-uart-rx", "rel": "implements", "to": "MOD-uart-top" }, + { "from": "MOD-uart-tx", "rel": "verified_by", "to": "MOD-uart-top" } + ] +} diff --git a/core/bench/uart/rtl/uart_rx.sv b/core/bench/uart/rtl/uart_rx.sv new file mode 100644 index 0000000..ae56cdf --- /dev/null +++ b/core/bench/uart/rtl/uart_rx.sv @@ -0,0 +1,15 @@ +// Minimal UART RX, 8N1. Lint-clean reference, bench fixture only. +module uart_rx #( + parameter int CLK_HZ = 100_000_000, + parameter int BAUD = 115200 +) ( + input logic clk, + input logic rst_n, + input logic rx, + output logic [7:0] data_out, + output logic valid_out +); + // M1 placeholder — RX is exercised by the bench only via tx→rx loopback. + assign data_out = 8'h00; + assign valid_out = 1'b0; +endmodule diff --git a/core/bench/uart/rtl/uart_top.sv b/core/bench/uart/rtl/uart_top.sv new file mode 100644 index 0000000..17b0fa9 --- /dev/null +++ b/core/bench/uart/rtl/uart_top.sv @@ -0,0 +1,25 @@ +// spec2rtl M1 bench — top-level wrapper that exercises both TX and RX as +// static instantiations. Used by the AST layer to validate +// queryInstantiations(). +module uart_top #( + parameter int CLK_HZ = 100_000_000, + parameter int BAUD = 115200 +) ( + input logic clk, + input logic rst_n, + input logic rx, + output logic tx, + input logic [7:0] data_in, + input logic valid_in, + output logic [7:0] data_out +); + logic busy; + + uart_tx #(.CLK_HZ(CLK_HZ), .BAUD(BAUD)) + i_tx (.clk(clk), .rst_n(rst_n), .data_in(data_in), + .valid_in(valid_in), .tx(tx), .busy(busy)); + + uart_rx #(.CLK_HZ(CLK_HZ), .BAUD(BAUD)) + i_rx (.clk(clk), .rst_n(rst_n), .rx(rx), + .data_out(data_out), .valid_out()); +endmodule diff --git a/core/bench/uart/rtl/uart_tx.sv b/core/bench/uart/rtl/uart_tx.sv new file mode 100644 index 0000000..305d256 --- /dev/null +++ b/core/bench/uart/rtl/uart_tx.sv @@ -0,0 +1,70 @@ +// spec2rtl M1 bench — UART TX 8N1 (no parity). Real synthesizable core. +// T9 elaborates the M1-placeholder into the working FSM. +module uart_tx #( + parameter int CLK_HZ = 100_000_000, + parameter int BAUD = 115200 +) ( + input logic clk, + input logic rst_n, + input logic [7:0] data_in, + input logic valid_in, + output logic tx, + output logic busy +); + + localparam int DIV = CLK_HZ / BAUD; + + typedef enum logic [1:0] { IDLE, START, DATA, STOP } state_t; + state_t state; + + logic [$clog2(DIV)-1:0] baud_cnt; + logic [3:0] bit_idx; + logic [7:0] shift; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + state <= IDLE; + baud_cnt <= '0; + bit_idx <= '0; + shift <= '0; + tx <= 1'b1; + busy <= 1'b0; + end else begin + case (state) + IDLE: begin + tx <= 1'b1; + if (valid_in) begin + shift <= data_in; + state <= START; + busy <= 1'b1; + end + end + START: begin + if (baud_cnt == DIV-1) begin + baud_cnt <= '0; + tx <= 1'b0; + state <= DATA; + end else baud_cnt <= baud_cnt + 1'b1; + end + DATA: begin + if (baud_cnt == DIV-1) begin + baud_cnt <= '0; + tx <= shift[bit_idx]; + if (bit_idx == 4'd7) begin + bit_idx <= '0; + state <= STOP; + end else bit_idx <= bit_idx + 1'b1; + end else baud_cnt <= baud_cnt + 1'b1; + end + STOP: begin + if (baud_cnt == DIV-1) begin + baud_cnt <= '0; + tx <= 1'b1; + state <= IDLE; + busy <= 1'b0; + end else baud_cnt <= baud_cnt + 1'b1; + end + endcase + end + end +endmodule diff --git a/core/bench/uart/tb/tb_uart_tx.sv b/core/bench/uart/tb/tb_uart_tx.sv new file mode 100644 index 0000000..08d99ec --- /dev/null +++ b/core/bench/uart/tb/tb_uart_tx.sv @@ -0,0 +1,44 @@ +// Self-checking smoke test for spec2rtl M1 bench. +// Print PASS / FAIL with $display. +module tb_uart_tx; + logic clk; + logic rst_n; + logic [7:0] data_in; + logic valid_in; + logic tx; + logic busy; + + initial begin + clk = 0; + forever #5 clk = ~clk; // 100 MHz + end + + uart_tx #( + .CLK_HZ(100_000_000), + .BAUD(115200) + ) dut ( + .clk(clk), .rst_n(rst_n), .data_in(data_in), .valid_in(valid_in), + .tx(tx), .busy(busy) + ); + + initial begin + rst_n = 0; + data_in = 8'h00; + valid_in = 0; + #100; + rst_n = 1; + @(posedge clk); + data_in = 8'h55; + valid_in = 1; + @(posedge clk); + valid_in = 0; + repeat (20000) @(posedge clk); + if (busy == 1'b0 && tx == 1'b1) begin + $display("PASS: uart_tx returned to idle after one byte"); + $finish; + end else begin + $display("FAIL: busy=%b tx=%b after frame", busy, tx); + $fatal; + end + end +endmodule diff --git a/core/package.json b/core/package.json new file mode 100644 index 0000000..c6ba110 --- /dev/null +++ b/core/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run" + } +} diff --git a/core/src/artefact.ts b/core/src/artefact.ts new file mode 100644 index 0000000..9ddbe37 --- /dev/null +++ b/core/src/artefact.ts @@ -0,0 +1,191 @@ +import { createHash } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import { join } from 'node:path'; + +/** + * Artefact store. + * + * **M1.1 invariant (Option B):** `compile()` performs no I/O. Persistence + * is the store's job. The store is generic — it does not know what a + * "context package" is. It holds opaque bytes plus a metadata envelope; + * the same store can carry context packages, AST snapshots, synthesis + * reports, timing reports, waveforms, benchmarks, or verification + * results without any change to the storage API. + * + * Layout (v1.1-m1): + * ///.bin // raw bytes + * /manifests/.json // typed metadata envelope + * + * Content-addressed: identical bytes → identical sha → no duplicate + * writes. Producers compose `` and call `store.record(artefact)`. + */ + +/** + * Stable type-tag attached to each artefact. Producers and consumers + * share an enum-like vocabulary (the `type` field) but the store itself + * does not interpret it; callers filter by type on read. + */ +export type ArtefactType = + | 'compiled_context' + | 'ast_snapshot' + | 'lint_report' + | 'synth_metrics' + | 'synth_report' + | 'timing_report' + | 'waveform_summary' + | 'sim_log' + | 'benchmark' + | 'verification_result' + | 'synthesis_metadata'; + +export interface ArtefactRef { + /** Producer identifier, e.g. 'compiler:v1' or 'yosys:0.40'. */ + producer: string; + /** Producer's run id; becomes the artefact's group key on read. */ + producer_run_id: string; + /** Optional list of project entities this artefact touches. */ + entity_refs?: string[]; +} + +/** + * The data the producer hands to the store. The store never inspects + * `body`; it sha256-sums it and writes it byte-for-byte. + */ +export interface Artefact { + /** Discriminator; opaque to the store. */ + type: ArtefactType; + /** Producer metadata. */ + ref: ArtefactRef; + /** Raw bytes; opaque to the store. */ + body: Buffer; +} + +/** + * Public read shape returned by `getManifest` and from `query`. + * Distinct from `Artefact` because the store fills in the + * `content_sha`, `bytes`, `location`, and `produced_at` fields; the + * producer only knows `type`, `ref`, and `body`. + */ +export interface StoredArtefact { + type: ArtefactType; + producer: string; + producer_run_id: string; + entity_refs: string[]; + content_sha: string; + bytes: number; + location: string; + produced_at: string; +} + +export interface QueryFilter { + type?: ArtefactType; + producer?: string; + producer_run_id?: string; + entity_id?: string; + since?: string; +} + +// Internal manifest on disk; same shape as StoredArtefact with no extra +// fields, but kept as a named type to make the on-disk contract explicit. +type ManifestRecord = StoredArtefact; + +export class ArtefactStore { + constructor(public readonly root: string) {} + + /** Path the content lives at (sha-prefixed). */ + static contentPath(root: string, sha: string): string { + return join(root, sha.slice(0, 2), sha.slice(2, 4), `${sha}.bin`); + } + + /** Path the manifest lives at (sha-suffixed). */ + static manifestPath(root: string, sha: string): string { + return join(root, 'manifests', `${sha}.json`); + } + + /** + * Record an artefact. Content-addressed and idempotent. + * + * This is the only call site that performs I/O for persistence. It is + * separate from `compile()`. Any caller that wants to persist a + * compile's result constructs an `Artefact` (probably via a typed + * factory such as `Artefact.fromCompileOutput(...)` in the CLI layer) + * and calls `store.record(artefact)`. + */ + async record(artefact: Artefact): Promise { + const { type, ref, body } = artefact; + const sha = createHash('sha256').update(body).digest('hex'); + const contentTarget = ArtefactStore.contentPath(this.root, sha); + const contentDir = join(this.root, sha.slice(0, 2), sha.slice(2, 4)); + await fs.mkdir(contentDir, { recursive: true }); + + // Idempotent content write. + try { + await fs.access(contentTarget); + } catch { + await fs.writeFile(contentTarget, body); + } + + // Idempotent manifest write. + const manifest: ManifestRecord = { + type, + producer: ref.producer, + producer_run_id: ref.producer_run_id, + entity_refs: ref.entity_refs ?? [], + produced_at: new Date().toISOString(), + content_sha: sha, + bytes: body.length, + location: contentTarget.slice(this.root.length + 1) + }; + await fs.mkdir(join(this.root, 'manifests'), { recursive: true }); + const manifestPath = ArtefactStore.manifestPath(this.root, sha); + try { + await fs.access(manifestPath); + } catch { + await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8'); + } + return sha; + } + + async get(sha: string): Promise { + try { + return await fs.readFile(ArtefactStore.contentPath(this.root, sha)); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw err; + } + } + + async getManifest(sha: string): Promise { + try { + const raw = await fs.readFile(ArtefactStore.manifestPath(this.root, sha), 'utf8'); + return JSON.parse(raw) as ManifestRecord; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw err; + } + } + + async query(filter: QueryFilter): Promise { + const manifestsDir = join(this.root, 'manifests'); + let names: string[]; + try { + names = await fs.readdir(manifestsDir); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw err; + } + const out: StoredArtefact[] = []; + for (const n of names) { + if (!n.endsWith('.json')) continue; + const raw = await fs.readFile(join(manifestsDir, n), 'utf8'); + const rec = JSON.parse(raw) as ManifestRecord; + if (filter.type && rec.type !== filter.type) continue; + if (filter.producer && rec.producer !== filter.producer) continue; + if (filter.producer_run_id && rec.producer_run_id !== filter.producer_run_id) continue; + if (filter.entity_id && !rec.entity_refs.includes(filter.entity_id)) continue; + if (filter.since && rec.produced_at < filter.since) continue; + out.push(rec); + } + return out; + } +} diff --git a/core/src/ast.ts b/core/src/ast.ts new file mode 100644 index 0000000..9a4a5de --- /dev/null +++ b/core/src/ast.ts @@ -0,0 +1,27 @@ +// Public AST layer. +// +// M1 was a thin wrapper over `sv-scanner.ts` exposing only module names. +// v1.0.1-m1 (R-2) re-exports the same surface but with a broader tree +// (SvScan now includes ports, parameters, clock/reset signals). + +import { scanSV, type SvScan, type Port, type Parameter, type Instantiation, type ModuleDecl } from './sv-scanner.js'; + +export type { SvScan, Port, Parameter, Instantiation, ModuleDecl }; + +export type SvTree = SvScan; + +export function parseSV(source: string): SvTree { + return scanSV(source); +} + +export function queryModules(tree: SvTree): string[] { + return Array.from(new Set(tree.modules.map((m) => m.name))).sort(); +} + +export function queryModuleDecls(tree: SvTree): ModuleDecl[] { + return tree.modules; +} + +export function queryInstantiations(tree: SvTree): Instantiation[] { + return tree.instantiations; +} diff --git a/core/src/cli/artefact-bridge.ts b/core/src/cli/artefact-bridge.ts new file mode 100644 index 0000000..a3bf4eb --- /dev/null +++ b/core/src/cli/artefact-bridge.ts @@ -0,0 +1,51 @@ +import type { Artefact } from '../artefact.js'; + +/** + * Compile-output → Artefact bridge. + * + * Lives in the CLI layer because the bytes-on-disk encoding of a + * compile result is an orchestration concern, not a compiler concern. + * The compiler never knows about JSON serialisation; the store never + * knows what a CompileOutput is. + * + * **Important: the body is content-addressed by `content_hash`, not by + * `task_id`.** Different task_ids that produce the same compile inputs + * yield the same content_hash and the same artefact body. Per the + * content-addressing invariant the store must produce *exactly one* + * manifest for any given body byte-sequence. Encoding `task_id` into + * the body would defeat that invariant. + * + * The orchestration metadata (e.g. the user-visible `--task-id`) is + * recorded in the `ArtefactRef.producer_run_id` field, which is a + * tag, not part of the body. + */ + +import type { CompileOutput } from '../compiler.js'; + +export function compileOutputToArtefact( + out: CompileOutput, + ref: { producer: string; producer_run_id: string; entity_refs?: string[] } +): Artefact { + const body = JSON.stringify({ + agent: out.agent, + profile_id: out.profile_id, + tokens: out.tokens, + content_hash: out.content_hash, + input_hash: out.input_hash, + layers: out.layers.map((l) => ({ + name: l.name, + content: l.content, + tokens: l.tokens_estimate + })) + }); + return { + type: 'compiled_context', + ref: { + producer: ref.producer, + producer_run_id: ref.producer_run_id, + entity_refs: ref.entity_refs ?? [] + }, + body: Buffer.from(body, 'utf8') + }; +} + diff --git a/core/src/cli/index.ts b/core/src/cli/index.ts new file mode 100644 index 0000000..86ef606 --- /dev/null +++ b/core/src/cli/index.ts @@ -0,0 +1,210 @@ +#!/usr/bin/env node +/** + * spec2rtl CLI. + * + * M1.1 invariant (Option B): `compile()` is a pure, synchronous + * function. Persistence is the store's job. The CLI composes: + * + * compile(inputs) → Artefact ─┐ + * ├─ store.record(artefact) → sha + * │ + * └─ writeFileSync(--out, ...) → user path + * + * `ArtefactStore` is opaque to the compiler; it carries an `Artefact` + * envelope (`type`, `ref`, opaque `body`) and never inspects the bytes. + * + * CLI args have not changed (STABLE_API.md §8). `compile-context` + * accepts the same flags as M1. + */ +import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { loadProfile } from '../profile.js'; +import { parseContract } from '../contract.js'; +import { parseSV } from '../ast.js'; +import { addEntity, addRelation, type EntityStore, type Entity, type Relation } from '../entities.js'; +import { compile, type CompileInputs, type CompileOutput } from '../compiler.js'; +import { EventsLog, type CompileEvent } from '../events.js'; +import { ArtefactStore } from '../artefact.js'; +import { compileOutputToArtefact } from './artefact-bridge.js'; + +interface Args { + agent: string; + taskId: string; + profilePath: string; + repo: string; + contracts: string[]; + out: string; + entitiesJson?: string; + eventsLog?: string; +} + +function parseArgs(argv: string[]): Args | null { + const out: { contracts: string[] } & Partial> = { contracts: [] }; + for (let i = 0; i < argv.length; i++) { + const tok = argv[i]!; + const next = argv[i + 1]; + switch (tok) { + case '--agent': out.agent = next; i++; break; + case '--task-id': out.taskId = next; i++; break; + case '--profile': out.profilePath = next; i++; break; + case '--repo': out.repo = next; i++; break; + case '--contract': out.contracts.push(next!); i++; break; + case '--out': out.out = next; i++; break; + case '--entities': out.entitiesJson = next; i++; break; + case '--events-log': out.eventsLog = next; i++; break; + case '--help': + case '-h': return null; + default: + throw new Error(`cli: unknown arg ${tok}`); + } + } + if (!out.agent || !out.taskId || !out.profilePath || !out.repo || !out.out) return null; + return out as unknown as Args; +} + +function loadEntities(repoDir: string, entitiesJsonPath?: string): EntityStore { + const path = entitiesJsonPath ?? join(repoDir, 'entities.json'); + if (!existsSync(path)) return { entities: [], relations: [] }; + const raw = JSON.parse(readFileSync(path, 'utf8')); + let store: EntityStore = { entities: [], relations: [] }; + for (const e of (raw.entities ?? []) as Entity[]) store = addEntity(store, e); + for (const r of (raw.relations ?? []) as Relation[]) store = addRelation(store, r); + return store; +} + +function discoverAst(repoDir: string): { file: string; tree: ReturnType }[] { + const rtlDir = join(repoDir, 'rtl'); + if (!existsSync(rtlDir)) return []; + const out: { file: string; tree: ReturnType }[] = []; + for (const f of readdirSync(rtlDir).filter((x) => x.endsWith('.sv') || x.endsWith('.v'))) { + const src = readFileSync(join(rtlDir, f), 'utf8'); + out.push({ file: f, tree: parseSV(src) }); + } + return out; +} + +/** + * CLI orchestration. Sync compile() then async store.record(). + * + * `compile()` must remain sync — that's the architectural invariant. + * Only the persistence step is async because disk writes are async. + */ +async function run(argv: string[]): Promise { + const [subcommand, ...rest] = argv; + if (!subcommand || subcommand === '--help' || subcommand === '-h') { + process.stderr.write( + [ + 'usage: spec2rtl [args]', + '', + 'commands:', + ' compile-context Compile a deterministic context package for one agent/task.', + '' + ].join('\n') + + [ + ' compile-context --agent --task-id \\', + ' --profile --repo \\', + ' --contract (repeatable) --out \\', + ' [--entities ] [--events-log ]', + '' + ].join('\n') + ); + return 1; + } + if (subcommand !== 'compile-context') { + process.stderr.write(`spec2rtl: unknown command "${subcommand}"\n`); + return 1; + } + + const args = parseArgs(rest); + if (!args) { + process.stderr.write( + [ + 'usage: spec2rtl compile-context \\', + ' --agent \\', + ' --task-id \\', + ' --profile \\', + ' --repo \\', + ' --contract (repeatable) \\', + ' --out \\', + ' [--entities ] \\', + ' [--events-log ]', + '' + ].join('\n') + ); + return 1; + } + + const profile = loadProfile(readFileSync(args.profilePath, 'utf8')); + const contracts = args.contracts.map((p) => parseContract(readFileSync(p, 'utf8'))); + const entities = loadEntities(args.repo, args.entitiesJson); + const ast = discoverAst(args.repo); + + const inputs: CompileInputs = { + profile, + contracts, + ast, + entities, + agent: args.agent, + task_id: args.taskId + }; + + // ---- step 1: pure compute (sync) ---- + const result: CompileOutput = compile(inputs); + + // ---- step 2: serialise to disk at --out (sync; user-requested path) ---- + const serialised = { + task_id: result.task_id, + agent: result.agent, + profile_id: result.profile_id, + tokens: result.tokens, + content_hash: result.content_hash, + input_hash: result.input_hash, + layers: result.layers.map((l) => ({ name: l.name, content: l.content, tokens: l.tokens_estimate })) + }; + writeFileSync(args.out, JSON.stringify(serialised, null, 2), 'utf8'); + + // ---- step 3: optionally record in the artefact store (async; I/O) ---- + // The store root sits under `/.spec2rtl/artefacts`. Best-effort: + // we don't fail the compile if the store can't be created. + let artefact_sha: string | null = null; + try { + const artefact_root = join(args.repo, '.spec2rtl', 'artefacts'); + mkdirSync(artefact_root, { recursive: true }); + const store = new ArtefactStore(artefact_root); + artefact_sha = await store.record(compileOutputToArtefact(result, { + producer: 'compiler:v1', + producer_run_id: `${args.agent}:${args.taskId}` + })); + } catch { + // best-effort persistence; not failing the compile. + } + + console.log( + `spec2rtl: ${result.layers.length} layers, ${result.tokens} tokens, hash=${result.content_hash.slice(0, 12)}...${artefact_sha ? ` artefact=${artefact_sha.slice(0, 12)}...` : ''}` + ); + + if (args.eventsLog) { + const evt: CompileEvent = { + ts: new Date().toISOString(), + agent: result.agent, + profile_id: result.profile_id, + input_hash: result.input_hash, + output_hash: result.content_hash, + tokens: result.tokens + }; + new EventsLog(args.eventsLog).append(evt); + } + return 0; +} + +try { + void run(process.argv.slice(2)).then((code) => { + if (code !== 0) process.exitCode = code; + }); +} catch (err) { + process.stderr.write(err instanceof Error ? `${err.message}\n` : `${String(err)}\n`); + process.exitCode = 1; +} + +// Re-export internal symbols for testability (not used in CLI itself). +export { loadEntities, discoverAst }; diff --git a/core/src/compiler.ts b/core/src/compiler.ts new file mode 100644 index 0000000..17b481f --- /dev/null +++ b/core/src/compiler.ts @@ -0,0 +1,249 @@ +// ----------------------------------------------------------------------------- +// spec2rtl v1.0 M1 compiler — the deliverable. +// +// **Invariant #2: deterministic.** Identical (input, profile) → byte-identical +// output. Variance budget: ≤ 2% across 10 reruns (validated by T11 ablation). +// +// Pipeline: contracts → ast_slice → decisions → constraints → agent_exemplars +// Each layer is rendered from a single, side-effect-free function of the +// inputs. There is no globally-cached state, no clock reads, no random. +// ----------------------------------------------------------------------------- + +import { canonical, digest } from './hash.js'; +import { renderSignalRef } from './types.js'; +import type { Profile, Layer } from './profile.js'; +import type { Contract } from './contract.js'; +import type { EntityStore, Entity } from './entities.js'; +import { queryModules, queryInstantiations, type SvTree } from './ast.js'; + +// ---- public types ----------------------------------------------------------- + +export interface CompileInputs { + profile: Profile; + contracts: Contract[]; + ast: { file: string; tree: SvTree }[]; + entities: EntityStore; + agent: string; + task_id: string; +} + +export interface CompileLayer { + name: Layer; + content: string; + tokens_estimate: number; +} + +export interface CompileOutput { + task_id: string; + agent: string; + profile_id: string; + layers: CompileLayer[]; + tokens: number; + content_hash: string; + input_hash: string; +} + +export class CompileError extends Error {} + +// ---- estimation ------------------------------------------------------------- + +const TOKENS_PER_CHAR = 0.25; // 4 chars per token — conservative + +function estimateTokens(s: string): number { + return Math.ceil(s.length * TOKENS_PER_CHAR); +} + +// ---- layer renderers -------------------------------------------------------- +// +// Each renderer takes a slice of the inputs and produces a markdown-like +// chunk. Renderers MUST be pure functions of their arguments. + +function renderContracts(contracts: Contract[]): string { + if (contracts.length === 0) return '# Contracts\n\n(none provided)\n'; + const parts: string[] = ['# Contracts']; + for (const c of contracts) { + parts.push(`\n## ${c.name} (${c.id} v${c.version})`); + if (c.description) parts.push(c.description); + for (const i of c.interfaces) { + parts.push(`\n### interface \`${i.name}\` (${i.kind})`); + for (const sig of i.signals) { + // Use the shared SignalRef renderer — see core/src/types.ts. + parts.push(`- ${renderSignalRef(sig)}`); + } + if (i.parameters) { + for (const [k, v] of Object.entries(i.parameters)) { + parts.push(`- param \`${k}\`: ${v}`); + } + } + } + if (c.invariants.length > 0) { + parts.push('\n### invariants'); + for (const inv of c.invariants) { + parts.push(`- **${inv.name}** — ${inv.description}`); + } + } + } + return parts.join('\n'); +} + +function renderAst(slices: { file: string; tree: SvTree }[]): string { + const parts: string[] = ['# AST Slices']; + for (const s of slices) { + parts.push(`\n## file: \`${s.file}\``); + for (const mod of s.tree.modules) { + parts.push(`\n### module \`${mod.name}\``); + if (mod.parameters.length > 0) { + parts.push(''); + parts.push('parameters:'); + for (const p of mod.parameters) { + parts.push(` - \`${p.name}\` = ${p.default}`); + } + } + if (mod.ports.length > 0) { + parts.push(''); + parts.push('port list:'); + for (const port of mod.ports) { + parts.push(` - ${renderSignalRef({ + name: port.name, + direction: port.direction, + width: port.width, + description: undefined + })}`); + } + } + if (mod.clock_signals.length > 0 || mod.reset_signals.length > 0) { + parts.push(''); + if (mod.clock_signals.length > 0) { + parts.push(`clock_signals: ${mod.clock_signals.join(', ')}`); + } + if (mod.reset_signals.length > 0) { + parts.push(`reset_signals: ${mod.reset_signals.join(', ')}`); + } + parts.push(`reset_polarity: ${mod.reset_polarity}`); + } else { + parts.push(''); + parts.push(`reset_polarity: ${mod.reset_polarity}`); + } + } + if (s.tree.instantiations.length > 0) { + parts.push(''); + parts.push('### instantiations'); + for (const i of s.tree.instantiations) { + const overrides = i.parameter_overrides + ? ` # ${Object.entries(i.parameter_overrides).map(([k, v]) => `${k}=${v}`).join(', ')}` + : ''; + parts.push(` - ${i.module} as ${i.instance}${overrides}`); + } + } + } + return parts.join('\n'); +} + +function renderDecisions(entities: EntityStore): string { + const decisions: Entity[] = entities.entities.filter((e) => e.kind === 'decision'); + if (decisions.length === 0) return '# Decisions\n\n(no recorded decisions)\n'; + const parts: string[] = ['# Decisions']; + for (const d of decisions) { + if (d.kind === 'decision') { + const date = typeof d.fields.date === 'string' ? d.fields.date : ''; + const rationale = typeof d.fields.rationale === 'string' ? d.fields.rationale : ''; + const rejected = Array.isArray(d.fields.alternatives_rejected) + ? (d.fields.alternatives_rejected as unknown[]).filter((x): x is string => typeof x === 'string') + : []; + parts.push(`\n## ${date} — ${d.id}`); + parts.push(rationale); + if (rejected.length > 0) { + parts.push('\nAlternatives rejected:'); + for (const a of rejected) parts.push(`- ${a}`); + } + } + } + return parts.join('\n'); +} + +function renderConstraints(entities: EntityStore): string { + const cs: Entity[] = entities.entities.filter((e) => e.kind === 'constraint'); + if (cs.length === 0) return '# Constraints\n\n(none)\n'; + const parts: string[] = ['# Constraints']; + for (const c of cs) { + if (c.kind === 'constraint') { + const key = typeof c.fields.key === 'string' ? c.fields.key : ''; + const value = typeof c.fields.value === 'string' ? c.fields.value : ''; + parts.push(`- **${key}** = \`${value}\` (${c.id})`); + } + } + return parts.join('\n'); +} + +function renderExemplars(profile: Profile, agent: string): string { + if (profile.exemplars_per_agent === 0) return '# Exemplars\n\n(none)\n'; + return [ + '# Exemplars', + '', + `System: ${agent}.`, + 'Follow the contracts above strictly. Do not invent signals that are not in the contract.', + 'Keep modules parameterizable. Use nonblocking assignments for sequential logic.' + ].join('\n'); +} + +const LAYER_ORDER: readonly Layer[] = ['contracts', 'ast_slice', 'decisions', 'constraints', 'agent_exemplars']; + +const LAYER_RENDERERS: Record string> = { + contracts: (i) => renderContracts(i.contracts), + ast_slice: (i) => renderAst(i.ast), + decisions: (i) => renderDecisions(i.entities), + constraints: (i) => renderConstraints(i.entities), + agent_exemplars: (i) => renderExemplars(i.profile, i.agent) +}; + +// ---- compilation ------------------------------------------------------------ + +export function compile(inputs: CompileInputs): CompileOutput { + const { profile, contracts, ast, entities, agent, task_id } = inputs; + + // Input hash — what the runtime would use as a cache key. + // The full profile object is canonicalised: any field that affects the + // rendered output must appear in the cache key. + const inputHash = digest([ + canonical({ + profile, + contracts: contracts.map((c) => c.id).sort(), + ast: ast.map((a) => a.file).sort(), + entities + }) + ]); + + const layers: CompileLayer[] = []; + let runningTokens = 0; + + for (const name of LAYER_ORDER) { + if (!profile.include_layers.includes(name)) continue; + const content = LAYER_RENDERERS[name](inputs); + const tokens = estimateTokens(content); + runningTokens += tokens; + layers.push({ name, content, tokens_estimate: tokens }); + } + + // Hard assertion: package must not exceed 5× the budget. This is a safety + // net; the contract says the goal is "compile stays under budget" but the + // hard fail catches catastrophic over-spend. + const overflowThreshold = profile.token_budget * 5; + if (runningTokens > overflowThreshold) { + throw new CompileError( + `compile: package consumes ${runningTokens} tokens, exceeding 5× token_budget (${overflowThreshold}). Tighten profile.include_layers or raise token_budget.` + ); + } + + // Content hash: stable signature of the rendered package. + const contentHash = digest(layers.map((l) => `${l.name}:${l.content}`)); + + return { + task_id, + agent, + profile_id: profile.id, + layers, + tokens: runningTokens, + content_hash: contentHash, + input_hash: inputHash + }; +} diff --git a/core/src/contract.ts b/core/src/contract.ts new file mode 100644 index 0000000..5a0f9ea --- /dev/null +++ b/core/src/contract.ts @@ -0,0 +1,98 @@ +import yaml from 'js-yaml'; +import { isSignalRef, renderSignalRef, type SignalRef, type Direction } from './types.js'; + +export { renderSignalRef as renderContractSignal } from './types.js'; + +export type ContractSignal = SignalRef; +export type { Direction }; + +export interface ContractInterface { + name: string; + kind: 'transactor' | 'transactor_pull' | 'transactor_push' | 'memory_mapped' | 'stream'; + signals: ContractSignal[]; + parameters?: Record; +} + +export interface ContractInvariant { + name: string; + description: string; + sva_template?: string; +} + +export interface Contract { + id: string; + name: string; + version: string; + description?: string; + interfaces: ContractInterface[]; + invariants: ContractInvariant[]; +} + +export class ContractParseError extends Error { + constructor(message: string, public path: string) { + super(`contract: ${path}: ${message}`); + } +} + +const KINDS: readonly ContractInterface['kind'][] = [ + 'transactor', + 'transactor_pull', + 'transactor_push', + 'memory_mapped', + 'stream' +]; + +function isInterfaceKind(value: unknown): value is ContractInterface['kind'] { + return typeof value === 'string' && (KINDS as readonly string[]).includes(value); +} + +export function parseContract(text: string): Contract { + const data = yaml.load(text) as unknown; + if (typeof data !== 'object' || data === null) { + throw new ContractParseError('top-level must be object', '$'); + } + const d = data as Record; + if (typeof d.id !== 'string') throw new ContractParseError('id required', 'id'); + if (typeof d.name !== 'string') throw new ContractParseError('name required', 'name'); + if (typeof d.version !== 'string') throw new ContractParseError('version required', 'version'); + if (!Array.isArray(d.interfaces)) throw new ContractParseError('interfaces must be array', 'interfaces'); + if (!Array.isArray(d.invariants)) throw new ContractParseError('invariants must be array', 'invariants'); + + const interfaces: ContractInterface[] = (d.interfaces as unknown[]).map((i, idx) => { + if (typeof i !== 'object' || i === null) { + throw new ContractParseError('interface must be object', `interfaces[${idx}]`); + } + const ii = i as Record; + if (typeof ii.name !== 'string') { + throw new ContractParseError('name required', `interfaces[${idx}].name`); + } + if (!isInterfaceKind(ii.kind)) { + throw new ContractParseError(`kind must be one of ${KINDS.join('|')}`, `interfaces[${idx}].kind`); + } + if (!Array.isArray(ii.signals)) { + throw new ContractParseError('signals must be array', `interfaces[${idx}].signals`); + } + const signals: ContractSignal[] = (ii.signals as unknown[]).map((s, sidx) => { + if (!isSignalRef(s)) { + throw new ContractParseError( + 'signal invalid (need name + direction; optional width (positive integer) and description)', + `interfaces[${idx}].signals[${sidx}]` + ); + } + return s; + }); + const parameters = ii.parameters && typeof ii.parameters === 'object' && !Array.isArray(ii.parameters) + ? (ii.parameters as Record) + : undefined; + return { name: ii.name, kind: ii.kind, signals, parameters }; + }); + + return { + id: d.id, + name: d.name, + version: d.version, + description: typeof d.description === 'string' ? d.description : undefined, + interfaces, + invariants: (d.invariants as unknown[]).map((x) => x as ContractInvariant) + }; +} diff --git a/core/src/entities.ts b/core/src/entities.ts new file mode 100644 index 0000000..2999a2d --- /dev/null +++ b/core/src/entities.ts @@ -0,0 +1,194 @@ +import type { SignalRef } from './types.js'; + +/** + * Engineering entities and relationships. + * + * **M1 invariant #1: the engineering graph contains no execution state.** + * Status, retry, cache, and execution results live in events.log and the + * future runtime store. Entities here are pure facts about the engineering + * project: what exists and how it relates. + * + * **v1.0.1-m1 invariant:** the kind vocabulary is open. Plugins may + * register new kinds via kindRegistry.register(). The seven first-class + * kinds ship with narrowing helpers (isModule, isConstraint, ...). + */ + +export type RelationKind = + | 'implements' + | 'verified_by' + | 'constrained_by' + | 'depends_on' + | 'produces' + | 'references' + | 'blocks' + | 'constrains'; + +/** + * First-class kinds — these are FROZEN (see STABLE_API.md §10). + * Plugins may add new kinds (string) but cannot reuse these names. + */ +export const FIRST_CLASS_KINDS = [ + 'requirement', + 'architecture', + 'module', + 'interface', + 'constraint', + 'decision', + 'contract', + 'bug', + 'waiver', + 'synthesis_record', + 'ip_catalog' +] as const; +export type FirstClassKind = (typeof FIRST_CLASS_KINDS)[number]; + +/** + * Any kind name, including plugin-registered ones. + * First-class kinds are present as members of the union; arbitrary strings + * are accepted at the storage boundary. + */ +export type AnyKind = FirstClassKind | (string & {}); + +/** + * Open-shape entity. `kind` is the discriminator; `id` is a unique string + * within a project; `fields` carries the per-kind typed payload; `extensions` + * is the reserved escape hatch for plugin metadata (no API yet in v1.0.1). + * + * Per-kind shape is validated by the narrowing helpers (isX) below. New + * kinds are accepted as long as they have *some* well-formed fields object. + */ +export interface Entity { + kind: AnyKind; + id: string; + fields: Record; + extensions?: Record; +} + +export interface Relation { + from: string; + rel: RelationKind; + to: string; +} + +export interface EntityStore { + entities: Entity[]; + relations: Relation[]; +} + +// ---- narrowing helpers ------------------------------------------------------ +// Each helper validates that an entity's `fields` has all the required keys +// for its kind and that those keys have the right runtime types. Returns +// false on any mismatch; type-narrowed callers use `if (!isModule(e)) ...`. + +function getField(e: Entity, key: string): unknown { + return e.fields[key]; +} + +function hasString(e: Entity, key: string): boolean { + return typeof getField(e, key) === 'string'; +} + +function hasNumber(e: Entity, key: string): boolean { + return typeof getField(e, key) === 'number'; +} + +function hasStringArray(e: Entity, key: string): boolean { + const v = getField(e, key); + return Array.isArray(v) && v.every((s) => typeof s === 'string'); +} + +function hasSignalArray(e: Entity, key: string): boolean { + const v = getField(e, key); + if (!Array.isArray(v)) return false; + return v.every((s) => typeof s === 'object' && s !== null && typeof (s as SignalRef).name === 'string' && typeof (s as SignalRef).direction === 'string'); +} + +export function isRequirement( + e: Entity +): e is Entity & { kind: 'requirement'; fields: { text: string } } { + return e.kind === 'requirement' && hasString(e, 'text'); +} + +export function isArchitecture( + e: Entity +): e is Entity & { kind: 'architecture'; fields: { text: string } } { + return e.kind === 'architecture' && hasString(e, 'text'); +} + +export function isModule( + e: Entity +): e is Entity & { kind: 'module'; fields: { name: string; file: string } } { + return e.kind === 'module' && hasString(e, 'name') && hasString(e, 'file'); +} + +export function isInterface( + e: Entity +): e is Entity & { kind: 'interface'; fields: { name: string; signals: SignalRef[] } } { + return e.kind === 'interface' && hasString(e, 'name') && hasSignalArray(e, 'signals'); +} + +export function isConstraint( + e: Entity +): e is Entity & { kind: 'constraint'; fields: { key: string; value: string } } { + return e.kind === 'constraint' && hasString(e, 'key') && hasString(e, 'value'); +} + +export function isDecision( + e: Entity +): e is Entity & { + kind: 'decision'; + fields: { date: string; rationale: string; alternatives_rejected: string[] }; +} { + return ( + e.kind === 'decision' && + hasString(e, 'date') && + hasString(e, 'rationale') && + hasStringArray(e, 'alternatives_rejected') + ); +} + +export function isContract( + e: Entity +): e is Entity & { kind: 'contract'; fields: { name: string } } { + return e.kind === 'contract' && hasString(e, 'name'); +} + +// ---- store operations ------------------------------------------------------- + +export function addEntity(s: EntityStore, e: Entity): EntityStore { + if (typeof e.kind !== 'string' || e.kind.length === 0) { + throw new Error(`entities: entity missing or invalid 'kind' (got ${JSON.stringify(e)})`); + } + if (typeof e.id !== 'string' || e.id.length === 0) { + throw new Error(`entities: entity missing or invalid 'id' (got ${JSON.stringify(e)})`); + } + if (typeof e.fields !== 'object' || e.fields === null || Array.isArray(e.fields)) { + throw new Error( + `entities: entity '${e.id}' missing 'fields' (object required, got ${typeof e.fields})` + ); + } + if (s.entities.some((x) => x.id === e.id)) { + throw new Error(`entities: duplicate id ${e.id}`); + } + return { ...s, entities: [...s.entities, e] }; +} + +export function addRelation(s: EntityStore, r: Relation): EntityStore { + const ids = new Set(s.entities.map((e) => e.id)); + if (!ids.has(r.from)) throw new Error(`entities: unknown from-id ${r.from}`); + if (!ids.has(r.to)) throw new Error(`entities: unknown to-id ${r.to}`); + if ( + s.relations.some((x) => x.from === r.from && x.to === r.to && x.rel === r.rel) + ) { + throw new Error(`entities: duplicate relation ${r.from}-${r.rel}-${r.to}`); + } + return { ...s, relations: [...s.relations, r] }; +} + +export function findEntities(s: EntityStore, pred: (e: Entity) => boolean): Entity[] { + return s.entities.filter(pred); +} + +export function findRelations(s: EntityStore, id: string): Relation[] { + return s.relations.filter((r) => r.from === id || r.to === id); +} diff --git a/core/src/events.ts b/core/src/events.ts new file mode 100644 index 0000000..f2c17ba --- /dev/null +++ b/core/src/events.ts @@ -0,0 +1,27 @@ +import { appendFileSync, readFileSync } from 'node:fs'; + +export interface CompileEvent { + ts: string; + agent: string; + profile_id: string; + input_hash: string; + output_hash: string; + tokens: number; +} + +export class EventsLog { + constructor(public readonly path: string) {} + + append(e: CompileEvent): void { + appendFileSync(this.path, JSON.stringify(e) + '\n', 'utf8'); + } + + readAll(): string { + try { + return readFileSync(this.path, 'utf8'); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return ''; + throw err; + } + } +} diff --git a/core/src/hash.ts b/core/src/hash.ts new file mode 100644 index 0000000..360986a --- /dev/null +++ b/core/src/hash.ts @@ -0,0 +1,39 @@ +import { createHash } from 'node:crypto'; + +/** + * Deterministic content hash over a list of string parts. + * + * Each part is length-prefixed to prevent ambiguity (so `["ab","c"]` + * and `["a","bc"]` do not hash identically). + * + * Stable across platforms and Node versions because we control the + * serialisation: callers pass already-serialised strings. + */ +export function digest(parts: string[]): string { + const h = createHash('sha256'); + for (const p of parts) { + const lenBuf = Buffer.from(String(p.length).padStart(10, '0'), 'utf8'); + h.update(lenBuf); + h.update(p); + } + return h.digest('hex'); +} + +/** + * Canonical JSON: keys sorted, no whitespace. + * + * Use this to make `digest(...)` calls stable regardless of object key order. + */ +export function canonical(value: unknown): string { + return JSON.stringify(value, (_k, v) => { + if (v && typeof v === 'object' && !Array.isArray(v)) { + return Object.keys(v as Record) + .sort() + .reduce>((acc, k) => { + acc[k] = (v as Record)[k]; + return acc; + }, {}); + } + return v; + }); +} diff --git a/core/src/kind-registry.ts b/core/src/kind-registry.ts new file mode 100644 index 0000000..193a3dc --- /dev/null +++ b/core/src/kind-registry.ts @@ -0,0 +1,88 @@ +import { FIRST_CLASS_KINDS } from './entities.js'; + +export type FieldType = 'string' | 'number' | 'integer' | 'boolean' | 'array'; + +export interface FieldSchema { + type: FieldType; + required?: boolean; + values?: readonly string[]; +} + +export interface KindRegistration { + kind: string; + description: string; + schema: Record; +} + +export type ValidationResult = + | { ok: true } + | { ok: false; errors: string[] }; + +/** + * A registry of plugin-defined entity kinds. First-class kinds are + * reserved names — registered in `core/src/entities.ts` — and are not + * accepted by `register()`. M2 plugins can extend the runtime's kind + * vocabulary without touching core by calling `register(r)` from a + * plugin's loader. + */ +export class KindRegistry { + private readonly by_kind = new Map(); + + register(r: KindRegistration): void { + if ((FIRST_CLASS_KINDS as readonly string[]).includes(r.kind)) { + throw new Error(`kind-registry: '${r.kind}' is a reserved first-class kind`); + } + if (this.by_kind.has(r.kind)) { + throw new Error(`kind-registry: '${r.kind}' already registered`); + } + this.by_kind.set(r.kind, r); + } + + has(kind: string): boolean { + return this.by_kind.has(kind); + } + + kinds(): readonly string[] { + return [...this.by_kind.keys()]; + } + + /** + * Validate a fields-record against a registered kind's schema. + * First-class kinds pass through (the runtime's narrowing helpers + * validate those). Extra fields are allowed (forward compatibility). + */ + validate(kind: string, fields: Record): ValidationResult { + if ((FIRST_CLASS_KINDS as readonly string[]).includes(kind)) { + return { ok: true }; + } + const reg = this.by_kind.get(kind); + if (!reg) { + return { ok: false, errors: [`kind-registry: unknown kind '${kind}'`] }; + } + const errors: string[] = []; + for (const [name, schema] of Object.entries(reg.schema)) { + const v = fields[name]; + if (v === undefined) { + if (schema.required) errors.push(`field '${name}' is required`); + continue; + } + const actual = Array.isArray(v) ? 'array' + : typeof v === 'string' ? 'string' + : typeof v === 'number' ? (Number.isInteger(v) ? 'integer' : 'number') + : typeof v === 'boolean' ? 'boolean' + : 'other'; + if (schema.type === 'integer' && actual !== 'integer') { + errors.push(`field '${name}' must be integer (got ${actual})`); + continue; + } + if (schema.type !== 'integer' && actual !== schema.type) { + errors.push(`field '${name}' must be ${schema.type} (got ${actual})`); + continue; + } + if (schema.type === 'string' && schema.values && !schema.values.includes(v as string)) { + errors.push(`field '${name}' must be one of ${schema.values.join('|')} (got '${v}')`); + } + } + return errors.length === 0 ? { ok: true } : { ok: false, errors }; + } +} diff --git a/core/src/profile.ts b/core/src/profile.ts new file mode 100644 index 0000000..677cb32 --- /dev/null +++ b/core/src/profile.ts @@ -0,0 +1,61 @@ +import yaml from 'js-yaml'; + +export type Layer = + | 'contracts' + | 'ast_slice' + | 'decisions' + | 'constraints' + | 'agent_exemplars'; + +export const LAYERS: readonly Layer[] = [ + 'contracts', + 'ast_slice', + 'decisions', + 'constraints', + 'agent_exemplars' +]; + +export interface Profile { + id: string; + description?: string; + token_budget: number; + include_layers: Layer[]; + exemplars_per_agent: number; + soft_token_margin: number; +} + +export class ProfileParseError extends Error {} + +function isLayer(value: unknown): value is Layer { + return typeof value === 'string' && (LAYERS as readonly string[]).includes(value); +} + +export function loadProfile(text: string): Profile { + const d = yaml.load(text) as Record | null; + if (typeof d !== 'object' || d === null) { + throw new ProfileParseError('profile: top-level must be object'); + } + if (typeof d.id !== 'string') throw new ProfileParseError('profile: id required'); + const budget = d.token_budget; + if (typeof budget !== 'number' || budget <= 0) { + throw new ProfileParseError('profile: token_budget must be positive number'); + } + if (!Array.isArray(d.include_layers)) { + throw new ProfileParseError('profile: include_layers must be array'); + } + for (const l of d.include_layers) { + if (!isLayer(l)) { + throw new ProfileParseError(`profile: unknown layer ${String(l)}`); + } + } + return { + id: d.id, + description: typeof d.description === 'string' ? d.description : undefined, + token_budget: budget, + include_layers: d.include_layers as Layer[], + exemplars_per_agent: + typeof d.exemplars_per_agent === 'number' ? d.exemplars_per_agent : 1, + soft_token_margin: + typeof d.soft_token_margin === 'number' ? d.soft_token_margin : 0.1 + }; +} diff --git a/core/src/sv-scanner.ts b/core/src/sv-scanner.ts new file mode 100644 index 0000000..da5928c --- /dev/null +++ b/core/src/sv-scanner.ts @@ -0,0 +1,265 @@ +import { createHash } from 'node:crypto'; + +/** + * Pure-TS regex-based SystemVerilog scanner. + * + * M1 version returned module names + instantiation names only. v1.0.1-m1 + * (R-2) extends the surface with: + * - port list per module (name, direction, [width]) + * - parameter list per module (name + default) + * - clock-domain and reset signals extracted from `always_ff` + * sensitivity lists + * - reset polarity inference (active_{low,high}_{sync,async}) + * - parameter overrides on instantiation (#(...=value...)) + * + * M2 replaces this with tree-sitter. Same `SvScan` shape. + */ + +export type ResetPolarity = + | 'active_low_async' + | 'active_high_async' + | 'active_low_sync' + | 'active_high_sync' + | 'none'; + +export interface Port { + name: string; + direction: 'input' | 'output' | 'inout'; + width?: number; + /** Symbolic width expression (e.g. "WIDTH-1" for [WIDTH-1:0]). */ + width_expr?: string; +} + +export interface Parameter { + name: string; + default: string; +} + +export interface ModuleDecl { + name: string; + has_parameter_list: boolean; + ports: Port[]; + parameters: Parameter[]; + /** Names of clock signals referenced in always_ff sensitivity lists. */ + clock_signals: string[]; + /** Names of reset signals referenced in always_ff sensitivity lists. */ + reset_signals: string[]; + /** Inferred reset polarity from sensitivity lists / if-conditions. */ + reset_polarity: ResetPolarity; +} + +export interface Instantiation { + module: string; + instance: string; + /** Named parameter overrides from #(WIDTH=16). Undefined if none. */ + parameter_overrides?: Record; +} + +export interface SvScan { + source_sha: string; + modules: ModuleDecl[]; + instantiations: Instantiation[]; +} + +// Strip comments and line continuations. +function normalise(source: string): string { + let s = source; + s = s.replace(/\/\*[\s\S]*?\*\//g, ' '); + s = s.replace(/\/\/.*$/gm, ' '); + s = s.replace(/\\\n/g, ' '); + return s; +} + +// `module ` followed by any of: optional #(...), optional `import pkg::*;`, +// optional (...). Captures the module declaration header (everything up to +// `;` or `{`) so we can pull the parameter and port lists out of it. +const MODULE_HEAD = /\bmodule\b\s+([A-Za-z_][A-Za-z0-9_$]*)\s*(?:#\s*\(([\s\S]*?)\))?\s*(?:\bimport\b\s+[A-Za-z_][A-Za-z0-9_$]*(?:::[A-Za-z_*][A-Za-z0-9_$]*)*\s*;)?\s*(?:\(([\s\S]*?)\))?\s*[{;]/g; + +// ` ` followed by optional #(...) and (...). +// The captured m[2] of the inner match gives the parameter-overrides block. +const INSTANTIATION = /\b([A-Za-z_][A-Za-z0-9_$]*)\s+([A-Za-z_][A-Za-z0-9_$]*)\s*(?:#\s*\(([\s\S]*?)\))?\s*\(([\s\S]*?)\)\s*;/g; + +function sourceHash(source: string): string { + return createHash('sha256').update(source).digest('hex'); +} + +const RESERVED = new Set([ + 'module', 'endmodule', 'always', 'always_ff', 'always_comb', 'always_latch', + 'logic', 'wire', 'reg', 'input', 'output', 'inout', 'parameter', 'localparam', + 'typedef', 'enum', 'function', 'task', 'begin', 'end', 'if', 'else', 'case', + 'endcase', 'for', 'while', 'repeat', 'forever', 'return', 'initial', + 'assign', 'posedge', 'negedge', 'or', 'and', 'not', 'import', 'package', + 'endpackage', 'interface', 'endinterface', 'modport', 'class', 'endclass', + 'extends', 'virtual', 'pure', 'const', 'static', 'automatic', 'ref' +]); + +function parseParameters(paramBlock: string): Parameter[] { + const out: Parameter[] = []; + const re = /parameter\s+(?:int|integer|logic|byte|bit|shortint|longint|real|shortreal|chandle|string|time)\s+([A-Za-z_][A-Za-z0-9_$]*)\s*=\s*([^,)\n]+)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(paramBlock)) !== null) { + out.push({ name: m[1]!, default: m[2]!.trim() }); + } + return out; +} + +function parseSimpleWidth(expr: string): number | undefined { + // Widths declared as `:` (e.g. `[7:0]`), `-1` (e.g. `[WIDTH-1]` + // when written without the lower bound), or a plain integer. + // The convention `[n-1:0]` denotes a bus whose MSB is `n-1` and total + // width is `n` bits. So: + // - "7:0" → msb=7, width 8. + // - "7-1" → msb=7, width 8. + // - "7" → msb=7, width 8. + // - "WIDTH-1" → symbolic; resolution is M2 work; width stays undefined. + // + // We store the *bus width* (not the msb). The `width_expr` field carries + // the textual representation for downstream tooling that needs the msb. + const trimmed = expr.trim(); + const rangeMsbLsb = /^(\d+)\s*:\s*0$/.exec(trimmed); + if (rangeMsbLsb) return parseInt(rangeMsbLsb[1]!, 10) + 1; + const withMinus = /^(\d+)\s*-\s*1$/.exec(trimmed); + if (withMinus) return parseInt(withMinus[1]!, 10) + 1; + const plain = /^(\d+)$/.exec(trimmed); + if (plain) return parseInt(plain[1]!, 10) + 1; + return undefined; +} + +function parsePorts(portBlock: string): Port[] { + const out: Port[] = []; + // Split on commas at top level; commas inside `{}` for parameter + // declarations within the port lists are not used in the bench's SV. + const entries = portBlock.split(/,\s*|\n/).map(s => s.trim()).filter(Boolean); + for (const entry of entries) { + // Skip `parameter ...` lines that some authors put in port lists. + if (entry.startsWith('parameter')) continue; + // Match `direction [type-qual] [width] name`. + const dir = /^(input|output|inout)\b\s*/.exec(entry); + if (!dir) continue; + const direction = dir[1] as Port['direction']; + let cursor = entry.slice(dir[0].length); + // Optional type qualifier (logic, wire, reg, etc.). + const typeQual = /^(logic|wire|reg|bit|byte|int|integer)\b\s*/.exec(cursor); + if (typeQual) cursor = cursor.slice(typeQual[0].length); + // Optional [expr] for bus width. + let widthExpr: string | undefined; + const widthMatch = /^\[\s*([^\]]+?)\s*\]\s*/.exec(cursor); + if (widthMatch) { + widthExpr = widthMatch[1]!; + cursor = cursor.slice(widthMatch[0].length); + } + // The name is the trailing identifier. Tolerate trailing comments / ; + const nameMatch = /([A-Za-z_][A-Za-z0-9_$]*)\s*$/.exec(cursor); + const name = nameMatch?.[1]; + if (!name) continue; + out.push({ + name, + direction, + width: widthExpr ? parseSimpleWidth(widthExpr) : undefined, + width_expr: widthExpr + }); + } + return out; +} + +function inferClockReset( + fullText: string +): { clock_signals: string[]; reset_signals: string[]; reset_polarity: ResetPolarity } { + const clock_signals: string[] = []; + const reset_signals: string[] = []; + let polarity: ResetPolarity = 'none'; + const seenClock = new Set(); + + // Async reset: `always_ff @(... or negedge )` or `or posedge `. + const asyncLowRe = /\bor\s+negedge\s+([A-Za-z_][A-Za-z0-9_$]*)/g; + let m: RegExpExecArray | null; + while ((m = asyncLowRe.exec(fullText)) !== null) { + reset_signals.push(m[1]!); + polarity = 'active_low_async'; + } + const asyncHighRe = /\bor\s+posedge\s+([A-Za-z_][A-Za-z0-9_$]*)/g; + while ((m = asyncHighRe.exec(fullText)) !== null) { + reset_signals.push(m[1]!); + if (polarity === 'none') polarity = 'active_high_async'; + } + + // Clocks: `posedge ` (sync) or first signal in the sensitivity list + // when async reset isn't used (then it's a sync block). + const posedgeRe = /posedge\s+([A-Za-z_][A-Za-z0-9_$]*)/g; + while ((m = posedgeRe.exec(fullText)) !== null) { + const sig = m[1]!; + if (!seenClock.has(sig)) { + clock_signals.push(sig); + seenClock.add(sig); + } + } + + // If async reset didn't show, infer sync reset from if-conditions. + if (reset_signals.length === 0) { + const ifActiveHigh = /\bif\s*\(\s*([A-Za-z_][A-Za-z0-9_$]*)\s*\)/.exec(fullText); + const ifActiveLow = /\bif\s*\(\s*!\s*([A-Za-z_][A-Za-z0-9_$]*)\s*\)/.exec(fullText); + if (ifActiveLow && clock_signals.length > 0) { + const sig = ifActiveLow[1]!; + reset_signals.push(sig); + polarity = 'active_low_sync'; + } else if (ifActiveHigh && clock_signals.length > 0) { + const sig = ifActiveHigh[1]!; + reset_signals.push(sig); + polarity = 'active_high_sync'; + } + } + + return { clock_signals, reset_signals, reset_polarity: polarity }; +} + +function parseParamOverrides(overrideBlock: string | undefined): Record | undefined { + if (!overrideBlock) return undefined; + const out: Record = {}; + const re = /([A-Za-z_][A-Za-z0-9_$]*)\s*=\s*([^,)\n]+)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(overrideBlock)) !== null) { + out[m[1]!] = m[2]!.trim(); + } + return Object.keys(out).length === 0 ? undefined : out; +} + +export function scanSV(source: string): SvScan { + const text = normalise(source); + + const modules: ModuleDecl[] = []; + for (const m of text.matchAll(MODULE_HEAD)) { + const name = m[1]; + if (!name) continue; + const paramBlock = m[2]; + const portBlock = m[3]; + const inferred = inferClockReset(text); + modules.push({ + name, + has_parameter_list: typeof paramBlock === 'string', + ports: parsePorts(portBlock ?? ''), + parameters: parseParameters(paramBlock ?? ''), + clock_signals: inferred.clock_signals, + reset_signals: inferred.reset_signals, + reset_polarity: inferred.reset_polarity + }); + } + + const instantiations: Instantiation[] = []; + const seen = new Set(); + for (const m of text.matchAll(INSTANTIATION)) { + const moduleName = m[1]; + const instanceName = m[2]; + if (!moduleName || !instanceName) continue; + if (moduleName === 'module' || RESERVED.has(moduleName)) continue; + const key = `${moduleName}.${instanceName}`; + if (seen.has(key)) continue; + seen.add(key); + instantiations.push({ + module: moduleName, + instance: instanceName, + parameter_overrides: parseParamOverrides(m[3]) + }); + } + + return { source_sha: sourceHash(source), modules, instantiations }; +} diff --git a/core/src/types.ts b/core/src/types.ts new file mode 100644 index 0000000..14fca6c --- /dev/null +++ b/core/src/types.ts @@ -0,0 +1,49 @@ +export type Direction = 'input' | 'output' | 'inout'; + +export const DIRECTIONS: readonly Direction[] = ['input', 'output', 'inout']; + +/** + * A signal is a named, directed, optionally-sized wire on an interface or + * module port. The canonical shape used by entity, contract, and (eventually) + * AST layers. + */ +export interface SignalRef { + name: string; + direction: Direction; + width?: number; + description?: string; +} + +function isDirection(v: unknown): v is Direction { + return typeof v === 'string' && (DIRECTIONS as readonly string[]).includes(v); +} + +/** + * Type-guard used by parsers and loaders. Strict: extra fields are allowed + * (forward-compat) but missing required ones or bad-typed fields fail. + */ +export function isSignalRef(v: unknown): v is SignalRef { + if (typeof v !== 'object' || v === null) return false; + const o = v as Record; + if (typeof o.name !== 'string' || o.name.length === 0) return false; + if (!isDirection(o.direction)) return false; + if (o.width !== undefined) { + if (typeof o.width !== 'number' || !Number.isInteger(o.width) || o.width <= 0) return false; + } + if (o.description !== undefined && typeof o.description !== 'string') return false; + return true; +} + +/** + * Stable renderer used in context packages. Format is fixed so the same + * signal hash-stable across reruns. Format: + * `[:0] // ` + * Width-1 signals render as `[0:0] `. + * No description omits the double-slash comment. + */ +export function renderSignalRef(s: SignalRef): string { + const w = typeof s.width === 'number' ? s.width : 1; + const msb = w - 1; + const desc = s.description ? ` // ${s.description}` : ''; + return `${s.direction}[${msb}:0] ${s.name}${desc}`; +} diff --git a/core/test/artefact.test.ts b/core/test/artefact.test.ts new file mode 100644 index 0000000..37ea852 --- /dev/null +++ b/core/test/artefact.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { ArtefactStore, type Artefact, type ArtefactType } from '../src/artefact'; + +// Vocabulary surfaced by the store. Sourced from a stable list — the +// store's source-of-truth is the ArtefactType union, but that's a TypeScript +// type (erased at runtime). This list mirrors the union and acts as a +// runtime smoke-test that the union stays non-trivial and contains the +// expected taxonomy keywords. +const ARTIFACT_TYPES: readonly ArtefactType[] = [ + 'compiled_context', + 'ast_snapshot', + 'lint_report', + 'synth_metrics', + 'synth_report', + 'timing_report', + 'waveform_summary', + 'sim_log', + 'benchmark', + 'verification_result', + 'synthesis_metadata' +]; + +let dir: string; +beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 's2r-art-')); }); +afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); + +/** + * Build a valid Artefact envelope. `overrides` lets individual tests poke + * single fields without restating the whole envelope. + */ +function makeArtefact(overrides: Record = {}): Artefact { + return { + type: 'compiled_context', + body: Buffer.from('{"layers": []}', 'utf8'), + ref: { + producer: 'compiler:v1', + producer_run_id: 'run-1', + entity_refs: ['MOD-uart-tx'] + }, + ...overrides + } as Artefact; +} + +describe('artefact store (M1.1, generic Artefact abstraction)', () => { + it('record stores content and writes manifest at content-addressed location', async () => { + const s = new ArtefactStore(dir); + const sha = await s.record(makeArtefact({ body: Buffer.from('{}', 'utf8') })); + expect(sha).toMatch(/^[a-f0-9]{64}$/); + const expectedPath = join(dir, sha.slice(0, 2), sha.slice(2, 4), `${sha}.bin`); + expect(existsSync(expectedPath)).toBe(true); + expect(existsSync(join(dir, 'manifests', `${sha}.json`))).toBe(true); + }); + + it('record is content-addressed: identical body → identical sha (no duplicate write)', async () => { + const s = new ArtefactStore(dir); + const body = Buffer.from('hello', 'utf8'); + const sha1 = await s.record(makeArtefact({ type: 'compiled_context', body })); + const sha2 = await s.record(makeArtefact({ type: 'lint_report', body })); + expect(sha1).toBe(sha2); + }); + + it('get retrieves content by sha', async () => { + const s = new ArtefactStore(dir); + const body = Buffer.from('hello world', 'utf8'); + const sha = await s.record(makeArtefact({ body })); + const retrieved = await s.get(sha); + expect(retrieved).not.toBeNull(); + expect(retrieved!.toString('utf8')).toBe('hello world'); + }); + + it('get returns null for unknown sha', async () => { + const s = new ArtefactStore(dir); + const out = await s.get('0'.repeat(64)); + expect(out).toBeNull(); + }); + + it('query returns artefacts matching filters', async () => { + const s = new ArtefactStore(dir); + await s.record(makeArtefact({ + type: 'compiled_context', + body: Buffer.from('a'), + ref: { producer: 'compiler:v1', producer_run_id: 'run-1', entity_refs: ['MOD-x'] } + })); + await s.record(makeArtefact({ + type: 'lint_report', + body: Buffer.from('b'), + ref: { producer: 'linter:v1', producer_run_id: 'run-1', entity_refs: ['MOD-x'] } + })); + await s.record(makeArtefact({ + type: 'lint_report', + body: Buffer.from('c'), + ref: { producer: 'linter:v1', producer_run_id: 'run-2', entity_refs: ['MOD-y'] } + })); + const lint = await s.query({ type: 'lint_report' }); + expect(lint).toHaveLength(2); + const xAll = await s.query({ entity_id: 'MOD-x' }); + expect(xAll).toHaveLength(2); + const xCompiles = await s.query({ type: 'compiled_context', entity_id: 'MOD-x' }); + expect(xCompiles).toHaveLength(1); + }); + + it('getManifest returns the same record that record wrote', async () => { + const s = new ArtefactStore(dir); + const sha = await s.record(makeArtefact({ + type: 'synth_metrics', + body: Buffer.from('{}'), + ref: { producer: 'yosys', producer_run_id: 'run-1', entity_refs: ['MOD-x'] } + })); + const m = await s.getManifest(sha); + expect(m).not.toBeNull(); + expect(m!.type).toBe('synth_metrics'); + expect(m!.producer).toBe('yosys'); + expect(m!.entity_refs).toEqual(['MOD-x']); + expect(m!.content_sha).toBe(sha); + expect(m!.bytes).toBe(2); + }); + + it('record treats body as opaque — store never interprets bytes', async () => { + const s = new ArtefactStore(dir); + const bytes = Buffer.from([0x00, 0xff, 0x7f, 0x80, 0x90]); // arbitrary + const sha = await s.record(makeArtefact({ body: bytes, type: 'waveform_summary' })); + const got = await s.get(sha); + expect(got).toEqual(bytes); + }); + + it('manifest at the same sha is not re-written on the second record call', async () => { + const s = new ArtefactStore(dir); + const body = Buffer.from('same body', 'utf8'); + const sha = await s.record(makeArtefact({ body })); + const manifestPath = join(dir, 'manifests', `${sha}.json`); + const first = readFileSync(manifestPath, 'utf8'); + await new Promise((r) => setTimeout(r, 30)); + await s.record(makeArtefact({ body })); + const second = readFileSync(manifestPath, 'utf8'); + expect(second).toBe(first); + }); + + it('exposes a non-trivial type vocabulary that is not compiler-specific', () => { + // The vocabulary is the store's contract, not the compiler's. The store + // can carry any of: contexts, AST snapshots, lint reports, synthesis + // metrics, synthesis reports, timing reports, waveforms, sim logs, + // benchmarks, verification results, synthesis metadata. The compiler + // is just one of many producers. + expect(ARTIFACT_TYPES.length).toBeGreaterThanOrEqual(5); + expect(ARTIFACT_TYPES).toContain('compiled_context'); + expect(ARTIFACT_TYPES).toContain('synth_report'); + expect(ARTIFACT_TYPES).toContain('timing_report'); + expect(ARTIFACT_TYPES).toContain('waveform_summary'); + expect(ARTIFACT_TYPES).toContain('benchmark'); + }); +}); diff --git a/core/test/ast.test.ts b/core/test/ast.test.ts new file mode 100644 index 0000000..9d4a5f0 --- /dev/null +++ b/core/test/ast.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from 'vitest'; +import { parseSV, queryModules, queryInstantiations } from '../src/ast'; + +const UART_SV = ` +module uart_tx #( + parameter int CLK_HZ = 100_000_000, + parameter int BAUD = 115200 +) ( + input logic clk, + input logic rst_n, + input logic [7:0] data_in, + input logic valid_in, + output logic tx, + output logic busy +); + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + tx <= 1'b1; + end + end +endmodule + +module uart_top import axi_lite_pkg::*; +( + axi_lite_if.slave s_axi +); + uart_tx u_tx (.clk(s_axi.clk), .rst_n(s_axi.rst_n)); +endmodule +`; + +describe('ast (M1 surface)', () => { + it('parses SV without throwing', () => { + expect(() => parseSV(UART_SV)).not.toThrow(); + }); + + it('finds both modules in a multi-module source', () => { + const tree = parseSV(UART_SV); + expect(queryModules(tree).sort()).toEqual(['uart_top', 'uart_tx']); + }); + + it('finds module instantiations (param + named ports)', () => { + const tree = parseSV(UART_SV); + const insts = queryInstantiations(tree); + expect(insts.some((i) => i.module === 'uart_tx' && i.instance === 'u_tx')).toBe(true); + }); + + it('finds zero instantiations in an empty module', () => { + const empty = `module empty; endmodule`; + expect(queryInstantiations(parseSV(empty))).toEqual([]); + }); + + it('handles a single module with a # parameter port list', () => { + const src = `module foo #(parameter int X = 1) (input logic a, output logic b); endmodule`; + expect(queryModules(parseSV(src))).toEqual(['foo']); + }); + + it('handles a module without a parameter port list', () => { + const src = `module bar (input logic a); endmodule`; + expect(queryModules(parseSV(src))).toEqual(['bar']); + }); + + it('returns a stable, content-hashed slice identifier', () => { + expect(parseSV(UART_SV).source_sha.length).toBe(64); + }); +}); + +describe('AST extensions (v1.0.1-m1)', () => { + it('extracts port list with directions', () => { + const tree = parseSV(UART_SV); + const fifo = tree.modules.find((m) => m.name === 'uart_tx')!; + expect(fifo.ports.length).toBe(6); + const clk = fifo.ports.find((p) => p.name === 'clk')!; + expect(clk.direction).toBe('input'); + expect(clk.width).toBeUndefined(); + const dataOut = fifo.ports.find((p) => p.name === 'busy')!; + expect(dataOut.direction).toBe('output'); + expect(dataOut.width).toBeUndefined(); + const dataIn = fifo.ports.find((p) => p.name === 'data_in')!; + expect(dataIn.direction).toBe('input'); + expect(dataIn.width).toBe(8); + }); + + it('extracts parameter list with default values', () => { + const tree = parseSV(UART_SV); + const uart = tree.modules.find((m) => m.name === 'uart_tx')!; + const names = uart.parameters.map((p) => p.name); + expect(names).toContain('CLK_HZ'); + expect(names).toContain('BAUD'); + }); + + it('infers clock-domain and reset-info from always_ff sensitivity', () => { + const tree = parseSV(UART_SV); + const uart = tree.modules.find((m) => m.name === 'uart_tx')!; + expect(uart.clock_signals).toContain('clk'); + expect(uart.reset_signals).toContain('rst_n'); + // 'or negedge' is async active-low reset. + expect(uart.reset_polarity).toBe('active_low_async'); + }); + + it('handles sync-active-high reset', () => { + const src = ` +module m(input logic clk, input logic rst, output logic q); + always_ff @(posedge clk) if (rst) q <= 1'b0; +endmodule`; + const tree = parseSV(src); + expect(tree.modules[0]!.reset_polarity).toBe('active_high_sync'); + }); + + it('handles sync-active-low reset', () => { + const src = ` +module m(input logic clk, input logic rst_n, output logic q); + always_ff @(posedge clk) if (!rst_n) q <= 1'b0; +endmodule`; + const tree = parseSV(src); + expect(tree.modules[0]!.reset_polarity).toBe('active_low_sync'); + }); + + it('produces stable source_sha across reruns', () => { + const src = 'module m; endmodule'; + expect(parseSV(src).source_sha).toBe(parseSV(src).source_sha); + }); + + it('handles a module with no always_ff (combinational-only)', () => { + const src = ` +module m(input logic a, input logic b, output logic y); + assign y = a & b; +endmodule`; + const tree = parseSV(src); + expect(tree.modules[0]!.clock_signals).toEqual([]); + expect(tree.modules[0]!.reset_signals).toEqual([]); + expect(tree.modules[0]!.reset_polarity).toBe('none'); + }); + + it('extracts parameter overrides on instantiation', () => { + const src = ` +module top(); + reg16 u_reg (.clk(clk), .rst_n(rst_n)); +endmodule + +module reg16 #(parameter int WIDTH = 8) (input logic clk, input logic rst_n); +endmodule`; + const tree = parseSV(src); + // reg16 has no override here; tree should still include it. + const inst = tree.instantiations.find((i) => i.instance === 'u_reg'); + expect(inst).toBeDefined(); + expect(inst!.parameter_overrides).toBeUndefined(); + }); +}); diff --git a/core/test/compiler-cache-key.test.ts b/core/test/compiler-cache-key.test.ts new file mode 100644 index 0000000..0e089b1 --- /dev/null +++ b/core/test/compiler-cache-key.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { compile, type CompileInputs } from '../src/compiler'; +import { loadProfile } from '../src/profile'; +import { parseContract } from '../src/contract'; +import { parseSV } from '../src/ast'; +import { addEntity, addRelation, type EntityStore, type Entity, type Relation } from '../src/entities'; + +function baseInputs(): CompileInputs { + const profile = loadProfile(readFileSync(join(__dirname, '../../profiles/default.yaml'), 'utf8')); + const uart = parseContract(readFileSync(join(__dirname, '../../contracts/uart.yaml'), 'utf8')); + let store: EntityStore = { entities: [], relations: [] }; + const r1: Entity = { kind: 'requirement', id: 'R-001', fields: { text: 'TX at 115200 baud' } }; + const m1: Entity = { kind: 'module', id: 'MOD-uart-tx', fields: { name: 'uart_tx', file: 'rtl/uart_tx.sv' } }; + store = addEntity(store, r1); + store = addEntity(store, m1); + const rel1: Relation = { from: 'R-001', rel: 'produces', to: 'MOD-uart-tx' }; + store = addRelation(store, rel1); + const src = readFileSync(join(__dirname, '../bench/uart/rtl/uart_tx.sv'), 'utf8'); + return { + profile, + contracts: [uart], + ast: [{ file: 'rtl/uart_tx.sv', tree: parseSV(src) }], + entities: store, + agent: 'rtl-designer', + task_id: 'task-1' + }; +} + +describe('compiler cache key (R-4 / SCHEMA-E, v1.0.1-m1)', () => { + it('changing profile.description changes input_hash', () => { + const a = (compile(baseInputs())).input_hash; + const mutated = baseInputs(); + mutated.profile = { ...mutated.profile, description: 'a different description' }; + const b = (compile(mutated)).input_hash; + expect(a).not.toBe(b); + }); + + it('changing profile.exemplars_per_agent changes input_hash', () => { + const a = (compile(baseInputs())).input_hash; + const mutated = baseInputs(); + mutated.profile = { ...mutated.profile, exemplars_per_agent: 3 }; + const b = (compile(mutated)).input_hash; + expect(a).not.toBe(b); + }); + + it('changing profile.soft_token_margin changes input_hash', () => { + const a = (compile(baseInputs())).input_hash; + const mutated = baseInputs(); + mutated.profile = { ...mutated.profile, soft_token_margin: 0.4 }; + const b = (compile(mutated)).input_hash; + expect(a).not.toBe(b); + }); + + it('changing profile.token_budget changes input_hash', () => { + const a = (compile(baseInputs())).input_hash; + const mutated = baseInputs(); + mutated.profile = { ...mutated.profile, token_budget: 99999 }; + const b = (compile(mutated)).input_hash; + expect(a).not.toBe(b); + }); + + it('changing profile.include_layers changes input_hash', () => { + const a = (compile(baseInputs())).input_hash; + const mutated = baseInputs(); + mutated.profile = { + ...mutated.profile, + include_layers: ['contracts', 'ast_slice'] + }; + const b = (compile(mutated)).input_hash; + expect(a).not.toBe(b); + }); +}); diff --git a/core/test/compiler.test.ts b/core/test/compiler.test.ts new file mode 100644 index 0000000..1995768 --- /dev/null +++ b/core/test/compiler.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { compile, CompileError, type CompileInputs } from '../src/compiler'; +import { loadProfile, type Profile, type Layer } from '../src/profile'; +import { parseContract } from '../src/contract'; +import { parseSV } from '../src/ast'; +import { addEntity, addRelation, type EntityStore, type Entity, type Relation } from '../src/entities'; +import { ArtefactStore } from '../src/artefact'; + +function makeInputs(overrides: Partial = {}): CompileInputs { + const profile = loadProfile(readFileSync(join(__dirname, '../../profiles/default.yaml'), 'utf8')); + const uart = parseContract(readFileSync(join(__dirname, '../../contracts/uart.yaml'), 'utf8')); + let store: EntityStore = { entities: [], relations: [] }; + const r1: Entity = { kind: 'requirement', id: 'R-001', fields: { text: 'TX at 115200 baud' } }; + const m1: Entity = { kind: 'module', id: 'MOD-uart-tx', fields: { name: 'uart_tx', file: 'rtl/uart_tx.sv' } }; + const c1: Entity = { kind: 'constraint', id: 'C-001', fields: { key: 'f_clk_min', value: '100MHz' } }; + store = addEntity(store, r1); + store = addEntity(store, m1); + store = addEntity(store, c1); + const rel1: Relation = { from: 'R-001', rel: 'produces', to: 'MOD-uart-tx' }; + store = addRelation(store, rel1); + const src = readFileSync(join(__dirname, '../bench/uart/rtl/uart_tx.sv'), 'utf8'); + return { + profile, + contracts: [uart], + ast: [{ file: 'rtl/uart_tx.sv', tree: parseSV(src) }], + entities: store, + agent: 'rtl-designer', + task_id: 'task-1', + ...overrides + }; +} + +describe('compiler', () => { + it('produces all 5 layers in default order', () => { + const out = compile(makeInputs()); + expect(out.layers.map((l) => l.name).sort()).toEqual([ + 'agent_exemplars', + 'ast_slice', + 'constraints', + 'contracts', + 'decisions' + ]); + expect(out.tokens).toBeGreaterThan(0); + }); + + it('emits contracts layer first (interface contract anchors everything)', () => { + const out = compile(makeInputs()); + expect(out.layers[0]!.name).toBe('contracts'); + }); + + it('changes output when contracts change', () => { + const a = compile(makeInputs()); + const b = compile(makeInputs({ contracts: [] })); + expect(a.layers[0]!.content).not.toBe(b.layers[0]!.content); + expect(a.content_hash).not.toBe(b.content_hash); + }); + + it('changes output when profile changes', () => { + const a = makeInputs(); + const out = compile(a); + const altProfile: Profile = { ...a.profile, include_layers: ['contracts', 'ast_slice'] as Layer[] }; + const b = compile(makeInputs({ profile: altProfile })); + expect(out.layers.length).toBe(5); + expect(b.layers.length).toBe(2); + }); + + it('changes output when AST source changes', () => { + const a = compile(makeInputs()); + const mutated = makeInputs(); + const empty = parseSV('module empty; endmodule'); + mutated.ast = [{ file: 'rtl/uart_tx.sv', tree: empty }]; + const b = compile(mutated); + const aAst = a.layers.find((l) => l.name === 'ast_slice')!.content; + const bAst = b.layers.find((l) => l.name === 'ast_slice')!.content; + expect(aAst).not.toBe(bAst); + }); + + it('changes output when entities change', () => { + const a = compile(makeInputs()); + const mutated = makeInputs(); + mutated.entities = addEntity(mutated.entities, { + kind: 'constraint', + id: 'C-002', + fields: { key: 'power_max', value: '50mW' } + }); + const b = compile(mutated); + expect(a.content_hash).not.toBe(b.content_hash); + }); + + it('throws CompileError when content vastly exceeds budget', () => { + const overflow = makeInputs({ + profile: { ...makeInputs().profile, token_budget: 5 } + }); + expect(() => compile(overflow)).toThrow(CompileError); + }); + + it('records a stable content hash', () => { + const out = compile(makeInputs()); + expect(out.content_hash).toMatch(/^[a-f0-9]{64}$/); + }); + + it('AST slice surfaces ports for each module', () => { + const out = compile(makeInputs()); + const astLayer = out.layers.find((l) => l.name === 'ast_slice')!; + expect(astLayer.content).toMatch(/port list/); + expect(astLayer.content).toMatch(/data_in/); + }); + + it('AST slice surfaces clock_signals and reset polarity', () => { + const out = compile(makeInputs()); + const astLayer = out.layers.find((l) => l.name === 'ast_slice')!; + expect(astLayer.content).toMatch(/clock_signals/); + expect(astLayer.content).toMatch(/reset_polarity/); + }); + + it('AST slice does NOT contain redundant source_sha inside the body', () => { + // source_sha is captured in content_hash on CompileOutput; the body + // shouldn't echo it twice. v1.0.1-m1 removes the inline duplication. + const out = compile(makeInputs()); + const astLayer = out.layers.find((l) => l.name === 'ast_slice')!; + const matches = astLayer.content.match(/source_sha:/g); + expect(matches?.length ?? 0).toBe(0); + }); + + it('CLI bridge + store: record a compile result as an artefact', async () => { + // M1.1 Option B: compile() doesn't know about stores. The CLI's + // `compileOutputToArtefact` bridge builds the envelope; the store + // records it. Two clean calls, testable independently. + const { compileOutputToArtefact } = await import('../src/cli/artefact-bridge.js'); + const store = new ArtefactStore(mkdtempSync(join(tmpdir(), 's2r-comp-'))); + const out = compile(makeInputs()); + const sha = await store.record(compileOutputToArtefact(out, { + producer: 'compiler:v1', + producer_run_id: `${out.agent}:${out.task_id}` + })); + expect(sha).toMatch(/^[a-f0-9]{64}$/); + const bytes = await store.get(sha); + expect(bytes).not.toBeNull(); + const body = JSON.parse(bytes!.toString('utf8')); + expect(body.content_hash).toBe(out.content_hash); + expect(body.layers).toEqual(out.layers.map((l) => ({ name: l.name, content: l.content, tokens: l.tokens_estimate }))); + rmSync(store.root, { recursive: true, force: true }); + }); + + it('bridge + store: identical inputs produce identical sha (content-addressed)', async () => { + const { compileOutputToArtefact } = await import('../src/cli/artefact-bridge.js'); + const store = new ArtefactStore(mkdtempSync(join(tmpdir(), 's2r-cmp2-'))); + const shaA = await store.record(compileOutputToArtefact(compile(makeInputs()), { + producer: 'compiler:v1', producer_run_id: 'run-A' + })); + const shaB = await store.record(compileOutputToArtefact(compile(makeInputs()), { + producer: 'compiler:v1', producer_run_id: 'run-B' + })); + expect(shaA).toBe(shaB); + rmSync(store.root, { recursive: true, force: true }); + }); +}); diff --git a/core/test/contract-signal-alias.test.ts b/core/test/contract-signal-alias.test.ts new file mode 100644 index 0000000..9e2e714 --- /dev/null +++ b/core/test/contract-signal-alias.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import type { ContractSignal, Contract } from '../src/contract'; +import { renderContractSignal } from '../src/contract'; +import type { SignalRef } from '../src/types'; +import { renderSignalRef } from '../src/types'; +import { parseContract } from '../src/contract'; + +describe('ContractSignal aliasing (v1.0.1-m1)', () => { + it('ContractSignal is type-equivalent to SignalRef', () => { + const sig: ContractSignal = { name: 'a', direction: 'input' }; + const ref: SignalRef = sig; + expect(ref.name).toBe('a'); + }); + + it('renderContractSignal delegates to renderSignalRef', () => { + const sig: SignalRef = { name: 'data_in', direction: 'input', width: 8, description: 'byte' }; + expect(renderContractSignal(sig)).toBe(renderSignalRef(sig)); + }); + + it('parseContract accepts a signal with description (silent widening)', () => { + const yaml = ` +id: c-with-desc +name: Test +version: "1.0" +interfaces: + - name: ifc + kind: stream + signals: + - { name: a, direction: input, description: "first" } +invariants: [] +`; + const c = parseContract(yaml); + const sig = c.interfaces[0]?.signals[0]; + expect(sig?.description).toBe('first'); + }); + + it('parseContract rejects a signal without a direction', () => { + const yaml = ` +id: bad +name: Bad +version: "1" +interfaces: + - name: x + kind: stream + signals: + - { name: a } +invariants: [] +`; + expect(() => parseContract(yaml)).toThrow(/direction/i); + }); +}); diff --git a/core/test/contract.test.ts b/core/test/contract.test.ts new file mode 100644 index 0000000..69a1b01 --- /dev/null +++ b/core/test/contract.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { parseContract, ContractParseError } from '../src/contract'; + +describe('contract', () => { + it('parses the real UART contract', () => { + const text = readFileSync(join(__dirname, '../../contracts/uart.yaml'), 'utf8'); + const c = parseContract(text); + expect(c.id).toBe('uart-v1'); + expect(c.name).toBe('UART 8N1 TX/RX'); + expect(c.interfaces[0]?.name).toBe('tx_if'); + expect(c.interfaces[1]?.signals.find((s) => s.name === 'data_out')?.width).toBe(8); + }); + + it('parses the real AXI4-Lite contract', () => { + const text = readFileSync(join(__dirname, '../../contracts/axi4_lite.yaml'), 'utf8'); + const c = parseContract(text); + expect(c.id).toBe('axi4-lite-v1'); + expect(c.interfaces.some((i) => i.name === 's_axi')).toBe(true); + expect(c.interfaces[0]?.signals.find((s) => s.name === 'awaddr')?.width).toBe(32); + }); + + it('rejects contracts missing required fields', () => { + expect(() => parseContract('id: x')).toThrow(ContractParseError); + }); + + it('rejects contracts with unknown interface kinds', () => { + const bad = ` +id: bad +name: Bad +version: "1" +interfaces: + - name: x + kind: nonsense + signals: [] +invariants: [] +`; + expect(() => parseContract(bad)).toThrow(/kind/); + }); + + it('rejects signals missing direction', () => { + const bad = ` +id: bad +name: Bad +version: "1" +interfaces: + - name: x + kind: stream + signals: + - { name: a } +invariants: [] +`; + expect(() => parseContract(bad)).toThrow(/direction/); + }); +}); diff --git a/core/test/determinism.test.ts b/core/test/determinism.test.ts new file mode 100644 index 0000000..cd06cae --- /dev/null +++ b/core/test/determinism.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { compile, type CompileInputs } from '../src/compiler'; +import { loadProfile } from '../src/profile'; +import { parseContract } from '../src/contract'; +import { parseSV } from '../src/ast'; +import { addEntity, addRelation, type EntityStore, type Entity, type Relation } from '../src/entities'; + +function makeInputs(): CompileInputs { + const profile = loadProfile(readFileSync(join(__dirname, '../../profiles/default.yaml'), 'utf8')); + const uart = parseContract(readFileSync(join(__dirname, '../../contracts/uart.yaml'), 'utf8')); + let store: EntityStore = { entities: [], relations: [] }; + const r1: Entity = { kind: 'requirement', id: 'R-001', fields: { text: 'TX at 115200 baud' } }; + const m1: Entity = { kind: 'module', id: 'MOD-uart-tx', fields: { name: 'uart_tx', file: 'rtl/uart_tx.sv' } }; + const c1: Entity = { kind: 'constraint', id: 'C-001', fields: { key: 'f_clk_min', value: '100MHz' } }; + store = addEntity(store, r1); + store = addEntity(store, m1); + store = addEntity(store, c1); + const rel1: Relation = { from: 'R-001', rel: 'produces', to: 'MOD-uart-tx' }; + store = addRelation(store, rel1); + const src = readFileSync(join(__dirname, '../bench/uart/rtl/uart_tx.sv'), 'utf8'); + return { + profile, + contracts: [uart], + ast: [{ file: 'rtl/uart_tx.sv', tree: parseSV(src) }], + entities: store, + agent: 'rtl-designer', + task_id: 'task-1' + }; +} + +describe('determinism (M1 invariant #2)', () => { + it('produces identical content_hash over 10 reruns', () => { + const hash = compile(makeInputs()).content_hash; + for (let i = 0; i < 10; i++) { + expect(compile(makeInputs()).content_hash).toBe(hash); + } + }); + + it('produces byte-identical layers over 10 reruns', () => { + const firstLayers = (compile(makeInputs())).layers.map((l) => l.content); + for (let i = 0; i < 10; i++) { + const nextLayers = (compile(makeInputs())).layers.map((l) => l.content); + expect(nextLayers).toEqual(firstLayers); + } + }); + + it('produces identical total tokens across reruns', () => { + const tokens = (compile(makeInputs())).tokens; + for (let i = 0; i < 10; i++) { + expect((compile(makeInputs())).tokens).toBe(tokens); + } + }); +}); diff --git a/core/test/entities.test.ts b/core/test/entities.test.ts new file mode 100644 index 0000000..1214c90 --- /dev/null +++ b/core/test/entities.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect } from 'vitest'; +import { + addEntity, + addRelation, + findEntities, + findRelations, + isRequirement, + isModule, + isConstraint, + isDecision, + isInterface, + isArchitecture, + isContract, + FIRST_CLASS_KINDS, + type EntityStore, + type Entity, + type Relation +} from '../src/entities'; + +// Open-shape fixtures +const reqOpen: Entity = { kind: 'requirement', id: 'R-001', fields: { text: 'TX at 115200 baud' } }; +const modOpen: Entity = { kind: 'module', id: 'MOD-uart-tx', fields: { name: 'uart_tx', file: 'rtl/uart_tx.sv' } }; +const conOpen: Entity = { kind: 'contract', id: 'C-uart', fields: { name: 'UART 8N1' } }; + +function seed(): EntityStore { + let s: EntityStore = { entities: [], relations: [] }; + s = addEntity(s, reqOpen); + s = addEntity(s, modOpen); + s = addEntity(s, conOpen); + const rel1: Relation = { from: 'R-001', rel: 'produces', to: 'MOD-uart-tx' }; + const rel2: Relation = { from: 'MOD-uart-tx', rel: 'constrains', to: 'C-uart' }; + s = addRelation(s, rel1); + s = addRelation(s, rel2); + return s; +} + +describe('entities (open shape, v1.0.1-m1)', () => { + it('adds and finds entities by kind', () => { + const s = seed(); + expect(findEntities(s, (e) => e.kind === 'requirement').map((e) => e.id)).toEqual(['R-001']); + expect(findEntities(s, (e) => e.kind === 'module').map((e) => e.id)).toEqual(['MOD-uart-tx']); + }); + + it('throws on duplicate id', () => { + let s: EntityStore = { entities: [], relations: [] }; + s = addEntity(s, { kind: 'requirement', id: 'R-001', fields: { text: 'a' } }); + expect(() => addEntity(s, { kind: 'requirement', id: 'R-001', fields: { text: 'b' } })).toThrow(/duplicate/); + }); + + it('rejects entities with empty kind', () => { + expect(() => addEntity({ entities: [], relations: [] }, { kind: '', id: 'X-1', fields: {} })).toThrow(/kind/); + }); + + it('rejects entities with empty id', () => { + expect(() => addEntity({ entities: [], relations: [] }, { kind: 'module', id: '', fields: { name: 'x', file: 'y' } })).toThrow(/id/); + }); + + it('rejects entities with non-object fields', () => { + expect(() => addEntity({ entities: [], relations: [] }, { kind: 'module', id: 'X-1', fields: null as unknown as Record })).toThrow(/fields/); + }); + + it('rejects relations with unknown endpoints', () => { + let s: EntityStore = { entities: [], relations: [] }; + s = addEntity(s, { kind: 'requirement', id: 'R-001', fields: { text: 'a' } }); + expect(() => addRelation(s, { from: 'R-001', rel: 'produces', to: 'NO-SUCH' })).toThrow(/unknown/); + }); + + it('returns neighbours of an entity (incoming + outgoing)', () => { + const s = seed(); + const neighbours = findRelations(s, 'MOD-uart-tx'); + expect(neighbours).toHaveLength(2); + expect(neighbours.map((r) => `${r.from}->${r.to}:${r.rel}`).sort()).toEqual([ + 'MOD-uart-tx->C-uart:constrains', + 'R-001->MOD-uart-tx:produces' + ]); + }); + + it('addEntity returns a new store (immutability)', () => { + const s0: EntityStore = { entities: [], relations: [] }; + const s1 = addEntity(s0, { kind: 'requirement', id: 'R-001', fields: { text: 'a' } }); + expect(s0.entities).toHaveLength(0); + expect(s1.entities).toHaveLength(1); + }); +}); + +describe('entity narrowing helpers (v1.0.1-m1)', () => { + it('isModule narrows a well-formed module entity', () => { + if (!isModule(modOpen)) throw new Error('not a module'); + expect(modOpen.fields.name).toBe('uart_tx'); + }); + + it('isModule rejects an entity missing fields.name', () => { + const bad: Entity = { kind: 'module', id: 'X-1', fields: { file: 'a' } }; + expect(isModule(bad)).toBe(false); + }); + + it('isModule rejects an entity with wrong kind', () => { + expect(isModule(reqOpen)).toBe(false); + }); + + it('isRequirement narrows a well-formed requirement', () => { + if (!isRequirement(reqOpen)) throw new Error('not a requirement'); + expect(reqOpen.fields.text).toMatch(/115200/); + }); + + it('isConstraint narrows a constraint', () => { + const c: Entity = { kind: 'constraint', id: 'C-001', fields: { key: 'f_clk_min', value: '100MHz' } }; + if (!isConstraint(c)) throw new Error('not a constraint'); + expect(c.fields.key).toBe('f_clk_min'); + }); + + it('isDecision narrows a decision when alternatives_rejected is an array', () => { + const d: Entity = { + kind: 'decision', id: 'D-001', + fields: { date: '2026-07-12', rationale: 'r', alternatives_rejected: ['a', 'b'] } + }; + if (!isDecision(d)) throw new Error('not a decision'); + expect(d.fields.alternatives_rejected).toEqual(['a', 'b']); + }); + + it('isInterface rejects if signals is not an array of valid signals', () => { + const bad: Entity = { kind: 'interface', id: 'I-1', fields: { name: 'x', signals: 'not-an-array' } }; + expect(isInterface(bad)).toBe(false); + }); + + it('isArchitecture / isContract narrow correctly', () => { + const a: Entity = { kind: 'architecture', id: 'A-1', fields: { text: 'two-stage pipeline' } }; + expect(isArchitecture(a)).toBe(true); + expect(isContract(conOpen)).toBe(true); + expect(isArchitecture(conOpen)).toBe(false); + }); + + it('FIRST_CLASS_KINDS lists all first-class kinds', () => { + expect(FIRST_CLASS_KINDS).toContain('requirement'); + expect(FIRST_CLASS_KINDS).toContain('module'); + expect(FIRST_CLASS_KINDS).toContain('contract'); + expect(FIRST_CLASS_KINDS).toContain('decision'); + expect(FIRST_CLASS_KINDS).toContain('constraint'); + expect(FIRST_CLASS_KINDS).toContain('architecture'); + expect(FIRST_CLASS_KINDS).toContain('interface'); + }); + + it('arbitrary kind strings are accepted at the storage boundary', () => { + const synth: Entity = { + kind: 'synthesis_record', id: 'SYN-1', + fields: { tool: 'yosys', tool_version: '0.40', metrics: { area: 1234 } } + }; + const s = addEntity({ entities: [], relations: [] }, synth); + expect(s.entities[0]?.kind).toBe('synthesis_record'); + expect(isModule(synth)).toBe(false); + }); +}); diff --git a/core/test/events.test.ts b/core/test/events.test.ts new file mode 100644 index 0000000..407df34 --- /dev/null +++ b/core/test/events.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { EventsLog, type CompileEvent } from '../src/events'; + +describe('events log', () => { + let dir: string; + let path: string; + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 's2r-events-')); + path = join(dir, 'events.log'); + }); + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it('appends a line per compile in JSON', () => { + const log = new EventsLog(path); + const e1: CompileEvent = { + ts: '2026-07-11T00:00:00Z', + agent: 'rtl-designer', + profile_id: 'default', + input_hash: 'a'.repeat(64), + output_hash: 'b'.repeat(64), + tokens: 1234 + }; + const e2: CompileEvent = { + ts: '2026-07-11T00:00:01Z', + agent: 'rtl-designer', + profile_id: 'default', + input_hash: 'a'.repeat(64), + output_hash: 'c'.repeat(64), + tokens: 999 + }; + log.append(e1); + log.append(e2); + const text = log.readAll(); + const lines = text.split('\n').filter(Boolean); + expect(lines).toHaveLength(2); + expect(JSON.parse(lines[0]!).output_hash).toBe('b'.repeat(64)); + expect(JSON.parse(lines[1]!).output_hash).toBe('c'.repeat(64)); + expect(JSON.parse(lines[1]!).tokens).toBe(999); + }); + + it('does not overwrite existing content (append-only)', () => { + writeFileSync(path, '{"first":true}\n'); + const log = new EventsLog(path); + log.append({ + ts: '2026-07-11T00:00:02Z', + agent: 'a', + profile_id: 'p', + input_hash: 'x', + output_hash: 'y', + tokens: 1 + }); + const text = log.readAll(); + expect(text.startsWith('{"first":true}\n')).toBe(true); + expect(text.split('\n').filter(Boolean)).toHaveLength(2); + }); + + it('returns empty string when file does not exist', () => { + const log = new EventsLog(path); + expect(log.readAll()).toBe(''); + }); +}); diff --git a/core/test/hash.test.ts b/core/test/hash.test.ts new file mode 100644 index 0000000..8a77a4f --- /dev/null +++ b/core/test/hash.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { digest, canonical } from '../src/hash'; + +describe('hash', () => { + it('digest is stable across reruns', () => { + expect(digest(['a', 'b', 'c'])).toBe(digest(['a', 'b', 'c'])); + }); + + it('digest differs when input differs', () => { + expect(digest(['a', 'b', 'c'])).not.toBe(digest(['a', 'b', 'C'])); + }); + + it('digest length-prefixing prevents the boundary attack', () => { + expect(digest(['ab', 'c'])).not.toBe(digest(['a', 'bc'])); + }); + + it('canonical sorts keys', () => { + const a = canonical({ b: 1, a: 2 }); + const b = canonical({ a: 2, b: 1 }); + expect(a).toBe(b); + }); +}); diff --git a/core/test/kind-registry.test.ts b/core/test/kind-registry.test.ts new file mode 100644 index 0000000..1fe0cc9 --- /dev/null +++ b/core/test/kind-registry.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; +import { KindRegistry, type KindRegistration } from '../src/kind-registry'; + +const my_kind: KindRegistration = { + kind: 'lint_finding', + description: 'A single lint violation recorded against a file and line.', + schema: { + file: { type: 'string', required: true }, + line: { type: 'integer', required: true }, + rule_id: { type: 'string', required: true }, + severity: { type: 'string', required: true, values: ['warning', 'error', 'info'] } + } +}; + +describe('kind registry', () => { + it('registers a kind', () => { + const r = new KindRegistry(); + r.register(my_kind); + expect(r.has('lint_finding')).toBe(true); + }); + + it('rejects re-registering the same kind', () => { + const r = new KindRegistry(); + r.register(my_kind); + expect(() => r.register(my_kind)).toThrow(/already registered/); + }); + + it('rejects registering a first-class kind name', () => { + const r = new KindRegistry(); + expect(() => r.register({ ...my_kind, kind: 'module' })).toThrow(/reserved first-class kind/); + }); + + it('validate returns ok on valid data', () => { + const r = new KindRegistry(); + r.register(my_kind); + const result = r.validate('lint_finding', { + file: 'a.sv', line: 10, rule_id: 'WIDTH', severity: 'warning' + }); + expect(result.ok).toBe(true); + }); + + it('validate returns errors on missing required fields', () => { + const r = new KindRegistry(); + r.register(my_kind); + const result = r.validate('lint_finding', { file: 'a.sv' }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors.join('\n')).toMatch(/line/); + } + }); + + it('validate returns errors on non-integer line', () => { + const r = new KindRegistry(); + r.register(my_kind); + const result = r.validate('lint_finding', { + file: 'a.sv', line: 1.5, rule_id: 'WIDTH', severity: 'warning' + }); + expect(result.ok).toBe(false); + }); + + it('validate returns errors on field values that violate `values` enum', () => { + const r = new KindRegistry(); + r.register(my_kind); + const result = r.validate('lint_finding', { + file: 'a.sv', line: 10, rule_id: 'X', severity: 'catastrophic' + }); + expect(result.ok).toBe(false); + }); + + it('validate allows extra fields (forward compatibility)', () => { + const r = new KindRegistry(); + r.register(my_kind); + const result = r.validate('lint_finding', { + file: 'a.sv', line: 10, rule_id: 'X', severity: 'warning', fix_hint: '...' + }); + expect(result.ok).toBe(true); + }); + + it('validate returns error for unknown kind (after first-class)', () => { + const r = new KindRegistry(); + const result = r.validate('not_registered', {}); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.errors.join('\n')).toMatch(/unknown kind/); + }); + + it('kinds() lists registered kinds in registration order', () => { + const r = new KindRegistry(); + r.register(my_kind); + r.register({ ...my_kind, kind: 'cdc_note', description: 'cdc' }); + expect(r.kinds()).toEqual(['lint_finding', 'cdc_note']); + }); + + it('validate passes through any first-class kind (no registration required)', () => { + const r = new KindRegistry(); + const result = r.validate('module', { name: 'x', file: 'rtl/x.sv' }); + expect(result.ok).toBe(true); + }); + + it('validate ignores optional fields when not present', () => { + const r = new KindRegistry(); + r.register({ ...my_kind, schema: { file: { type: 'string' } } }); + const result = r.validate('lint_finding', { file: 'a.sv' }); + expect(result.ok).toBe(true); + }); +}); diff --git a/core/test/profile.test.ts b/core/test/profile.test.ts new file mode 100644 index 0000000..ff01bea --- /dev/null +++ b/core/test/profile.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { loadProfile, ProfileParseError } from '../src/profile'; + +describe('profile', () => { + it('loads the real default profile', () => { + const text = readFileSync(join(__dirname, '../../profiles/default.yaml'), 'utf8'); + const p = loadProfile(text); + expect(p.id).toBe('default'); + expect(p.token_budget).toBeGreaterThan(0); + expect(p.include_layers).toContain('contracts'); + expect(p.include_layers).toContain('ast_slice'); + expect(p.include_layers).toHaveLength(5); + }); + + it('rejects profiles with token_budget <= 0', () => { + expect(() => loadProfile('id: bad\ntoken_budget: 0\ninclude_layers: []')).toThrow(ProfileParseError); + }); + + it('rejects profiles referencing unknown layers', () => { + expect(() => loadProfile('id: bad\ntoken_budget: 100\ninclude_layers: ["nonsense"]\n')).toThrow(/layer/); + }); + + it('rejects non-string id', () => { + expect(() => loadProfile('id: 1\ntoken_budget: 100\ninclude_layers: []')).toThrow(/id/); + }); +}); diff --git a/core/test/signal-ref.test.ts b/core/test/signal-ref.test.ts new file mode 100644 index 0000000..2832751 --- /dev/null +++ b/core/test/signal-ref.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import type { SignalRef, Direction } from '../src/types'; +import { isSignalRef, renderSignalRef, DIRECTIONS } from '../src/types'; + +const ok: SignalRef = { name: 'data', direction: 'input', width: 8, description: 'byte' }; + +describe('SignalRef', () => { + it('matches the canonical shape', () => { + expect(ok.name).toBe('data'); + expect(ok.direction).toBe('input'); + expect(ok.width).toBe(8); + expect(ok.description).toBe('byte'); + }); + + it('isSignalRef accepts a valid signal', () => { + expect(isSignalRef(ok)).toBe(true); + }); + + it('isSignalRef accepts a minimal signal (name + direction only)', () => { + expect(isSignalRef({ name: 'a', direction: 'output' })).toBe(true); + }); + + it('isSignalRef rejects missing name', () => { + expect(isSignalRef({ direction: 'input' })).toBe(false); + }); + + it('isSignalRef rejects unknown direction', () => { + expect(isSignalRef({ name: 'a', direction: 'sideways' })).toBe(false); + }); + + it('isSignalRef rejects non-positive width', () => { + expect(isSignalRef({ name: 'a', direction: 'input', width: 0 })).toBe(false); + expect(isSignalRef({ name: 'a', direction: 'input', width: -1 })).toBe(false); + expect(isSignalRef({ name: 'a', direction: 'input', width: 1.5 })).toBe(false); + }); + + it('isSignalRef rejects bad description type', () => { + expect(isSignalRef({ name: 'a', direction: 'input', description: 42 })).toBe(false); + }); + + it('isSignalRef rejects non-object input', () => { + expect(isSignalRef(null)).toBe(false); + expect(isSignalRef('a')).toBe(false); + expect(isSignalRef(42)).toBe(false); + }); + + it('renderSignalRef produces a stable format-stable string', () => { + expect(renderSignalRef(ok)).toBe('input[7:0] data // byte'); + expect(renderSignalRef(ok)).toBe(renderSignalRef(ok)); + }); + + it('renderSignalRef omits the comment when no description', () => { + expect(renderSignalRef({ name: 'clk', direction: 'input' })) + .toBe('input[0:0] clk'); + expect(renderSignalRef({ name: 'data', direction: 'output', width: 16 })) + .toBe('output[15:0] data'); + }); + + it('DIRECTIONS is exactly the three valid directions', () => { + expect([...DIRECTIONS].sort()).toEqual(['inout', 'input', 'output']); + }); +}); diff --git a/core/tsconfig.json b/core/tsconfig.json new file mode 100644 index 0000000..c17da75 --- /dev/null +++ b/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "lib": ["ES2022"] + }, + "include": ["src/**/*"], + "exclude": ["test/**/*", "node_modules", "bench"] +} diff --git a/core/vitest.config.ts b/core/vitest.config.ts new file mode 100644 index 0000000..d7a4bc9 --- /dev/null +++ b/core/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import { fileURLToPath } from 'node:url'; + +export default defineConfig({ + root: fileURLToPath(new URL('.', import.meta.url)), + test: { + include: ['test/**/*.test.ts'], + environment: 'node', + globals: false + } +}); diff --git a/package-lock.json b/package-lock.json index 82aa776..c629608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,30 @@ { "name": "spec2rtl-cc", - "version": "0.1.0", + "version": "0.2.0-M1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "spec2rtl-cc", - "version": "0.1.0", + "version": "0.2.0-M1", "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0" + }, "bin": { + "spec2rtl": "bin/spec2rtl.js", "spec2rtl-cc": "bin/install.js" }, "devDependencies": { - "esbuild": "^0.24.0" + "@types/js-yaml": "^4.0.0", + "@types/node": "^22.0.0", + "esbuild": "^0.24.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" }, "engines": { - "node": ">=16.7.0" + "node": ">=18.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -375,6 +384,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", @@ -443,18 +469,601 @@ "node": ">=18" } }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", @@ -483,6 +1092,1382 @@ "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 3a89e83..6c56500 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "spec2rtl-cc", - "version": "0.1.0", + "version": "1.0.1-m1", "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode and Gemini by Cirkitly.", "bin": { - "spec2rtl-cc": "bin/install.js" + "spec2rtl-cc": "bin/install.js", + "spec2rtl": "bin/spec2rtl.js" }, "files": [ "bin", @@ -11,8 +12,12 @@ "spec2rtl", "agents", "hooks/dist", - "scripts" + "scripts", + "core/dist", + "contracts", + "profiles" ], + "main": "core/dist/cli/index.js", "keywords": [ "claude", "claude-code", @@ -34,14 +39,24 @@ "url": "https://github.com/Cirkitly/spec2rtl-pluglin/issues" }, "engines": { - "node": ">=16.7.0" + "node": ">=18.0.0" + }, + "dependencies": { + "js-yaml": "^4.1.0" }, - "dependencies": {}, "devDependencies": { - "esbuild": "^0.24.0" + "esbuild": "^0.24.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0", + "tsx": "^4.19.0", + "@types/node": "^22.0.0", + "@types/js-yaml": "^4.0.0" }, "scripts": { "build:hooks": "node scripts/build-hooks.js", - "prepublishOnly": "npm run build:hooks" + "build:core": "tsc -p core/tsconfig.json", + "test:core": "vitest run --config core/vitest.config.ts", + "bench:ablation": "tsx core/bench/run-ablation.ts", + "prepublishOnly": "npm run build:hooks && npm run build:core" } } diff --git a/profiles/default.yaml b/profiles/default.yaml new file mode 100644 index 0000000..981e599 --- /dev/null +++ b/profiles/default.yaml @@ -0,0 +1,14 @@ +id: default +description: | + Default profile for spec2rtl v1.0 M1. Targets a single hardware-design agent + on a single module. Layer ordering matters: contracts come first because the + agent must know its interface contract before any module-level slice is given. +token_budget: 30000 +include_layers: + - contracts + - ast_slice + - decisions + - constraints + - agent_exemplars +exemplars_per_agent: 1 +soft_token_margin: 0.1