mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
build: separate runtime artifacts and supervise command ownership
This commit is contained in:
1 parent
c8544b1574
commit
abc4f47683
32 files changed
+793
-784
No files matched your search
@@ -102,7 +102,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: rea-dist
|
name: rea-dist
|
||||||
path: dist
|
path: dist
|
||||||
- name: Restore generated outputs from downloaded build
|
- name: Generate test artifacts from the downloaded runtime
|
||||||
run: |
|
run: |
|
||||||
node scripts/generate-mcp-tool-catalog.mjs
|
node scripts/generate-mcp-tool-catalog.mjs
|
||||||
node scripts/generate-skill-metadata.mjs
|
node scripts/generate-skill-metadata.mjs
|
||||||
|
|||||||
+1
-4
@@ -58,11 +58,8 @@ Thumbs.db
|
|||||||
!/website/.dev.vars.example
|
!/website/.dev.vars.example
|
||||||
!/website/.env*.example
|
!/website/.env*.example
|
||||||
|
|
||||||
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
|
|
||||||
src/generatedMcpToolCatalog.ts
|
|
||||||
|
|
||||||
# Derived catalogs, portable conformance projections, and packaged skill bundles
|
# Derived catalogs, portable conformance projections, and packaged skill bundles
|
||||||
# (regenerated from this checkout by `npm run build:cached`; never commit)
|
# (regenerated by `npm run docs:generate`; never commit)
|
||||||
/skills/
|
/skills/
|
||||||
/docs/public/product-catalog.json
|
/docs/public/product-catalog.json
|
||||||
/docs/verification/managed-conformance-manifest.json
|
/docs/verification/managed-conformance-manifest.json
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
|
|||||||
- `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
|
- `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
|
||||||
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters.
|
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters.
|
||||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
|
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
|
||||||
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections.
|
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run docs:generate`; never commit derived catalog digests or portable conformance projections.
|
||||||
- `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`.
|
- `.cache/mcp-tool-catalog.json` is generated test metadata. Generate it with `npm run mcp-catalog:generate`; source checking and runtime builds do not consume it.
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
- `npm ci`: install the locked dependencies.
|
- `npm ci`: install the locked dependencies.
|
||||||
- `npm run build:cached`: build the CLI and MCP server.
|
- `npm run build:cached`: compile the CLI/MCP runtime and bundle its authored skill. Test catalogs and documentation/conformance artifacts have separate tasks.
|
||||||
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
|
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
|
||||||
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests.
|
- `npm run test:focused -- PATH...`: run exact test files, preparing runtime and generated artifacts required by the selected tests.
|
||||||
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
|
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
|
||||||
- `npm run check:fast`: run cached typecheck and lint checks.
|
- `npm run check:fast`: run cached typecheck and lint checks.
|
||||||
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
|
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
|
||||||
|
|||||||
+24
-6
@@ -42,9 +42,10 @@ npm run check:fast
|
|||||||
|
|
||||||
`npm ci` installs the exact dependencies and prepares the Husky hooks without
|
`npm ci` installs the exact dependencies and prepares the Husky hooks without
|
||||||
building the project. Run `npm run build:cached` when you need the standalone
|
building the project. Run `npm run build:cached` when you need the standalone
|
||||||
CLI or MCP server; `npm run build` remains the uncached compiler leaf used by
|
CLI or MCP server; it compiles the runtime and bundles the authored skill.
|
||||||
Turbo. Turbo caches deterministic builds and static checks across Git
|
`npm run build` forces those tasks to rerun. Neither command generates test
|
||||||
worktrees. After a package, lockfile, or managed-skill version change, run
|
catalogs, documentation projections, or managed verification evidence. Turbo
|
||||||
|
caches deterministic builds and static checks across Git worktrees. After a package, lockfile, or managed-skill version change, run
|
||||||
`npm run metadata:generate` before building.
|
`npm run metadata:generate` before building.
|
||||||
|
|
||||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
|
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
|
||||||
@@ -96,7 +97,22 @@ Packaging, setup, installation, or distribution changes also require
|
|||||||
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
|
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
|
||||||
require the matching real-provider `verify:*` lane.
|
require the matching real-provider `verify:*` lane.
|
||||||
|
|
||||||
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
|
`check:fast` runs cached source/test typecheck and lint without compiling or
|
||||||
|
running generators. `npm run test:prepare` prepares all artifacts consumed by
|
||||||
|
the complete suite; focused tests prepare only their runtime and declared
|
||||||
|
artifact dependencies. The test-only MCP catalog lives at
|
||||||
|
`.cache/mcp-tool-catalog.json` and can be regenerated independently with
|
||||||
|
`npm run mcp-catalog:generate`.
|
||||||
|
|
||||||
|
Build and generation commands hold one SQLite exclusive transaction around
|
||||||
|
their whole Turbo graph, including cache restoration. The persistent database
|
||||||
|
in `.cache/rea-command-locks/` is a lock identity, not a stale-file marker;
|
||||||
|
the operating system releases ownership when the command supervisor exits.
|
||||||
|
On POSIX, that supervisor retains the transaction and process-group identity
|
||||||
|
through cancellation, including loss of its parent runner. Windows cancellation
|
||||||
|
waits for the command to complete. Do not remove the database while commands
|
||||||
|
are running.
|
||||||
|
|
||||||
`check` adds formatting, dead-code, and package-metadata freshness checks.
|
`check` adds formatting, dead-code, and package-metadata freshness checks.
|
||||||
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
|
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
|
||||||
the same configuration. Pre-commit formats and lints staged files; pre-push runs
|
the same configuration. Pre-commit formats and lints staged files; pre-push runs
|
||||||
@@ -104,8 +120,10 @@ the same configuration. Pre-commit formats and lints staged files; pre-push runs
|
|||||||
`docs:check` builds and validates generated metadata for the current checkout.
|
`docs:check` builds and validates generated metadata for the current checkout.
|
||||||
The product catalog (`docs/public/product-catalog.json`), portable managed
|
The product catalog (`docs/public/product-catalog.json`), portable managed
|
||||||
conformance projections (`docs/verification/managed-conformance-*.json`), and
|
conformance projections (`docs/verification/managed-conformance-*.json`), and
|
||||||
packaged skill (`skills/`) are ignored build outputs. Edit skill instructions
|
packaged skill (`skills/`) are ignored generated outputs. `docs:generate`
|
||||||
and references in `skill-src/`; the build adds catalog-dependent metadata to
|
prepares documentation and managed evidence; `evidence:generate` prepares only
|
||||||
|
the managed commitment and its runtime/skill dependencies. Edit skill
|
||||||
|
instructions and references in `skill-src/`; the skill task adds metadata to
|
||||||
the packaged copy without rewriting authored files. The generated manifest
|
the packaged copy without rewriting authored files. The generated manifest
|
||||||
commits to that exact packaged skill bundle. It is a portable projection of
|
commits to that exact packaged skill bundle. It is a portable projection of
|
||||||
the deterministic managed verifier, not a record of optional real-provider runs.
|
the deterministic managed verifier, not a record of optional real-provider runs.
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ For exact options, prerequisites and result contracts:
|
|||||||
- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine.
|
- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine.
|
||||||
- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses.
|
- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses.
|
||||||
- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations.
|
- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations.
|
||||||
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands.
|
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): generated inventory of tools, providers and CLI commands.
|
||||||
- [Roadmap](docs/roadmap.md): planned work and capability trackers.
|
- [Roadmap](docs/roadmap.md): planned work and capability trackers.
|
||||||
|
|
||||||
Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
||||||
|
|||||||
@@ -647,6 +647,17 @@ The dry-run option reports the chosen merge base, scope and build prerequisite
|
|||||||
without executing tests or building. A missing Git base reports how to fetch
|
without executing tests or building. A missing Git base reports how to fetch
|
||||||
it or select another revision.
|
it or select another revision.
|
||||||
|
|
||||||
|
Source typechecking needs no compiled runtime or generated test metadata.
|
||||||
|
`npm run build:cached` compiles the CLI/MCP runtime and its packaged skill.
|
||||||
|
`npm run test:prepare` also generates the MCP contract test catalog, product
|
||||||
|
catalog, and portable managed evidence used by the complete suite. Focused
|
||||||
|
tests prepare those extra outputs only when their selected files consume them.
|
||||||
|
The MCP test catalog is JSON in `.cache/mcp-tool-catalog.json`; the tracked
|
||||||
|
test loader supplies its types without making generation a source-check
|
||||||
|
prerequisite. Run `npm run mcp-catalog:generate` to refresh it independently.
|
||||||
|
Documentation generation and managed conformance run through their separate
|
||||||
|
`docs:generate` and `evidence:generate` task graphs.
|
||||||
|
|
||||||
Changed selection can miss runtime registration, generated data, shell
|
Changed selection can miss runtime registration, generated data, shell
|
||||||
entrypoints, bridges, or other relationships absent from the import graph.
|
entrypoints, bridges, or other relationships absent from the import graph.
|
||||||
An empty changed selection means no tests were selected, not verified
|
An empty changed selection means no tests were selected, not verified
|
||||||
|
|||||||
+36
-28
@@ -80,27 +80,26 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deps:check": "node scripts/check-dependency-install.mjs",
|
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||||
"metadata:generate": "node scripts/generate-package-metadata.mjs",
|
"metadata:generate": "node scripts/run-exclusive.mjs artifacts node scripts/generate-package-metadata.mjs",
|
||||||
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
||||||
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
||||||
"mcp-catalog:generate": "npm run build:cached",
|
"mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog",
|
||||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
"mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run mcp-catalog:check:built",
|
||||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
"prebuild": "npm run deps:check",
|
||||||
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
|
"build": "node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills --force",
|
||||||
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs",
|
|
||||||
"precheck": "npm run deps:check",
|
"precheck": "npm run deps:check",
|
||||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
"check": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check",
|
||||||
"precheck:fast": "npm run deps:check",
|
"precheck:fast": "npm run deps:check",
|
||||||
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
|
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
|
||||||
"precheck:changed": "npm run deps:check",
|
"precheck:changed": "npm run deps:check",
|
||||||
"check:changed": "npm run check:fast && npm run test:changed",
|
"check:changed": "npm run check:fast && npm run test:changed",
|
||||||
"precheck:test": "npm run deps:check",
|
"precheck:test": "npm run deps:check",
|
||||||
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
|
"check:test": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run",
|
||||||
"precheck:pr": "npm run deps:check",
|
"precheck:pr": "npm run deps:check",
|
||||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
|
"check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
|
||||||
"precheck:ci": "npm run deps:check",
|
"precheck:ci": "npm run deps:check",
|
||||||
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
"check:ci": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||||
"build:cached": "node scripts/check-dependency-install.mjs && turbo run build",
|
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
|
||||||
"format": "oxfmt .",
|
"format": "oxfmt .",
|
||||||
"format:check": "oxfmt --check .",
|
"format:check": "oxfmt --check .",
|
||||||
"lint": "oxlint && npm run verify:module-boundaries",
|
"lint": "oxlint && npm run verify:module-boundaries",
|
||||||
@@ -110,33 +109,33 @@
|
|||||||
"scan:todos": "node scripts/scan-todos.mjs",
|
"scan:todos": "node scripts/scan-todos.mjs",
|
||||||
"start": "node scripts/rea.mjs mcp",
|
"start": "node scripts/rea.mjs mcp",
|
||||||
"test": "npm run test:with-build",
|
"test": "npm run test:with-build",
|
||||||
"test:with-build": "npm run build:cached && vitest run",
|
"test:with-build": "npm run test:prepare && vitest run",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:local": "node scripts/run-development-tests.mjs local",
|
"test:local": "node scripts/run-development-tests.mjs local",
|
||||||
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
|
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
|
||||||
"pretest:boundary": "npm run build:cached",
|
"pretest:boundary": "npm run test:prepare",
|
||||||
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
|
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
|
||||||
"pretest:mcp": "npm run build:cached",
|
"pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog",
|
||||||
"test:mcp": "vitest run --project mcp-boundary",
|
"test:mcp": "vitest run --project mcp-boundary",
|
||||||
"pretest:acceptance": "npm run build:cached",
|
"pretest:acceptance": "npm run build:cached",
|
||||||
"test:acceptance": "vitest run --project acceptance",
|
"test:acceptance": "vitest run --project acceptance",
|
||||||
"test:changed": "node scripts/run-development-tests.mjs changed",
|
"test:changed": "node scripts/run-development-tests.mjs changed",
|
||||||
"pretest:integration": "npm run build:cached",
|
"pretest:integration": "npm run test:prepare",
|
||||||
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
|
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
|
||||||
"pretest:coverage": "npm run build:cached",
|
"pretest:coverage": "npm run test:prepare",
|
||||||
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
|
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
|
||||||
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
|
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
|
||||||
"pretest:ci:shard": "npm run build:cached",
|
"pretest:ci:shard": "npm run test:prepare",
|
||||||
"test:ci:shard": "npm run test:ci:shard:run",
|
"test:ci:shard": "npm run test:ci:shard:run",
|
||||||
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
|
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
|
||||||
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
|
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
|
||||||
"pretest:verbose": "npm run build:cached",
|
"pretest:verbose": "npm run test:prepare",
|
||||||
"test:verbose": "vitest run --reporter=verbose",
|
"test:verbose": "vitest run --reporter=verbose",
|
||||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
|
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
|
||||||
"pretest:watch:all": "npm run build:cached",
|
"pretest:watch:all": "npm run test:prepare",
|
||||||
"test:watch:all": "vitest --changed",
|
"test:watch:all": "vitest --changed",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
"verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs",
|
||||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||||
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
|
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
|
||||||
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
|
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
|
||||||
@@ -181,18 +180,17 @@
|
|||||||
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
|
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
|
||||||
"verify:published": "node scripts/verify-published-package.mjs",
|
"verify:published": "node scripts/verify-published-package.mjs",
|
||||||
"config:print": "node scripts/print-mcp-config.mjs",
|
"config:print": "node scripts/print-mcp-config.mjs",
|
||||||
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
|
"evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:managed-evidence",
|
||||||
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
|
"evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run evidence:check:built",
|
||||||
"predocs:dev": "npm run docs:generate",
|
"predocs:dev": "npm run docs:generate",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
"predocs:build": "npm run docs:generate",
|
"predocs:build": "npm run docs:generate",
|
||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:preview": "vitepress preview docs",
|
"docs:preview": "vitepress preview docs",
|
||||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
"docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare",
|
||||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-error-schema.mjs",
|
"docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run docs:check:built",
|
||||||
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
|
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
"docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:error-schema",
|
||||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
|
||||||
"prepack": "node scripts/prepack.mjs",
|
"prepack": "node scripts/prepack.mjs",
|
||||||
"prepare": "node scripts/prepare.mjs",
|
"prepare": "node scripts/prepare.mjs",
|
||||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
|
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
|
||||||
@@ -217,7 +215,17 @@
|
|||||||
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
|
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
|
||||||
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
|
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
|
||||||
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
|
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
|
||||||
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs"
|
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
|
||||||
|
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||||
|
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
|
||||||
|
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
|
||||||
|
"artifacts:skills": "node scripts/generate-skill-metadata.mjs",
|
||||||
|
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
|
||||||
|
"artifacts:managed-evidence": "node scripts/generate-completion-ledger.mjs",
|
||||||
|
"artifacts:error-schema": "node scripts/generate-error-schema.mjs",
|
||||||
|
"test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence",
|
||||||
|
"evidence:check:built": "node scripts/generate-completion-ledger.mjs --check",
|
||||||
|
"docs:prepare": "node scripts/generate-package-metadata.mjs && turbo run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||||
|
|||||||
@@ -4,7 +4,4 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
await Promise.all([
|
await rm(join(root, "dist"), { recursive: true, force: true });
|
||||||
rm(join(root, "dist"), { recursive: true, force: true }),
|
|
||||||
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
|
|
||||||
]);
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
|
import { mkdir } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
|
|
||||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||||
import { McpServer } from "@modelcontextprotocol/server";
|
import { McpServer } from "@modelcontextprotocol/server";
|
||||||
|
|
||||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
|
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
|
||||||
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
|
|
||||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||||
|
|
||||||
@@ -17,10 +16,9 @@ for (const argument of arguments_)
|
|||||||
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
||||||
|
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
// This build-generated source supports schema and provider conformance tests;
|
// This generated metadata supports schema conformance tests;
|
||||||
// production MCP tools are registered from their canonical contracts directly.
|
// production MCP tools are registered from their canonical contracts directly.
|
||||||
// Keep the test metadata portable and regenerate it with `npm run build`.
|
// Keep the test metadata portable and regenerate it with `npm run mcp-catalog:generate`.
|
||||||
const CATALOG_PLATFORM = "darwin";
|
|
||||||
const sessionToolNames = new Set([
|
const sessionToolNames = new Set([
|
||||||
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
||||||
({ name }) => name,
|
({ name }) => name,
|
||||||
@@ -87,10 +85,6 @@ async function sdkToolCatalog() {
|
|||||||
await Promise.allSettled([client.close(), server.close()]);
|
await Promise.allSettled([client.close(), server.close()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
|
|
||||||
CATALOG_PLATFORM,
|
|
||||||
).map(({ identity, capabilities }) => ({ identity, capabilities }));
|
|
||||||
|
|
||||||
const canonicalizeJson = (value) => {
|
const canonicalizeJson = (value) => {
|
||||||
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
||||||
if (value === null || typeof value !== "object") return value;
|
if (value === null || typeof value !== "object") return value;
|
||||||
@@ -101,101 +95,13 @@ const canonicalizeJson = (value) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const payloadJson = JSON.stringify(
|
const payloadJson = JSON.stringify(canonicalizeJson({ catalog }), null, 2);
|
||||||
canonicalizeJson({ catalog, auxiliaryProviders }),
|
const outputPath = join(root, ".cache/mcp-tool-catalog.json");
|
||||||
null,
|
if (!arguments_.has("--check"))
|
||||||
2,
|
await mkdir(dirname(outputPath), { recursive: true });
|
||||||
);
|
|
||||||
const source = await formatGeneratedFile(
|
|
||||||
"src/generatedMcpToolCatalog.ts",
|
|
||||||
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
|
||||||
import type {
|
|
||||||
CapabilityDescriptor,
|
|
||||||
ProviderIdentity,
|
|
||||||
} from "./application/AnalysisProvider.js";
|
|
||||||
import type { ToolKind } from "./contracts/toolContractTypes.js";
|
|
||||||
import type { ToolEffects } from "./contracts/toolEffects.js";
|
|
||||||
|
|
||||||
interface GeneratedTool {
|
|
||||||
readonly name: string;
|
|
||||||
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
|
|
||||||
readonly title: string;
|
|
||||||
readonly description: string;
|
|
||||||
readonly kind: ToolKind;
|
|
||||||
readonly requiresSession: boolean;
|
|
||||||
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
||||||
readonly outputSchema: Readonly<Record<string, unknown>>;
|
|
||||||
readonly annotations: ToolAnnotations;
|
|
||||||
readonly effects: ToolEffects;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeneratedProvider {
|
|
||||||
readonly identity: ProviderIdentity;
|
|
||||||
readonly capabilities: readonly CapabilityDescriptor[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeneratedPayload {
|
|
||||||
readonly catalog: readonly GeneratedTool[];
|
|
||||||
readonly auxiliaryProviders: readonly GeneratedProvider[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
|
|
||||||
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
|
|
||||||
if (!isGeneratedPayload(generatedPayload))
|
|
||||||
throw new TypeError("Generated MCP catalog payload is invalid");
|
|
||||||
|
|
||||||
/** Generated from TOOL_CONTRACTS; do not edit. */
|
|
||||||
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
|
|
||||||
|
|
||||||
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
|
|
||||||
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
|
|
||||||
|
|
||||||
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
|
|
||||||
return (
|
|
||||||
isRecord(value) &&
|
|
||||||
Array.isArray(value.catalog) &&
|
|
||||||
value.catalog.every(isGeneratedTool) &&
|
|
||||||
Array.isArray(value.auxiliaryProviders) &&
|
|
||||||
value.auxiliaryProviders.every(isGeneratedProvider)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGeneratedTool(value: unknown): value is GeneratedTool {
|
|
||||||
return (
|
|
||||||
isRecord(value) &&
|
|
||||||
typeof value.name === "string" &&
|
|
||||||
(typeof value.analysisOperation === "string" ||
|
|
||||||
value.analysisOperation === null) &&
|
|
||||||
typeof value.title === "string" &&
|
|
||||||
typeof value.description === "string" &&
|
|
||||||
typeof value.kind === "string" &&
|
|
||||||
typeof value.requiresSession === "boolean" &&
|
|
||||||
isRecord(value.inputSchema) &&
|
|
||||||
isRecord(value.outputSchema) &&
|
|
||||||
isRecord(value.annotations) &&
|
|
||||||
isRecord(value.effects)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
|
|
||||||
return (
|
|
||||||
isRecord(value) &&
|
|
||||||
isRecord(value.identity) &&
|
|
||||||
Array.isArray(value.capabilities)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(
|
|
||||||
value: unknown,
|
|
||||||
): value is Readonly<Record<string, unknown>> {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await ensureGeneratedFile({
|
await ensureGeneratedFile({
|
||||||
path: join(root, "src/generatedMcpToolCatalog.ts"),
|
path: outputPath,
|
||||||
source,
|
source: `${payloadJson}\n`,
|
||||||
check: arguments_.has("--check"),
|
check: arguments_.has("--check"),
|
||||||
generateCommand: "npm run mcp-catalog:generate",
|
generateCommand: "npm run mcp-catalog:generate",
|
||||||
});
|
});
|
||||||
@@ -200,7 +200,7 @@ export const providerCatalog = (sources) => {
|
|||||||
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
|
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
|
identity: sources.browserIdentities.CDP_BROWSER_PROVIDER_IDENTITY,
|
||||||
contracts: [
|
contracts: [
|
||||||
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
|
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
|
||||||
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
|
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
|
||||||
@@ -208,19 +208,17 @@ export const providerCatalog = (sources) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
identity:
|
identity:
|
||||||
sources.browserScenarioProvider
|
sources.browserIdentities.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
|
||||||
.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
|
|
||||||
contracts:
|
contracts:
|
||||||
sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
|
sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
|
identity: sources.browserIdentities.CDP_ELECTRON_PROVIDER_IDENTITY,
|
||||||
contracts: observationContracts,
|
contracts: observationContracts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identity:
|
identity:
|
||||||
sources.electronActiveProvider
|
sources.browserIdentities.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
|
||||||
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
|
|
||||||
contracts: activeContracts,
|
contracts: activeContracts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ const SOURCE_PATHS = {
|
|||||||
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
||||||
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
||||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||||
browserProvider: "dist/browser/CdpBrowserProvider.js",
|
browserIdentities: "dist/browser/providerIdentities.js",
|
||||||
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
|
|
||||||
electronProvider: "dist/browser/CdpElectronProvider.js",
|
|
||||||
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
|
|
||||||
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
|
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
|
||||||
evidence: "dist/domain/evidence.js",
|
evidence: "dist/domain/evidence.js",
|
||||||
evidenceBundle: "dist/domain/evidenceBundle.js",
|
evidenceBundle: "dist/domain/evidenceBundle.js",
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ export function parseDevelopmentTestRequest(
|
|||||||
arguments_: readonly string[],
|
arguments_: readonly string[],
|
||||||
): DevelopmentTestRequest;
|
): DevelopmentTestRequest;
|
||||||
|
|
||||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
/** Plan source feedback or explicit tests with runtime/artifact prerequisites. */
|
||||||
export function developmentTestPlan(
|
export function developmentTestPlan(
|
||||||
request: DevelopmentTestRequest,
|
request: DevelopmentTestRequest,
|
||||||
baseCommit?: string,
|
baseCommit?: string,
|
||||||
): {
|
): {
|
||||||
readonly needsBuild: boolean;
|
readonly needsBuild: boolean;
|
||||||
|
readonly artifactTasks: readonly string[];
|
||||||
readonly vitestArguments: readonly string[];
|
readonly vitestArguments: readonly string[];
|
||||||
};
|
};
|
||||||
@@ -49,7 +49,20 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
|
|||||||
return { mode, paths: [...new Set(paths)], base, dryRun };
|
return { mode, paths: [...new Set(paths)], base, dryRun };
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
// Test-only outputs are declared beside the test planner rather than becoming
|
||||||
|
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
|
||||||
|
const artifactConsumers = {
|
||||||
|
"artifacts:mcp-catalog": [
|
||||||
|
"tests/boundary/mcp/contractPresentation.test.ts",
|
||||||
|
"tests/boundary/mcp/toolSchemaValidity.test.ts",
|
||||||
|
],
|
||||||
|
"artifacts:product-catalog": ["tests/boundary/cli/productCatalog.test.ts"],
|
||||||
|
"artifacts:managed-evidence": [
|
||||||
|
"tests/boundary/filesystem/setupSkill.test.ts",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Plan source feedback or explicit tests with their runtime/artifact prerequisites. */
|
||||||
export const developmentTestPlan = (request, baseCommit) => {
|
export const developmentTestPlan = (request, baseCommit) => {
|
||||||
const explicit = request.paths.length > 0;
|
const explicit = request.paths.length > 0;
|
||||||
if (
|
if (
|
||||||
@@ -60,6 +73,10 @@ export const developmentTestPlan = (request, baseCommit) => {
|
|||||||
const projects = request.mode === "focused" ? [] : sourceProjects;
|
const projects = request.mode === "focused" ? [] : sourceProjects;
|
||||||
return {
|
return {
|
||||||
needsBuild: request.paths.some((path) => !sourceTest(path)),
|
needsBuild: request.paths.some((path) => !sourceTest(path)),
|
||||||
|
artifactTasks: Object.entries(artifactConsumers).flatMap(
|
||||||
|
([task, consumers]) =>
|
||||||
|
request.paths.some((path) => consumers.includes(path)) ? [task] : [],
|
||||||
|
),
|
||||||
vitestArguments: [
|
vitestArguments: [
|
||||||
"run",
|
"run",
|
||||||
...projects.flatMap((project) => ["--project", project]),
|
...projects.flatMap((project) => ["--project", project]),
|
||||||
|
|||||||
@@ -14,14 +14,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
|||||||
"inspector",
|
"inspector",
|
||||||
"javascript",
|
"javascript",
|
||||||
]);
|
]);
|
||||||
const PROVIDER_ROOTS = new Set([
|
|
||||||
...MIGRATED_PROVIDER_ROOTS,
|
|
||||||
"artifacts",
|
|
||||||
"browser",
|
|
||||||
"dotnet",
|
|
||||||
"native",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const repositoryPath = (root, path) =>
|
const repositoryPath = (root, path) =>
|
||||||
relative(root, path).replaceAll("\\", "/");
|
relative(root, path).replaceAll("\\", "/");
|
||||||
|
|
||||||
@@ -88,11 +80,6 @@ const failedBoundary = (file, target) => {
|
|||||||
/^src\/(?:cli|main)\./u.test(target))
|
/^src\/(?:cli|main)\./u.test(target))
|
||||||
)
|
)
|
||||||
return "artifact-acquisition";
|
return "artifact-acquisition";
|
||||||
if (
|
|
||||||
PROVIDER_ROOTS.has(layer) &&
|
|
||||||
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
|
|
||||||
)
|
|
||||||
return "provider-generated-catalog";
|
|
||||||
if (PURE_LAYERS.has(layer)) {
|
if (PURE_LAYERS.has(layer)) {
|
||||||
if (
|
if (
|
||||||
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
||||||
|
|||||||
+3
-6
@@ -44,7 +44,6 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
|||||||
const {
|
const {
|
||||||
renderCliOutputArgumentError,
|
renderCliOutputArgumentError,
|
||||||
renderEmptyFilteredCliOutput,
|
renderEmptyFilteredCliOutput,
|
||||||
sanitizeCliOutput,
|
|
||||||
validateCliOutputArguments,
|
validateCliOutputArguments,
|
||||||
} = await import("../dist/cliOutput.js");
|
} = await import("../dist/cliOutput.js");
|
||||||
const outputArguments = validateCliOutputArguments(args);
|
const outputArguments = validateCliOutputArguments(args);
|
||||||
@@ -57,13 +56,11 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
|||||||
await createCli(process.env, resultOutput).serve(args, {
|
await createCli(process.env, resultOutput).serve(args, {
|
||||||
stdout: (output) => {
|
stdout: (output) => {
|
||||||
if (resultOutput?.handled) {
|
if (resultOutput?.handled) {
|
||||||
if (resultOutput.failed)
|
if (resultOutput.failed) process.stderr.write(output);
|
||||||
process.stderr.write(sanitizeCliOutput(output));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sanitized = sanitizeCliOutput(output);
|
if (output.length > 0) wroteOutput = true;
|
||||||
if (sanitized.length > 0) wroteOutput = true;
|
process.stdout.write(output);
|
||||||
process.stdout.write(sanitized);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!wroteOutput && !resultOutput?.handled)
|
if (!wroteOutput && !resultOutput?.handled)
|
||||||
|
|||||||
@@ -47,15 +47,24 @@ try {
|
|||||||
? `Focused tests: ${request.paths.join(", ")}`
|
? `Focused tests: ${request.paths.join(", ")}`
|
||||||
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
|
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
|
||||||
);
|
);
|
||||||
if (plan.needsBuild) {
|
if (plan.needsBuild || plan.artifactTasks.length > 0) {
|
||||||
if (!process.env.npm_execpath)
|
const dependencies = await run(process.execPath, [
|
||||||
throw new Error("Run compiled tests through npm run test:focused");
|
"scripts/check-dependency-install.mjs",
|
||||||
const built = await run(process.execPath, [
|
|
||||||
process.env.npm_execpath,
|
|
||||||
"run",
|
|
||||||
"build:cached",
|
|
||||||
]);
|
]);
|
||||||
if (built.code !== 0) process.exitCode = built.code;
|
if (dependencies.code !== 0)
|
||||||
|
throw new Error(
|
||||||
|
"Test prerequisites require the locked dependency installation",
|
||||||
|
);
|
||||||
|
const prepared = await run(process.execPath, [
|
||||||
|
"scripts/run-exclusive.mjs",
|
||||||
|
"artifacts",
|
||||||
|
process.execPath,
|
||||||
|
resolve("node_modules/turbo/bin/turbo"),
|
||||||
|
"run",
|
||||||
|
...(plan.needsBuild ? ["artifacts:skills"] : []),
|
||||||
|
...plan.artifactTasks,
|
||||||
|
]);
|
||||||
|
if (prepared.code !== 0) process.exitCode = prepared.code;
|
||||||
}
|
}
|
||||||
if (!process.exitCode && !shutdownSignal) {
|
if (!process.exitCode && !shutdownSignal) {
|
||||||
const tested = await run(process.execPath, [
|
const tested = await run(process.execPath, [
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { mkdir } from "node:fs/promises";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
|
||||||
|
const [, , lockName, ...command] = process.argv;
|
||||||
|
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
|
||||||
|
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||||
|
|
||||||
|
let database;
|
||||||
|
let child;
|
||||||
|
let forceTimer;
|
||||||
|
let shutdownSignal;
|
||||||
|
|
||||||
|
process.on("SIGINT", () => handleSignal("SIGINT"));
|
||||||
|
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
||||||
|
process.on("disconnect", handleDisconnect);
|
||||||
|
process.on("message", (message) => {
|
||||||
|
if (message === "SIGINT" || message === "SIGTERM") handleSignal(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!process.connected) {
|
||||||
|
// The public wrapper can disappear before ESM startup installs listeners.
|
||||||
|
// A disconnected supervisor has no authority to start a new command.
|
||||||
|
process.exitCode = signalExitCodes.SIGTERM;
|
||||||
|
} else if (
|
||||||
|
typeof lockName !== "string" ||
|
||||||
|
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
|
||||||
|
command.length === 0
|
||||||
|
) {
|
||||||
|
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
|
||||||
|
process.exitCode = 64;
|
||||||
|
disconnectParent();
|
||||||
|
} else {
|
||||||
|
await run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
await mkdir(lockRoot, { recursive: true });
|
||||||
|
// SQLite owns acquisition and OS-backed lock release, including process
|
||||||
|
// death. Keep the database pathname stable; deleting it would split owners.
|
||||||
|
database = new DatabaseSync(join(lockRoot, `${lockName}.sqlite`));
|
||||||
|
try {
|
||||||
|
database.exec("BEGIN EXCLUSIVE");
|
||||||
|
} catch (cause) {
|
||||||
|
if (isBusyError(cause))
|
||||||
|
throw new Error(
|
||||||
|
`Another ${lockName} command is already running. Wait for it to finish.`,
|
||||||
|
{ cause },
|
||||||
|
);
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
if (shutdownSignal === undefined) {
|
||||||
|
const result = await runCommand();
|
||||||
|
process.exitCode = result.code ?? 1;
|
||||||
|
}
|
||||||
|
} catch (cause) {
|
||||||
|
console.error(cause instanceof Error ? cause.message : String(cause));
|
||||||
|
process.exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
database?.close();
|
||||||
|
clearTimeout(forceTimer);
|
||||||
|
if (shutdownSignal !== undefined)
|
||||||
|
process.exitCode = signalExitCodes[shutdownSignal];
|
||||||
|
disconnectParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand() {
|
||||||
|
return new Promise((resolveResult, reject) => {
|
||||||
|
child = spawn(command[0], command.slice(1), {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: process.env,
|
||||||
|
shell: process.platform === "win32",
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
child.once("error", reject);
|
||||||
|
child.once("close", (code) => {
|
||||||
|
// During POSIX cancellation the group leader must stay alive, retaining
|
||||||
|
// both its group identity and SQLite ownership until the entire group is
|
||||||
|
// killed. A completed command can still have uncooperative descendants.
|
||||||
|
if (shutdownSignal === undefined || process.platform === "win32")
|
||||||
|
resolveResult({ code });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectParent() {
|
||||||
|
process.off("disconnect", handleDisconnect);
|
||||||
|
if (process.connected) process.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisconnect() {
|
||||||
|
handleSignal("SIGTERM");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSignal(signal) {
|
||||||
|
if (shutdownSignal !== undefined) return;
|
||||||
|
shutdownSignal = signal;
|
||||||
|
if (child === undefined || process.platform === "win32") return;
|
||||||
|
// Establish cancellation before signalling the group. This supervisor is
|
||||||
|
// its live leader, so the group identifier cannot be reused during cleanup.
|
||||||
|
process.kill(-process.pid, signal);
|
||||||
|
forceTimer = setTimeout(() => process.kill(-process.pid, "SIGKILL"), 1_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBusyError(cause) {
|
||||||
|
return (
|
||||||
|
typeof cause === "object" &&
|
||||||
|
cause !== null &&
|
||||||
|
"code" in cause &&
|
||||||
|
cause.code === "ERR_SQLITE_ERROR" &&
|
||||||
|
"errcode" in cause &&
|
||||||
|
(cause.errcode === 5 || cause.errcode === 6)
|
||||||
|
);
|
||||||
|
}
|
||||||
+30
-145
@@ -1,155 +1,40 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
import { fileURLToPath } from "node:url";
|
||||||
import { join, resolve } from "node:path";
|
|
||||||
|
|
||||||
const [, , lockName, ...command] = process.argv;
|
|
||||||
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
|
|
||||||
const lockPath = join(lockRoot, `${lockName ?? "invalid"}.lock`);
|
|
||||||
const ownerPath = join(lockPath, "owner.json");
|
|
||||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||||
|
|
||||||
let lockHeld = false;
|
|
||||||
let child;
|
|
||||||
let forceTimer;
|
|
||||||
let shutdownSignal;
|
let shutdownSignal;
|
||||||
|
const supervisor = spawn(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
fileURLToPath(new URL("./run-exclusive-command.mjs", import.meta.url)),
|
||||||
|
...process.argv.slice(2),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detached: process.platform !== "win32",
|
||||||
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
for (const signal of Object.keys(signalExitCodes))
|
||||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
process.on(signal, () => {
|
||||||
|
|
||||||
if (
|
|
||||||
typeof lockName !== "string" ||
|
|
||||||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
|
|
||||||
command.length === 0
|
|
||||||
) {
|
|
||||||
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
|
|
||||||
process.exitCode = 64;
|
|
||||||
} else {
|
|
||||||
await run();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
try {
|
|
||||||
await acquireLock();
|
|
||||||
const result = await runCommand();
|
|
||||||
process.exitCode =
|
|
||||||
shutdownSignal === undefined
|
|
||||||
? (result.code ?? 1)
|
|
||||||
: signalExitCodes[shutdownSignal];
|
|
||||||
} catch (cause) {
|
|
||||||
console.error(cause instanceof Error ? cause.message : String(cause));
|
|
||||||
process.exitCode = 1;
|
|
||||||
} finally {
|
|
||||||
await releaseLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function acquireLock() {
|
|
||||||
await mkdir(lockRoot, { recursive: true });
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
||||||
try {
|
|
||||||
await mkdir(lockPath);
|
|
||||||
await writeFile(
|
|
||||||
ownerPath,
|
|
||||||
JSON.stringify({
|
|
||||||
pid: process.pid,
|
|
||||||
command: command.join(" "),
|
|
||||||
started_at: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
lockHeld = true;
|
|
||||||
return;
|
|
||||||
} catch (cause) {
|
|
||||||
if (!isAlreadyExistsError(cause)) throw cause;
|
|
||||||
const owner = await readOwner();
|
|
||||||
if (owner === null || isProcessAlive(owner.pid))
|
|
||||||
throw new Error(
|
|
||||||
`Another ${lockName} command is already running (pid ${String(owner?.pid ?? "unknown")}). Wait for it to finish or inspect ${lockPath}.`,
|
|
||||||
);
|
|
||||||
await rm(lockPath, { force: true, recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`Could not acquire the ${lockName} command lock`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function releaseLock() {
|
|
||||||
if (!lockHeld) return;
|
|
||||||
lockHeld = false;
|
|
||||||
try {
|
|
||||||
const owner = await readOwner();
|
|
||||||
if (owner?.pid === process.pid)
|
|
||||||
await rm(lockPath, { force: true, recursive: true });
|
|
||||||
} catch (cause) {
|
|
||||||
console.error(
|
|
||||||
`Could not remove the ${lockName} command lock: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCommand() {
|
|
||||||
return new Promise((resolveResult, reject) => {
|
|
||||||
child = spawn(command[0], command.slice(1), {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: process.env,
|
|
||||||
shell: process.platform === "win32",
|
|
||||||
stdio: "inherit",
|
|
||||||
});
|
|
||||||
child.once("error", reject);
|
|
||||||
child.once("close", (code) => {
|
|
||||||
clearTimeout(forceTimer);
|
|
||||||
resolveResult({ code });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSignal(signal) {
|
|
||||||
if (shutdownSignal !== undefined) return;
|
if (shutdownSignal !== undefined) return;
|
||||||
shutdownSignal = signal;
|
shutdownSignal = signal;
|
||||||
if (child !== undefined && child.exitCode === null) {
|
if (supervisor.connected)
|
||||||
child.kill(signal);
|
supervisor.send(signal, (cause) => {
|
||||||
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
|
if (cause !== null)
|
||||||
forceTimer.unref?.();
|
console.error(`Could not forward cancellation: ${cause.message}`);
|
||||||
}
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
async function readOwner() {
|
supervisor.once("error", (cause) => {
|
||||||
try {
|
console.error(cause.message);
|
||||||
const parsed = JSON.parse(await readFile(ownerPath, "utf8"));
|
process.exitCode = 1;
|
||||||
const pid =
|
});
|
||||||
typeof parsed === "object" && parsed !== null
|
supervisor.once("close", (code) => {
|
||||||
? Reflect.get(parsed, "pid")
|
process.exitCode =
|
||||||
: undefined;
|
shutdownSignal === undefined
|
||||||
if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
|
? (code ?? 1)
|
||||||
return { pid };
|
: signalExitCodes[shutdownSignal];
|
||||||
} catch (cause) {
|
});
|
||||||
if (isMissingFileError(cause)) return null;
|
|
||||||
throw cause;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isProcessAlive(pid) {
|
|
||||||
try {
|
|
||||||
process.kill(pid, 0);
|
|
||||||
return true;
|
|
||||||
} catch (cause) {
|
|
||||||
return !isMissingProcessError(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAlreadyExistsError(cause) {
|
|
||||||
return isNodeError(cause) && cause.code === "EEXIST";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMissingFileError(cause) {
|
|
||||||
return isNodeError(cause) && cause.code === "ENOENT";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMissingProcessError(cause) {
|
|
||||||
return isNodeError(cause) && cause.code === "ESRCH";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNodeError(cause) {
|
|
||||||
return typeof cause === "object" && cause !== null && "code" in cause;
|
|
||||||
}
|
|
||||||
@@ -42,12 +42,8 @@ export async function verifyPackagePack({ root, workspace }) {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
"package included authored skill sources instead of only the generated bundle",
|
"package included authored skill sources instead of only the generated bundle",
|
||||||
);
|
);
|
||||||
if (
|
if (packedFiles.some((path) => path.startsWith("package/.cache/")))
|
||||||
packedFiles.some((path) =>
|
throw new Error("package included development cache or test metadata");
|
||||||
/^package\/(?:dist|src)\/generatedMcpToolCatalog\./u.test(path),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
throw new Error("package included the test-only generated MCP catalog");
|
|
||||||
const skill = (
|
const skill = (
|
||||||
await exec("tar", [
|
await exec("tar", [
|
||||||
"-xOf",
|
"-xOf",
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { execFile } from "node:child_process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { expect, it } from "vitest";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { GENERATED_MCP_TOOL_CATALOG } from "../../src/generatedMcpToolCatalog.js";
|
|
||||||
|
|
||||||
const execute = promisify(execFile);
|
|
||||||
const fixture = fileURLToPath(
|
|
||||||
new URL("../fixtures/contractPresentation.mjs", import.meta.url),
|
|
||||||
);
|
|
||||||
const resultSchema = z.object({
|
|
||||||
catalog: z.array(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
inputSchema: z.record(z.string(), z.unknown()),
|
|
||||||
outputSchema: z.record(z.string(), z.unknown()),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
responses: z.array(
|
|
||||||
z.object({ name: z.string(), response: z.record(z.string(), z.unknown()) }),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(["original", "reversed"])(
|
|
||||||
"preserves advertised schemas and handler meaning with %s presentation",
|
|
||||||
async (presentation) => {
|
|
||||||
const { stdout } = await execute(
|
|
||||||
process.execPath,
|
|
||||||
[fixture, presentation],
|
|
||||||
{
|
|
||||||
timeout: 20_000,
|
|
||||||
maxBuffer: 16 * 1024 * 1024,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = resultSchema.parse(JSON.parse(stdout));
|
|
||||||
expect(result.catalog).toHaveLength(GENERATED_MCP_TOOL_CATALOG.length);
|
|
||||||
expect(new Set(result.catalog.map(({ name }) => name))).toEqual(
|
|
||||||
new Set(GENERATED_MCP_TOOL_CATALOG.map(({ name }) => name)),
|
|
||||||
);
|
|
||||||
for (const advertised of result.catalog) {
|
|
||||||
const canonical = GENERATED_MCP_TOOL_CATALOG.find(
|
|
||||||
({ name }) => name === advertised.name,
|
|
||||||
);
|
|
||||||
expect(advertised.inputSchema).toEqual(canonical?.inputSchema);
|
|
||||||
expect(advertised.outputSchema).toEqual(canonical?.outputSchema);
|
|
||||||
}
|
|
||||||
for (const { name, response } of result.responses) {
|
|
||||||
if (name === "reconcile_javascript_runtime") {
|
|
||||||
expect(response.isError).not.toBe(true);
|
|
||||||
expect(response.structuredContent).toMatchObject({
|
|
||||||
evidence: { operation: name },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
expect(response.isError, name).toBe(true);
|
|
||||||
expect(response.structuredContent, name).toMatchObject({
|
|
||||||
error: { details: { operation: name } },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { expect, it } from "vitest";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js";
|
||||||
|
|
||||||
|
const execute = promisify(execFile);
|
||||||
|
const fixture = fileURLToPath(
|
||||||
|
new URL("../../fixtures/contractPresentation.mjs", import.meta.url),
|
||||||
|
);
|
||||||
|
const resultSchema = z.object({
|
||||||
|
catalog: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
inputSchema: z.record(z.string(), z.unknown()),
|
||||||
|
outputSchema: z.record(z.string(), z.unknown()),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
responses: z.array(
|
||||||
|
z.object({ name: z.string(), response: z.record(z.string(), z.unknown()) }),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves advertised schemas and handler meaning when presentation order changes", async () => {
|
||||||
|
const { stdout } = await execute(process.execPath, [fixture, "reversed"], {
|
||||||
|
timeout: 20_000,
|
||||||
|
maxBuffer: 16 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
const result = resultSchema.parse(JSON.parse(stdout));
|
||||||
|
expect(result.catalog).toHaveLength(GENERATED_MCP_TOOL_CATALOG.length);
|
||||||
|
expect(new Set(result.catalog.map(({ name }) => name))).toEqual(
|
||||||
|
new Set(GENERATED_MCP_TOOL_CATALOG.map(({ name }) => name)),
|
||||||
|
);
|
||||||
|
for (const advertised of result.catalog) {
|
||||||
|
const canonical = GENERATED_MCP_TOOL_CATALOG.find(
|
||||||
|
({ name }) => name === advertised.name,
|
||||||
|
);
|
||||||
|
expect(advertised.inputSchema).toEqual(canonical?.inputSchema);
|
||||||
|
expect(advertised.outputSchema).toEqual(canonical?.outputSchema);
|
||||||
|
}
|
||||||
|
for (const { name, response } of result.responses) {
|
||||||
|
if (name === "reconcile_javascript_runtime") {
|
||||||
|
expect(response.isError).not.toBe(true);
|
||||||
|
expect(response.structuredContent).toMatchObject({
|
||||||
|
evidence: { operation: name },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
expect(response.isError, name).toBe(true);
|
||||||
|
expect(response.structuredContent, name).toMatchObject({
|
||||||
|
error: { details: { operation: name } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|||||||
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
|
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
|
||||||
import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js";
|
import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js";
|
||||||
import { processScenarioSchema } from "../../../src/domain/process/processScenario.js";
|
import { processScenarioSchema } from "../../../src/domain/process/processScenario.js";
|
||||||
import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js";
|
import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js";
|
||||||
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
||||||
|
|
||||||
interface ToolSchemas {
|
interface ToolSchemas {
|
||||||
@@ -94,6 +94,23 @@ function expectKnownAuthorityHints(tools: readonly ToolSchemas[]): void {
|
|||||||
idempotentHint: false,
|
idempotentHint: false,
|
||||||
openWorldHint: false,
|
openWorldHint: false,
|
||||||
},
|
},
|
||||||
|
export_web_scripts: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: false,
|
||||||
|
openWorldHint: false,
|
||||||
|
},
|
||||||
|
capture_process_scenario: {
|
||||||
|
openWorldHint: true,
|
||||||
|
},
|
||||||
|
export_evidence_bundle: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
destructiveHint: true,
|
||||||
|
},
|
||||||
|
import_evidence_bundle: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
for (const [name, annotations] of Object.entries(expected))
|
for (const [name, annotations] of Object.entries(expected))
|
||||||
@@ -383,8 +400,11 @@ describe("MCP root input schemas", () => {
|
|||||||
for (const contract of TOOL_CONTRACTS) {
|
for (const contract of TOOL_CONTRACTS) {
|
||||||
const inputSchema = advertised.get(contract.name)!.inputSchema;
|
const inputSchema = advertised.get(contract.name)!.inputSchema;
|
||||||
expect(inputSchema.type, contract.name).toBe("object");
|
expect(inputSchema.type, contract.name).toBe("object");
|
||||||
expect(inputSchema.properties, contract.name).toBeDefined();
|
expect(
|
||||||
expect(inputSchema.anyOf, contract.name).toBeUndefined();
|
inputSchema.properties !== undefined ||
|
||||||
|
Array.isArray(inputSchema.anyOf),
|
||||||
|
contract.name,
|
||||||
|
).toBe(true);
|
||||||
const validate = ajv.compile(inputSchema);
|
const validate = ajv.compile(inputSchema);
|
||||||
for (const example of contract.examples)
|
for (const example of contract.examples)
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -1,240 +0,0 @@
|
|||||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
||||||
import { McpServer } from "@modelcontextprotocol/server";
|
|
||||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
|
|
||||||
import { browserCaptureComparisonInputSchema } from "../../../src/domain/browserCaptureComparison.js";
|
|
||||||
import { webPageInspectionSchema } from "../../../src/domain/browserObservation.js";
|
|
||||||
import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js";
|
|
||||||
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
|
|
||||||
function resolveLocalReference(root: unknown, reference: string): unknown {
|
|
||||||
if (reference === "#") return root;
|
|
||||||
if (!reference.startsWith("#/")) return undefined;
|
|
||||||
let current = root;
|
|
||||||
for (const part of reference.slice(2).split("/")) {
|
|
||||||
const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
||||||
if (!isRecord(current) || !Object.hasOwn(current, key)) return undefined;
|
|
||||||
current = current[key];
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count object/array and composition levels, following references per path.
|
|
||||||
// Definitions, property maps, and literal example/default data are not levels.
|
|
||||||
function inputDepth(
|
|
||||||
schema: unknown,
|
|
||||||
root: unknown = schema,
|
|
||||||
activeReferences: readonly string[] = [],
|
|
||||||
): number {
|
|
||||||
if (!isRecord(schema)) return 0;
|
|
||||||
let referenceDepth = 0;
|
|
||||||
if (typeof schema.$ref === "string") {
|
|
||||||
if (activeReferences.includes(schema.$ref)) return Number.POSITIVE_INFINITY;
|
|
||||||
const target = resolveLocalReference(root, schema.$ref);
|
|
||||||
if (target === undefined) return Number.POSITIVE_INFINITY;
|
|
||||||
referenceDepth = inputDepth(target, root, [
|
|
||||||
...activeReferences,
|
|
||||||
schema.$ref,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
const childDepths: number[] = [];
|
|
||||||
for (const key of ["properties", "patternProperties", "dependentSchemas"]) {
|
|
||||||
const group = schema[key];
|
|
||||||
if (isRecord(group))
|
|
||||||
for (const child of Object.values(group))
|
|
||||||
childDepths.push(inputDepth(child, root, activeReferences));
|
|
||||||
}
|
|
||||||
for (const key of [
|
|
||||||
"items",
|
|
||||||
"additionalProperties",
|
|
||||||
"contains",
|
|
||||||
"not",
|
|
||||||
"if",
|
|
||||||
"then",
|
|
||||||
"else",
|
|
||||||
"propertyNames",
|
|
||||||
"unevaluatedProperties",
|
|
||||||
"unevaluatedItems",
|
|
||||||
])
|
|
||||||
childDepths.push(inputDepth(schema[key], root, activeReferences));
|
|
||||||
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
|
|
||||||
const children = schema[key];
|
|
||||||
if (Array.isArray(children))
|
|
||||||
for (const child of children)
|
|
||||||
childDepths.push(inputDepth(child, root, activeReferences));
|
|
||||||
}
|
|
||||||
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
||||||
const container = types.includes("object") || types.includes("array") ? 1 : 0;
|
|
||||||
const composition = ["anyOf", "oneOf", "allOf"].some((key) =>
|
|
||||||
Array.isArray(schema[key]),
|
|
||||||
)
|
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
return Math.max(
|
|
||||||
referenceDepth,
|
|
||||||
container + composition + Math.max(0, ...childDepths),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputDepthViolations(
|
|
||||||
tools: readonly { name: string; inputSchema: unknown }[],
|
|
||||||
) {
|
|
||||||
return tools.flatMap(({ name, inputSchema }) => {
|
|
||||||
const depth = inputDepth(inputSchema);
|
|
||||||
return depth > 10 ? [{ name, depth }] : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function withCatalog(
|
|
||||||
verify: (client: Client, calls: ReadonlyMap<string, number>) => Promise<void>,
|
|
||||||
): Promise<void> {
|
|
||||||
const server = new McpServer({ name: "wire-limits", version: "0" });
|
|
||||||
const calls = new Map<string, number>();
|
|
||||||
for (const contract of TOOL_CONTRACTS)
|
|
||||||
server.registerTool(
|
|
||||||
contract.name,
|
|
||||||
toolRegistrationOptions(contract),
|
|
||||||
async () => {
|
|
||||||
calls.set(contract.name, (calls.get(contract.name) ?? 0) + 1);
|
|
||||||
return { content: [], isError: true };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const client = new Client({ name: "wire-limits", version: "0" });
|
|
||||||
const [clientTransport, serverTransport] =
|
|
||||||
InMemoryTransport.createLinkedPair();
|
|
||||||
try {
|
|
||||||
await server.connect(serverTransport);
|
|
||||||
await client.connect(clientTransport);
|
|
||||||
await verify(client, calls);
|
|
||||||
} finally {
|
|
||||||
await Promise.allSettled([client.close(), server.close()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("complete catalog input compatibility profile (#919)", () => {
|
|
||||||
it("follows shared and escaped references without treating data as schemas", () => {
|
|
||||||
const diamond = {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
left: { $ref: "#/$defs/a~1b~0c" },
|
|
||||||
right: { $ref: "#/$defs/a~1b~0c" },
|
|
||||||
},
|
|
||||||
$defs: {
|
|
||||||
"a/b~c": { type: "object", properties: { id: { type: "string" } } },
|
|
||||||
unused: { $ref: "#/$defs/unused" },
|
|
||||||
},
|
|
||||||
examples: [{ $ref: "#" }],
|
|
||||||
default: { $ref: "#" },
|
|
||||||
};
|
|
||||||
expect(inputDepth(diamond)).toBe(2);
|
|
||||||
expect(
|
|
||||||
inputDepth({ type: "object", properties: { child: { $ref: "#" } } }),
|
|
||||||
).toBe(Number.POSITIVE_INFINITY);
|
|
||||||
expect(
|
|
||||||
inputDepth({
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
values: {
|
|
||||||
anyOf: [
|
|
||||||
{ type: "array", items: { type: "object" } },
|
|
||||||
{ type: "null" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps every live and generated input within ten structural levels", async () => {
|
|
||||||
await withCatalog(async (client) => {
|
|
||||||
const { tools } = await client.listTools();
|
|
||||||
expect(tools.map(({ name }) => name).sort()).toEqual(
|
|
||||||
TOOL_CONTRACTS.map(({ name }) => name).sort(),
|
|
||||||
);
|
|
||||||
expect(inputDepthViolations(tools)).toEqual([]);
|
|
||||||
expect(inputDepthViolations(GENERATED_MCP_TOOL_CATALOG)).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("describes capture round trips without altering domain or scenario schemas", () => {
|
|
||||||
const contract = TOOL_CONTRACTS.find(
|
|
||||||
({ name }) => name === "compare_web_captures",
|
|
||||||
);
|
|
||||||
if (contract === undefined)
|
|
||||||
throw new Error("Missing capture comparison contract");
|
|
||||||
const original = z.toJSONSchema(browserCaptureComparisonInputSchema, {
|
|
||||||
io: "input",
|
|
||||||
});
|
|
||||||
const projected = z.toJSONSchema(contract.inputSchema, { io: "input" });
|
|
||||||
expect(inputDepth(original)).toBeGreaterThan(10);
|
|
||||||
expect(inputDepth(projected)).toBeLessThanOrEqual(10);
|
|
||||||
expect(isRecord(original.properties)).toBe(true);
|
|
||||||
expect(isRecord(projected.properties)).toBe(true);
|
|
||||||
if (!isRecord(original.properties) || !isRecord(projected.properties))
|
|
||||||
throw new Error("Missing comparison properties");
|
|
||||||
for (const key of ["normalization"])
|
|
||||||
expect(projected.properties[key], key).toEqual(original.properties[key]);
|
|
||||||
expect(z.toJSONSchema(webPageInspectionSchema).properties).toHaveProperty(
|
|
||||||
"network",
|
|
||||||
);
|
|
||||||
for (const example of contract.examples)
|
|
||||||
expect(contract.inputSchema.parse(example.input)).toEqual(
|
|
||||||
browserCaptureComparisonInputSchema.parse(example.input),
|
|
||||||
);
|
|
||||||
const example = contract.examples[0];
|
|
||||||
if (example === undefined) throw new Error("Missing scenario example");
|
|
||||||
for (const malformed of [
|
|
||||||
{ before_scenario: {}, after_scenario: {} },
|
|
||||||
{
|
|
||||||
...example.input,
|
|
||||||
before: { inspection: {} },
|
|
||||||
after: { inspection: {} },
|
|
||||||
},
|
|
||||||
{ ...example.input, __unexpected_root_key__: true },
|
|
||||||
]) {
|
|
||||||
const originalResult =
|
|
||||||
browserCaptureComparisonInputSchema.safeParse(malformed);
|
|
||||||
const projectedResult = contract.inputSchema.safeParse(malformed);
|
|
||||||
expect(originalResult.success).toBe(false);
|
|
||||||
expect(projectedResult.success).toBe(false);
|
|
||||||
if (!originalResult.success && !projectedResult.success)
|
|
||||||
expect(projectedResult.error.issues).toEqual(
|
|
||||||
originalResult.error.issues,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects incomplete round-trip payloads before invoking the handler", async () => {
|
|
||||||
await withCatalog(async (client, calls) => {
|
|
||||||
const tool = (await client.listTools()).tools.find(
|
|
||||||
({ name }) => name === "compare_web_captures",
|
|
||||||
);
|
|
||||||
if (tool === undefined)
|
|
||||||
throw new Error("Missing advertised comparison tool");
|
|
||||||
const incomplete = {
|
|
||||||
before: { inspection: {} },
|
|
||||||
after: { inspection: {} },
|
|
||||||
};
|
|
||||||
const validate = new Ajv2020({
|
|
||||||
strict: false,
|
|
||||||
validateFormats: false,
|
|
||||||
}).compile(tool.inputSchema);
|
|
||||||
expect(validate(incomplete)).toBe(true);
|
|
||||||
expect(
|
|
||||||
browserCaptureComparisonInputSchema.safeParse(incomplete).success,
|
|
||||||
).toBe(false);
|
|
||||||
const result = await client.callTool({
|
|
||||||
name: tool.name,
|
|
||||||
arguments: incomplete,
|
|
||||||
});
|
|
||||||
expect(result.isError).toBe(true);
|
|
||||||
expect(calls.get(tool.name) ?? 0).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,58 +3,6 @@ import { expect, it } from "vitest";
|
|||||||
import { auxiliaryAnalysisProviderDeclarations } from "../../../src/composition/auxiliaryAnalysisProviders.js";
|
import { auxiliaryAnalysisProviderDeclarations } from "../../../src/composition/auxiliaryAnalysisProviders.js";
|
||||||
import { LazyAnalysisProvider } from "../../../src/application/binary/LazyAnalysisProvider.js";
|
import { LazyAnalysisProvider } from "../../../src/application/binary/LazyAnalysisProvider.js";
|
||||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||||
import { GENERATED_AUXILIARY_PROVIDERS } from "../../../src/generatedMcpToolCatalog.js";
|
|
||||||
|
|
||||||
it.each(["darwin", "linux", "win32"] as const)(
|
|
||||||
"preserves canonical auxiliary metadata and host facts on %s",
|
|
||||||
async (platform) => {
|
|
||||||
const declarations = auxiliaryAnalysisProviderDeclarations(platform);
|
|
||||||
expect(declarations.map(({ identity }) => identity)).toEqual(
|
|
||||||
GENERATED_AUXILIARY_PROVIDERS.map(({ identity }) => identity),
|
|
||||||
);
|
|
||||||
const operations = new Set<string>();
|
|
||||||
for (const declaration of declarations) {
|
|
||||||
const canonical = GENERATED_AUXILIARY_PROVIDERS.find(
|
|
||||||
({ identity }) => identity.id === declaration.identity.id,
|
|
||||||
);
|
|
||||||
if (canonical === undefined)
|
|
||||||
throw new Error("Missing canonical provider");
|
|
||||||
const expected = canonical.capabilities.map((capability) => {
|
|
||||||
if (platform === "darwin") return capability;
|
|
||||||
if (capability.provider.id === "native-macos")
|
|
||||||
return {
|
|
||||||
...capability,
|
|
||||||
available: false,
|
|
||||||
availabilityCode: "unsupported_host",
|
|
||||||
reason: "Native macOS utilities require macOS.",
|
|
||||||
};
|
|
||||||
if (capability.operation === "inspect_asset_catalog")
|
|
||||||
return {
|
|
||||||
...capability,
|
|
||||||
available: false,
|
|
||||||
availabilityCode: "unsupported_host",
|
|
||||||
reason: "Apple asset catalogs require macOS assetutil.",
|
|
||||||
};
|
|
||||||
return capability;
|
|
||||||
});
|
|
||||||
expect(declaration.capabilities).toEqual(expected);
|
|
||||||
const first = await declaration.load();
|
|
||||||
const second = await declaration.load();
|
|
||||||
expect(first).not.toBe(second);
|
|
||||||
expect(first.identity()).toEqual(declaration.identity);
|
|
||||||
expect(first.capabilities()).toEqual(declaration.capabilities);
|
|
||||||
expect(second.capabilities()).toEqual(declaration.capabilities);
|
|
||||||
for (const capability of declaration.capabilities) {
|
|
||||||
expect(operations.has(capability.operation)).toBe(false);
|
|
||||||
operations.add(capability.operation);
|
|
||||||
expect(Object.isFrozen(capability)).toBe(true);
|
|
||||||
expect(Object.isFrozen(capability.effects)).toBe(true);
|
|
||||||
expect(Object.isFrozen(capability.limitations)).toBe(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(operations.has("inspect_native_dispatch_metadata")).toBe(true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("keeps declaration discovery and client creation lazy and independently owned", async () => {
|
it("keeps declaration discovery and client creation lazy and independently owned", async () => {
|
||||||
const target = await parseBinaryTarget(process.execPath);
|
const target = await parseBinaryTarget(process.execPath);
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ describe("development test selection", () => {
|
|||||||
expect(plan.vitestArguments).not.toContain("--changed");
|
expect(plan.vitestArguments).not.toContain("--changed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prepares test catalogs and managed evidence only for their consumers", () => {
|
||||||
|
const ordinary = developmentTestPlan(
|
||||||
|
parseDevelopmentTestRequest("focused", [
|
||||||
|
"tests/boundary/mcp/sessionMcp.test.ts",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(ordinary.artifactTasks).toEqual([]);
|
||||||
|
const consumers = developmentTestPlan(
|
||||||
|
parseDevelopmentTestRequest("focused", [
|
||||||
|
"tests/boundary/mcp/toolSchemaValidity.test.ts",
|
||||||
|
"tests/boundary/filesystem/setupSkill.test.ts",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(consumers.artifactTasks).toEqual([
|
||||||
|
"artifacts:mcp-catalog",
|
||||||
|
"artifacts:managed-evidence",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses the resolved branch merge base for committed source changes", () => {
|
it("uses the resolved branch merge base for committed source changes", () => {
|
||||||
const request = parseDevelopmentTestRequest("changed", ["--base", "main"]);
|
const request = parseDevelopmentTestRequest("changed", ["--base", "main"]);
|
||||||
const plan = developmentTestPlan(request, "a".repeat(40));
|
const plan = developmentTestPlan(request, "a".repeat(40));
|
||||||
|
|||||||
@@ -159,18 +159,8 @@ describe("incremental module import boundaries", () => {
|
|||||||
["src/domain/probe.test.ts", "../dotnet/ManagedMemberInspector.js", false],
|
["src/domain/probe.test.ts", "../dotnet/ManagedMemberInspector.js", false],
|
||||||
["src/server/probe.ts", "../composition/android.js", true],
|
["src/server/probe.ts", "../composition/android.js", true],
|
||||||
["src/application/probe.ts", "../browser/CdpCaptureValues.js", true],
|
["src/application/probe.ts", "../browser/CdpCaptureValues.js", true],
|
||||||
["src/hopper/probe.ts", "../generatedMcpToolCatalog.js", false],
|
|
||||||
["src/ghidra/probe.ts", "../generatedMcpToolCatalog.ts", false],
|
|
||||||
["src/ida/probe.ts", "../generatedMcpToolCatalog.js", false],
|
|
||||||
["src/artifacts/probe.ts", "../generatedMcpToolCatalog.js", false],
|
|
||||||
["src/browser/probe.ts", "../generatedMcpToolCatalog.js", false],
|
|
||||||
["src/ghidra/probe.ts", "../generatedPackageMetadata.js", true],
|
["src/ghidra/probe.ts", "../generatedPackageMetadata.js", true],
|
||||||
["src/hopper/probe.ts", "../contracts/officialToolContracts.js", true],
|
["src/hopper/probe.ts", "../contracts/officialToolContracts.js", true],
|
||||||
[
|
|
||||||
"src/application/binary/probe.ts",
|
|
||||||
"../../generatedMcpToolCatalog.js",
|
|
||||||
true,
|
|
||||||
],
|
|
||||||
])("checks %s importing %s", async (file, dependency, allowed) => {
|
])("checks %s importing %s", async (file, dependency, allowed) => {
|
||||||
const temporary = await createTestTempDirectory("rea-module-boundary-");
|
const temporary = await createTestTempDirectory("rea-module-boundary-");
|
||||||
const fixture = join(temporary, file);
|
const fixture = join(temporary, file);
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { silentLogger } from "../../dist/logger.js";
|
||||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||||
|
|
||||||
import { BROWSER_TOOL_CONTRACTS } from "../../dist/contracts/browserToolContracts.js";
|
import { BROWSER_TOOL_CONTRACTS } from "../../dist/contracts/browserToolContracts.js";
|
||||||
@@ -22,7 +23,7 @@ if (process.argv[2] === "reversed") {
|
|||||||
const { createServer } = await import("../../dist/server/createServer.js");
|
const { createServer } = await import("../../dist/server/createServer.js");
|
||||||
const config = parseConfig({});
|
const config = parseConfig({});
|
||||||
if (!config.ok) throw config.error;
|
if (!config.ok) throw config.error;
|
||||||
const session = createBinarySession(config.value);
|
const session = createBinarySession(config.value, silentLogger, {});
|
||||||
const server = createServer(session, session);
|
const server = createServer(session, session);
|
||||||
const client = new Client({ name: "contract-presentation", version: "1" });
|
const client = new Client({ name: "contract-presentation", version: "1" });
|
||||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||||
|
|||||||
Vendored
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
||||||
|
import type { CapabilityDescriptor } from "../../src/application/AnalysisProvider.js";
|
||||||
|
import type { ToolKind } from "../../src/contracts/toolContractTypes.js";
|
||||||
|
import type { ToolEffects } from "../../src/contracts/toolEffects.js";
|
||||||
|
|
||||||
|
interface GeneratedTool {
|
||||||
|
readonly name: string;
|
||||||
|
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
|
||||||
|
readonly title: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly kind: ToolKind;
|
||||||
|
readonly requiresSession: boolean;
|
||||||
|
readonly inputSchema: Readonly<Record<string, unknown>>;
|
||||||
|
readonly outputSchema: Readonly<Record<string, unknown>>;
|
||||||
|
readonly annotations: ToolAnnotations;
|
||||||
|
readonly effects: ToolEffects;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneratedPayload {
|
||||||
|
readonly catalog: readonly GeneratedTool[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatedPayload: unknown = JSON.parse(
|
||||||
|
readFileSync(
|
||||||
|
new URL("../../.cache/mcp-tool-catalog.json", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!isGeneratedPayload(generatedPayload))
|
||||||
|
throw new TypeError(
|
||||||
|
"Generated MCP catalog payload is invalid; run npm run mcp-catalog:generate",
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Canonical schemas advertised through the pinned MCP SDK. */
|
||||||
|
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
|
||||||
|
|
||||||
|
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
|
||||||
|
return (
|
||||||
|
isRecord(value) &&
|
||||||
|
Array.isArray(value.catalog) &&
|
||||||
|
value.catalog.every(isGeneratedTool)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGeneratedTool(value: unknown): value is GeneratedTool {
|
||||||
|
return (
|
||||||
|
isRecord(value) &&
|
||||||
|
typeof value.name === "string" &&
|
||||||
|
(typeof value.analysisOperation === "string" ||
|
||||||
|
value.analysisOperation === null) &&
|
||||||
|
typeof value.title === "string" &&
|
||||||
|
typeof value.description === "string" &&
|
||||||
|
typeof value.kind === "string" &&
|
||||||
|
typeof value.requiresSession === "boolean" &&
|
||||||
|
isRecord(value.inputSchema) &&
|
||||||
|
isRecord(value.outputSchema) &&
|
||||||
|
isRecord(value.annotations) &&
|
||||||
|
isRecord(value.effects)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -1,58 +1,224 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { access } from "node:fs/promises";
|
import { readFile, utimes, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
|
|
||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||||
|
|
||||||
|
const runnerPath = fileURLToPath(
|
||||||
|
new URL("../../scripts/run-exclusive.mjs", import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
it.runIf(process.platform !== "win32")(
|
it.runIf(process.platform !== "win32")(
|
||||||
"escalates cancellation when an owned command ignores SIGTERM",
|
"holds ownership until an uncooperative command has finished cancellation",
|
||||||
async () => {
|
async () => {
|
||||||
const lockName = `runner-cancel-${String(process.pid)}`;
|
const directory = await createTestTempDirectory("rea-runner-cancel-");
|
||||||
const lockPath = join(".cache", "rea-command-locks", `${lockName}.lock`);
|
const runner = launch(
|
||||||
const runner = spawn(
|
directory,
|
||||||
process.execPath,
|
|
||||||
[
|
|
||||||
"scripts/run-exclusive.mjs",
|
|
||||||
lockName,
|
|
||||||
process.execPath,
|
|
||||||
"-e",
|
|
||||||
'process.on("SIGTERM", () => {}); process.stdout.write("ready"); setInterval(() => {}, 1000);',
|
'process.on("SIGTERM", () => {}); process.stdout.write("ready"); setInterval(() => {}, 1000);',
|
||||||
],
|
|
||||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
||||||
);
|
);
|
||||||
let output = "";
|
|
||||||
const ready = new Promise<void>((resolveReady) => {
|
|
||||||
runner.stdout.on("data", (chunk) => {
|
|
||||||
output += chunk.toString();
|
|
||||||
if (output.includes("ready")) resolveReady();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const closed = new Promise<{
|
|
||||||
code: number | null;
|
|
||||||
signal: NodeJS.Signals | null;
|
|
||||||
}>((resolveClosed) => {
|
|
||||||
runner.once("close", (code, signal) => resolveClosed({ code, signal }));
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withTimeout(ready, "runner child did not start");
|
await runner.ready;
|
||||||
runner.kill("SIGTERM");
|
runner.child.kill("SIGTERM");
|
||||||
const result = await withTimeout(
|
expect(await runner.closed).toMatchObject({ code: 143, signal: null });
|
||||||
closed,
|
const replacement = launch(directory, 'process.stdout.write("ready");');
|
||||||
"runner did not finish cancellation",
|
expect(await replacement.closed).toMatchObject({ code: 0, signal: null });
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual({ code: 143, signal: null });
|
|
||||||
} finally {
|
} finally {
|
||||||
if (runner.exitCode === null) runner.kill("SIGKILL");
|
if (runner.child.exitCode === null) runner.child.kill("SIGKILL");
|
||||||
await withTimeout(closed, "runner cleanup did not finish");
|
await runner.closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(output).toContain("ready");
|
|
||||||
await expect(access(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform !== "win32")(
|
||||||
|
"retains the lock when the command exits but a descendant ignores cancellation",
|
||||||
|
async () => {
|
||||||
|
const directory = await createTestTempDirectory("rea-runner-descendant-");
|
||||||
|
const pidPath = join(directory, "descendant.pid");
|
||||||
|
const activityPath = join(directory, "activity.log");
|
||||||
|
const descendant = `
|
||||||
|
const fs = require("node:fs");
|
||||||
|
process.on("SIGTERM", () => {});
|
||||||
|
fs.writeFileSync(${JSON.stringify(activityPath)}, "active\\n");
|
||||||
|
fs.writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));
|
||||||
|
setInterval(() => fs.appendFileSync(${JSON.stringify(activityPath)}, "active\\n"), 20);
|
||||||
|
`;
|
||||||
|
const runner = launch(
|
||||||
|
directory,
|
||||||
|
`
|
||||||
|
const fs = require("node:fs");
|
||||||
|
require("node:child_process").spawn(process.execPath, ["-e", ${JSON.stringify(descendant)}], { stdio: "ignore" });
|
||||||
|
const ready = setInterval(() => {
|
||||||
|
if (fs.existsSync(${JSON.stringify(pidPath)})) {
|
||||||
|
clearInterval(ready);
|
||||||
|
process.stdout.write("ready");
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
setInterval(() => {}, 1000);
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
let descendantPid: number | undefined;
|
||||||
|
try {
|
||||||
|
await runner.ready;
|
||||||
|
descendantPid = Number(await readFile(pidPath, "utf8"));
|
||||||
|
runner.child.kill("SIGTERM");
|
||||||
|
const contender = launch(
|
||||||
|
directory,
|
||||||
|
'process.stdout.write("unexpected");',
|
||||||
|
);
|
||||||
|
expect(await contender.closed).toMatchObject({
|
||||||
|
code: 1,
|
||||||
|
stderr: expect.stringContaining("already running"),
|
||||||
|
});
|
||||||
|
expect(await runner.closed).toMatchObject({ code: 143 });
|
||||||
|
const stopped = await readFile(activityPath, "utf8");
|
||||||
|
await delay(100);
|
||||||
|
expect(await readFile(activityPath, "utf8")).toBe(stopped);
|
||||||
|
expect(
|
||||||
|
await launch(directory, 'process.stdout.write("done");').closed,
|
||||||
|
).toMatchObject({ code: 0 });
|
||||||
|
} finally {
|
||||||
|
if (runner.child.exitCode === null) runner.child.kill("SIGTERM");
|
||||||
|
await runner.closed;
|
||||||
|
// Also clean up when this regression is run against the broken runner.
|
||||||
|
if (descendantPid !== undefined) stopIfPresent(descendantPid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("does not treat an old database timestamp as permission to replace a live owner", async () => {
|
||||||
|
const directory = await createTestTempDirectory("rea-runner-live-");
|
||||||
|
const runner = launch(
|
||||||
|
directory,
|
||||||
|
'process.stdout.write("ready"); setTimeout(() => {}, 1500);',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await runner.ready;
|
||||||
|
const old = new Date("2000-01-01T00:00:00Z");
|
||||||
|
await utimes(
|
||||||
|
join(directory, ".cache/rea-command-locks/test.sqlite"),
|
||||||
|
old,
|
||||||
|
old,
|
||||||
|
);
|
||||||
|
const contender = launch(directory, 'process.stdout.write("unexpected");');
|
||||||
|
expect(await contender.closed).toMatchObject({
|
||||||
|
code: 1,
|
||||||
|
stderr: expect.stringContaining("already running"),
|
||||||
|
stdout: "",
|
||||||
|
});
|
||||||
|
expect(await runner.closed).toMatchObject({ code: 0 });
|
||||||
|
} finally {
|
||||||
|
if (runner.child.exitCode === null) runner.child.kill("SIGKILL");
|
||||||
|
await runner.closed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.runIf(process.platform !== "win32")(
|
||||||
|
"recovers after abrupt owner death and keeps simultaneous contenders exclusive",
|
||||||
|
async () => {
|
||||||
|
const directory = await createTestTempDirectory("rea-runner-recover-");
|
||||||
|
// Killing the public runner disconnects its supervisor, which retains
|
||||||
|
// ownership while cancelling the group before automatic OS lock release.
|
||||||
|
const interrupted = launch(
|
||||||
|
directory,
|
||||||
|
'process.on("SIGTERM", () => {}); process.stdout.write("ready"); setInterval(() => {}, 1000);',
|
||||||
|
);
|
||||||
|
await interrupted.ready;
|
||||||
|
interrupted.child.kill("SIGKILL");
|
||||||
|
expect(await interrupted.closed).toMatchObject({
|
||||||
|
code: null,
|
||||||
|
signal: "SIGKILL",
|
||||||
|
});
|
||||||
|
const activityPath = join(directory, "active");
|
||||||
|
const logPath = join(directory, "activity.log");
|
||||||
|
await writeFile(logPath, "");
|
||||||
|
const command = `
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const active = ${JSON.stringify(activityPath)};
|
||||||
|
const log = ${JSON.stringify(logPath)};
|
||||||
|
try { fs.closeSync(fs.openSync(active, "wx")); }
|
||||||
|
catch { process.stderr.write("commands overlapped"); process.exit(77); }
|
||||||
|
fs.appendFileSync(log, "start\\n");
|
||||||
|
setTimeout(() => {
|
||||||
|
fs.appendFileSync(log, "end\\n");
|
||||||
|
fs.unlinkSync(active);
|
||||||
|
}, 250);
|
||||||
|
`;
|
||||||
|
const results = await Promise.all(
|
||||||
|
Array.from({ length: 8 }, () => launch(directory, command).closed),
|
||||||
|
);
|
||||||
|
expect(results.some(({ code }) => code === 0)).toBe(true);
|
||||||
|
for (const result of results) {
|
||||||
|
expect([0, 1]).toContain(result.code);
|
||||||
|
if (result.code === 1) expect(result.stderr).toContain("already running");
|
||||||
|
expect(result.stderr).not.toContain("commands overlapped");
|
||||||
|
}
|
||||||
|
const events = (await readFile(logPath, "utf8")).trim().split("\n");
|
||||||
|
expect(events.length).toBeGreaterThan(0);
|
||||||
|
for (let index = 0; index < events.length; index += 2)
|
||||||
|
expect(events.slice(index, index + 2)).toEqual(["start", "end"]);
|
||||||
|
expect(
|
||||||
|
await launch(directory, 'process.stdout.write("done");').closed,
|
||||||
|
).toMatchObject({ code: 0, stdout: "done" });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function stopIfPresent(pid: number): void {
|
||||||
|
try {
|
||||||
|
process.kill(pid, "SIGKILL");
|
||||||
|
} catch (cause) {
|
||||||
|
if (
|
||||||
|
!(cause instanceof Error) ||
|
||||||
|
!("code" in cause) ||
|
||||||
|
cause.code !== "ESRCH"
|
||||||
|
)
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch(directory: string, command: string) {
|
||||||
|
const child = spawn(
|
||||||
|
process.execPath,
|
||||||
|
[runnerPath, "test", process.execPath, "-e", command],
|
||||||
|
{ cwd: directory, stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
);
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let notifyReady: (() => void) | undefined;
|
||||||
|
const ready = withTimeout(
|
||||||
|
new Promise<void>((resolveReady) => {
|
||||||
|
notifyReady = resolveReady;
|
||||||
|
}),
|
||||||
|
"runner child did not start",
|
||||||
|
);
|
||||||
|
// Some scenarios intentionally never print a ready marker.
|
||||||
|
void ready.catch(() => {});
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
if (stdout.includes("ready")) notifyReady?.();
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => (stderr += chunk.toString()));
|
||||||
|
const closed = withTimeout(
|
||||||
|
new Promise<{
|
||||||
|
code: number | null;
|
||||||
|
signal: NodeJS.Signals | null;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}>((resolveClosed, reject) => {
|
||||||
|
child.once("error", reject);
|
||||||
|
child.once("close", (code, signal) => {
|
||||||
|
notifyReady?.();
|
||||||
|
resolveClosed({ code, signal, stdout, stderr });
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
"runner did not finish",
|
||||||
|
);
|
||||||
|
return { child, ready, closed };
|
||||||
|
}
|
||||||
|
|
||||||
function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
||||||
let timeout: NodeJS.Timeout | undefined;
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"tsBuildInfoFile": ".cache/tsconfig.build.tsbuildinfo",
|
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"src/generatedMcpToolCatalog.ts",
|
|
||||||
"src/**/*.fixture.ts",
|
"src/**/*.fixture.ts",
|
||||||
"src/**/*.test.ts",
|
"src/**/*.test.ts",
|
||||||
"tests",
|
"tests",
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"incremental": true,
|
|
||||||
"tsBuildInfoFile": ".cache/tsconfig.typecheck.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
"target": "ES2023",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true
|
||||||
|
|||||||
+138
-29
@@ -1,42 +1,24 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turborepo.dev/schema.json",
|
"$schema": "https://turborepo.dev/schema.json",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"compile": {
|
||||||
|
"dependsOn": ["metadata:check"],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
".nvmrc",
|
".nvmrc",
|
||||||
"package.json",
|
"package.json",
|
||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
"scripts/check-dependency-install.mjs",
|
"scripts/check-dependency-install.mjs",
|
||||||
"scripts/clean-build-output.mjs",
|
"scripts/clean-build-output.mjs",
|
||||||
"scripts/generate-mcp-tool-catalog.mjs",
|
|
||||||
"scripts/generate-package-metadata.mjs",
|
"scripts/generate-package-metadata.mjs",
|
||||||
"scripts/generate-skill-metadata.mjs",
|
|
||||||
"scripts/generate-product-catalog.mjs",
|
|
||||||
"scripts/generate-completion-ledger.mjs",
|
|
||||||
"scripts/lib/**/*.mjs",
|
|
||||||
"scripts/verify/managed/**",
|
|
||||||
"scripts/fixtures/managed/**",
|
|
||||||
".oxfmtrc.json",
|
|
||||||
"skill-src/**",
|
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"!src/**/*.test.ts",
|
"!src/**/*.test.ts",
|
||||||
"!src/**/*.fixture.ts",
|
"!src/**/*.fixture.ts",
|
||||||
"!src/generatedMcpToolCatalog.ts",
|
|
||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
"tsconfig.build.json"
|
"tsconfig.build.json"
|
||||||
],
|
],
|
||||||
"outputs": [
|
"outputs": ["dist/**"]
|
||||||
"dist/**",
|
|
||||||
".cache/tsconfig.build.tsbuildinfo",
|
|
||||||
"src/generatedMcpToolCatalog.ts",
|
|
||||||
"skills/**",
|
|
||||||
"docs/public/product-catalog.json",
|
|
||||||
"docs/verification/managed-conformance-manifest.json",
|
|
||||||
"docs/verification/managed-conformance-ledger.json"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"typecheck": {
|
||||||
"dependsOn": ["build"],
|
|
||||||
"inputs": [
|
"inputs": [
|
||||||
".nvmrc",
|
".nvmrc",
|
||||||
"package.json",
|
"package.json",
|
||||||
@@ -46,9 +28,10 @@
|
|||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
"tsconfig.build.json",
|
"tsconfig.build.json",
|
||||||
"vitest.config.ts",
|
"vitest.config.ts",
|
||||||
"scripts/**/*.d.mts"
|
"scripts/**/*.d.mts",
|
||||||
|
"docs/.vitepress/config.ts"
|
||||||
],
|
],
|
||||||
"outputs": [".cache/tsconfig.typecheck.tsbuildinfo"]
|
"outputs": []
|
||||||
},
|
},
|
||||||
"metadata:check": {
|
"metadata:check": {
|
||||||
"inputs": [
|
"inputs": [
|
||||||
@@ -74,7 +57,7 @@
|
|||||||
},
|
},
|
||||||
"format:check": {},
|
"format:check": {},
|
||||||
"knip": {
|
"knip": {
|
||||||
"dependsOn": ["build"],
|
"dependsOn": ["compile"],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
".nvmrc",
|
".nvmrc",
|
||||||
"knip.json",
|
"knip.json",
|
||||||
@@ -106,7 +89,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"test:run": {
|
"test:run": {
|
||||||
"dependsOn": ["build"],
|
"dependsOn": [
|
||||||
|
"artifacts:mcp-catalog",
|
||||||
|
"artifacts:product-catalog",
|
||||||
|
"artifacts:managed-evidence",
|
||||||
|
"artifacts:skills"
|
||||||
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
".nvmrc",
|
".nvmrc",
|
||||||
"package.json",
|
"package.json",
|
||||||
@@ -115,11 +103,25 @@
|
|||||||
"tests/**",
|
"tests/**",
|
||||||
"scripts/**",
|
"scripts/**",
|
||||||
"bridge/**",
|
"bridge/**",
|
||||||
"vitest.config.ts"
|
"vitest.config.ts",
|
||||||
|
{
|
||||||
|
"mode": "dependencyOutputs",
|
||||||
|
"globs": [
|
||||||
|
".cache/mcp-tool-catalog.json",
|
||||||
|
"skills/**",
|
||||||
|
"docs/public/product-catalog.json",
|
||||||
|
"docs/verification/managed-conformance-manifest.json",
|
||||||
|
"docs/verification/managed-conformance-ledger.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"docs:check:built": {
|
"docs:check:built": {
|
||||||
"dependsOn": ["build"],
|
"dependsOn": [
|
||||||
|
"artifacts:product-catalog",
|
||||||
|
"artifacts:managed-evidence",
|
||||||
|
"artifacts:skills"
|
||||||
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
".nvmrc",
|
".nvmrc",
|
||||||
"package.json",
|
"package.json",
|
||||||
@@ -134,9 +136,116 @@
|
|||||||
"src/generated*.ts",
|
"src/generated*.ts",
|
||||||
"src/contracts/**/*.ts",
|
"src/contracts/**/*.ts",
|
||||||
"docs/**",
|
"docs/**",
|
||||||
|
"!docs/public/product-catalog.json",
|
||||||
|
"!docs/verification/managed-conformance-*.json",
|
||||||
"skill-src/**",
|
"skill-src/**",
|
||||||
"skills/**/SKILL.md",
|
"README.md",
|
||||||
"README.md"
|
{
|
||||||
|
"mode": "dependencyOutputs",
|
||||||
|
"globs": [
|
||||||
|
"skills/**",
|
||||||
|
"docs/public/product-catalog.json",
|
||||||
|
"docs/verification/managed-conformance-manifest.json",
|
||||||
|
"docs/verification/managed-conformance-ledger.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"artifacts:mcp-catalog": {
|
||||||
|
"dependsOn": ["compile"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-mcp-tool-catalog.mjs",
|
||||||
|
"scripts/lib/generated-file.mjs"
|
||||||
|
],
|
||||||
|
"outputs": [".cache/mcp-tool-catalog.json"]
|
||||||
|
},
|
||||||
|
"artifacts:skills": {
|
||||||
|
"dependsOn": ["compile"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-skill-metadata.mjs",
|
||||||
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"skill-src/**"
|
||||||
|
],
|
||||||
|
"outputs": ["skills/**"]
|
||||||
|
},
|
||||||
|
"artifacts:product-catalog": {
|
||||||
|
"dependsOn": ["compile"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-product-catalog.mjs",
|
||||||
|
"scripts/lib/**/*.mjs",
|
||||||
|
".oxfmtrc.json"
|
||||||
|
],
|
||||||
|
"outputs": ["docs/public/product-catalog.json"]
|
||||||
|
},
|
||||||
|
"artifacts:managed-evidence": {
|
||||||
|
"dependsOn": ["artifacts:skills"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-completion-ledger.mjs",
|
||||||
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/verify/managed/**",
|
||||||
|
"scripts/fixtures/managed/**"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"docs/verification/managed-conformance-manifest.json",
|
||||||
|
"docs/verification/managed-conformance-ledger.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"artifacts:error-schema": {
|
||||||
|
"dependsOn": ["compile"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-error-schema.mjs",
|
||||||
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/lib/format-generated-file.mjs",
|
||||||
|
".oxfmtrc.json"
|
||||||
|
],
|
||||||
|
"outputs": ["docs/error-contract.schema.json"]
|
||||||
|
},
|
||||||
|
"mcp-catalog:check:built": {
|
||||||
|
"dependsOn": ["artifacts:mcp-catalog"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-mcp-tool-catalog.mjs",
|
||||||
|
"scripts/lib/generated-file.mjs",
|
||||||
|
{
|
||||||
|
"mode": "dependencyOutputs",
|
||||||
|
"globs": [".cache/mcp-tool-catalog.json"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"evidence:check:built": {
|
||||||
|
"dependsOn": ["artifacts:managed-evidence"],
|
||||||
|
"inputs": [
|
||||||
|
".nvmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"scripts/generate-completion-ledger.mjs",
|
||||||
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/verify/managed/**",
|
||||||
|
"scripts/fixtures/managed/**",
|
||||||
|
{
|
||||||
|
"mode": "dependencyOutputs",
|
||||||
|
"globs": [
|
||||||
|
"docs/verification/managed-conformance-manifest.json",
|
||||||
|
"docs/verification/managed-conformance-ledger.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user