diff --git a/.github/workflows/action-consumer-smoke.yml b/.github/workflows/action-consumer-smoke.yml index 077a0d1..65a7235 100644 --- a/.github/workflows/action-consumer-smoke.yml +++ b/.github/workflows/action-consumer-smoke.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: config: examples/github-actions/consumer/upstream-radar.config.json fail-on: high diff --git a/.github/workflows/observe-dsh-plugin-install.yml b/.github/workflows/observe-dsh-plugin-install.yml index 77136dc..8773294 100644 --- a/.github/workflows/observe-dsh-plugin-install.yml +++ b/.github/workflows/observe-dsh-plugin-install.yml @@ -28,6 +28,11 @@ on: required: false default: '' type: string + node_major: + description: Node.js major version inside the isolated observer image + required: false + default: '22' + type: string workflow_call: inputs: plugin: @@ -47,6 +52,10 @@ on: required: false default: '' type: string + node_major: + required: false + default: '22' + type: string # The target package executes arbitrary lifecycle and load code. This workflow # deliberately has no write permission and declares no secrets. @@ -72,6 +81,7 @@ jobs: RADAR_CASE_ID: ${{ inputs.case_id }} RADAR_TIMEOUT_SECONDS: ${{ inputs.timeout_seconds }} RADAR_ALLOW_BUILDS: ${{ inputs.allow_builds }} + RADAR_NODE_MAJOR: ${{ inputs.node_major }} run: | set -euo pipefail exact_package='^(@[^/@[:space:]]+/[^@/[:space:]]+|[^@/[:space:]]+)@[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' @@ -89,6 +99,10 @@ jobs: echo 'case_id must be a short lowercase label' >&2 exit 1 fi + if [[ ! "$RADAR_NODE_MAJOR" =~ ^(1[6-9]|2[0-9]|3[0-9]|40)$ ]]; then + echo 'node_major must be a supported Node.js major version between 16 and 40' >&2 + exit 1 + fi if [[ ! "$RADAR_TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || (( RADAR_TIMEOUT_SECONDS < 30 || RADAR_TIMEOUT_SECONDS > 600 )); then echo 'timeout_seconds must be an integer between 30 and 600' >&2 exit 1 @@ -111,9 +125,11 @@ jobs: done fi - - name: Build the pinned observer image + - name: Build the selected observer runtime image shell: bash - run: docker build --file docker/dsh-install-observer.Dockerfile --tag upstream-radar-install-observer:${{ github.run_id }} . + env: + RADAR_NODE_MAJOR: ${{ inputs.node_major }} + run: docker build --build-arg NODE_MAJOR="$RADAR_NODE_MAJOR" --file docker/dsh-install-observer.Dockerfile --tag upstream-radar-install-observer:${{ github.run_id }} . - name: Install and load inside the restricted container id: observe @@ -122,6 +138,7 @@ jobs: env: RADAR_PLUGIN: ${{ inputs.plugin }} RADAR_DSH_VERSION: ${{ inputs.dsh_version }} + RADAR_CASE_ID: ${{ inputs.case_id }} RADAR_TIMEOUT_SECONDS: ${{ inputs.timeout_seconds }} RADAR_IMAGE: upstream-radar-install-observer:${{ github.run_id }} RADAR_REPORT_DIRECTORY: ${{ runner.temp }}/upstream-radar-install-observation @@ -163,6 +180,7 @@ jobs: "$RADAR_IMAGE" \ "$RADAR_PLUGIN" \ --dsh-version "$RADAR_DSH_VERSION" \ + --case-id "$RADAR_CASE_ID" \ --isolation-provider github-actions-hosted-runner \ --timeout "$RADAR_TIMEOUT_SECONDS" \ --execute \ @@ -180,6 +198,7 @@ jobs: env: RADAR_PLUGIN: ${{ inputs.plugin }} RADAR_DSH_VERSION: ${{ inputs.dsh_version }} + RADAR_NODE_MAJOR: ${{ inputs.node_major }} RADAR_OBSERVER_EXIT: ${{ steps.observe.outputs.exit }} RADAR_REPORT: ${{ runner.temp }}/upstream-radar-install-observation/report.json run: | @@ -192,6 +211,7 @@ jobs: '', `- Plugin: \`${inline(process.env.RADAR_PLUGIN)}\``, `- DSH: \`${inline(process.env.RADAR_DSH_VERSION)}\``, + `- Requested Node major: \`${inline(process.env.RADAR_NODE_MAJOR)}\``, `- Observer exit: \`${inline(process.env.RADAR_OBSERVER_EXIT)}\``, ] if (!existsSync(process.env.RADAR_REPORT)) { @@ -203,10 +223,15 @@ jobs: lines.push('- Result: `unknown` — report JSON was invalid.') } else { lines.push( + `- Profile graph coverage: ${inline(report.resolution?.profileLockfile?.nodes ?? 'unknown')} node(s), ${inline(report.resolution?.profileLockfile?.edges ?? 'unknown')} edge(s), ${inline(report.resolution?.profileLockfile?.unresolved ?? 'unknown')} unresolved edge(s)`, + `- Effective runtime graph: \`${inline(report.resolution?.runtimeGraph?.digest ?? 'not established')}\`; ${inline(report.resolution?.runtimeGraph?.nodes ?? 'unknown')} node(s), ${inline(report.resolution?.runtimeGraph?.edges ?? 'unknown')} edge(s), ${inline(report.resolution?.runtimeGraph?.unresolved ?? 'unknown')} required unresolved edge(s), ${inline(report.resolution?.runtimeGraph?.optionalUnavailable ?? 0)} optional package(s) unavailable on this runtime`, + `- Plugin peer contracts: ${inline(report.resolution?.runtimeGraph?.pluginPeerContracts?.satisfied ?? 'unknown')}/${inline(report.resolution?.runtimeGraph?.pluginPeerContracts?.declared ?? 'unknown')} satisfied; ${inline(report.resolution?.runtimeGraph?.pluginPeerContracts?.mismatched ?? 'unknown')} mismatched, ${inline(report.resolution?.runtimeGraph?.pluginPeerContracts?.missing ?? 'unknown')} missing, ${inline(report.resolution?.runtimeGraph?.pluginPeerContracts?.indeterminate ?? 'unknown')} indeterminate`, + `- Effective runtime graph collector: ${inline(report.resolution?.runtimeGraphError ?? 'captured')}`, `- Result: \`${inline(report.result)}\` — ${inline(report.reason)}`, `- Exact artifact: \`sha256:${inline(report.artifact?.sha256)}\``, `- Runtime: Node \`${inline(report.runtime?.nodeVersion)}\` on \`${inline(`${report.runtime?.platform}/${report.runtime?.architecture}`)}\`; pnpm \`${inline(report.runtime?.packageManager?.version)}\``, `- Plugin Node requirement: \`${inline(report.artifact?.nodeEngine ?? 'not declared')}\``, + `- Resolved profile graph: \`${inline(report.resolution?.profileLockfile?.graphDigest ?? 'not established')}\``, `- Approved dependency builds: \`${inline(report.boundary?.approvedDependencyBuilds?.join(', ') || 'none')}\``, `- Declared lifecycle scripts: \`${inline(report.artifact?.lifecycleScripts?.join(', ') || 'none')}\``, `- Install behavior: ${count(report.observations?.install?.processes)} process exec(s), ${count(report.observations?.install?.network)} network attempt(s), ${count(report.observations?.install?.fileWrites)} file-write syscall(s)`, @@ -215,10 +240,22 @@ jobs: `- Isolation: \`${inline(report.boundary?.isolationProviderClaim)}\`; no repository secrets were passed into the container.`, ) const endpoints = (report.observations?.install?.network ?? []).slice(0, 5) + const graphGaps = (report.resolution?.runtimeGraph?.unresolvedDependencies + ?? report.resolution?.profileLockfile?.unresolvedDependencies + ?? []).slice(0, 8) + const peerIssues = (report.resolution?.runtimeGraph?.pluginPeerContracts?.issues ?? []).slice(0, 8) if (endpoints.length > 0) { lines.push('- First observed install destinations:') for (const event of endpoints) lines.push(` - \`${inline(`${event.address}${event.port === undefined ? '' : `:${event.port}`}`)}\` (${inline(event.operation)}, count ${inline(event.count)})`) } + if (graphGaps.length > 0) { + lines.push('- First unresolved required graph edges:') + for (const gap of graphGaps) lines.push(` - \`${inline(gap.from)}\` → \`${inline(gap.name)}\` (${inline(gap.kind)}: \`${inline(gap.spec)}\`)`) + } + if (peerIssues.length > 0) { + lines.push('- Direct plugin peer-contract findings:') + for (const issue of peerIssues) lines.push(` - \`${inline(issue.name)}\`: ${inline(issue.status)}; requires \`${inline(issue.required)}\`${issue.resolvedVersion === undefined ? '' : `, resolved \`${inline(issue.resolvedVersion)}\``}; static use: \`${inline(issue.staticUsage ?? 'unknown')}\``) + } } } lines.push('', 'This is bounded behavior evidence from a disposable VM plus restricted container. It is not a proof that adversarial code is safe.', '') @@ -234,7 +271,7 @@ jobs: if-no-files-found: warn retention-days: 30 - - name: Fail the check unless the exact pair installed and loaded + - name: Validate that a trustworthy compatibility result survived if: always() shell: bash env: @@ -251,9 +288,16 @@ jobs: console.error('The machine-readable install observation is invalid JSON.') process.exit(1) } - if (report.result !== 'compatible') { - console.error(`DSH install observation: ${report.result ?? 'unknown'} — ${report.reason ?? 'no reason recorded'}`) + const trustworthyResults = new Set([ + 'compatible', + 'runtime-incompatible', + 'peer-contract-incompatible', + 'install-failed', + 'load-failed', + ]) + if (!trustworthyResults.has(report.result)) { + console.error(`DSH install observer did not establish a trustworthy result: ${report.result ?? 'unknown'} — ${report.reason ?? 'no reason recorded'}`) process.exit(1) } - console.log(`Compatible: ${report.artifact?.spec} with DSH ${report.dshVersion}`) + console.log(`Observed: ${report.artifact?.spec} with DSH ${report.dshVersion} on Node ${report.runtime?.nodeVersion} -> ${report.result}`) NODE diff --git a/.github/workflows/review-dsh-plugin.yml b/.github/workflows/review-dsh-plugin.yml index 9d863b1..3f79d50 100644 --- a/.github/workflows/review-dsh-plugin.yml +++ b/.github/workflows/review-dsh-plugin.yml @@ -25,7 +25,7 @@ jobs: - name: Inspect and load-test the exact plugin id: radar continue-on-error: true - uses: MicroMilo/upstream-radar@v0.40.0 + uses: MicroMilo/upstream-radar@v0.41.0 with: inspect-package: ${{ inputs.plugin }} inspect-fail-on: never diff --git a/.github/workflows/upstream-observer.yml b/.github/workflows/upstream-observer.yml index 427f9e2..1d24d49 100644 --- a/.github/workflows/upstream-observer.yml +++ b/.github/workflows/upstream-observer.yml @@ -100,7 +100,7 @@ jobs: echo "exit=$observer_exit" >> "$GITHUB_OUTPUT" exit "$observer_exit" - - name: Plan isolated install observations only for changed coordinates + - name: Reconcile the current DSH compatibility matrix id: install-plan env: OBSERVER_JSON: ${{ runner.temp }}/upstream-radar-observer.json @@ -108,7 +108,8 @@ jobs: node scripts/write-dsh-install-plan.mjs \ examples/dsh/install-observer/targets.json \ observations.json \ - "$OBSERVER_JSON" + "$OBSERVER_JSON" \ + compatibility-ledger.json # The state is written even when an upstream source or the optional # Agent fails. Pending tasks and the last trustworthy observations must @@ -124,7 +125,7 @@ jobs: fi git config user.name 'upstream-radar[bot]' git config user.email 'upstream-radar[bot]@users.noreply.github.com' - git add observations.json + git add -- observations.json if git diff --cached --quiet; then echo 'No observation point changed; no commit created.' exit 0 @@ -132,8 +133,10 @@ jobs: git commit -m 'chore: update upstream observation point [skip ci]' git push - # Every matrix entry gets a fresh GitHub-hosted VM. The reusable workflow - # adds a restricted container and never receives the observer's model key. + # Every selected matrix entry gets a fresh GitHub-hosted VM. The reusable + # workflow adds a restricted container and never receives the model key. + # Selection is reconciliation-driven: missing, stale, or invalidated cells + # run even when neither package published a new version. install-observation: needs: observe if: needs.observe.outputs.run_install_observation == 'true' @@ -147,5 +150,119 @@ jobs: plugin: ${{ matrix.plugin }} dsh_version: ${{ needs.observe.outputs.install_dsh_version }} case_id: ${{ matrix.id }} + node_major: ${{ matrix.nodeMajor }} timeout_seconds: '180' allow_builds: ${{ matrix.allowedBuilds }} + + # Dynamic reports are useful only if they become durable, comparable + # evidence. This job accepts reports only when their exact case, plugin, + # DSH version, runtime major, and build-approval policy match the static + # matrix that scheduled them. Missing reports remain unsatisfied and will be + # selected again on the next daily reconciliation. + reconcile-install-observations: + needs: [observe, install-observation] + if: always() && needs.observe.result == 'success' && needs.observe.outputs.run_install_observation == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + issues: write + steps: + - name: Check out the latest durable observation state + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 1 + + - name: Set up pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + version: 11.3.0 + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + + - name: Build the checked-out Radar + run: | + pnpm install --frozen-lockfile + pnpm build + + - name: Materialize the exact scheduled matrix + env: + INSTALL_MATRIX: ${{ needs.observe.outputs.install_matrix }} + INSTALL_MATRIX_PATH: ${{ runner.temp }}/upstream-radar-install-matrix.json + run: | + node --input-type=module <<'NODE' + import { writeFile } from 'node:fs/promises' + const matrix = JSON.parse(process.env.INSTALL_MATRIX ?? '') + await writeFile(process.env.INSTALL_MATRIX_PATH, `${JSON.stringify(matrix, null, 2)}\n`, 'utf8') + NODE + + - name: Download isolated observation reports + id: download-reports + continue-on-error: true + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: upstream-radar-install-*-${{ github.run_id }} + path: ${{ runner.temp }}/upstream-radar-install-reports + + - name: Reconcile dynamic evidence into the compatibility ledger + id: ledger + env: + INSTALL_MATRIX_PATH: ${{ runner.temp }}/upstream-radar-install-matrix.json + INSTALL_REPORTS_PATH: ${{ runner.temp }}/upstream-radar-install-reports + LEDGER_REPORT: ${{ runner.temp }}/upstream-radar-compatibility-ledger.md + COMPATIBILITY_IR: compatibility-ir.json + COMPATIBILITY_REVERSE_INDEX: compatibility-reverse-index.json + run: | + node scripts/merge-dsh-compatibility-ledger.mjs \ + compatibility-ledger.json \ + "$INSTALL_MATRIX_PATH" \ + "$INSTALL_REPORTS_PATH" \ + "$LEDGER_REPORT" \ + "$COMPATIBILITY_IR" \ + "$COMPATIBILITY_REVERSE_INDEX" + cat "$LEDGER_REPORT" >> "$GITHUB_STEP_SUMMARY" + + - name: Keep compatibility evidence artifacts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: upstream-radar-compatibility-ledger-${{ github.run_id }} + path: | + ${{ runner.temp }}/upstream-radar-compatibility-ledger.md + compatibility-ledger.json + compatibility-ir.json + compatibility-reverse-index.json + if-no-files-found: warn + retention-days: 30 + + # The consumer is part of the transaction: a ledger that cannot create, + # update or close its managed incident is not persisted as delivered. + # The next scheduled run will therefore reproduce and retry the cell. + - name: Create, update or close compatibility incidents + env: + GITHUB_TOKEN: ${{ github.token }} + run: node scripts/sync-dsh-compatibility-issues.mjs compatibility-ledger.json + + - name: Persist compatibility evidence + shell: bash + run: | + set -euo pipefail + if [[ ! -f compatibility-ledger.json || ! -f compatibility-ir.json || ! -f compatibility-reverse-index.json ]]; then + echo 'complete compatibility evidence was not produced; nothing to persist.' + exit 0 + fi + git config user.name 'upstream-radar[bot]' + git config user.email 'upstream-radar[bot]@users.noreply.github.com' + git add -- compatibility-ledger.json compatibility-ir.json compatibility-reverse-index.json + if git diff --cached --quiet; then + echo 'No compatibility evidence changed; no commit created.' + exit 0 + fi + git commit -m 'chore: update DSH compatibility evidence [skip ci]' + git push diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d71f31..c1db64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,72 @@ All notable changes to Upstream Radar are documented here. +## [Unreleased] + +### Closed-loop compatibility incidents + +- Reconcile actionable isolated DSH incompatibilities into one managed GitHub + issue per stable target/runtime cell. Persistent failures update the same + issue, regressions reopen it, and a compatible retest comments and closes it. +- Treat reproduced incompatibility as a successful observer result while still + failing missing, malformed, rejected, or `unknown` evidence. Scanner failure + therefore cannot be presented as a plugin defect. +- Keep issue delivery and compatibility-ledger persistence in one retryable + scheduled transaction, and classify a fixed replacement artifact as a + resolved incompatibility before generic artifact drift. +- Add the published `@sanqi-normal/dsh-webui-market-plugin@0.5.5` repair to the + maintained dynamic matrix, bringing the corpus to ten public plugins. + +### DSH contract evidence + +- Materialize the exact plugin-to-DSH-host compatibility frontier into + `compatibility-ir.json` and a host-package reverse index after every accepted + isolated observation. +- Resolve every non-optional plugin peer from the real DSH profile before the + direct plugin import and headless boot. A missing or out-of-range host peer + is no longer hidden by a successful install/load stage. +- Preserve bounded static evidence beside each peer relation: runtime literal + import observed, type-only reference observed, no literal reference observed, + or scan incomplete. This keeps a declaration drift distinct from an already + reproduced runtime crash. +- Maintain the OpenPencil cell on Node 24, matching its published Node engine, + and add a reproducible current-DSH contract-drift case. + +## [0.41.0] - 2026-08-21 + +### Continuously reconciled DSH compatibility + +- Replace the change-only install plan with a compatibility ledger. The daily + static lane now forms the current plugin × DSH × Node/runtime-policy matrix + and selects only cells that are missing, stale, or invalidated by static + evidence or execution-contract drift. +- Treat a new DSH/plugin publication as an immediate retest signal, rather than + the only way a dynamic compatibility check can run. Existing evidence is + revalidated after a reviewed seven-day window. +- Bind every dynamic report to a scheduled case id, exact plugin coordinate, + DSH release, Node major, and approved dependency-build list before accepting + it into durable state. Missing or malformed reports remain unsatisfied. + +### Static + dynamic evidence + +- Record the bounded SHA-256 and dependency-graph digest of the DSH profile + lockfile produced by the real isolated install, including DSH's pnpm virtual + store layout. A green install/load does not satisfy a compatibility cell + until that graph is complete; missing or unresolved graph evidence stays + actionable. +- Build a second, effective runtime graph from the installed plugin profile and + DSH's shared host dependency plane. This resolves normal DSH-provided peers + without hiding genuine missing edges, and makes the effective graph—not the + profile lockfile alone—the compatibility-coverage criterion. +- Surface `resolution-drift` when the same exact plugin/DSH/runtime contract + resolves to a different profile dependency graph on a later clean install. +- Add configured Node runtime profiles. A static Node-engine mismatch prevents + plugin execution on the wrong runtime and automatically schedules a + potentially matching alternative profile once, avoiding a false global + incompatibility claim. +- Add a final Actions reconciliation job that collects per-VM reports, updates + `compatibility-ledger.json`, and writes only new/changed compatibility facts. + ## [0.40.0] - 2026-08-21 ### Awesome DSH cohort diff --git a/README.md b/README.md index 06b55f4..45307b7 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,10 @@ **Compatibility evidence for the [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) plugin ecosystem.** -A plugin repository can look healthy while its published artifact cannot install -on the current DSH runtime. Upstream Radar binds the exact plugin artifact, DSH -release, Node runtime, dependency graph, and install policy into one reviewable -compatibility record—then identifies which plugins and authors are affected when -that evidence changes. +A plugin repository can look healthy while its published artifact has drifted +from the DSH host it actually runs in. Upstream Radar joins what a plugin +declares with what an isolated DSH profile really resolves, then keeps that +relationship current as DSH and plugins change. The core object is an exact environment, not a package name or a diff: @@ -23,27 +22,32 @@ plugin tarball SHA-256 × DSH version × Node/pnpm baseline × approved dependen ```mermaid flowchart TB - Input["DSH releases · plugin source/npm · advisory feeds"] --> IR["Exact-coordinate compatibility IR"] - IR --> Static["Static lane
identity · graph · vulnerabilities"] - IR --> Runtime["Isolated lane on a fresh VM
install → register → load"] - Static --> Evidence["Versioned exact-pair evidence"] - Runtime --> Evidence - Evidence --> Action["Verdict · reverse impact · author fix · optional Agent"] + Artifact["Exact plugin tarball"] --> Static["Static: peer declaration + literal import evidence"] + DSH["Exact DSH + Node runtime"] --> Runtime["Fresh VM + restricted container"] + Static --> Runtime + Runtime --> Resolve["Dynamic: install → register → profile resolver → import → headless boot"] + Resolve --> IR["Compatibility IR: declared range ↔ resolved host version"] + IR --> Ledger["Current-cell ledger"] + IR --> Impact["Reverse impact index + author-facing repair"] + Ledger --> Incident["Managed compatibility incident"] + Incident --> Repair["Plugin or DSH repair"] + Repair --> Artifact ``` -Radar establishes deterministic facts; the Agent interprets project impact. A -model never decides whether versions match, invents a dependency path, or turns -missing evidence into a green result. +Radar establishes deterministic facts; an optional Agent may explain project +impact afterward. A model never decides whether versions match or turns missing +evidence into a green result. ## What it answers | Question | Evidence returned | | --- | --- | -| Does this exact plugin work with this DSH release? | Tarball identity, Node contract, install, registration, and load result. | -| Why did it fail? | The failed stage, bounded command evidence, required build approval, or incompatible runtime range. | -| What enters the DSH profile? | Exact npm/pnpm nodes, duplicate versions, root-to-dependency paths, and unresolved edges. | -| Which plugins are exposed to an upstream change? | A reverse index with every known downstream path and its coverage status. | -| What can the author repair? | The concrete package, version, lockfile, DSH declaration, or installation contract involved. | +| Does the declared plugin contract align with this DSH release? | Exact tarball, Node contract, install/registration/load evidence, and every direct peer's resolved host version. | +| Is it a runtime break or a declaration drift? | Static literal-import classification beside the dynamic resolver result. | +| What enters the DSH profile? | Exact npm/pnpm nodes, host-plane joins, duplicate versions, paths, and unresolved edges. | +| Which plugins are exposed to an upstream dependency change? | A materialized reverse index from host package to exact plugin cells. | +| What can the author repair? | One package/range/API boundary with the evidence needed to reproduce it. | +| What happens after a break is found? | One managed issue is created, updated on repeat failures, reopened on regression, and closed only after a clean retest. | ## Proven on real DSH plugins @@ -51,12 +55,17 @@ missing evidence into a green result. [`awesome-dsh-plugin`](examples/dsh/awesome-observer/README.md); 6 independently matched npm artifacts enter the isolated matrix and 2 remain correctly GitHub-only. -- Tested **9 exact artifacts** against DSH `0.1.1-rc.1` in separate disposable - VMs. Eight installed, registered, and loaded under their recorded contracts. -- Stopped `@zseven-w/dsh-openpencil@0.1.0-rc.1` before plugin execution because - its artifact requires Node `>=24.11.0` while the runner provides Node `22.23.2`. +- In a fresh VM, tested `@zseven-w/dsh-openpencil@0.1.0-rc.1` against current + DSH `0.1.1-rc.1` on Node 24. Install, registration, direct import, and + headless boot passed; the exact host-contract check found **12/14** peers + aligned, one type-only peer declaration missing, and one runtime `react-dom` + range drift. [Read the reproducible case.](examples/dsh/install-observer/reports/2026-08-22-openpencil-node24.md) - Proved `dsh-better-sidebar@0.14.0` succeeds only after the documented `node-pty` build is explicitly approved and the native toolchain is present. +- Found that `@sanqi-normal/dsh-webui-market-plugin@0.5.4` could not form a + clean DSH dependency graph, gave the author an exact repair path, and added + the published `0.5.5` repair to the maintained isolated matrix. + [See the author-confirmed case.](https://github.com/Sanqi-normal/dsh-webui-market-plugin/issues/5) - Built a reverse index from **37 real plugin graphs and 1,025 dependency coordinates**, while preserving 13 missing-graph targets as evidence gaps. @@ -67,15 +76,15 @@ or inspect the [first 50-plugin corpus](examples/dsh/first-batch/README.md). ```bash # Network-free product walkthrough -npx --yes upstream-radar@0.40.0 demo +npx --yes upstream-radar@0.41.0 demo # Static review of a public DSH plugin; no install or plugin execution -npx --yes upstream-radar@0.40.0 scan \ +npx --yes upstream-radar@0.41.0 scan \ https://github.com/PlutoKeating/dsh-lark-bot \ --fail-on never # Exact artifact review plus a DSH load matrix -npx --yes upstream-radar@0.40.0 review dsh-plugin \ +npx --yes upstream-radar@0.41.0 review dsh-plugin \ dsh-cloudflare-browser-run@0.1.3 \ --dsh-version 0.1.0-rc.8,0.1.1-rc.1 ``` @@ -84,18 +93,38 @@ The code-executing path is deliberately separate. Run **Actions → Observe one DSH plugin install** to give one exact pair its own secret-free GitHub-hosted VM and restricted container. -## Always-on today - -The scheduled observer watches 13 DSH/core/plugin targets and stores the last -trusted source, npm, lockfile, and alignment observations. A new exact DSH -publication selects the maintained plugin matrix; a mapped plugin publication -selects that plugin. Only affected pairs enter the isolated runtime lane. - -When nothing changed, Radar stays quiet: the verified steady-state run produced -no Agent call, no install job, and no timestamp-only state commit. - -**Current boundary:** unchanged plugin/DSH pairs retain their previous evidence; -the current scheduler does not yet periodically re-run every existing pair. +## Always-on now + +The scheduled observer watches DSH, plugin source/npm/lockfile evidence every +day, then reconciles a checked-in **compatibility ledger** against the desired +current matrix. An isolated run is selected when its exact cell is missing, +older than seven days, or invalidated by a DSH/plugin coordinate, source graph, +runtime, or build-policy change. A package update therefore accelerates a +retest; it is no longer the only trigger. + +Every report must prove its exact plugin × DSH × Node runtime × build-approval +cell before it can update the [ledger](compatibility-ledger.json). The Action +also materializes a bounded [compatibility IR](compatibility-ir.json) and +[reverse index](compatibility-reverse-index.json). The IR does not copy the +entire pnpm tree: it preserves the compatibility frontier—the plugin's declared +non-optional peer range, static use evidence, and concrete package version that +the final DSH profile resolves. + +A Node-engine mismatch stops before plugin execution; a maintained target can +also select its required Node profile explicitly. The profile lockfile and +effective profile-plus-DSH-host graph are compared on every retest. An +install/load green result cannot close a cell while a required direct host peer +is missing, outside its declared range, or indeterminate. + +When all cells are current, the runtime lane stays quiet. Missing or malformed +reports never turn green: they remain unsatisfied and are selected again. + +Actionable incompatibilities become managed issues in the Radar repository. +The same stable cell owns the issue across plugin and DSH releases: repeated +failures update it, a regression reopens it, and a later compatible isolated +run comments with fresh evidence and closes it. `unknown` is deliberately not +an accusation against a plugin; it fails the observer lane and waits for a +trustworthy rerun instead of opening an incident. ## Safety boundary @@ -106,11 +135,15 @@ the current scheduler does not yet periodically re-run every existing pair. | Exact-version advisory matching and dependency paths | Every matched plugin is exploitable | | Exact-pair compatibility results with explicit coverage | One successful load covers every plugin business action | +An external symlink in a DSH profile never expands static read scope. Radar only +uses an outside host plane after it has been discovered from the verified DSH +process that is actually running the profile. + ## Use it in DSH or CI ```bash # Generate a reviewable DSH inventory and wiring -npx --yes upstream-radar@0.40.0 setup +npx --yes upstream-radar@0.41.0 setup ``` The repository also ships a [reusable GitHub Action](action.yml), maintained diff --git a/action.yml b/action.yml index 95b1a33..6006f42 100644 --- a/action.yml +++ b/action.yml @@ -49,7 +49,7 @@ inputs: version: description: Exact upstream-radar npm version to execute. required: false - default: 0.40.0 + default: 0.41.0 node-version: description: Node.js version used to run the CLI. required: false diff --git a/compatibility-ir.json b/compatibility-ir.json new file mode 100644 index 0000000..3070b47 --- /dev/null +++ b/compatibility-ir.json @@ -0,0 +1,5 @@ +{ + "schema": "upstream-radar.dsh-compatibility-ir/v1alpha1", + "cells": [], + "relations": [] +} diff --git a/compatibility-ledger.json b/compatibility-ledger.json new file mode 100644 index 0000000..5aaa3be --- /dev/null +++ b/compatibility-ledger.json @@ -0,0 +1,4 @@ +{ + "schema": "upstream-radar.dsh-compatibility-ledger/v1alpha1", + "entries": [] +} diff --git a/compatibility-reverse-index.json b/compatibility-reverse-index.json new file mode 100644 index 0000000..abc2500 --- /dev/null +++ b/compatibility-reverse-index.json @@ -0,0 +1,4 @@ +{ + "schema": "upstream-radar.dsh-compatibility-reverse-index/v1alpha1", + "dependencies": [] +} diff --git a/docker/dsh-install-observer.Dockerfile b/docker/dsh-install-observer.Dockerfile index 4b1deb6..dc24601 100644 --- a/docker/dsh-install-observer.Dockerfile +++ b/docker/dsh-install-observer.Dockerfile @@ -1,6 +1,7 @@ # syntax=docker/dockerfile:1 -FROM node:22-bookworm-slim AS build +ARG NODE_MAJOR=22 +FROM node:${NODE_MAJOR}-bookworm-slim AS build RUN corepack enable \ && corepack prepare pnpm@11.3.0 --activate @@ -12,7 +13,7 @@ COPY src ./src COPY test ./test RUN pnpm run build -FROM node:22-bookworm-slim AS runtime +FROM node:${NODE_MAJOR}-bookworm-slim AS runtime # DSH rc.7 and rc.8 declare pnpm@11.7.0 in the official source tree. RUN apt-get update \ diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 6eb8a6d..c530053 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -98,7 +98,7 @@ FIRST EPSS estimated exploitation probability: 97.2% (percentile 100.0%) 想立即检查一个真实发布的 DSH 插件,可以在空目录直接运行: ```bash -npx --yes upstream-radar@0.40.0 inspect dsh-feishu-bot@0.15.8 --deep +npx --yes upstream-radar@0.41.0 inspect dsh-feishu-bot@0.15.8 --deep ``` 它会直接输出简短的准入结论、覆盖情况、依赖数量、漏洞数量和下一步,不需要先 @@ -111,7 +111,7 @@ npx --yes upstream-radar@0.40.0 inspect dsh-feishu-bot@0.15.8 --deep npm 解析环境中目前无法建立完整依赖图: ```bash -npx --yes upstream-radar@0.40.0 inspect \ +npx --yes upstream-radar@0.41.0 inspect \ @sanqi-normal/dsh-webui-market-plugin@0.5.4 \ --deep --fail-on never ``` @@ -336,9 +336,9 @@ npm: dsh-feishu-bot@0.15.8 会继续显示为 `incomplete`,不会被当成已经确认的故障。 ```bash -npx --yes upstream-radar@0.40.0 graph reverse ./plugin-reports \ +npx --yes upstream-radar@0.41.0 graph reverse ./plugin-reports \ --output ./reverse-dependency-index.json -npx --yes upstream-radar@0.40.0 observe ./targets.yml \ +npx --yes upstream-radar@0.41.0 observe ./targets.yml \ --reverse-index ./reverse-dependency-index.json \ --state ./observations.json --report ./upstream-radar-observer.md ``` @@ -376,7 +376,7 @@ Radar 会在三层目录内自动找到它;npm 名称和源码 manifest 不一 想明确选择锁文件时再加 `--lockfile`: ```bash -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ https://github.com/PlutoKeating/dsh-lark-bot \ --state ./observations.json --report ./upstream-radar-observer.md ``` @@ -399,7 +399,7 @@ workspace 边保留为未解析证据;不会安装或启动 DSH。 会额外指定 `--package`: ```bash -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ https://github.com/PlutoKeating/dsh-lark-bot \ --package dsh-feishu-bot \ --lockfile pnpm-lock.yaml --lockfile-type pnpm \ @@ -677,7 +677,7 @@ cd my-dsh-plugin pnpm install --ignore-scripts # 把插件放进 DSH profile 前,先读取精确依赖图。 -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar graph pnpm-lock pnpm-lock.yaml --json +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar graph pnpm-lock pnpm-lock.yaml --json ``` 这棵图会保留精确的 DSH 包版本,也会把未解析的可选 peer 明确显示出来;它不会加载生成的插件,也不会运行 lifecycle script。审查后,把下面这个完整 workflow 复制到 `.github/workflows/upstream-radar.yml`: @@ -699,7 +699,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: fail-on: high fail-on-compatibility: breaking @@ -710,7 +710,7 @@ Action 会自动识别唯一的 `pnpm-lock.yaml`,检查同一棵精确依赖 也可以直接检查一个真实发布的 DSH 包: ```bash -npx --yes upstream-radar@0.40.0 inspect dsh-feishu-bot@0.15.8 --deep +npx --yes upstream-radar@0.41.0 inspect dsh-feishu-bot@0.15.8 --deep ``` 这次检查的结论是 `REVIEW`:registry 完整性、签名、provenance 和 89 个已解析包都 @@ -723,7 +723,7 @@ npx --yes upstream-radar@0.40.0 inspect dsh-feishu-bot@0.15.8 --deep 如果不想分别执行 `inspect`、打包和 `probe`,可以用一个命令完成一次 DSH 插件审查: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar review dsh-plugin \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar review dsh-plugin \ dsh-cloudflare-browser-run@0.1.1 \ --dsh-version 0.1.0-rc.6,0.1.0-rc.7 ``` @@ -889,7 +889,7 @@ pnpm run try:dsh 在把项目接入兼容性门禁前,可以先运行离线规则 benchmark: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar benchmark compatibility +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar benchmark compatibility ``` 它覆盖六类契约:安全补丁、只需要项目分析的变化、不兼容的 DSH peer、发布者明确声明 breaking、候选传递依赖漏洞,以及候选依赖图不完整。这个命令不会联网、安装包、加载插件或启动 DSH;它验证的是 Radar 的确定性规则以及 `breaking`/`any` 门禁行为,不是运行时兼容性证明。 @@ -902,7 +902,7 @@ pnpm dlx --package=upstream-radar@0.40.0 upstream-radar benchmark compatibility # 打包精确版本,并明确不运行它的 lifecycle script。 npm pack --ignore-scripts dsh-plugin@1.2.3 -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar probe dsh-load \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar probe dsh-load \ ./dsh-plugin-1.2.3.tgz \ --dsh-version 0.1.0-rc.6 ``` @@ -928,7 +928,7 @@ pnpm run showcase:dsh-probe 如果要比较多个 DSH 版本,可以使用矩阵入口: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar probe dsh-matrix \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar probe dsh-matrix \ ./dsh-plugin-1.2.3.tgz \ --dsh-version 0.1.0-rc.3 \ --dsh-version 0.1.0-rc.6 \ @@ -946,7 +946,7 @@ JSON 结果结构见[矩阵结果 schema](../schemas/dsh-load-matrix.schema.json ```yaml steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: fail-on: high # 可选:把确定性的 DSH/插件兼容性破坏也作为 CI 失败条件 @@ -955,12 +955,12 @@ steps: threat-intel: true ``` -这个 Action 只是 `radar check --frozen --state :memory: --fail-on high --fail-on-compatibility breaking --json` 的薄封装。`--frozen` 是有意的:它只使用配置文件里的依赖图,不会尝试读取 runner 上不存在的本地 DSH profile。`threat-intel` 默认是 `false`,这样普通 CI 门禁不会因为额外查询变重;设置为 `true` 后,Job Summary 和原始 JSON 会包含 CISA KEV 与 FIRST EPSS 的优先级证据。每次运行彼此独立;发现达到阈值的漏洞或选择的兼容性变化时返回 `2`,运行或漏洞源出错时返回 `1`。`breaking` 只拦截有 confirmed/strong 信号的兼容性事件,`any` 会拦截所有活动兼容性事件,默认值是 `never`。除了原始 JSON 日志,Action 还会把经过转义的简短摘要写入 GitHub Job Summary,定时任务失败时可以直接看到受影响的包、准确依赖路径、已经发布的修复版本(如果有)、一行优先级证据和建议的下一步。这个入口不会投递 DSH Agent 任务,也不会修改分支;需要持续监控和项目级分析时,仍使用原生 DSH bundle。建议把 Action 固定到类似 `v0.40.0` 的发布标签,并根据团队策略固定 checkout Action。 +这个 Action 只是 `radar check --frozen --state :memory: --fail-on high --fail-on-compatibility breaking --json` 的薄封装。`--frozen` 是有意的:它只使用配置文件里的依赖图,不会尝试读取 runner 上不存在的本地 DSH profile。`threat-intel` 默认是 `false`,这样普通 CI 门禁不会因为额外查询变重;设置为 `true` 后,Job Summary 和原始 JSON 会包含 CISA KEV 与 FIRST EPSS 的优先级证据。每次运行彼此独立;发现达到阈值的漏洞或选择的兼容性变化时返回 `2`,运行或漏洞源出错时返回 `1`。`breaking` 只拦截有 confirmed/strong 信号的兼容性事件,`any` 会拦截所有活动兼容性事件,默认值是 `never`。除了原始 JSON 日志,Action 还会把经过转义的简短摘要写入 GitHub Job Summary,定时任务失败时可以直接看到受影响的包、准确依赖路径、已经发布的修复版本(如果有)、一行优先级证据和建议的下一步。这个入口不会投递 DSH Agent 任务,也不会修改分支;需要持续监控和项目级分析时,仍使用原生 DSH bundle。建议把 Action 固定到类似 `v0.41.0` 的发布标签,并根据团队策略固定 checkout Action。 如果仓库还没有提交 Radar 配置,最短接入方式是省略 `config`、`pnpm-lock` 和 `npm-lock`。checkout 之后,Action 会自动使用唯一存在的 `pnpm-lock.yaml` 或 `package-lock.json`,生成临时的审查清单,再执行同一个 frozen 检查: ```yaml -- uses: MicroMilo/upstream-radar@v0.40.0 +- uses: MicroMilo/upstream-radar@v0.41.0 with: fail-on: high ``` @@ -970,7 +970,7 @@ steps: 如果要在插件进入 DSH 前审查精确发布物,可以增加 `inspect-package`: ```yaml -- uses: MicroMilo/upstream-radar@v0.40.0 +- uses: MicroMilo/upstream-radar@v0.41.0 with: inspect-package: dsh-cloudflare-browser-run@0.1.1 # review 是安全默认值;只有允许覆盖不完整时才使用 block @@ -982,7 +982,7 @@ steps: 如果仓库只有 pnpm 锁文件,还没有提交 Radar 配置,可以让 Action 在同一个 job 中生成配置;可直接复制[pnpm workflow 示例](../examples/github-actions/upstream-radar-pnpm.yml): ```yaml -- uses: MicroMilo/upstream-radar@v0.40.0 +- uses: MicroMilo/upstream-radar@v0.41.0 with: pnpm-lock: pnpm-lock.yaml fail-on: high @@ -996,7 +996,7 @@ npm 项目可以改用 `npm-lock: package-lock.json`;`pnpm-lock` 和 `npm-lock 调用方需要先 checkout 仓库。这个 Action 不会安装项目依赖,也不会执行项目的 lifecycle script;它只读取提交到仓库的依赖图并查询配置中的上游漏洞源。如果需要完全显式的底层命令,等价写法是: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar radar check \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar radar check \ ./upstream-radar.config.json --frozen --state :memory: --fail-on high \ --fail-on-compatibility breaking --json ``` @@ -1004,7 +1004,7 @@ pnpm dlx --package=upstream-radar@0.40.0 upstream-radar radar check \ 如果还要检查一个已发布插件能否跨多个 DSH 版本加载,可以增加三个 input: ```yaml -- uses: MicroMilo/upstream-radar@v0.40.0 +- uses: MicroMilo/upstream-radar@v0.41.0 id: radar with: config: upstream-radar.config.json diff --git a/docs/architecture.md b/docs/architecture.md index 5ea571c..547471e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -50,6 +50,8 @@ Edges retain whether they are runtime, development, optional, or peer dependenci For a real DSH profile, initialization follows the installed `node_modules` resolution tree exposed by that profile without importing package code or running lifecycle scripts. This captures duplicate versions and profile-local overrides. During a native DSH run, the adapter also starts from the exact `process.argv[1]` entrypoint, verifies the nearest manifest is exactly `@deepseek-ai/dsh`, and discovers the DSH process's usable `node_modules` plane with bounded read-only filesystem checks. It never imports DSH or executes package code to do this. DSH may maintain a shared host plane outside the profile for its runtime closure; Radar prefers the plane discovered from the running process and uses the profile-level plane as a fallback when it is not available. It marks those physical nodes as `dsh-host`, includes their exact versions in OSV matching, and records the evidence source in the graph and status snapshot. A required edge that is absent from both places stays explicit and makes coverage incomplete. Radar preserves npm's `peerDependenciesMeta.optional` declaration, so an optional platform package does not become a required coverage failure. A missing `@deepseek-ai/dsh-*` or Cordis peer is separately counted as an unobserved DSH host dependency; without an exact host version, Radar does not query or guess it. The public npm deep collector remains available for explicit registry comparisons; it resolves in a temporary project with lifecycle scripts disabled and parses `package-lock.json`. In both paths, an unresolved edge stays explicit; it is never silently counted as checked. +An external symlink in a profile is not proof that its target belongs to DSH. If that target leaves the configured profile plane and no verified running-process host plane is available, the collector refuses it rather than widening the static read boundary. The live adapter can then rebuild the same profile from the exact DSH entrypoint and accept only paths inside that bounded host plane. + The pre-install `graph npm-lock` and `graph pnpm-lock` commands use bounded, dependency-free parsers for npm v2/v3 and pnpm v6/v9 lockfiles. The npm parser can synthesize a project root from `packages[""]`, while the pnpm parser can synthesize one from the `importers` section; both retain unresolved or ambiguous dependency targets instead of guessing. They do not run package managers, install hooks, plugin code, or network requests; their output uses the same canonical graph shape as the installed collector, so a CI job can inspect the graph before DSH admission. `init --pnpm-lock ` and `init --npm-lock ` wrap the collectors in a normal static Radar config. When `package.json` is beside the lockfile, the CLI reads the exact root name and version from it; for npm project roots, `packages[""]` becomes a synthetic workspace node and root development dependencies are excluded. `--root @` remains an explicit override for another workspace root. The config can be passed to `radar check` or `radar watch`, which then uses the same exact-version OSV matching and durable event lifecycle as an installed DSH profile. This keeps the pre-install path useful for plugin authors and CI without pretending that it has already loaded the plugin into DSH. @@ -109,34 +111,86 @@ marked complete. ## Isolated install/load observation The upstream observer's static job and the dynamic execution job are separate -trust domains. The static job watches the official `@deepseek-ai/dsh` package -and maintained plugin coordinates. A baseline, source-only commit, or unchanged -published coordinate does nothing. A new exact DSH publication selects the -maintained plugin corpus; a mapped plugin publication selects only that plugin. +trust domains. The static job watches the official `@deepseek-ai/dsh` package, +maintained plugin coordinates, source/lockfile graph facts, and the reviewed +execution contract. It reconciles those facts against a durable compatibility +ledger. A cell is selected not only after a DSH/plugin publication, but also +when it has no evidence, its evidence is older than the corpus refresh window, +or its static facts, runtime profile, or approved-build policy no longer match. +Package changes are therefore an immediate invalidation signal rather than the +sole trigger for code execution. Each selected matrix entry starts on a fresh GitHub-hosted VM. The target code then runs inside a restricted container that receives no repository/model secret, host workspace, or Docker socket. Radar first packs the exact npm coordinate with lifecycle scripts disabled and verifies its identity and DSH -bundle declaration. DSH installs that local tarball with lifecycle scripts -enabled, registers it, and loads it with `--dump-config`. Linux `strace` evidence -and before/after filesystem snapshots are kept separately for install and load. +bundle declaration. From that same tarball it records every non-optional peer +range and performs a bounded, syntax-only scan for literal runtime imports, +type-only references, or no observed literal reference. This is deliberately +not a proof that a dynamic import cannot exist. + +DSH installs the local tarball with lifecycle scripts enabled, registers it, +then runs a trusted one-shot wrapper from the profile. The wrapper first calls +`import.meta.resolve()` for every declared required peer, records the result in +a new controlled file, imports the plugin from the profile's real Node +resolution anchor, and boots the `headless` profile with `--help`. Radar then +reads the concrete package manifest that Node resolved and compares the actual +version to the declared range. This forces both direct plugin imports and the +Cordis loader to resolve the bundle while asking the one-shot surface to exit. +`--dump-config` is intentionally not used as the runtime check because it +composes patches without evaluating bundle code. Linux `strace` evidence and +before/after filesystem snapshots are kept separately for install and boot. ```text -exact DSH/plugin coordinate change - -> deterministic install plan +desired plugin × DSH × runtime-policy cell + -> static reconciliation against the compatibility ledger + -> missing / stale / invalidated cell only -> one fresh hosted VM per plugin -> restricted container -> exact tarball SHA-256 + -> static peer declarations + literal-use classification -> traced DSH install -> registration check - -> traced DSH load - -> bounded JSON artifact + -> per-peer profile resolution + -> traced DSH headless boot + -> bounded JSON artifact + complete profile-plus-DSH-host graph + -> exact-cell ledger merge + compatibility IR + reverse index + -> managed issue create / update / reopen / resolve + -> persist the reconciled ledger and observation point ``` -The result vocabulary separates `install-failed`, `load-failed`, `compatible`, -and `unknown`; missing or truncated tracing never becomes a clean result. This -backend is useful compatibility and behavior evidence, not hostile-code proof. +The result vocabulary separates `install-failed`, `load-failed`, +`peer-contract-incompatible`, `compatible`, and `unknown`; missing or truncated tracing never becomes a clean result. A +static Node-engine mismatch is recorded before plugin execution and may open a +configured alternate runtime cell, so a Node 22 failure is not prematurely +called a global plugin failure. The ledger accepts a dynamic report only if its +case id, plugin tarball, DSH version, Node major, and build approvals match the +static plan. It also compares the resulting profile-lockfile digest on each +retest; an exact pair that resolves a different transitive graph is reported as +`resolution-drift` even if its install/load result remains compatible. This +same pair is not considered covered when its final effective profile-plus-host +graph cannot be completed: that is retained as an evidence gap, not hidden +behind a green install result. The collector separates missing optional +platform binaries from required runtime/peer edges, then compares every direct +required plugin peer with the concrete version exposed by DSH. A missing or +out-of-range host peer is `peer-contract-incompatible` even if a superficial +configuration composition succeeded. The static-use tag prevents a missing +type-only declaration from being presented as a reproduced runtime crash; a +runtime import plus an out-of-range host version is a stronger follow-up signal +but still does not claim every client path has executed. The ledger materializes +these direct relations into a compact compatibility IR and host-package reverse +index, so a later DSH host dependency change can be routed directly to exact +plugin cells. This backend is useful compatibility and behavior evidence, not +hostile-code proof. + +Issue reconciliation is part of the same scheduled transaction as the ledger +merge. An actionable result (`runtime-incompatible`, +`peer-contract-incompatible`, `install-failed`, or `load-failed`) owns one +issue in Radar's repository through a stable target/runtime case id. A later +failure updates or reopens that issue; a complete `compatible` observation adds +the exact retest evidence and closes it. `unknown` never opens a plugin issue. +If GitHub issue reconciliation fails, the new ledger is not persisted, so the +next schedule retries the delivery instead of silently marking it handled. Docker shares the hosted VM kernel, and code in the same container may attempt to tamper with its trace/output. Moving the collector outside a Firecracker guest is the later high-assurance boundary. diff --git a/docs/showcase.md b/docs/showcase.md index 8f6d395..c7e0980 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -210,7 +210,7 @@ When the native DSH adapter is running, it additionally verifies the exact `@dee For a runner that does not have DSH installed, commit the generated config after review and run one frozen check: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar radar check \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar radar check \ ./upstream-radar.config.json \ --frozen --state :memory: --fail-on high --json ``` @@ -224,7 +224,7 @@ The published Action packages the same frozen check so a DSH plugin project does ```yaml steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: config: upstream-radar.config.json fail-on: high @@ -257,7 +257,7 @@ It packs without lifecycle scripts, runs one temporary DSH profile per version, The package includes a no-network compatibility benchmark for the deterministic gate itself: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar benchmark compatibility +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar benchmark compatibility ``` It covers a safe patch, analysis-only structural change, DSH peer exclusion, explicit publisher breaking language, a vulnerable candidate dependency, and incomplete candidate coverage. A passing benchmark means the rule contract has not regressed; it does not mean a real plugin is runtime-compatible. The real DSH consumer workflow below remains the integration proof. @@ -271,7 +271,7 @@ The repository also carries a copyable consumer smoke under [`examples/github-ac The `probe dsh-load` command gives the compatibility question its own bounded surface. It takes one exact `.tgz`, uses one exact DSH version, and creates a disposable `headless` profile: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar probe dsh-load \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar probe dsh-load \ ./dsh-plugin-1.2.3.tgz \ --dsh-version 0.1.0-rc.6 --json ``` @@ -296,10 +296,11 @@ The broader headless smoke proves that Radar can hand an event to a DSH Agent. T pnpm run showcase:dsh-runtime ``` -It creates a temporary plugin with a peer on the exact `@deepseek-ai/cordis` version installed by `@deepseek-ai/dsh@0.1.0-rc.6`, installs both the current Radar bundle and that plugin into a disposable profile, and starts the real DSH process. A local HTTPS OSV-compatible server returns one deterministic advisory only for that host version. Radar first records the profile fallback, then the native adapter refreshes the graph from the running process and persists: +It creates a temporary plugin with a peer on the exact `@deepseek-ai/cordis` version installed by `@deepseek-ai/dsh@0.1.0-rc.6`, installs both the current Radar bundle and that plugin into a disposable profile, and starts the real DSH process. A local HTTPS OSV-compatible server returns one deterministic advisory only for that host version. The profile-only reader first refuses to follow the profile's external host symlink; after DSH exposes its exact entrypoint, the native adapter constructs the bounded process host plane and persists: ```text -profile fallback -> running DSH process +static profile link: refused outside its trust boundary +verified running DSH process -> bounded host plane affected: @deepseek-ai/cordis@4.0.1 affectedSources: dsh-host path: showcase-dsh-host-peer -> @deepseek-ai/cordis diff --git a/examples/dsh/README.md b/examples/dsh/README.md index b9fdbfd..c2dcf0f 100644 --- a/examples/dsh/README.md +++ b/examples/dsh/README.md @@ -117,14 +117,14 @@ This is a separate deterministic showcase. It installs the current packed Radar The command fails unless it observes all of these facts: -- the generated profile graph records the host plane as `dsh-profile-fallback`; -- the running DSH refresh changes the graph source to `dsh-process`; +- the static profile reader refuses an external host link instead of widening its read boundary; +- the exact running DSH process supplies the bounded `dsh-process` host plane; - the refreshed graph contains the exact DSH runtime and its reachable host packages, not only the plugin's declared peer; - the persisted event affects the exact `@deepseek-ai/cordis` version and keeps `affectedSources = ["dsh-host"]`; - the dependency path is preserved from the temporary plugin to the host package; - the DSH Agent accepts the analysis result. -The checked-in [host-runtime result](reports/dsh-runtime-host.json) is generated by `pnpm run showcase:dsh-runtime:report`. The current snapshot observes 510 packages in the profile fallback and 62 reachable packages from the running process's exact host plane before matching the synthetic Cordis advisory. It proves runtime dependency observation and routing; the advisory is deliberately local and synthetic, so it is not evidence about a real DSH vulnerability. +The checked-in [host-runtime result](reports/dsh-runtime-host.json) is generated by `pnpm run showcase:dsh-runtime:report`. The current snapshot records the rejected external profile link, then observes 447 packages from the verified running-process host plane before matching the synthetic Cordis advisory. It proves runtime dependency observation and routing; the advisory is deliberately local and synthetic, so it is not evidence about a real DSH vulnerability. ## Pre-install pnpm graph proof diff --git a/examples/dsh/install-observer/README.md b/examples/dsh/install-observer/README.md index dae77d5..c199eab 100644 --- a/examples/dsh/install-observer/README.md +++ b/examples/dsh/install-observer/README.md @@ -1,11 +1,12 @@ # DSH isolated install observer This directory is the maintained dynamic-test corpus for Upstream Radar. It is -small on purpose: static checks can cover the wider plugin inventory every day; -code execution happens only after an exact DSH or mapped plugin publication -changes. The corpus currently contains nine published plugins: the original -three behavior cases plus six identity-checked targets imported from the -[`awesome-dsh-plugin` cohort](../awesome-observer/README.md). +small on purpose: static checks cover the wider plugin inventory every day; +dynamic code execution establishes behavior evidence for the current active +matrix. The corpus currently contains ten published plugins: the original +three behavior cases, six identity-checked targets imported from the +[`awesome-dsh-plugin` cohort](../awesome-observer/README.md), and the published +repair for the first author-confirmed dependency case. ## What runs where @@ -26,9 +27,13 @@ Inside the restricted container, Radar: 7. installs the local tarball through DSH with lifecycle scripts enabled and only the dependency-build approvals explicitly declared for that target; 8. verifies that DSH registered the bundle; -9. loads the profile with `--dump-config`; +9. runs a trusted one-shot wrapper from the real profile that resolves every + declared non-optional peer with `import.meta.resolve()`, imports the plugin, + and boots DSH headless with `--help`; 10. records install and load process execution, network destinations, write-like - file syscalls, and final filesystem changes; and + file syscalls, final filesystem changes, the resulting DSH profile lockfile, + the effective profile-plus-DSH-host graph, and static literal-import evidence + for every declared peer; and 11. destroys the container and hosted VM after preserving bounded JSON. The container is read-only except for a memory-backed sandbox and one output @@ -43,7 +48,14 @@ toolchain. ## When the matrix runs [`targets.json`](targets.json) is not a popularity list. It is the set of exact -plugins whose install/load behavior we commit to retesting. +plugins whose install/load behavior we commit to retesting. The durable +[`compatibility-ledger.json`](../../../compatibility-ledger.json) holds the most +recent evidence for every active plugin × DSH × Node/runtime-policy cell. +The same reconciliation writes a compact +[`compatibility-ir.json`](../../../compatibility-ir.json) and +[`compatibility-reverse-index.json`](../../../compatibility-reverse-index.json): +one exact peer declaration paired with the concrete host version DSH resolved, +then the inverse `host package → affected plugin cell` lookup. An entry may declare `allowedBuilds` when the plugin's documented installation contract explicitly approves named dependency scripts. The names become pnpm @@ -51,15 +63,32 @@ contract explicitly approves named dependency scripts. The names become pnpm An absent list means no dependency build is approved; Radar never turns one blocked script into a global “allow all” policy. -- A new exact `@deepseek-ai/dsh` package tests every enabled corpus entry at - its latest successfully observed npm coordinate; the checked-in exact spec - is the fallback when no trustworthy observation exists yet. -- A new exact package for a plugin mapped by `observerTargetId` tests only that - plugin, using the newly observed version. -- A source-only commit, unchanged package coordinate, baseline, or unrelated - target does not execute plugin code. -- Every selected plugin receives a separate hosted VM through the workflow - matrix. +- Every daily static pass builds the desired current matrix from the observed + DSH/plugin coordinates, source/lockfile graph facts, and the reviewed runtime + contract. +- A cell runs when it is missing, has become older than `refreshAfterHours`, or + its static evidence, runtime image, or allowed-build policy differs from the + record in the ledger. DSH and mapped plugin publications are immediate + invalidation signals, not the sole source of work. +- A `runtime-incompatible` result records the artifact's Node engine before any + plugin code runs. If another configured runtime could satisfy that range, + Radar adds one alternate-runtime cell rather than calling the plugin globally + incompatible. +- Every selected cell receives a separate hosted VM through the workflow + matrix. The ledger accepts a report only when its case label, tarball, DSH + version, Node major, and explicit build approvals match the static plan. +- A missing or malformed report is never saved as compatible. It remains + unsatisfied and will be selected again on the next reconciliation. +- A `compatible` result only closes its matrix cell when the final effective + profile-plus-DSH-host graph is complete and every direct required peer + resolves inside the real profile with a version satisfying the declared + range. A green install/load with missing, out-of-range, or unresolved + contract evidence remains an explicit evidence gap. The report keeps a + bounded sample of unresolved edges and labels static peer use as runtime, + type-only, no literal reference observed, or scan-incomplete. +- An actionable incompatibility creates one managed issue keyed by the stable + target/runtime cell. Persistent failures update that issue, regressions + reopen it, and a later compatible retest adds evidence and closes it. ## Result semantics @@ -67,15 +96,23 @@ blocked script into a global “allow all” policy. | --- | --- | | `compatible` | The exact tarball installed, registered, and loaded under the exact DSH release and recorded build-approval set, with readable bounded traces. | | `runtime-incompatible` | The exact tarball requires a Node version that excludes the isolated runtime. No plugin or dependency code is executed. | +| `peer-contract-incompatible` | Install and load passed, but a declared required peer is missing from the actual DSH profile or its resolved version is outside the declared range. This is not by itself proof that every UI/business path fails. | | `install-failed` | The traced install failed or DSH did not register the plugin. | | `load-failed` | Installation and registration passed, but the traced profile load failed. | | `unknown` | The artifact, DSH bootstrap, timeout/output bound, tracer, or collector could not establish a reliable result. | The report separately preserves `captured`, `truncated`, and `missing` trace coverage. A failed attempt is not silently converted into a compatibility -result. The JSON artifact and Job Summary are written first, then the GitHub -check fails unless the result is `compatible`, so a scheduled regression is -visible without somebody opening the artifact by hand. +result. The JSON artifact and Job Summary are written first. A reproduced +incompatibility is a successful observer result and is reconciled into a +managed issue; the check fails only when no trustworthy result survives, such +as a missing, malformed, rejected, or `unknown` report. This distinction keeps +product findings separate from observer-infrastructure failures. + +The [OpenPencil current-DSH case](reports/2026-08-22-openpencil-node24.md) +shows the full distinction in practice: headless load passed, but one +type-only peer declaration was absent from DSH and one runtime `react-dom` +import resolved outside the plugin's declared range. ## Security boundary diff --git a/examples/dsh/install-observer/reports/2026-08-22-openpencil-node24.md b/examples/dsh/install-observer/reports/2026-08-22-openpencil-node24.md new file mode 100644 index 0000000..9659bce --- /dev/null +++ b/examples/dsh/install-observer/reports/2026-08-22-openpencil-node24.md @@ -0,0 +1,52 @@ +# Real compatibility case: OpenPencil on current DSH + +This is a reproducible contract-drift case, not a claim that OpenPencil's UI is +known to crash. The evidence comes from a fresh GitHub-hosted VM and restricted +container: [Actions run 32502554160](https://github.com/MicroMilo/upstream-radar/actions/runs/32502554160). + +| Exact input | Observed value | +| --- | --- | +| Plugin artifact | `@zseven-w/dsh-openpencil@0.1.0-rc.1` | +| Tarball SHA-256 | `a4563e560e91bcd3a2a9302ee7dbee046b146c1bd0de6b27b7c7524fd52e77ca` | +| DSH | `@deepseek-ai/dsh@0.1.1-rc.1` | +| Runtime | Node `24.19.0`, pnpm `11.7.0`, Linux x64 | +| Effective graph | 448 nodes, 2,026 edges, 59 unavailable optional platform packages | + +## What actually passed + +The exact tarball was packed with scripts disabled, then DSH initialized a new +profile, installed the local tarball, registered its bundle, imported the +plugin from the profile's real module-resolution anchor, and booted DSH +headless. All six stages passed: runtime, artifact, profile, install, +registration, and load. + +## What the static and dynamic evidence disagree on + +Radar read 14 non-optional peer declarations from that same packed artifact, +then ran `import.meta.resolve()` for each one inside the final DSH profile and +checked the concrete package manifest that Node resolved. + +| Declared peer | Plugin declaration | DSH actually resolves | Static artifact use | Interpretation | +| --- | --- | --- | --- | --- | +| `@deepseek-ai/dsh-client-ui-slots` | `^0.1.0-rc.6` | not resolved | Type-only reference observed | The package currently declares a required host peer that the current DSH profile does not provide. The published source only showed type references, so this is a declaration/typing contract drift, not proof of a headless runtime crash. | +| `react-dom` | `^18.2.0` | `19.2.8` | Runtime import observed | The client bundle imports `react-dom`, while the current host exposes React DOM 19 outside the declared range. Headless boot passed, but a client/UI path still needs explicit React 19 validation. | + +The remaining **12/14** direct peer contracts were satisfied. There were no +indeterminate contracts. The one required unresolved graph edge is the missing +`dsh-client-ui-slots` contract above; optional platform variants are reported +separately and do not turn into a false failure. + +## Author-facing repair path + +1. Decide whether `dsh-client-ui-slots` is needed at runtime. If it is only a + compile-time type, remove it from required `peerDependencies`, make it + optional, or obtain the type through a supported development dependency. If + the web bundle needs it at runtime, align the plugin with the current DSH + client API instead. +2. Exercise the OpenPencil client path against DSH's React DOM 19. If it is + supported, widen the peer range deliberately (for example, after tests); + otherwise keep the range and declare the DSH version boundary clearly. + +The follow-up test should rerun this exact artifact/DSH/Node cell. A green +headless load alone is insufficient: Radar will close the cell only when all +required direct host contracts resolve and satisfy their declared ranges. diff --git a/examples/dsh/install-observer/targets.json b/examples/dsh/install-observer/targets.json index 2684670..9bc611c 100644 --- a/examples/dsh/install-observer/targets.json +++ b/examples/dsh/install-observer/targets.json @@ -1,5 +1,10 @@ { "schema": "upstream-radar.dsh-install-targets/v1alpha1", + "refreshAfterHours": 168, + "runtimeProfiles": [ + { "id": "node22", "nodeMajor": 22 }, + { "id": "node24", "nodeMajor": 24 } + ], "plugins": [ { "id": "feishu-bot", @@ -51,10 +56,17 @@ "observerTargetId": "dsh-market", "reason": "The in-product plugin market is a high-leverage downstream consumer of DSH compatibility changes." }, + { + "id": "sanqi-market", + "spec": "@sanqi-normal/dsh-webui-market-plugin@0.5.5", + "observerTargetId": "dsh-webui-market-plugin", + "reason": "The first author-confirmed repair case proves that a new published artifact can resolve a previously broken DSH host dependency contract." + }, { "id": "openpencil", "spec": "@zseven-w/dsh-openpencil@0.1.0-rc.1", "observerTargetId": "dsh-openpencil", + "runtimeProfiles": ["node24"], "reason": "A Node 24-targeted design plugin that exercises host tools and a broad web-client peer surface." } ] diff --git a/examples/dsh/reports/dsh-composer-expand-lockfile-feedback.md b/examples/dsh/reports/dsh-composer-expand-lockfile-feedback.md index ae0780e..7a980e3 100644 --- a/examples/dsh/reports/dsh-composer-expand-lockfile-feedback.md +++ b/examples/dsh/reports/dsh-composer-expand-lockfile-feedback.md @@ -19,7 +19,7 @@ From a clean checkout, the finding is reproducible without installing or executing the plugin: ```bash -npx --yes upstream-radar@0.40.0 scan . --json +npx --yes upstream-radar@0.41.0 scan . --json ``` The current static scan reports: diff --git a/examples/dsh/reports/dsh-core-artifact-rc7-2026-08-18.md b/examples/dsh/reports/dsh-core-artifact-rc7-2026-08-18.md index 1f7c1fc..f807660 100644 --- a/examples/dsh/reports/dsh-core-artifact-rc7-2026-08-18.md +++ b/examples/dsh/reports/dsh-core-artifact-rc7-2026-08-18.md @@ -43,5 +43,5 @@ graph, the unresolved peer boundary, the vulnerability result, and the two remaining review actions in one report. This report was produced by the current branch, not the already published -`upstream-radar@0.40.0`. Review the Draft PR before relying on the fallback +`upstream-radar@0.41.0`. Review the Draft PR before relying on the fallback resolver from npm. diff --git a/examples/dsh/reports/dsh-feishu-bot-0.15.4-probe.md b/examples/dsh/reports/dsh-feishu-bot-0.15.4-probe.md index d95966b..000120e 100644 --- a/examples/dsh/reports/dsh-feishu-bot-0.15.4-probe.md +++ b/examples/dsh/reports/dsh-feishu-bot-0.15.4-probe.md @@ -21,7 +21,7 @@ Reproduce it with: ```bash npm pack --ignore-scripts --pack-destination /tmp dsh-feishu-bot@0.15.4 -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar probe dsh-matrix \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar probe dsh-matrix \ /tmp/dsh-feishu-bot-0.15.4.tgz \ --dsh-version 0.1.0-rc.6,0.1.0-rc.7 ``` diff --git a/examples/dsh/reports/dsh-progress-viz-public-url-2026-08-18.md b/examples/dsh/reports/dsh-progress-viz-public-url-2026-08-18.md index 871651d..37ca701 100644 --- a/examples/dsh/reports/dsh-progress-viz-public-url-2026-08-18.md +++ b/examples/dsh/reports/dsh-progress-viz-public-url-2026-08-18.md @@ -8,7 +8,7 @@ graph without installing anything. ## Reproduction ```bash -npx --yes upstream-radar@0.40.0 scan \ +npx --yes upstream-radar@0.41.0 scan \ https://github.com/2008924/dsh-progress-viz \ --fail-on never ``` @@ -22,7 +22,7 @@ query an advisory service, or call an LLM. ```text Reading 2008924/dsh-progress-viz (plugin directory: plugin) without installing dependencies or running code... -Upstream Radar 0.40.0 +Upstream Radar 0.41.0 Target: dsh-progress-viz-plugin@0.1.0 Artifact: sha256:c9b6b2dc31a458b480587f6200772fe72d4af5451b1e71d41594c5017be929c5 DSH bundle: yes (./cordis.patch.yml) diff --git a/examples/dsh/reports/dsh-runtime-host.json b/examples/dsh/reports/dsh-runtime-host.json index 857dd58..b2f7135 100644 --- a/examples/dsh/reports/dsh-runtime-host.json +++ b/examples/dsh/reports/dsh-runtime-host.json @@ -2,15 +2,18 @@ "dshPackage": "@deepseek-ai/dsh@0.1.0-rc.6", "plugin": "showcase-dsh-host-peer@1.0.0", "hostPackage": "@deepseek-ai/cordis@4.0.1", - "initialProfileOnlyGraph": { - "hostRuntimeSource": "dsh-profile-fallback", - "hostRuntimePackages": 510 + "staticProfileBoundary": { + "result": "refused-external-host-link" + }, + "configuredProcessGraph": { + "hostRuntimeSource": "dsh-process", + "hostRuntimePackages": 447 }, "refreshedGraph": { "source": "dsh-process", - "hostRuntimePackages": 62, + "hostRuntimePackages": 447, "hostNode": { - "id": "dsh-host/node_modules/@deepseek-ai/cordis", + "id": "dsh-host/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis", "name": "@deepseek-ai/cordis", "version": "4.0.1", "source": "dsh-host" @@ -39,11 +42,11 @@ ] }, "dshAnalysisResults": 1, - "feedRequests": 59, + "feedRequests": 191, "modelRequests": 3, "webhookRequests": 1, "webhookEventIds": [ - "event-e9cea0f53dbdca810d0e99c2" + "event-3dd57ae0f6e770bb6f51ca1f" ], "webhookEndpointPersisted": false, "dshProcessPollCompleted": true, diff --git a/examples/dsh/reports/dsh-tui-source-vs-npm-2026-08-18.md b/examples/dsh/reports/dsh-tui-source-vs-npm-2026-08-18.md index 5afd78a..7f6dd46 100644 --- a/examples/dsh/reports/dsh-tui-source-vs-npm-2026-08-18.md +++ b/examples/dsh/reports/dsh-tui-source-vs-npm-2026-08-18.md @@ -13,11 +13,11 @@ No DSH profile, plugin code, lifecycle script, or LLM was started. ## Commands ```bash -npx --yes upstream-radar@0.40.0 scan \ +npx --yes upstream-radar@0.41.0 scan \ https://github.com/ccch1mneyyy/dsh-TUI \ --fail-on never -npx --yes upstream-radar@0.40.0 inspect \ +npx --yes upstream-radar@0.41.0 inspect \ npm:@deepseek-harness-tui/dsh-tui@0.8.0 \ --deep --fail-on never ``` diff --git a/examples/dsh/reports/lockfile-metadata-follow-up-2026-08-18.md b/examples/dsh/reports/lockfile-metadata-follow-up-2026-08-18.md index 78652ba..9e0c2d7 100644 --- a/examples/dsh/reports/lockfile-metadata-follow-up-2026-08-18.md +++ b/examples/dsh/reports/lockfile-metadata-follow-up-2026-08-18.md @@ -8,7 +8,7 @@ release. ## Rechecked with the public CLI Each repository was cloned at its current public `main` commit and scanned with -`upstream-radar@0.40.0`. No package was installed, loaded, or executed. +`upstream-radar@0.41.0`. No package was installed, loaded, or executed. | Repository | Commit | `package.json` | `package-lock.json` root | Result | | --- | --- | ---: | ---: | --- | diff --git a/examples/github-actions/consumer/README.md b/examples/github-actions/consumer/README.md index 28c24ac..b790d12 100644 --- a/examples/github-actions/consumer/README.md +++ b/examples/github-actions/consumer/README.md @@ -40,7 +40,7 @@ The optional probe is load-only. It packs with `--ignore-scripts`, uses a tempor For your project, generate the config from the actual DSH profile instead of copying this package's snapshot: ```bash -pnpm dlx --package=upstream-radar@0.40.0 upstream-radar init \ +pnpm dlx --package=upstream-radar@0.41.0 upstream-radar init \ --profile \ --project-id \ --project-name "Your project" \ diff --git a/examples/github-actions/consumer/upstream-radar.yml b/examples/github-actions/consumer/upstream-radar.yml index f3d3563..9185068 100644 --- a/examples/github-actions/consumer/upstream-radar.yml +++ b/examples/github-actions/consumer/upstream-radar.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: config: examples/github-actions/consumer/upstream-radar.config.json fail-on: high diff --git a/examples/github-actions/dsh-plugin-review-minimal.yml b/examples/github-actions/dsh-plugin-review-minimal.yml index 44c6c10..3ce7404 100644 --- a/examples/github-actions/dsh-plugin-review-minimal.yml +++ b/examples/github-actions/dsh-plugin-review-minimal.yml @@ -24,7 +24,7 @@ jobs: - name: Inspect and load-test the exact plugin id: radar continue-on-error: true - uses: MicroMilo/upstream-radar@v0.40.0 + uses: MicroMilo/upstream-radar@v0.41.0 with: inspect-package: ${{ inputs.plugin }} inspect-fail-on: never diff --git a/examples/github-actions/upstream-observer-minimal.yml b/examples/github-actions/upstream-observer-minimal.yml index 6d4e962..d282b0e 100644 --- a/examples/github-actions/upstream-observer-minimal.yml +++ b/examples/github-actions/upstream-observer-minimal.yml @@ -53,7 +53,7 @@ jobs: llm_env_file="$RUNNER_TEMP/issue-locator.env" trap 'rm -f "$llm_env_file"' EXIT observe_args=( - npx --yes upstream-radar@0.40.0 observe "$repository" + npx --yes upstream-radar@0.41.0 observe "$repository" --ref "$ref" --state observations.json --report "$report" diff --git a/examples/github-actions/upstream-radar-npm.yml b/examples/github-actions/upstream-radar-npm.yml index ef50743..0b183ed 100644 --- a/examples/github-actions/upstream-radar-npm.yml +++ b/examples/github-actions/upstream-radar-npm.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: npm-lock: package-lock.json fail-on: high diff --git a/examples/github-actions/upstream-radar-pnpm.yml b/examples/github-actions/upstream-radar-pnpm.yml index 5e6d455..38edaf4 100644 --- a/examples/github-actions/upstream-radar-pnpm.yml +++ b/examples/github-actions/upstream-radar-pnpm.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: pnpm-lock: pnpm-lock.yaml fail-on: high diff --git a/examples/github-actions/upstream-radar.yml b/examples/github-actions/upstream-radar.yml index a476d39..043644f 100644 --- a/examples/github-actions/upstream-radar.yml +++ b/examples/github-actions/upstream-radar.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: MicroMilo/upstream-radar@v0.40.0 + - uses: MicroMilo/upstream-radar@v0.41.0 with: # The Action auto-detects one pnpm-lock.yaml or package-lock.json. fail-on: high diff --git a/examples/github-actions/upstream-scan-minimal.yml b/examples/github-actions/upstream-scan-minimal.yml index 14e296a..77763df 100644 --- a/examples/github-actions/upstream-scan-minimal.yml +++ b/examples/github-actions/upstream-scan-minimal.yml @@ -27,7 +27,7 @@ jobs: run: | set -euo pipefail report="$RUNNER_TEMP/upstream-radar-scan.json" - npx --yes upstream-radar@0.40.0 scan \ + npx --yes upstream-radar@0.41.0 scan \ "$OBSERVER_REPOSITORY_INPUT" \ --json --fail-on never > "$report" if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then diff --git a/examples/upstream-observer/README.md b/examples/upstream-observer/README.md index f06dd1e..0a96f3b 100644 --- a/examples/upstream-observer/README.md +++ b/examples/upstream-observer/README.md @@ -24,7 +24,7 @@ Replace it with the repositories your team depends on. For one repository, the shortest path skips YAML and lockfile configuration entirely: ```bash -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ https://github.com/PlutoKeating/dsh-lark-bot \ --state /tmp/upstream-radar-observations.json \ --report /tmp/upstream-radar-observer.md @@ -35,7 +35,7 @@ Radar automatically chooses the committed `pnpm-lock.yaml` or the explicit form below adds `--package`: ```bash -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ https://github.com/PlutoKeating/dsh-lark-bot \ --package dsh-feishu-bot \ --lockfile pnpm-lock.yaml --lockfile-type pnpm \ @@ -75,9 +75,9 @@ Build one index from saved DSH plugin scan/review/config JSON, then give it to the observer: ```bash -npx --yes upstream-radar@0.40.0 graph reverse ./plugin-reports \ +npx --yes upstream-radar@0.41.0 graph reverse ./plugin-reports \ --output ./reverse-dependency-index.json -npx --yes upstream-radar@0.40.0 observe ./targets.yml \ +npx --yes upstream-radar@0.41.0 observe ./targets.yml \ --reverse-index ./reverse-dependency-index.json \ --state /tmp/upstream-radar-observations.json \ --report /tmp/upstream-radar-observer.md @@ -99,7 +99,7 @@ and run `pnpm run showcase:observer` to replay a real The scheduled workflow uses the checked-in index directly: ```bash -npx --yes upstream-radar@0.40.0 observe ./targets.yml \ +npx --yes upstream-radar@0.41.0 observe ./targets.yml \ --reverse-index ../dsh/first-batch/reverse-dependency-index.json \ --state /tmp/upstream-radar-observations.json \ --report /tmp/upstream-radar-observer.md @@ -126,7 +126,7 @@ Run it from any directory with the published CLI: ```bash export GITHUB_TOKEN='a read-only token with repository metadata access' -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ /path/to/targets.yml \ --state /tmp/upstream-radar-observations.json \ --report /tmp/upstream-radar-observer.md diff --git a/examples/upstream-observer/reports/dsh-core-auto-discovery-2026-08-18.md b/examples/upstream-observer/reports/dsh-core-auto-discovery-2026-08-18.md index 9596c15..a18899c 100644 --- a/examples/upstream-observer/reports/dsh-core-auto-discovery-2026-08-18.md +++ b/examples/upstream-observer/reports/dsh-core-auto-discovery-2026-08-18.md @@ -30,5 +30,5 @@ than silently monitoring the repository root. Local `workspace:` links remain explicitly unresolved; Radar does not guess their published versions. This report was produced by the current branch, not the already published -`upstream-radar@0.40.0`. Review the Draft PR before using the auto-discovery +`upstream-radar@0.41.0`. Review the Draft PR before using the auto-discovery path from npm. diff --git a/examples/upstream-observer/reports/sanqi-maintainer-repair-live.md b/examples/upstream-observer/reports/sanqi-maintainer-repair-live.md index bb6cf56..974cd76 100644 --- a/examples/upstream-observer/reports/sanqi-maintainer-repair-live.md +++ b/examples/upstream-observer/reports/sanqi-maintainer-repair-live.md @@ -10,12 +10,12 @@ The baseline was taken at the commit that produced the published `0.5.4` artifact, then the same state was checked against the current `master`: ```bash -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ https://github.com/Sanqi-normal/dsh-webui-market-plugin \ --ref aa5f4efc7827176cce27c73f73a2f42514da1ebf \ --state observations.json --report baseline.md --json -npx --yes upstream-radar@0.40.0 observe \ +npx --yes upstream-radar@0.41.0 observe \ https://github.com/Sanqi-normal/dsh-webui-market-plugin \ --ref master \ --state observations.json --report repair.md --json diff --git a/package.json b/package.json index 6898117..5aaa560 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "upstream-radar", - "version": "0.40.0", + "version": "0.41.0", "description": "Always-on dependency and compatibility monitoring for DeepSeek Harness plugins, including exact paths, upstream changes, and isolated install/load evidence.", "type": "module", "license": "Apache-2.0", diff --git a/schemas/dsh-compatibility-ir.schema.json b/schemas/dsh-compatibility-ir.schema.json new file mode 100644 index 0000000..79c7120 --- /dev/null +++ b/schemas/dsh-compatibility-ir.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/MicroMilo/upstream-radar/blob/main/schemas/dsh-compatibility-ir.schema.json", + "title": "Upstream Radar DSH compatibility IR", + "description": "Normalized direct plugin-to-DSH-host compatibility relations produced from isolated observations.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "cells", "relations"], + "properties": { + "schema": { "const": "upstream-radar.dsh-compatibility-ir/v1alpha1" }, + "cells": { + "type": "array", + "maxItems": 500, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "caseId", "targetId", "observedAt", "result", "plugin", "runtime", "evidence"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 80 }, + "caseId": { "type": "string", "minLength": 1, "maxLength": 64 }, + "targetId": { "type": "string", "minLength": 1, "maxLength": 64 }, + "observedAt": { "type": "string", "format": "date-time" }, + "result": { "enum": ["compatible", "runtime-incompatible", "peer-contract-incompatible", "install-failed", "load-failed", "unknown"] }, + "plugin": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version", "staticFingerprint"], + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "version": { "type": "string", "minLength": 1, "maxLength": 256 }, + "artifactSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "staticFingerprint": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" } + } + }, + "runtime": { + "type": "object", + "additionalProperties": false, + "required": ["dshVersion", "nodeMajor", "nodeVersion", "platform", "architecture", "contractFingerprint"], + "properties": { + "dshVersion": { "type": "string", "minLength": 1, "maxLength": 256 }, + "nodeMajor": { "type": "integer", "minimum": 16, "maximum": 40 }, + "nodeVersion": { "type": "string", "minLength": 1, "maxLength": 256 }, + "platform": { "type": "string", "minLength": 1, "maxLength": 64 }, + "architecture": { "type": "string", "minLength": 1, "maxLength": 64 }, + "pnpmVersion": { "type": "string", "minLength": 1, "maxLength": 256 }, + "contractFingerprint": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "properties": { + "runtimeGraphDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "runtimeGraphNodes": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "runtimeGraphEdges": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "requiredUnresolved": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "declaredPeerContracts": { "type": "integer", "minimum": 0, "maximum": 64 } + } + } + } + } + }, + "relations": { + "type": "array", + "maxItems": 32000, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "cellId", "dependency"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 80 }, + "cellId": { "type": "string", "minLength": 1, "maxLength": 80 }, + "dependency": { + "type": "object", + "additionalProperties": false, + "required": ["name", "required", "status", "staticUsage"], + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "required": { "type": "string", "minLength": 1, "maxLength": 512 }, + "status": { "enum": ["satisfied", "mismatched", "indeterminate", "missing"] }, + "staticUsage": { "enum": ["runtime-import-observed", "type-only-reference-observed", "no-literal-reference-observed", "scan-incomplete"] }, + "resolvedVersion": { "type": "string", "minLength": 1, "maxLength": 256 } + } + } + } + } + } + } +} diff --git a/schemas/dsh-install-observation.schema.json b/schemas/dsh-install-observation.schema.json index 7d93993..c009ec6 100644 --- a/schemas/dsh-install-observation.schema.json +++ b/schemas/dsh-install-observation.schema.json @@ -5,7 +5,7 @@ "description": "Bounded process, network and filesystem evidence from installing and loading one exact npm artifact under one exact DSH version in an externally supplied disposable environment.", "type": "object", "additionalProperties": false, - "required": ["schema", "tool", "probe", "scope", "startedAt", "completedAt", "dshVersion", "runtime", "artifact", "stages", "observations", "filesystem", "result", "reason", "boundary"], + "required": ["schema", "tool", "probe", "scope", "startedAt", "completedAt", "dshVersion", "runtime", "artifact", "stages", "observations", "filesystem", "resolution", "result", "reason", "boundary"], "properties": { "schema": { "const": "upstream-radar.dsh-install-observation/v1alpha1" }, "tool": { @@ -21,6 +21,7 @@ "scope": { "const": "install-and-load-behavior" }, "startedAt": { "type": "string", "format": "date-time" }, "completedAt": { "type": "string", "format": "date-time" }, + "caseId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$" }, "dshVersion": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$" }, "runtime": { "type": "object", @@ -73,7 +74,8 @@ "load": { "$ref": "#/$defs/filesystemDiff" } } }, - "result": { "enum": ["compatible", "runtime-incompatible", "install-failed", "load-failed", "unknown"] }, + "resolution": { "$ref": "#/$defs/resolution" }, + "result": { "enum": ["compatible", "runtime-incompatible", "peer-contract-incompatible", "install-failed", "load-failed", "unknown"] }, "reason": { "type": "string", "maxLength": 4096 }, "boundary": { "type": "object", @@ -118,6 +120,137 @@ } } }, + "resolution": { + "type": "object", + "additionalProperties": false, + "properties": { + "profileLockfile": { + "type": "object", + "additionalProperties": false, + "required": ["sha256", "bytes"], + "properties": { + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "bytes": { "type": "integer", "minimum": 0, "maximum": 16777216 }, + "graphDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "nodes": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "edges": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "unresolved": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "unresolvedDependencies": { + "type": "array", + "maxItems": 32, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["from", "name", "spec", "kind"], + "properties": { + "from": { "type": "string", "minLength": 1, "maxLength": 512 }, + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "spec": { "type": "string", "minLength": 1, "maxLength": 512 }, + "kind": { "enum": ["runtime", "development", "optional", "peer", "host-runtime"] } + } + } + } + } + }, + "runtimeGraph": { + "type": "object", + "additionalProperties": false, + "required": ["digest", "nodes", "edges", "unresolved", "pluginPeerContracts"], + "properties": { + "digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + "nodes": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "edges": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "unresolved": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "unresolvedDependencies": { + "type": "array", + "maxItems": 32, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["from", "name", "spec", "kind"], + "properties": { + "from": { "type": "string", "minLength": 1, "maxLength": 512 }, + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "spec": { "type": "string", "minLength": 1, "maxLength": 512 }, + "kind": { "enum": ["runtime", "development", "optional", "peer", "host-runtime"] } + } + } + }, + "optionalUnavailable": { "type": "integer", "minimum": 0, "maximum": 250000 }, + "optionalUnavailableDependencies": { + "type": "array", + "maxItems": 32, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["from", "name", "spec", "kind"], + "properties": { + "from": { "type": "string", "minLength": 1, "maxLength": 512 }, + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "spec": { "type": "string", "minLength": 1, "maxLength": 512 }, + "kind": { "enum": ["runtime", "development", "optional", "peer", "host-runtime"] } + } + } + }, + "pluginPeerContracts": { + "type": "object", + "additionalProperties": false, + "required": ["declared", "satisfied", "mismatched", "indeterminate", "missing", "relations"], + "properties": { + "declared": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "satisfied": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "mismatched": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "indeterminate": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "missing": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "relations": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "required", "status", "staticUsage"], + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "required": { "type": "string", "minLength": 1, "maxLength": 512 }, + "status": { "enum": ["satisfied", "mismatched", "indeterminate", "missing"] }, + "staticUsage": { "enum": ["runtime-import-observed", "type-only-reference-observed", "no-literal-reference-observed", "scan-incomplete"] }, + "resolvedVersion": { "type": "string", "minLength": 1, "maxLength": 256 } + } + } + }, + "issues": { + "type": "array", + "maxItems": 32, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "required", "status", "staticUsage"], + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 214 }, + "required": { "type": "string", "minLength": 1, "maxLength": 512 }, + "status": { "enum": ["mismatched", "indeterminate", "missing"] }, + "staticUsage": { "enum": ["runtime-import-observed", "type-only-reference-observed", "no-literal-reference-observed", "scan-incomplete"] }, + "resolvedVersion": { "type": "string", "minLength": 1, "maxLength": 256 } + } + } + } + } + }, + "hostRuntime": { + "type": "object", + "additionalProperties": false, + "required": ["source", "resolvedNodes"], + "properties": { + "source": { "enum": ["dsh-profile-fallback", "dsh-process"] }, + "resolvedNodes": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "dshVersion": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$" } + } + } + } + }, + "runtimeGraphError": { "type": "string", "minLength": 1, "maxLength": 512 } + } + }, "stage": { "type": "object", "additionalProperties": false, diff --git a/scripts/dsh-runtime-graph-showcase.mjs b/scripts/dsh-runtime-graph-showcase.mjs index 10e4894..0ed86da 100644 --- a/scripts/dsh-runtime-graph-showcase.mjs +++ b/scripts/dsh-runtime-graph-showcase.mjs @@ -6,7 +6,14 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import process from 'node:process' -const { createRadarConfigFromDshProfile, discoverDshRuntimeNodeModulesDirectory, writeDshPatch, writeRadarConfig } = await import('../dist/src/index.js') +const { + createRadarConfigFromDshProfile, + discoverDshRuntimeHostNodeModulesDirectory, + discoverDshRuntimePackage, + discoverDshRuntimePackageDirectory, + writeDshPatch, + writeRadarConfig, +} = await import('../dist/src/index.js') const { emptyRadarState } = await import('../dist/src/radar.js') const { saveRadarState } = await import('../dist/src/radar-state.js') @@ -236,10 +243,6 @@ async function createLocalCertificate(scratch) { return { keyFile, certFile } } -function hostManifestPath(nodeModules, packageName) { - return join(nodeModules, ...packageName.split('/'), 'package.json') -} - async function main() { const scratch = await mkdtemp(join(tmpdir(), 'upstream-radar-dsh-runtime-')) const dshHome = join(scratch, 'dsh-home') @@ -268,11 +271,16 @@ async function main() { const captured = JSON.parse(await readFile(captureFile, 'utf8')) const entrypoint = captured?.argv?.[1] if (typeof entrypoint !== 'string') throw new Error('real DSH entrypoint was not captured') - const hostNodeModulesDirectory = discoverDshRuntimeNodeModulesDirectory(entrypoint) + // The exact runtime may be installed by pnpm dlx. In that topology the + // package-local node_modules misses sibling dependencies in `.pnpm`; the + // host plane is the bounded directory that contains both. + const hostNodeModulesDirectory = discoverDshRuntimeHostNodeModulesDirectory(entrypoint) + const hostRuntimePackage = discoverDshRuntimePackage(entrypoint) + const hostRuntimePackageDirectory = discoverDshRuntimePackageDirectory(entrypoint) if (hostNodeModulesDirectory === undefined) throw new Error('real DSH host dependency plane was not discovered') - const hostManifest = JSON.parse(await readFile(hostManifestPath(hostNodeModulesDirectory, HOST_PACKAGE_NAME), 'utf8')) - const hostPackageVersion = hostManifest.version - if (typeof hostPackageVersion !== 'string') throw new Error('real DSH host package has no exact version') + if (hostRuntimePackage === undefined || hostRuntimePackageDirectory === undefined) { + throw new Error('real DSH runtime package was not discovered') + } const pluginDirectory = join(scratch, 'plugin') const packDirectory = join(scratch, 'package') @@ -313,25 +321,43 @@ async function main() { workspace: ROOT, hostNodeModulesDirectory, hostRuntimeSource: 'dsh-process', + hostRuntimePackage, + hostRuntimePackageDirectory, }) const exactGraph = exact.projects[0]?.plugins[0]?.graph const exactHostNode = exactGraph?.nodes.find(node => node.name === HOST_PACKAGE_NAME && node.source === 'dsh-host') if (exactHostNode === undefined) throw new Error(`exact preflight graph did not resolve ${HOST_PACKAGE_NAME} from the DSH host plane`) + const hostPackageVersion = exactHostNode.version - // The generated config can already use DSH's profile-level fallback plane. - // The native DSH adapter must refresh it to the stronger process-level - // plane before polling, so the persisted event carries the right source. - const config = await createRadarConfigFromDshProfile({ - profileDirectory, - projectId: 'dsh-runtime-host-showcase', - projectName: 'DSH runtime host showcase', - workspace: ROOT, - }) + // A profile can contain symlinks into the DSH package-manager cache. A + // profile-only inspection deliberately refuses to follow that untrusted + // external link. Once the real DSH process gives us its exact package + // root, the earlier `exact` graph supplies a bounded, verified host plane. + let staticProfileBoundary + try { + await createRadarConfigFromDshProfile({ + profileDirectory, + projectId: 'dsh-runtime-host-showcase', + projectName: 'DSH runtime host showcase', + workspace: ROOT, + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (!message.includes('escapes the shared dependency plane')) throw error + staticProfileBoundary = 'refused-external-host-link' + } + if (staticProfileBoundary === undefined) { + throw new Error('profile-only graph unexpectedly followed an external DSH host link') + } + + // The native adapter receives the same exact running-process boundary and + // rebuilds this graph before it polls. Starting from verified evidence is + // safer than treating a profile-controlled external symlink as trusted. + const config = exact config.dshProfile = { name: 'headless' } - const initialGraph = config.projects[0]?.plugins[0]?.graph - const initialHostRuntime = initialGraph?.hostRuntime - if (initialHostRuntime?.source !== 'dsh-profile-fallback' || initialHostRuntime.resolvedNodes < 1) { - throw new Error(`initial profile-only graph did not use the expected DSH fallback plane: ${JSON.stringify(initialHostRuntime)}`) + const configuredHostRuntime = config.projects[0]?.plugins[0]?.graph.hostRuntime + if (configuredHostRuntime?.source !== 'dsh-process' || configuredHostRuntime.resolvedNodes < 1) { + throw new Error(`verified process graph did not preserve the expected DSH host plane: ${JSON.stringify(configuredHostRuntime)}`) } const configFile = join(scratch, 'upstream-radar.config.json') @@ -448,9 +474,12 @@ async function main() { dshPackage: DSH_PACKAGE, plugin: `${PLUGIN_NAME}@${PLUGIN_VERSION}`, hostPackage: `${HOST_PACKAGE_NAME}@${hostPackageVersion}`, - initialProfileOnlyGraph: { - hostRuntimeSource: initialHostRuntime.source, - hostRuntimePackages: initialHostRuntime.resolvedNodes, + staticProfileBoundary: { + result: staticProfileBoundary, + }, + configuredProcessGraph: { + hostRuntimeSource: configuredHostRuntime.source, + hostRuntimePackages: configuredHostRuntime.resolvedNodes, }, refreshedGraph: { source: exactGraph?.hostRuntime?.source, diff --git a/scripts/merge-dsh-compatibility-ledger.mjs b/scripts/merge-dsh-compatibility-ledger.mjs new file mode 100644 index 0000000..653baf6 --- /dev/null +++ b/scripts/merge-dsh-compatibility-ledger.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node + +import { appendFile, lstat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import process from 'node:process' +import { + emptyDshCompatibilityLedger, + mergeDshCompatibilityLedger, + renderDshCompatibilityLedgerMerge, +} from '../dist/src/dsh-compatibility-ledger.js' +import { + buildDshCompatibilityIR, + buildDshCompatibilityReverseIndex, +} from '../dist/src/dsh-compatibility-ir.js' + +const MAX_JSON_BYTES = 64 * 1024 * 1024 +const MAX_REPORTS = 100 + +async function readJson(path) { + const contents = await readFile(resolve(path), 'utf8') + if (Buffer.byteLength(contents) > MAX_JSON_BYTES) throw new Error(`${path} exceeds ${MAX_JSON_BYTES} bytes`) + return JSON.parse(contents) +} + +async function readOptionalLedger(path) { + try { + return await readJson(path) + } catch (error) { + if (error?.code === 'ENOENT') return emptyDshCompatibilityLedger() + throw error + } +} + +async function collectReportPaths(root) { + const result = [] + async function visit(path) { + if (result.length >= MAX_REPORTS) throw new Error(`report directory contains more than ${MAX_REPORTS} report files`) + let entries + try { + entries = await readdir(path, { withFileTypes: true }) + } catch (error) { + if (error?.code === 'ENOENT') return + throw error + } + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + const child = join(path, entry.name) + if (entry.isDirectory()) await visit(child) + else if (entry.isFile() && entry.name === 'report.json') result.push(child) + } + } + await visit(root) + return result.sort() +} + +async function readReports(root) { + const reports = [] + const rejected = [] + for (const path of await collectReportPaths(root)) { + try { + const metadata = await lstat(path) + if (!metadata.isFile() || metadata.size > MAX_JSON_BYTES) throw new Error('not a bounded regular JSON file') + reports.push(await readJson(path)) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + rejected.push(`${path}: ${message.replace(/[\u0000-\u001f\u007f]/g, '?').slice(0, 512)}`) + } + } + return { reports, rejected } +} + +const [ledgerPath, matrixPath, reportsPath, markdownPath, irPath, reverseIndexPath] = process.argv.slice(2) +if (ledgerPath === undefined || matrixPath === undefined || reportsPath === undefined || markdownPath === undefined + || irPath === undefined || reverseIndexPath === undefined) { + throw new Error('usage: merge-dsh-compatibility-ledger.mjs ') +} + +const matrix = await readJson(matrixPath) +if (typeof matrix !== 'object' || matrix === null || Array.isArray(matrix) || !Array.isArray(matrix.include)) { + throw new Error('install matrix must be an object with an include array') +} +const inputs = await readReports(resolve(reportsPath)) +const merged = mergeDshCompatibilityLedger({ + ledger: await readOptionalLedger(ledgerPath), + expected: matrix.include, + reports: inputs.reports, +}) +merged.rejectedReports.push(...inputs.rejected) +merged.rejectedReports.sort() + +await mkdir(dirname(resolve(ledgerPath)), { recursive: true }) +await mkdir(dirname(resolve(markdownPath)), { recursive: true }) +await mkdir(dirname(resolve(irPath)), { recursive: true }) +await mkdir(dirname(resolve(reverseIndexPath)), { recursive: true }) +await writeFile(resolve(ledgerPath), `${JSON.stringify(merged.ledger, null, 2)}\n`, 'utf8') +await writeFile(resolve(markdownPath), renderDshCompatibilityLedgerMerge(merged), 'utf8') +const ir = buildDshCompatibilityIR(merged.ledger) +const reverseIndex = buildDshCompatibilityReverseIndex(ir) +await writeFile(resolve(irPath), `${JSON.stringify(ir, null, 2)}\n`, 'utf8') +await writeFile(resolve(reverseIndexPath), `${JSON.stringify(reverseIndex, null, 2)}\n`, 'utf8') + +const actionable = merged.transitions.filter(item => ( + item.status !== 'compatible' && item.status !== 'persisting-incompatibility' +)).length +const summary = { + accepted: merged.acceptedCaseIds.length, + missing: merged.missingCaseIds.length, + rejected: merged.rejectedReports.length, + actionable, + cells: ir.cells.length, + relations: ir.relations.length, + dependencies: reverseIndex.dependencies.length, + transitions: merged.transitions, +} +process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`) + +if (process.env.GITHUB_OUTPUT !== undefined) { + await appendFile(process.env.GITHUB_OUTPUT, [ + `accepted=${summary.accepted}`, + `missing=${summary.missing}`, + `rejected=${summary.rejected}`, + `actionable=${summary.actionable}`, + `cells=${summary.cells}`, + `relations=${summary.relations}`, + `dependencies=${summary.dependencies}`, + ].join('\n') + '\n', 'utf8') +} + +if (summary.missing > 0 || summary.rejected > 0) { + console.error(`Compatibility reconciliation is incomplete: ${summary.missing} scheduled report(s) missing, ${summary.rejected} report(s) rejected.`) + process.exitCode = 1 +} diff --git a/scripts/sync-dsh-compatibility-issues.mjs b/scripts/sync-dsh-compatibility-issues.mjs new file mode 100644 index 0000000..39ed93f --- /dev/null +++ b/scripts/sync-dsh-compatibility-issues.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node + +import { appendFile, readFile } from 'node:fs/promises' +import { + buildDshCompatibilityIssuePlan, + DSH_COMPATIBILITY_ISSUE_LABELS, +} from '../dist/src/dsh-compatibility-issues.js' + +const [ledgerPath] = process.argv.slice(2) +if (ledgerPath === undefined) { + throw new Error('usage: sync-dsh-compatibility-issues.mjs ') +} + +const repository = process.env.GITHUB_REPOSITORY +const token = process.env.GITHUB_TOKEN +if (repository === undefined || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) { + throw new Error('GITHUB_REPOSITORY must identify the repository that owns compatibility incidents') +} +if (token === undefined || token.length < 20) throw new Error('GITHUB_TOKEN is required to reconcile compatibility incidents') + +const apiBase = (process.env.GITHUB_API_URL ?? 'https://api.github.com').replace(/\/$/, '') +const serverBase = (process.env.GITHUB_SERVER_URL ?? 'https://github.com').replace(/\/$/, '') +const runId = process.env.GITHUB_RUN_ID +const runUrl = runId === undefined ? undefined : `${serverBase}/${repository}/actions/runs/${encodeURIComponent(runId)}` + +async function github(path, options = {}) { + const response = await fetch(`${apiBase}${path}`, { + ...options, + headers: { + accept: 'application/vnd.github+json', + authorization: `Bearer ${token}`, + 'user-agent': 'upstream-radar/compatibility-issue-sync', + 'x-github-api-version': '2022-11-28', + ...(options.headers ?? {}), + }, + }) + const text = await response.text() + if (!response.ok) { + throw new Error(`GitHub API ${options.method ?? 'GET'} ${path} returned ${response.status}: ${text.slice(0, 1_024)}`) + } + return text === '' ? undefined : JSON.parse(text) +} + +async function listManagedCandidates() { + const issues = [] + for (let page = 1; page <= 10; page += 1) { + const batch = await github(`/repos/${repository}/issues?state=all&per_page=100&page=${page}`) + if (!Array.isArray(batch)) throw new Error('GitHub issues response is not an array') + for (const issue of batch) { + if (issue?.pull_request !== undefined) continue + if (!Number.isSafeInteger(issue?.number) || (issue.state !== 'open' && issue.state !== 'closed')) continue + issues.push({ + number: issue.number, + state: issue.state, + title: typeof issue.title === 'string' ? issue.title : '', + body: typeof issue.body === 'string' ? issue.body : '', + }) + } + if (batch.length < 100) break + if (page === 10) throw new Error('compatibility issue reconciliation exceeded 1,000 repository issues') + } + return issues +} + +async function ensureLabel(name, color, description) { + const path = `/repos/${repository}/labels/${encodeURIComponent(name)}` + const response = await fetch(`${apiBase}${path}`, { + headers: { + accept: 'application/vnd.github+json', + authorization: `Bearer ${token}`, + 'user-agent': 'upstream-radar/compatibility-issue-sync', + 'x-github-api-version': '2022-11-28', + }, + }) + if (response.ok) return + if (response.status !== 404) throw new Error(`GitHub label lookup for ${name} returned ${response.status}: ${(await response.text()).slice(0, 1_024)}`) + await github(`/repos/${repository}/labels`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ name, color, description }), + }) +} + +const ledger = JSON.parse(await readFile(ledgerPath, 'utf8')) +const existingIssues = await listManagedCandidates() +const plan = buildDshCompatibilityIssuePlan({ ledger, existingIssues, runUrl }) + +if (plan.actions.some(action => action.kind !== 'close')) { + await ensureLabel(DSH_COMPATIBILITY_ISSUE_LABELS[0], '0969da', 'Managed automatically by Upstream Radar') + await ensureLabel(DSH_COMPATIBILITY_ISSUE_LABELS[1], 'd73a4a', 'A reproduced DSH plugin compatibility incident') +} + +const applied = { create: 0, update: 0, reopen: 0, close: 0 } +for (const action of plan.actions) { + if (action.kind === 'create') { + await github(`/repos/${repository}/issues`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ title: action.title, body: action.body, labels: [...DSH_COMPATIBILITY_ISSUE_LABELS] }), + }) + } else if (action.kind === 'close') { + await github(`/repos/${repository}/issues/${action.issueNumber}/comments`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ body: action.comment }), + }) + await github(`/repos/${repository}/issues/${action.issueNumber}`, { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ state: 'closed', state_reason: 'completed' }), + }) + } else { + await github(`/repos/${repository}/issues/${action.issueNumber}`, { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + title: action.title, + body: action.body, + labels: [...DSH_COMPATIBILITY_ISSUE_LABELS], + ...(action.kind === 'reopen' ? { state: 'open', state_reason: 'reopened' } : {}), + }), + }) + } + applied[action.kind] += 1 +} + +const summary = { + repository, + activeCompatibilityIncidents: plan.openCaseIds.length, + ignoredUnknownCells: plan.ignoredUnknownCaseIds.length, + actions: applied, +} +process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`) + +if (process.env.GITHUB_STEP_SUMMARY !== undefined) { + const lines = [ + '', + '## Upstream Radar — compatibility incident reconciliation', + '', + `- Active compatibility incidents: **${summary.activeCompatibilityIncidents}**`, + `- Created: **${applied.create}**; updated: **${applied.update}**; reopened: **${applied.reopen}**; resolved and closed: **${applied.close}**`, + `- Unknown cells held for observer review: **${summary.ignoredUnknownCells}**`, + '', + ] + await appendFile(process.env.GITHUB_STEP_SUMMARY, lines.join('\n'), 'utf8') +} diff --git a/scripts/write-dsh-install-plan.mjs b/scripts/write-dsh-install-plan.mjs index 64a91ab..5d1d1e7 100644 --- a/scripts/write-dsh-install-plan.mjs +++ b/scripts/write-dsh-install-plan.mjs @@ -4,6 +4,7 @@ import { appendFile, readFile } from 'node:fs/promises' import { resolve } from 'node:path' import process from 'node:process' import { buildDshInstallPlan } from '../dist/src/dsh-install-plan.js' +import { emptyDshCompatibilityLedger } from '../dist/src/dsh-compatibility-ledger.js' const MAX_INPUT_BYTES = 256 * 1024 * 1024 @@ -13,15 +14,25 @@ async function readJson(path) { return JSON.parse(contents) } -const [corpusPath, statePath, reportPath] = process.argv.slice(2) -if (corpusPath === undefined || statePath === undefined || reportPath === undefined) { - throw new Error('usage: write-dsh-install-plan.mjs ') +async function readOptionalJson(path) { + try { + return await readJson(path) + } catch (error) { + if (error?.code === 'ENOENT') return emptyDshCompatibilityLedger() + throw error + } +} + +const [corpusPath, statePath, reportPath, ledgerPath] = process.argv.slice(2) +if (corpusPath === undefined || statePath === undefined || reportPath === undefined || ledgerPath === undefined) { + throw new Error('usage: write-dsh-install-plan.mjs ') } const plan = buildDshInstallPlan( await readJson(corpusPath), await readJson(statePath), await readJson(reportPath), + await readOptionalJson(ledgerPath), ) process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`) diff --git a/src/cli.ts b/src/cli.ts index aa2966c..67fd674 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -533,7 +533,7 @@ Usage: upstream-radar profile-check [profile-directory] [--patch ] [--report ] [--summary] [--json] upstream-radar probe dsh-load [--dsh-version ] [--timeout ] [--keep-profile] [--json] upstream-radar probe dsh-matrix --dsh-version [,,...] [--timeout ] [--keep-profile] [--json] - upstream-radar probe dsh-install [npm:]@ --dsh-version --isolation-provider --execute [--allow-build ]... [--timeout ] [--report ] [--json] + upstream-radar probe dsh-install [npm:]@ --dsh-version [--case-id ] --isolation-provider --execute [--allow-build ]... [--timeout ] [--report ] [--json] upstream-radar review dsh-plugin [npm:]@ --dsh-version ,,... [--json] upstream-radar demo [--json] upstream-radar case dsh-web-ui [--json] @@ -1248,6 +1248,7 @@ async function runDshInstallObservation(args: readonly string[]): Promise { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`) + return value as Record +} + +function boundedString(value: unknown, label: string, maximum: number): string { + if (typeof value !== 'string' || value.trim() === '' || value.length > maximum) { + throw new Error(`${label} must be a non-empty string no longer than ${maximum} characters`) + } + return value +} + +function exactVersion(value: unknown, label: string): string { + const parsed = boundedString(value, label, 256) + if (!EXACT_VERSION.test(parsed)) throw new Error(`${label} must be an exact semantic version`) + return parsed +} + +function boundedInteger(value: unknown, label: string, maximum: number): number { + if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > maximum) { + throw new Error(`${label} must be a bounded non-negative integer`) + } + return value as number +} + +function peerStatus(value: unknown, label: string): DshCompatibilityPeerStatus { + const status = boundedString(value, label, 32) + if (status !== 'satisfied' && status !== 'mismatched' && status !== 'indeterminate' && status !== 'missing') { + throw new Error(`${label} is unsupported`) + } + return status +} + +function peerStaticUsage(value: unknown, label: string): DshCompatibilityPeerContractRelation['staticUsage'] { + const usage = boundedString(value, label, 64) + if (usage !== 'runtime-import-observed' && usage !== 'type-only-reference-observed' + && usage !== 'no-literal-reference-observed' && usage !== 'scan-incomplete') { + throw new Error(`${label} is unsupported`) + } + return usage +} + +function result(value: unknown, label: string): DshCompatibilityLedgerEntry['result'] { + const parsed = boundedString(value, label, 64) + if (parsed !== 'compatible' && parsed !== 'runtime-incompatible' && parsed !== 'peer-contract-incompatible' + && parsed !== 'install-failed' && parsed !== 'load-failed' && parsed !== 'unknown') { + throw new Error(`${label} is unsupported`) + } + return parsed +} + +function packageName(value: unknown, label: string): string { + const name = boundedString(value, label, 214) + try { + const parsed = parseNpmSpec(`${name}@0.0.0`) + if (parsed.name !== name) throw new Error('canonical package name differs') + } catch { + throw new Error(`${label} must be an npm package name`) + } + return name +} + +function exactPackage(value: unknown, label: string): { name: string, version: string } { + const spec = boundedString(value, label, 512) + const parsed = parseNpmSpec(spec) + return { name: parsed.name, version: parsed.version } +} + +function sha256(value: unknown, label: string): string { + const parsed = boundedString(value, label, 64) + if (!SHA256.test(parsed)) throw new Error(`${label} must be a lowercase SHA-256 hex digest`) + return parsed +} + +function stableId(prefix: string, value: unknown): string { + const digest = createHash('sha256').update(JSON.stringify(value)).digest('hex') + return `${prefix}:${digest}` +} + +function relationId(cellId: string, relation: DshCompatibilityPeerContractRelation): string { + return stableId('relation', { + cellId, + name: relation.name, + required: relation.required, + status: relation.status, + staticUsage: relation.staticUsage, + ...(relation.resolvedVersion === undefined ? {} : { resolvedVersion: relation.resolvedVersion }), + }) +} + +function cellId(entry: DshCompatibilityLedgerEntry): string { + return stableId('cell', { + caseId: entry.caseId, + targetId: entry.targetId, + plugin: entry.plugin, + dshVersion: entry.dshVersion, + nodeMajor: entry.runtime.nodeMajor, + platform: entry.runtime.platform, + architecture: entry.runtime.architecture, + staticFingerprint: entry.staticFingerprint, + contractFingerprint: entry.contractFingerprint, + ...(entry.artifact.sha256 === undefined ? {} : { artifactSha256: entry.artifact.sha256 }), + }) +} + +function sortRelations(left: DshCompatibilityIrRelation, right: DshCompatibilityIrRelation): number { + return left.cellId.localeCompare(right.cellId) + || left.dependency.name.localeCompare(right.dependency.name) + || left.dependency.required.localeCompare(right.dependency.required) + || left.id.localeCompare(right.id) +} + +/** + * Build the compatibility IR from already-validated durable evidence. It does + * not fetch, install, or execute any target code. + */ +export function buildDshCompatibilityIR(ledgerInput: DshCompatibilityLedger | unknown): DshCompatibilityIR { + const ledger = parseDshCompatibilityLedger(ledgerInput) + const cells: DshCompatibilityIrCell[] = [] + const relations: DshCompatibilityIrRelation[] = [] + + for (const entry of ledger.entries) { + const plugin = parseNpmSpec(entry.plugin) + const id = cellId(entry) + const runtimeGraph = entry.resolution?.runtimeGraph + const contracts = runtimeGraph?.pluginPeerContracts + cells.push({ + id, + caseId: entry.caseId, + targetId: entry.targetId, + observedAt: entry.observedAt, + result: entry.result, + plugin: { + name: plugin.name, + version: plugin.version, + ...(entry.artifact.sha256 === undefined ? {} : { artifactSha256: entry.artifact.sha256 }), + staticFingerprint: entry.staticFingerprint, + }, + runtime: { + dshVersion: entry.dshVersion, + nodeMajor: entry.runtime.nodeMajor, + nodeVersion: entry.runtime.nodeVersion, + platform: entry.runtime.platform, + architecture: entry.runtime.architecture, + ...(entry.runtime.pnpmVersion === undefined ? {} : { pnpmVersion: entry.runtime.pnpmVersion }), + contractFingerprint: entry.contractFingerprint, + }, + evidence: { + ...(runtimeGraph?.digest === undefined ? {} : { runtimeGraphDigest: runtimeGraph.digest }), + ...(runtimeGraph?.nodes === undefined ? {} : { runtimeGraphNodes: runtimeGraph.nodes }), + ...(runtimeGraph?.edges === undefined ? {} : { runtimeGraphEdges: runtimeGraph.edges }), + ...(runtimeGraph?.unresolved === undefined ? {} : { requiredUnresolved: runtimeGraph.unresolved }), + ...(contracts === undefined ? {} : { declaredPeerContracts: contracts.declared }), + }, + }) + for (const relation of contracts?.relations ?? []) { + relations.push({ + id: relationId(id, relation), + cellId: id, + dependency: { + name: relation.name, + required: relation.required, + status: relation.status, + staticUsage: relation.staticUsage, + ...(relation.resolvedVersion === undefined ? {} : { resolvedVersion: relation.resolvedVersion }), + }, + }) + } + } + cells.sort((left, right) => left.caseId.localeCompare(right.caseId) || left.id.localeCompare(right.id)) + relations.sort(sortRelations) + return { schema: DSH_COMPATIBILITY_IR_SCHEMA, cells, relations } +} + +/** Create the bounded reverse view used when a DSH host dependency changes. */ +export function buildDshCompatibilityReverseIndex(irInput: DshCompatibilityIR | unknown): DshCompatibilityReverseIndex { + const ir = parseDshCompatibilityIR(irInput) + const cells = new Map(ir.cells.map(cell => [cell.id, cell])) + const dependencies = new Map() + for (const relation of ir.relations) { + const cell = cells.get(relation.cellId) + if (cell === undefined) throw new Error(`IR relation ${relation.id} refers to an unknown cell`) + const impacts = dependencies.get(relation.dependency.name) ?? [] + impacts.push({ + relationId: relation.id, + cellId: cell.id, + caseId: cell.caseId, + plugin: `${cell.plugin.name}@${cell.plugin.version}`, + dshVersion: cell.runtime.dshVersion, + nodeMajor: cell.runtime.nodeMajor, + required: relation.dependency.required, + status: relation.dependency.status, + staticUsage: relation.dependency.staticUsage, + ...(relation.dependency.resolvedVersion === undefined ? {} : { resolvedVersion: relation.dependency.resolvedVersion }), + }) + dependencies.set(relation.dependency.name, impacts) + } + return { + schema: DSH_COMPATIBILITY_REVERSE_INDEX_SCHEMA, + dependencies: [...dependencies.entries()] + .map(([name, impacts]) => ({ + name, + impacts: impacts.sort((left, right) => left.plugin.localeCompare(right.plugin) + || left.dshVersion.localeCompare(right.dshVersion) + || left.nodeMajor - right.nodeMajor + || left.caseId.localeCompare(right.caseId) + || left.relationId.localeCompare(right.relationId)), + })) + .sort((left, right) => left.name.localeCompare(right.name)), + } +} + +function parseCell(value: unknown, index: number): DshCompatibilityIrCell { + const item = record(value, `cells[${index}]`) + const plugin = record(item.plugin, `cells[${index}].plugin`) + const runtime = record(item.runtime, `cells[${index}].runtime`) + const evidence = record(item.evidence, `cells[${index}].evidence`) + const parsedPlugin = exactPackage(`${boundedString(plugin.name, `cells[${index}].plugin.name`, 214)}@${exactVersion(plugin.version, `cells[${index}].plugin.version`)}`, `cells[${index}].plugin`) + const artifactSha256 = plugin.artifactSha256 === undefined ? undefined : sha256(plugin.artifactSha256, `cells[${index}].plugin.artifactSha256`) + const observedAt = boundedString(item.observedAt, `cells[${index}].observedAt`, 64) + if (!Number.isFinite(Date.parse(observedAt))) throw new Error(`cells[${index}].observedAt must be an ISO timestamp`) + const nodeMajor = boundedInteger(runtime.nodeMajor, `cells[${index}].runtime.nodeMajor`, 40) + if (nodeMajor < 16) throw new Error(`cells[${index}].runtime.nodeMajor must be a supported Node.js major version`) + const pnpmVersion = runtime.pnpmVersion === undefined ? undefined : exactVersion(runtime.pnpmVersion, `cells[${index}].runtime.pnpmVersion`) + const runtimeGraphDigest = evidence.runtimeGraphDigest === undefined ? undefined : boundedString(evidence.runtimeGraphDigest, `cells[${index}].evidence.runtimeGraphDigest`, 80) + const runtimeGraphNodes = evidence.runtimeGraphNodes === undefined ? undefined : boundedInteger(evidence.runtimeGraphNodes, `cells[${index}].evidence.runtimeGraphNodes`, 100_000) + const runtimeGraphEdges = evidence.runtimeGraphEdges === undefined ? undefined : boundedInteger(evidence.runtimeGraphEdges, `cells[${index}].evidence.runtimeGraphEdges`, 250_000) + const requiredUnresolved = evidence.requiredUnresolved === undefined ? undefined : boundedInteger(evidence.requiredUnresolved, `cells[${index}].evidence.requiredUnresolved`, 250_000) + const declaredPeerContracts = evidence.declaredPeerContracts === undefined ? undefined : boundedInteger(evidence.declaredPeerContracts, `cells[${index}].evidence.declaredPeerContracts`, MAX_RELATIONS_PER_CELL) + const cell: DshCompatibilityIrCell = { + id: boundedString(item.id, `cells[${index}].id`, 80), + caseId: boundedString(item.caseId, `cells[${index}].caseId`, 64), + targetId: boundedString(item.targetId, `cells[${index}].targetId`, 64), + observedAt, + result: result(item.result, `cells[${index}].result`), + plugin: { + name: parsedPlugin.name, + version: parsedPlugin.version, + ...(artifactSha256 === undefined ? {} : { artifactSha256 }), + staticFingerprint: boundedString(plugin.staticFingerprint, `cells[${index}].plugin.staticFingerprint`, 80), + }, + runtime: { + dshVersion: exactVersion(runtime.dshVersion, `cells[${index}].runtime.dshVersion`), + nodeMajor, + nodeVersion: exactVersion(runtime.nodeVersion, `cells[${index}].runtime.nodeVersion`), + platform: boundedString(runtime.platform, `cells[${index}].runtime.platform`, 64), + architecture: boundedString(runtime.architecture, `cells[${index}].runtime.architecture`, 64), + ...(pnpmVersion === undefined ? {} : { pnpmVersion }), + contractFingerprint: boundedString(runtime.contractFingerprint, `cells[${index}].runtime.contractFingerprint`, 80), + }, + evidence: { + ...(runtimeGraphDigest === undefined ? {} : { runtimeGraphDigest }), + ...(runtimeGraphNodes === undefined ? {} : { runtimeGraphNodes }), + ...(runtimeGraphEdges === undefined ? {} : { runtimeGraphEdges }), + ...(requiredUnresolved === undefined ? {} : { requiredUnresolved }), + ...(declaredPeerContracts === undefined ? {} : { declaredPeerContracts }), + }, + } + return cell +} + +function parseRelation(value: unknown, index: number): DshCompatibilityIrRelation { + const item = record(value, `relations[${index}]`) + const dependency = record(item.dependency, `relations[${index}].dependency`) + const status = peerStatus(dependency.status, `relations[${index}].dependency.status`) + const resolvedVersion = dependency.resolvedVersion === undefined + ? undefined + : exactVersion(dependency.resolvedVersion, `relations[${index}].dependency.resolvedVersion`) + if ((status === 'satisfied' || status === 'mismatched') && resolvedVersion === undefined) { + throw new Error(`relations[${index}].dependency requires a resolvedVersion for ${status}`) + } + if (status === 'missing' && resolvedVersion !== undefined) { + throw new Error(`relations[${index}].dependency.resolvedVersion must be absent for a missing peer`) + } + return { + id: boundedString(item.id, `relations[${index}].id`, 80), + cellId: boundedString(item.cellId, `relations[${index}].cellId`, 80), + dependency: { + name: packageName(dependency.name, `relations[${index}].dependency.name`), + required: boundedString(dependency.required, `relations[${index}].dependency.required`, 512), + status, + staticUsage: peerStaticUsage(dependency.staticUsage, `relations[${index}].dependency.staticUsage`), + ...(resolvedVersion === undefined ? {} : { resolvedVersion }), + }, + } +} + +/** Validate a persisted compatibility IR before using it for impact routing. */ +export function parseDshCompatibilityIR(input: unknown): DshCompatibilityIR { + const root = record(input, 'DSH compatibility IR') + if (root.schema !== DSH_COMPATIBILITY_IR_SCHEMA) throw new Error(`DSH compatibility IR schema must be ${DSH_COMPATIBILITY_IR_SCHEMA}`) + if (!Array.isArray(root.cells) || root.cells.length > MAX_CELLS) throw new Error(`DSH compatibility IR cells must contain at most ${MAX_CELLS} entries`) + if (!Array.isArray(root.relations) || root.relations.length > MAX_RELATIONS) throw new Error(`DSH compatibility IR relations must contain at most ${MAX_RELATIONS} entries`) + const cells = root.cells.map(parseCell) + const cellIds = new Set() + for (const cell of cells) { + if (cellIds.has(cell.id)) throw new Error(`DSH compatibility IR has a duplicate cell id: ${cell.id}`) + cellIds.add(cell.id) + } + const relations = root.relations.map(parseRelation) + const relationIds = new Set() + const relationCounts = new Map() + for (const relation of relations) { + if (relationIds.has(relation.id)) throw new Error(`DSH compatibility IR has a duplicate relation id: ${relation.id}`) + if (!cellIds.has(relation.cellId)) throw new Error(`DSH compatibility IR relation ${relation.id} references an unknown cell`) + relationIds.add(relation.id) + relationCounts.set(relation.cellId, (relationCounts.get(relation.cellId) ?? 0) + 1) + } + for (const cell of cells) { + const count = relationCounts.get(cell.id) ?? 0 + if (count > MAX_RELATIONS_PER_CELL) throw new Error(`DSH compatibility IR cell ${cell.caseId} exceeds ${MAX_RELATIONS_PER_CELL} relations`) + if (cell.evidence.declaredPeerContracts !== undefined && count !== cell.evidence.declaredPeerContracts) { + throw new Error(`DSH compatibility IR cell ${cell.caseId} relation count does not match declared peer contracts`) + } + } + cells.sort((left, right) => left.caseId.localeCompare(right.caseId) || left.id.localeCompare(right.id)) + relations.sort(sortRelations) + return { schema: DSH_COMPATIBILITY_IR_SCHEMA, cells, relations } +} + +/** Validate the materialized reverse index before an observer consumes it. */ +export function parseDshCompatibilityReverseIndex(input: unknown): DshCompatibilityReverseIndex { + const root = record(input, 'DSH compatibility reverse index') + if (root.schema !== DSH_COMPATIBILITY_REVERSE_INDEX_SCHEMA) { + throw new Error(`DSH compatibility reverse index schema must be ${DSH_COMPATIBILITY_REVERSE_INDEX_SCHEMA}`) + } + if (!Array.isArray(root.dependencies) || root.dependencies.length > MAX_RELATIONS) { + throw new Error(`DSH compatibility reverse index dependencies must contain at most ${MAX_RELATIONS} entries`) + } + const seenDependencies = new Set() + const dependencies = root.dependencies.map((value, index): DshCompatibilityReverseDependency => { + const dependency = record(value, `dependencies[${index}]`) + const name = packageName(dependency.name, `dependencies[${index}].name`) + if (seenDependencies.has(name)) throw new Error(`DSH compatibility reverse index has duplicate dependency ${name}`) + seenDependencies.add(name) + if (!Array.isArray(dependency.impacts) || dependency.impacts.length === 0 || dependency.impacts.length > MAX_RELATIONS) { + throw new Error(`dependencies[${index}].impacts must contain between 1 and ${MAX_RELATIONS} entries`) + } + const seenRelations = new Set() + const impacts = dependency.impacts.map((valueToParse, impactIndex): DshCompatibilityReverseImpact => { + const impact = record(valueToParse, `dependencies[${index}].impacts[${impactIndex}]`) + const relationId = boundedString(impact.relationId, `dependencies[${index}].impacts[${impactIndex}].relationId`, 80) + if (seenRelations.has(relationId)) throw new Error(`dependencies[${index}].impacts has duplicate relation ${relationId}`) + seenRelations.add(relationId) + const resolvedVersion = impact.resolvedVersion === undefined + ? undefined + : exactVersion(impact.resolvedVersion, `dependencies[${index}].impacts[${impactIndex}].resolvedVersion`) + const status = peerStatus(impact.status, `dependencies[${index}].impacts[${impactIndex}].status`) + if ((status === 'satisfied' || status === 'mismatched') && resolvedVersion === undefined) { + throw new Error(`dependencies[${index}].impacts[${impactIndex}] requires a resolvedVersion for ${status}`) + } + if (status === 'missing' && resolvedVersion !== undefined) { + throw new Error(`dependencies[${index}].impacts[${impactIndex}].resolvedVersion must be absent for a missing peer`) + } + return { + relationId, + cellId: boundedString(impact.cellId, `dependencies[${index}].impacts[${impactIndex}].cellId`, 80), + caseId: boundedString(impact.caseId, `dependencies[${index}].impacts[${impactIndex}].caseId`, 64), + plugin: boundedString(impact.plugin, `dependencies[${index}].impacts[${impactIndex}].plugin`, 512), + dshVersion: exactVersion(impact.dshVersion, `dependencies[${index}].impacts[${impactIndex}].dshVersion`), + nodeMajor: boundedInteger(impact.nodeMajor, `dependencies[${index}].impacts[${impactIndex}].nodeMajor`, 40), + required: boundedString(impact.required, `dependencies[${index}].impacts[${impactIndex}].required`, 512), + status, + staticUsage: peerStaticUsage(impact.staticUsage, `dependencies[${index}].impacts[${impactIndex}].staticUsage`), + ...(resolvedVersion === undefined ? {} : { resolvedVersion }), + } + }) + return { name, impacts: impacts.sort((left, right) => left.relationId.localeCompare(right.relationId)) } + }) + dependencies.sort((left, right) => left.name.localeCompare(right.name)) + return { schema: DSH_COMPATIBILITY_REVERSE_INDEX_SCHEMA, dependencies } +} diff --git a/src/dsh-compatibility-issues.ts b/src/dsh-compatibility-issues.ts new file mode 100644 index 0000000..899c997 --- /dev/null +++ b/src/dsh-compatibility-issues.ts @@ -0,0 +1,241 @@ +import { + parseDshCompatibilityLedger, + type DshCompatibilityLedgerEntry, +} from './dsh-compatibility-ledger.js' + +const CASE_MARKER = // +const ACTIONABLE_RESULTS = new Set([ + 'runtime-incompatible', + 'peer-contract-incompatible', + 'install-failed', + 'load-failed', +]) + +export const DSH_COMPATIBILITY_ISSUE_LABELS = ['upstream-radar', 'dsh-compatibility'] as const + +export interface DshCompatibilityExistingIssue { + number: number + state: 'open' | 'closed' + title: string + body: string +} + +export type DshCompatibilityIssueAction = + | { + kind: 'create' + caseId: string + title: string + body: string + } + | { + kind: 'update' | 'reopen' + caseId: string + issueNumber: number + title: string + body: string + } + | { + kind: 'close' + caseId: string + issueNumber: number + comment: string + } + +export interface DshCompatibilityIssuePlan { + actions: DshCompatibilityIssueAction[] + openCaseIds: string[] + ignoredUnknownCaseIds: string[] +} + +function inline(value: string, maximum = 1_024): string { + return value + .replace(/[\u0000-\u001f\u007f`|<>]/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .slice(0, maximum) +} + +function runLink(runUrl: string | undefined): string { + if (runUrl === undefined) return 'not recorded' + try { + const parsed = new URL(runUrl) + if (parsed.protocol !== 'https:') return 'not recorded' + return `[GitHub Actions evidence](${parsed.toString()})` + } catch { + return 'not recorded' + } +} + +export function dshCompatibilityIssueMarker(caseId: string): string { + if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(caseId)) throw new Error(`invalid DSH compatibility case id: ${caseId}`) + return `` +} + +export function dshCompatibilityIssueCaseId(body: string): string | undefined { + return CASE_MARKER.exec(body)?.[1] +} + +function repairPath(entry: DshCompatibilityLedgerEntry): string[] { + if (entry.result === 'runtime-incompatible') { + return [ + 'Confirm which Node.js majors the plugin and DSH release are intended to support.', + 'Align the published `engines.node` contract or add a tested runtime cell that satisfies it.', + ] + } + if (entry.result === 'peer-contract-incompatible') { + return [ + 'Align required DSH/React peer ranges with the versions the final DSH profile actually provides.', + 'Re-run the same plugin, DSH and Node cell; a successful headless boot alone is not enough when a required peer remains missing or mismatched.', + ] + } + if (entry.result === 'install-failed') { + return [ + 'Reproduce the package-manager failure with the exact artifact and approved dependency-build policy below.', + 'Publish a corrected artifact or narrow the supported DSH/runtime contract, then let Radar perform the next clean install.', + ] + } + return [ + 'Reproduce the DSH registration/import/boot failure with the exact coordinates below.', + 'Fix the runtime integration or declare the unsupported DSH boundary, then let Radar perform the next clean load.', + ] +} + +function peerEvidence(entry: DshCompatibilityLedgerEntry): string[] { + const contracts = entry.resolution?.runtimeGraph?.pluginPeerContracts + if (contracts === undefined) return [] + const lines = [ + '', + '### Direct host contracts', + '', + `- Declared: **${contracts.declared}**; satisfied: **${contracts.satisfied}**; mismatched: **${contracts.mismatched}**; missing: **${contracts.missing}**; indeterminate: **${contracts.indeterminate}**.`, + ] + for (const issue of contracts.issues ?? []) { + lines.push(`- \`${inline(issue.name, 214)}\`: ${inline(issue.status, 32)}; requires \`${inline(issue.required, 512)}\`${issue.resolvedVersion === undefined ? '' : `, resolved \`${inline(issue.resolvedVersion, 256)}\``}; static use: \`${inline(issue.staticUsage, 64)}\`.`) + } + return lines +} + +export function renderDshCompatibilityIssue( + entry: DshCompatibilityLedgerEntry, + runUrl?: string, +): { title: string, body: string } { + const title = `[DSH compatibility] ${inline(entry.plugin, 160)} on DSH ${inline(entry.dshVersion, 64)} / Node ${entry.runtime.nodeMajor}: ${entry.result}`.slice(0, 256) + const graph = entry.resolution?.runtimeGraph + const lockfile = entry.resolution?.profileLockfile + const lines = [ + dshCompatibilityIssueMarker(entry.caseId), + '', + 'Upstream Radar reproduced an incompatibility in a disposable GitHub-hosted environment. This is an internal compatibility incident; forward it to an upstream author only after reviewing the attached evidence.', + '', + '### Exact cell', + '', + `- Plugin: \`${inline(entry.plugin, 512)}\``, + `- DSH: \`@deepseek-ai/dsh@${inline(entry.dshVersion, 128)}\``, + `- Runtime: Node \`${inline(entry.runtime.nodeVersion, 64)}\` on \`${inline(`${entry.runtime.platform}/${entry.runtime.architecture}`, 128)}\``, + `- Result: \`${entry.result}\``, + `- Observed: \`${inline(entry.observedAt, 64)}\``, + `- Artifact: ${entry.artifact.sha256 === undefined ? 'digest unavailable' : `\`sha256:${inline(entry.artifact.sha256, 64)}\``}`, + `- Evidence run: ${runLink(runUrl)}`, + '', + '### Reproduced failure', + '', + inline(entry.reason, 4_096), + ] + if (graph !== undefined || lockfile !== undefined) { + lines.push( + '', + '### Dependency evidence', + '', + `- Final profile graph: ${lockfile === undefined ? 'not established' : `${lockfile.nodes} node(s), ${lockfile.edges} edge(s), ${lockfile.unresolved} unresolved edge(s)`}.`, + `- Effective runtime graph: ${graph === undefined ? 'not established' : `${graph.nodes} node(s), ${graph.edges} edge(s), ${graph.unresolved} required unresolved edge(s)`}.`, + ) + } + lines.push(...peerEvidence(entry), '', '### Repair path', '') + for (const item of repairPath(entry)) lines.push(`- ${item}`) + lines.push( + '', + 'This issue is managed by Upstream Radar. A later compatible observation for the same maintained cell will add a resolution note and close it automatically.', + '', + '_Bounded compatibility evidence is not a claim that third-party code is safe or malicious._', + '', + ) + return { title, body: lines.join('\n').slice(0, 60_000) } +} + +export function renderDshCompatibilityResolution(entry: DshCompatibilityLedgerEntry, runUrl?: string): string { + return [ + ``, + '', + 'Upstream Radar re-ran this maintained cell in a fresh isolated environment and the exact package now installs, registers and loads with a complete required host contract.', + '', + `- Plugin: \`${inline(entry.plugin, 512)}\``, + `- DSH: \`@deepseek-ai/dsh@${inline(entry.dshVersion, 128)}\``, + `- Runtime: Node \`${inline(entry.runtime.nodeVersion, 64)}\``, + `- Observed: \`${inline(entry.observedAt, 64)}\``, + `- Evidence run: ${runLink(runUrl)}`, + '', + 'Closing this compatibility incident as verified resolved.', + ].join('\n') +} + +function existingByCase(issues: readonly DshCompatibilityExistingIssue[]): Map { + const result = new Map() + for (const issue of issues) { + if (!Number.isSafeInteger(issue.number) || issue.number < 1) throw new Error('existing compatibility issue has an invalid number') + if (issue.state !== 'open' && issue.state !== 'closed') throw new Error(`issue #${issue.number} has an invalid state`) + const caseId = dshCompatibilityIssueCaseId(issue.body) + if (caseId === undefined) continue + const duplicate = result.get(caseId) + if (duplicate !== undefined) throw new Error(`duplicate managed compatibility issues for ${caseId}: #${duplicate.number} and #${issue.number}`) + result.set(caseId, issue) + } + return result +} + +/** Build an idempotent desired-state plan for issues in Radar's own repository. */ +export function buildDshCompatibilityIssuePlan(input: { + ledger: unknown + existingIssues: readonly DshCompatibilityExistingIssue[] + runUrl?: string +}): DshCompatibilityIssuePlan { + const ledger = parseDshCompatibilityLedger(input.ledger) + const existing = existingByCase(input.existingIssues) + const actions: DshCompatibilityIssueAction[] = [] + const openCaseIds: string[] = [] + const ignoredUnknownCaseIds: string[] = [] + + for (const entry of ledger.entries) { + const issue = existing.get(entry.caseId) + if (entry.result === 'unknown') { + ignoredUnknownCaseIds.push(entry.caseId) + continue + } + if (entry.result === 'compatible') { + if (issue?.state === 'open') { + actions.push({ + kind: 'close', + caseId: entry.caseId, + issueNumber: issue.number, + comment: renderDshCompatibilityResolution(entry, input.runUrl), + }) + } + continue + } + if (!ACTIONABLE_RESULTS.has(entry.result)) continue + openCaseIds.push(entry.caseId) + const rendered = renderDshCompatibilityIssue(entry, input.runUrl) + if (issue === undefined) { + actions.push({ kind: 'create', caseId: entry.caseId, ...rendered }) + } else if (issue.state === 'closed') { + actions.push({ kind: 'reopen', caseId: entry.caseId, issueNumber: issue.number, ...rendered }) + } else if (issue.title !== rendered.title || issue.body !== rendered.body) { + actions.push({ kind: 'update', caseId: entry.caseId, issueNumber: issue.number, ...rendered }) + } + } + + return { + actions, + openCaseIds: openCaseIds.sort(), + ignoredUnknownCaseIds: ignoredUnknownCaseIds.sort(), + } +} diff --git a/src/dsh-compatibility-ledger.ts b/src/dsh-compatibility-ledger.ts new file mode 100644 index 0000000..3cf01bf --- /dev/null +++ b/src/dsh-compatibility-ledger.ts @@ -0,0 +1,791 @@ +import { createHash } from 'node:crypto' +import { parseNpmSpec } from './npm.js' +import type { DshInstallObservationResult, DshInstallPeerStaticUsage } from './dsh-install-observation.js' + +/** + * Durable, current-state evidence for the exact plugin/DSH/runtime cells that + * Radar promises to keep checking. This is deliberately a ledger, not a test + * history database: one entry represents the latest trustworthy observation + * for one active cell. + */ +export const DSH_COMPATIBILITY_LEDGER_SCHEMA = 'upstream-radar.dsh-compatibility-ledger/v1alpha1' as const + +const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/ +const CASE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/ +const FINGERPRINT = /^sha256:[a-f0-9]{64}$/ +const RESULTS = new Set(['compatible', 'runtime-incompatible', 'peer-contract-incompatible', 'install-failed', 'load-failed', 'unknown']) +const MAX_ENTRIES = 500 +const MAX_REPORTS = 100 + +export interface DshCompatibilityProfileLockfile { + sha256: string + bytes: number + graphDigest?: string + nodes?: number + edges?: number + unresolved?: number + unresolvedDependencies?: DshCompatibilityProfileGraphGap[] +} + +export interface DshCompatibilityProfileGraphGap { + from: string + name: string + spec: string + kind: 'runtime' | 'development' | 'optional' | 'peer' | 'host-runtime' +} + +export interface DshCompatibilityRuntimeGraph { + digest: string + nodes: number + edges: number + /** Required runtime/peer gaps; optional platform variants are kept separately. */ + unresolved: number + unresolvedDependencies?: DshCompatibilityProfileGraphGap[] + optionalUnavailable?: number + optionalUnavailableDependencies?: DshCompatibilityProfileGraphGap[] + pluginPeerContracts?: DshCompatibilityPluginPeerContracts + hostRuntime?: { + source: 'dsh-profile-fallback' | 'dsh-process' + resolvedNodes: number + dshVersion?: string + } +} + +export interface DshCompatibilityPeerContractIssue { + name: string + required: string + status: 'mismatched' | 'indeterminate' | 'missing' + staticUsage: DshInstallPeerStaticUsage + resolvedVersion?: string +} + +/** One exact plugin peer relation, kept for the reverse compatibility index. */ +export interface DshCompatibilityPeerContractRelation { + name: string + required: string + status: 'satisfied' | 'mismatched' | 'indeterminate' | 'missing' + staticUsage: DshInstallPeerStaticUsage + resolvedVersion?: string +} + +export interface DshCompatibilityPluginPeerContracts { + declared: number + satisfied: number + mismatched: number + indeterminate: number + missing: number + relations: DshCompatibilityPeerContractRelation[] + issues?: DshCompatibilityPeerContractIssue[] +} + +export interface DshCompatibilityLedgerEntry { + caseId: string + targetId: string + plugin: string + dshVersion: string + runtime: { + nodeMajor: number + nodeVersion: string + platform: string + architecture: string + pnpmVersion?: string + } + staticFingerprint: string + contractFingerprint: string + observedAt: string + result: DshInstallObservationResult + reason: string + artifact: { + lifecycleScripts: string[] + sha256?: string + integrity?: string + nodeEngine?: string + } + resolution?: { + profileLockfile?: DshCompatibilityProfileLockfile + runtimeGraph?: DshCompatibilityRuntimeGraph + } + observer: { + schema: string + version: string + } +} + +export interface DshCompatibilityLedger { + schema: typeof DSH_COMPATIBILITY_LEDGER_SCHEMA + entries: DshCompatibilityLedgerEntry[] +} + +/** One matrix entry sent from the static reconciler to the isolated runner. */ +export interface DshCompatibilityExpectedCase { + id: string + targetId: string + plugin: string + dshVersion: string + nodeMajor: number + allowedBuilds: string + staticFingerprint: string + contractFingerprint: string + reasons: string[] +} + +export type DshCompatibilityTransitionStatus = + | 'compatible' + | 'artifact-drift' + | 'resolution-drift' + | 'new-incompatibility' + | 'changed-incompatibility' + | 'resolved-incompatibility' + | 'persisting-incompatibility' + +export interface DshCompatibilityTransition { + caseId: string + status: DshCompatibilityTransitionStatus + result: DshInstallObservationResult + reason: string + previousResult?: DshInstallObservationResult +} + +export interface DshCompatibilityLedgerMerge { + ledger: DshCompatibilityLedger + transitions: DshCompatibilityTransition[] + acceptedCaseIds: string[] + missingCaseIds: string[] + rejectedReports: string[] +} + +function record(value: unknown, label: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`) + return value as Record +} + +function boundedString(value: unknown, label: string, maximum: number): string { + if (typeof value !== 'string' || value.trim() === '' || value.length > maximum) { + throw new Error(`${label} must be a non-empty string no longer than ${maximum} characters`) + } + return value +} + +function optionalBoundedString(value: unknown, label: string, maximum: number): string | undefined { + return value === undefined ? undefined : boundedString(value, label, maximum) +} + +function exactVersion(value: unknown, label: string): string { + const parsed = boundedString(value, label, 256) + if (!EXACT_VERSION.test(parsed)) throw new Error(`${label} must be an exact semantic version`) + return parsed +} + +function caseId(value: unknown, label: string): string { + const parsed = boundedString(value, label, 64) + if (!CASE_ID.test(parsed)) throw new Error(`${label} must be a short lowercase label`) + return parsed +} + +function exactSpec(value: unknown, label: string): string { + const raw = boundedString(value, label, 512) + const parsed = parseNpmSpec(raw) + if (!EXACT_VERSION.test(parsed.version)) throw new Error(`${label} must be an exact npm package coordinate`) + return `${parsed.name}@${parsed.version}` +} + +function nodeMajor(value: unknown, label: string): number { + if (!Number.isSafeInteger(value) || (value as number) < 16 || (value as number) > 40) { + throw new Error(`${label} must be a supported Node.js major version`) + } + return value as number +} + +function positiveInteger(value: unknown, label: string, maximum = Number.MAX_SAFE_INTEGER): number { + if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > maximum) { + throw new Error(`${label} must be a bounded non-negative integer`) + } + return value as number +} + +function peerStaticUsage(value: unknown, label: string): DshInstallPeerStaticUsage { + const usage = boundedString(value, label, 64) + if (usage !== 'runtime-import-observed' && usage !== 'type-only-reference-observed' + && usage !== 'no-literal-reference-observed' && usage !== 'scan-incomplete') { + throw new Error(`${label} is unsupported`) + } + return usage +} + +function optionalDigest(value: unknown, label: string): string | undefined { + if (value === undefined) return undefined + const digest = boundedString(value, label, 128) + if (!/^sha256:[a-f0-9]{64}$/.test(digest)) throw new Error(`${label} must be a sha256 digest`) + return digest +} + +function optionalBareSha256(value: unknown, label: string): string | undefined { + if (value === undefined) return undefined + const digest = boundedString(value, label, 64) + if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error(`${label} must be a SHA-256 hex digest`) + return digest +} + +function parseLifecycleScripts(value: unknown, label: string): string[] { + if (!Array.isArray(value) || value.length > 4) throw new Error(`${label} must be an array of at most four lifecycle script names`) + const allowed = new Set(['preinstall', 'install', 'postinstall', 'prepare']) + const scripts = value.map((item, index) => boundedString(item, `${label}[${index}]`, 32)) + if (scripts.some(script => !allowed.has(script))) throw new Error(`${label} contains an unsupported lifecycle script name`) + if (new Set(scripts).size !== scripts.length) throw new Error(`${label} must not contain duplicate lifecycle script names`) + return scripts.sort() +} + +function parseGraphGaps( + item: Record, + label: string, + count: number | undefined, + field = 'unresolvedDependencies', +): DshCompatibilityProfileGraphGap[] | undefined { + const rawGaps = item[field] + if (rawGaps !== undefined && (!Array.isArray(rawGaps) || rawGaps.length > 32)) { + throw new Error(`${label}.${field} must be an array of at most 32 graph gaps`) + } + const unresolvedDependencies = rawGaps?.map((value, index): DshCompatibilityProfileGraphGap => { + const gap = record(value, `${label}.${field}[${index}]`) + const kind = boundedString(gap.kind, `${label}.${field}[${index}].kind`, 32) + if (!['runtime', 'development', 'optional', 'peer', 'host-runtime'].includes(kind)) { + throw new Error(`${label}.${field}[${index}].kind is unsupported`) + } + return { + from: boundedString(gap.from, `${label}.${field}[${index}].from`, 512), + name: boundedString(gap.name, `${label}.${field}[${index}].name`, 214), + spec: boundedString(gap.spec, `${label}.${field}[${index}].spec`, 512), + kind: kind as DshCompatibilityProfileGraphGap['kind'], + } + }) + if (unresolvedDependencies !== undefined && count !== undefined && unresolvedDependencies.length > count) { + throw new Error(`${label}.${field} cannot exceed its reported count`) + } + return unresolvedDependencies +} + +function parsePluginPeerContracts(value: unknown, label: string): DshCompatibilityPluginPeerContracts | undefined { + if (value === undefined) return undefined + const item = record(value, label) + const declared = positiveInteger(item.declared, `${label}.declared`, 100_000) + const satisfied = positiveInteger(item.satisfied, `${label}.satisfied`, 100_000) + const mismatched = positiveInteger(item.mismatched, `${label}.mismatched`, 100_000) + const indeterminate = positiveInteger(item.indeterminate, `${label}.indeterminate`, 100_000) + const missing = positiveInteger(item.missing, `${label}.missing`, 100_000) + if (satisfied + mismatched + indeterminate + missing !== declared) { + throw new Error(`${label} counts must add up to declared`) + } + const rawRelations = item.relations + if (!Array.isArray(rawRelations) || rawRelations.length > 64 || rawRelations.length !== declared) { + throw new Error(`${label}.relations must contain every declared peer, up to 64 entries`) + } + const relations = rawRelations.map((value, index): DshCompatibilityPeerContractRelation => { + const relation = record(value, `${label}.relations[${index}]`) + const status = boundedString(relation.status, `${label}.relations[${index}].status`, 32) + if (status !== 'satisfied' && status !== 'mismatched' && status !== 'indeterminate' && status !== 'missing') { + throw new Error(`${label}.relations[${index}].status is unsupported`) + } + const resolvedVersion = relation.resolvedVersion === undefined + ? undefined + : boundedString(relation.resolvedVersion, `${label}.relations[${index}].resolvedVersion`, 256) + if ((status === 'satisfied' || status === 'mismatched') && resolvedVersion === undefined) { + throw new Error(`${label}.relations[${index}] requires a resolvedVersion`) + } + if (status === 'missing' && resolvedVersion !== undefined) { + throw new Error(`${label}.relations[${index}].resolvedVersion must be absent for a missing peer`) + } + return { + name: boundedString(relation.name, `${label}.relations[${index}].name`, 214), + required: boundedString(relation.required, `${label}.relations[${index}].required`, 512), + status, + staticUsage: peerStaticUsage(relation.staticUsage, `${label}.relations[${index}].staticUsage`), + ...(resolvedVersion === undefined ? {} : { resolvedVersion }), + } + }).sort((left, right) => left.name.localeCompare(right.name)) + if (new Set(relations.map(relation => relation.name)).size !== relations.length) { + throw new Error(`${label}.relations must not contain duplicate peer names`) + } + const relationCounts = { + satisfied: relations.filter(relation => relation.status === 'satisfied').length, + mismatched: relations.filter(relation => relation.status === 'mismatched').length, + indeterminate: relations.filter(relation => relation.status === 'indeterminate').length, + missing: relations.filter(relation => relation.status === 'missing').length, + } + if (relationCounts.satisfied !== satisfied || relationCounts.mismatched !== mismatched + || relationCounts.indeterminate !== indeterminate || relationCounts.missing !== missing) { + throw new Error(`${label}.relations do not match the reported peer contract counts`) + } + const rawIssues = item.issues + if (rawIssues !== undefined && (!Array.isArray(rawIssues) || rawIssues.length > 32)) { + throw new Error(`${label}.issues must be an array of at most 32 peer contract issues`) + } + const issues = rawIssues?.map((value, index): DshCompatibilityPeerContractIssue => { + const issue = record(value, `${label}.issues[${index}]`) + const status = boundedString(issue.status, `${label}.issues[${index}].status`, 32) + if (status !== 'mismatched' && status !== 'indeterminate' && status !== 'missing') { + throw new Error(`${label}.issues[${index}].status is unsupported`) + } + const resolvedVersion = issue.resolvedVersion === undefined + ? undefined + : boundedString(issue.resolvedVersion, `${label}.issues[${index}].resolvedVersion`, 256) + if (status === 'missing' && resolvedVersion !== undefined) { + throw new Error(`${label}.issues[${index}].resolvedVersion must be absent for a missing peer`) + } + return { + name: boundedString(issue.name, `${label}.issues[${index}].name`, 214), + required: boundedString(issue.required, `${label}.issues[${index}].required`, 512), + status, + staticUsage: peerStaticUsage(issue.staticUsage, `${label}.issues[${index}].staticUsage`), + ...(resolvedVersion === undefined ? {} : { resolvedVersion }), + } + }) + if (issues !== undefined && issues.length > mismatched + indeterminate + missing) { + throw new Error(`${label}.issues cannot exceed the number of non-satisfied peers`) + } + return { + declared, + satisfied, + mismatched, + indeterminate, + missing, + relations, + ...(issues === undefined ? {} : { issues }), + } +} + +function parseProfileLockfile(value: unknown, label: string): DshCompatibilityProfileLockfile | undefined { + if (value === undefined) return undefined + const item = record(value, label) + const sha256 = optionalBareSha256(item.sha256, `${label}.sha256`) + if (sha256 === undefined) throw new Error(`${label}.sha256 is required`) + const graphDigest = optionalDigest(item.graphDigest, `${label}.graphDigest`) + const nodes = item.nodes === undefined ? undefined : positiveInteger(item.nodes, `${label}.nodes`, 100_000) + const edges = item.edges === undefined ? undefined : positiveInteger(item.edges, `${label}.edges`, 250_000) + const unresolved = item.unresolved === undefined ? undefined : positiveInteger(item.unresolved, `${label}.unresolved`, 250_000) + const unresolvedDependencies = parseGraphGaps(item, label, unresolved) + return { + sha256, + bytes: positiveInteger(item.bytes, `${label}.bytes`, 64 * 1024 * 1024), + ...(graphDigest === undefined ? {} : { graphDigest }), + ...(nodes === undefined ? {} : { nodes }), + ...(edges === undefined ? {} : { edges }), + ...(unresolved === undefined ? {} : { unresolved }), + ...(unresolvedDependencies === undefined ? {} : { unresolvedDependencies }), + } +} + +function parseRuntimeGraph(value: unknown, label: string): DshCompatibilityRuntimeGraph | undefined { + if (value === undefined) return undefined + const item = record(value, label) + const digest = optionalDigest(item.digest, `${label}.digest`) + if (digest === undefined) throw new Error(`${label}.digest is required`) + const unresolved = positiveInteger(item.unresolved, `${label}.unresolved`, 250_000) + const hostRuntime = item.hostRuntime === undefined ? undefined : record(item.hostRuntime, `${label}.hostRuntime`) + let parsedHostRuntime: DshCompatibilityRuntimeGraph['hostRuntime'] + if (hostRuntime !== undefined) { + const source = boundedString(hostRuntime.source, `${label}.hostRuntime.source`, 64) + if (source !== 'dsh-profile-fallback' && source !== 'dsh-process') { + throw new Error(`${label}.hostRuntime.source is unsupported`) + } + const dshVersion = hostRuntime.dshVersion === undefined + ? undefined + : exactVersion(hostRuntime.dshVersion, `${label}.hostRuntime.dshVersion`) + parsedHostRuntime = { + source, + resolvedNodes: positiveInteger(hostRuntime.resolvedNodes, `${label}.hostRuntime.resolvedNodes`, 100_000), + ...(dshVersion === undefined ? {} : { dshVersion }), + } + } + const unresolvedDependencies = parseGraphGaps(item, label, unresolved) + const optionalUnavailable = item.optionalUnavailable === undefined + ? undefined + : positiveInteger(item.optionalUnavailable, `${label}.optionalUnavailable`, 250_000) + const optionalUnavailableDependencies = parseGraphGaps( + item, + label, + optionalUnavailable, + 'optionalUnavailableDependencies', + ) + const pluginPeerContracts = parsePluginPeerContracts(item.pluginPeerContracts, `${label}.pluginPeerContracts`) + return { + digest, + nodes: positiveInteger(item.nodes, `${label}.nodes`, 100_000), + edges: positiveInteger(item.edges, `${label}.edges`, 250_000), + unresolved, + ...(unresolvedDependencies === undefined ? {} : { unresolvedDependencies }), + ...(optionalUnavailable === undefined ? {} : { optionalUnavailable }), + ...(optionalUnavailableDependencies === undefined ? {} : { optionalUnavailableDependencies }), + ...(pluginPeerContracts === undefined ? {} : { pluginPeerContracts }), + ...(parsedHostRuntime === undefined ? {} : { hostRuntime: parsedHostRuntime }), + } +} + +function parseEntry(value: unknown, index: number): DshCompatibilityLedgerEntry { + const item = record(value, `entries[${index}]`) + const runtime = record(item.runtime, `entries[${index}].runtime`) + const artifact = record(item.artifact, `entries[${index}].artifact`) + const observer = record(item.observer, `entries[${index}].observer`) + const result = boundedString(item.result, `entries[${index}].result`, 64) as DshInstallObservationResult + if (!RESULTS.has(result)) throw new Error(`entries[${index}].result is not a supported install observation result`) + const staticFingerprint = boundedString(item.staticFingerprint, `entries[${index}].staticFingerprint`, 80) + const contractFingerprint = boundedString(item.contractFingerprint, `entries[${index}].contractFingerprint`, 80) + if (!FINGERPRINT.test(staticFingerprint) || !FINGERPRINT.test(contractFingerprint)) { + throw new Error(`entries[${index}] fingerprints must be SHA-256 digests`) + } + const observedAt = boundedString(item.observedAt, `entries[${index}].observedAt`, 64) + if (!Number.isFinite(Date.parse(observedAt))) throw new Error(`entries[${index}].observedAt must be an ISO timestamp`) + const resolution = item.resolution === undefined ? undefined : record(item.resolution, `entries[${index}].resolution`) + const profileLockfile = resolution === undefined ? undefined : parseProfileLockfile(resolution.profileLockfile, `entries[${index}].resolution.profileLockfile`) + const runtimeGraph = resolution === undefined ? undefined : parseRuntimeGraph(resolution.runtimeGraph, `entries[${index}].resolution.runtimeGraph`) + const sha256 = optionalBareSha256(artifact.sha256, `entries[${index}].artifact.sha256`) + const integrity = optionalBoundedString(artifact.integrity, `entries[${index}].artifact.integrity`, 1_024) + const nodeEngine = optionalBoundedString(artifact.nodeEngine, `entries[${index}].artifact.nodeEngine`, 512) + const pnpmVersion = runtime.pnpmVersion === undefined ? undefined : exactVersion(runtime.pnpmVersion, `entries[${index}].runtime.pnpmVersion`) + return { + caseId: caseId(item.caseId, `entries[${index}].caseId`), + targetId: caseId(item.targetId, `entries[${index}].targetId`), + plugin: exactSpec(item.plugin, `entries[${index}].plugin`), + dshVersion: exactVersion(item.dshVersion, `entries[${index}].dshVersion`), + runtime: { + nodeMajor: nodeMajor(runtime.nodeMajor, `entries[${index}].runtime.nodeMajor`), + nodeVersion: exactVersion(runtime.nodeVersion, `entries[${index}].runtime.nodeVersion`), + platform: boundedString(runtime.platform, `entries[${index}].runtime.platform`, 64), + architecture: boundedString(runtime.architecture, `entries[${index}].runtime.architecture`, 64), + ...(pnpmVersion === undefined ? {} : { pnpmVersion }), + }, + staticFingerprint, + contractFingerprint, + observedAt, + result, + reason: boundedString(item.reason, `entries[${index}].reason`, 4_096), + artifact: { + lifecycleScripts: parseLifecycleScripts(artifact.lifecycleScripts, `entries[${index}].artifact.lifecycleScripts`), + ...(sha256 === undefined ? {} : { sha256 }), + ...(integrity === undefined ? {} : { integrity }), + ...(nodeEngine === undefined ? {} : { nodeEngine }), + }, + ...(profileLockfile === undefined && runtimeGraph === undefined + ? {} + : { resolution: { + ...(profileLockfile === undefined ? {} : { profileLockfile }), + ...(runtimeGraph === undefined ? {} : { runtimeGraph }), + } }), + observer: { + schema: boundedString(observer.schema, `entries[${index}].observer.schema`, 256), + version: boundedString(observer.version, `entries[${index}].observer.version`, 256), + }, + } +} + +export function emptyDshCompatibilityLedger(): DshCompatibilityLedger { + return { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries: [] } +} + +export function parseDshCompatibilityLedger(input: unknown): DshCompatibilityLedger { + const root = record(input, 'DSH compatibility ledger') + if (root.schema !== DSH_COMPATIBILITY_LEDGER_SCHEMA) { + throw new Error(`DSH compatibility ledger schema must be ${DSH_COMPATIBILITY_LEDGER_SCHEMA}`) + } + if (!Array.isArray(root.entries) || root.entries.length > MAX_ENTRIES) { + throw new Error(`DSH compatibility ledger entries must be an array of at most ${MAX_ENTRIES} entries`) + } + const entries = root.entries.map(parseEntry) + const ids = new Set() + for (const entry of entries) { + if (ids.has(entry.caseId)) throw new Error(`duplicate DSH compatibility ledger caseId: ${entry.caseId}`) + ids.add(entry.caseId) + } + entries.sort((left, right) => left.caseId.localeCompare(right.caseId)) + return { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries } +} + +function canonicalize(value: unknown): unknown { + if (Array.isArray(value)) return value.map(canonicalize) + if (typeof value !== 'object' || value === null) return value + return Object.fromEntries( + Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, canonicalize(item)]), + ) +} + +function fingerprint(value: unknown): string { + return `sha256:${createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex')}` +} + +/** Static facts that may invalidate a prior runtime observation. */ +export function createDshCompatibilityStaticFingerprint(value: unknown): string { + return fingerprint(value) +} + +/** The controlled execution contract, deliberately separate from source facts. */ +export function createDshCompatibilityContractFingerprint(value: { + plugin: string + dshVersion: string + nodeMajor: number + allowedBuilds: readonly string[] +}): string { + return fingerprint({ + probe: 'dsh-install/v1alpha1', + platform: 'linux', + architecture: 'x64', + packageManager: 'pnpm@11.7.0', + image: `node:${value.nodeMajor}-bookworm-slim`, + plugin: value.plugin, + dshVersion: value.dshVersion, + allowedBuilds: [...value.allowedBuilds].sort(), + }) +} + +export function dshCompatibilityCaseId(targetId: string, runtimeProfileId: string): string { + const combined = `${targetId}-${runtimeProfileId}` + if (!CASE_ID.test(combined)) throw new Error(`DSH compatibility case id is invalid: ${combined}`) + return combined +} + +function reportRecord(value: unknown, label: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${label} is not an object`) + return value as Record +} + +function reportString(value: unknown, label: string, maximum: number): string { + if (typeof value !== 'string' || value === '' || value.length > maximum) throw new Error(`${label} is invalid`) + return value +} + +function parseExpectedCases(input: readonly DshCompatibilityExpectedCase[]): Map { + if (input.length > MAX_REPORTS) throw new Error(`compatibility reconciliation accepts at most ${MAX_REPORTS} expected cases`) + const expected = new Map() + for (const item of input) { + const id = caseId(item.id, 'expected case id') + if (expected.has(id)) throw new Error(`duplicate expected compatibility case id: ${id}`) + const plugin = exactSpec(item.plugin, `expected case ${id} plugin`) + const dshVersion = exactVersion(item.dshVersion, `expected case ${id} DSH version`) + const allowedBuilds = item.allowedBuilds === '' ? [] : item.allowedBuilds.split(',') + if (allowedBuilds.some(name => !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name))) { + throw new Error(`expected case ${id} contains an invalid approved dependency build`) + } + if (new Set(allowedBuilds).size !== allowedBuilds.length) throw new Error(`expected case ${id} contains duplicate approved dependency builds`) + if (!FINGERPRINT.test(item.staticFingerprint) || !FINGERPRINT.test(item.contractFingerprint)) { + throw new Error(`expected case ${id} has an invalid fingerprint`) + } + expected.set(id, { + ...item, + id, + targetId: caseId(item.targetId, `expected case ${id} targetId`), + plugin, + dshVersion, + nodeMajor: nodeMajor(item.nodeMajor, `expected case ${id} nodeMajor`), + allowedBuilds: allowedBuilds.sort().join(','), + reasons: [...new Set(item.reasons)].sort(), + }) + } + return expected +} + +function parseObservationReport(value: unknown, expected: DshCompatibilityExpectedCase): DshCompatibilityLedgerEntry { + const report = reportRecord(value, `report for ${expected.id}`) + if (report.schema !== 'upstream-radar.dsh-install-observation/v1alpha1') throw new Error('report schema is not a DSH install observation') + if (report.probe !== 'dsh-install' || report.scope !== 'install-and-load-behavior') throw new Error('report does not describe an install-and-load observation') + if (caseId(report.caseId, 'report caseId') !== expected.id) throw new Error('report caseId does not match the scheduled case') + const dshVersion = exactVersion(report.dshVersion, 'report dshVersion') + if (dshVersion !== expected.dshVersion) throw new Error(`report DSH version ${dshVersion} does not match scheduled ${expected.dshVersion}`) + const artifact = reportRecord(report.artifact, 'report artifact') + const plugin = exactSpec(artifact.spec, 'report artifact spec') + if (plugin !== expected.plugin) throw new Error(`report artifact ${plugin} does not match scheduled ${expected.plugin}`) + const runtime = reportRecord(report.runtime, 'report runtime') + const runtimeNodeVersion = exactVersion(runtime.nodeVersion, 'report runtime.nodeVersion') + const actualMajor = Number(runtimeNodeVersion.split('.')[0]) + if (actualMajor !== expected.nodeMajor) throw new Error(`report Node ${runtimeNodeVersion} does not match scheduled Node ${expected.nodeMajor}`) + const packageManager = reportRecord(runtime.packageManager, 'report runtime.packageManager') + if (packageManager.name !== 'pnpm') throw new Error('report package manager is not pnpm') + const pnpmVersion = packageManager.version === undefined ? undefined : exactVersion(packageManager.version, 'report runtime.packageManager.version') + const boundary = reportRecord(report.boundary, 'report boundary') + const approved = parseLifecycleBuilds(boundary.approvedDependencyBuilds, 'report boundary.approvedDependencyBuilds') + if (approved.join(',') !== expected.allowedBuilds) throw new Error('report approved dependency builds do not match the scheduled policy') + const result = reportString(report.result, 'report result', 64) as DshInstallObservationResult + if (!RESULTS.has(result)) throw new Error('report result is unsupported') + const lifecycleScripts = parseLifecycleScripts(artifact.lifecycleScripts, 'report artifact.lifecycleScripts') + const sha256 = optionalBareSha256(artifact.sha256, 'report artifact.sha256') + const integrity = optionalBoundedString(artifact.integrity, 'report artifact.integrity', 1_024) + const nodeEngine = optionalBoundedString(artifact.nodeEngine, 'report artifact.nodeEngine', 512) + const resolutionRecord = report.resolution === undefined ? undefined : reportRecord(report.resolution, 'report resolution') + const profileLockfile = resolutionRecord === undefined ? undefined : parseProfileLockfile(resolutionRecord.profileLockfile, 'report resolution.profileLockfile') + const runtimeGraph = resolutionRecord === undefined ? undefined : parseRuntimeGraph(resolutionRecord.runtimeGraph, 'report resolution.runtimeGraph') + const tool = reportRecord(report.tool, 'report tool') + if (tool.name !== 'upstream-radar') throw new Error('report was not produced by upstream-radar') + const observedAt = reportString(report.completedAt, 'report completedAt', 64) + if (!Number.isFinite(Date.parse(observedAt))) throw new Error('report completedAt is not a timestamp') + return { + caseId: expected.id, + targetId: expected.targetId, + plugin, + dshVersion, + runtime: { + nodeMajor: expected.nodeMajor, + nodeVersion: runtimeNodeVersion, + platform: reportString(runtime.platform, 'report runtime.platform', 64), + architecture: reportString(runtime.architecture, 'report runtime.architecture', 64), + ...(pnpmVersion === undefined ? {} : { pnpmVersion }), + }, + staticFingerprint: expected.staticFingerprint, + contractFingerprint: expected.contractFingerprint, + observedAt, + result, + reason: reportString(report.reason, 'report reason', 4_096), + artifact: { + lifecycleScripts, + ...(sha256 === undefined ? {} : { sha256 }), + ...(integrity === undefined ? {} : { integrity }), + ...(nodeEngine === undefined ? {} : { nodeEngine }), + }, + ...(profileLockfile === undefined && runtimeGraph === undefined + ? {} + : { resolution: { + ...(profileLockfile === undefined ? {} : { profileLockfile }), + ...(runtimeGraph === undefined ? {} : { runtimeGraph }), + } }), + observer: { + schema: reportString(report.schema, 'report schema', 256), + version: reportString(tool.version, 'report tool.version', 256), + }, + } +} + +function parseLifecycleBuilds(value: unknown, label: string): string[] { + if (!Array.isArray(value) || value.length > 16) throw new Error(`${label} must be an array of at most 16 package names`) + const names = value.map((item, index) => reportString(item, `${label}[${index}]`, 214)) + if (names.some(name => !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name))) { + throw new Error(`${label} contains an invalid package name`) + } + if (new Set(names).size !== names.length) throw new Error(`${label} contains duplicate package names`) + return names.sort() +} + +function isIncompatible(result: DshInstallObservationResult): boolean { + return result !== 'compatible' +} + +function sameResolution( + previous: DshCompatibilityLedgerEntry['resolution'], + current: DshCompatibilityLedgerEntry['resolution'], +): boolean { + const beforeLockfile = previous?.profileLockfile + const afterLockfile = current?.profileLockfile + const sameLockfile = beforeLockfile === undefined || afterLockfile === undefined + ? beforeLockfile === afterLockfile + // pnpm can rewrite non-semantic metadata. Prefer the canonical graph when + // both runs established one; only fall back to raw lockfile bytes when it + // could not be parsed. + : beforeLockfile.graphDigest !== undefined && afterLockfile.graphDigest !== undefined + ? beforeLockfile.graphDigest === afterLockfile.graphDigest + : beforeLockfile.sha256 === afterLockfile.sha256 + const beforeRuntime = previous?.runtimeGraph + const afterRuntime = current?.runtimeGraph + const sameRuntime = beforeRuntime === undefined || afterRuntime === undefined + ? beforeRuntime === afterRuntime + : beforeRuntime.digest === afterRuntime.digest + return sameLockfile && sameRuntime +} + +function transition(previous: DshCompatibilityLedgerEntry | undefined, current: DshCompatibilityLedgerEntry): DshCompatibilityTransition { + const previousIncompatible = previous !== undefined && isIncompatible(previous.result) + const currentIncompatible = isIncompatible(current.result) + let status: DshCompatibilityTransitionStatus + if (previous === undefined) status = currentIncompatible ? 'new-incompatibility' : 'compatible' + else if (previousIncompatible && !currentIncompatible) status = 'resolved-incompatibility' + else if (!previousIncompatible && currentIncompatible) status = 'new-incompatibility' + else if (previousIncompatible && currentIncompatible && (previous.result !== current.result || previous.reason !== current.reason)) status = 'changed-incompatibility' + else if (currentIncompatible) status = 'persisting-incompatibility' + // Compatibility-state transitions take precedence over artifact drift. A + // fixed release necessarily changes its bytes; classifying that as generic + // drift would prevent the managed incident from ever closing. + else if (previous.artifact.sha256 !== undefined && current.artifact.sha256 !== undefined && previous.artifact.sha256 !== current.artifact.sha256) status = 'artifact-drift' + else if (!sameResolution(previous.resolution, current.resolution)) status = 'resolution-drift' + else status = 'compatible' + return { + caseId: current.caseId, + status, + result: current.result, + reason: current.reason, + ...(previous === undefined ? {} : { previousResult: previous.result }), + } +} + +/** + * Merge only reports that prove they belong to a scheduled exact cell. Bad or + * absent reports never become a green result; absent cells stay missing and + * therefore are planned again on the next reconciliation. + */ +export function mergeDshCompatibilityLedger(input: { + ledger: DshCompatibilityLedger + expected: readonly DshCompatibilityExpectedCase[] + reports: readonly unknown[] +}): DshCompatibilityLedgerMerge { + if (input.reports.length > MAX_REPORTS) throw new Error(`compatibility reconciliation accepts at most ${MAX_REPORTS} reports`) + const expected = parseExpectedCases(input.expected) + const previousByCase = new Map(input.ledger.entries.map(entry => [entry.caseId, entry])) + const nextByCase = new Map(previousByCase) + const transitions: DshCompatibilityTransition[] = [] + const accepted = new Set() + const rejectedReports: string[] = [] + + for (const [index, rawReport] of input.reports.entries()) { + try { + const raw = reportRecord(rawReport, `reports[${index}]`) + const rawCaseId = caseId(raw.caseId, `reports[${index}].caseId`) + const scheduled = expected.get(rawCaseId) + if (scheduled === undefined) throw new Error(`report case ${rawCaseId} was not scheduled in this reconciliation`) + if (accepted.has(rawCaseId)) throw new Error(`more than one report was supplied for scheduled case ${rawCaseId}`) + const entry = parseObservationReport(rawReport, scheduled) + nextByCase.set(entry.caseId, entry) + accepted.add(entry.caseId) + transitions.push(transition(previousByCase.get(entry.caseId), entry)) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + rejectedReports.push(`report ${index + 1}: ${message.replace(/[\u0000-\u001f\u007f]/g, '?').slice(0, 512)}`) + } + } + + const missingCaseIds = [...expected.keys()].filter(id => !accepted.has(id)).sort() + const entries = [...nextByCase.values()].sort((left, right) => left.caseId.localeCompare(right.caseId)) + return { + ledger: { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries }, + transitions: transitions.sort((left, right) => left.caseId.localeCompare(right.caseId)), + acceptedCaseIds: [...accepted].sort(), + missingCaseIds, + rejectedReports: rejectedReports.sort(), + } +} + +function inline(value: string): string { + return value.replace(/[\u0000-\u001f\u007f`|]/g, ' ').slice(0, 512) +} + +export function renderDshCompatibilityLedgerMerge(merge: DshCompatibilityLedgerMerge): string { + const lines = [ + '## Upstream Radar — DSH compatibility ledger', + '', + `- Accepted isolated reports: **${merge.acceptedCaseIds.length}**`, + `- Missing scheduled reports: **${merge.missingCaseIds.length}**`, + `- Rejected reports: **${merge.rejectedReports.length}**`, + `- Current active cells: **${merge.ledger.entries.length}**`, + ] + const actionable = merge.transitions.filter(item => item.status !== 'compatible' && item.status !== 'persisting-incompatibility') + if (actionable.length > 0) { + lines.push('', '### New or changed evidence', '') + for (const item of actionable) { + lines.push(`- \`${inline(item.caseId)}\`: **${inline(item.status)}** → \`${inline(item.result)}\` — ${inline(item.reason)}`) + } + } + if (merge.missingCaseIds.length > 0) { + lines.push('', '### Evidence still missing', '') + for (const id of merge.missingCaseIds) lines.push(`- \`${inline(id)}\` did not return a usable report; it remains unsatisfied and will be planned again.`) + } + if (merge.rejectedReports.length > 0) { + lines.push('', '### Rejected report evidence', '') + for (const reason of merge.rejectedReports) lines.push(`- ${inline(reason)}`) + } + lines.push('', 'The ledger records behavior evidence for an exact plugin × DSH × runtime contract. It does not claim that third-party code is safe.', '') + return lines.join('\n') +} diff --git a/src/dsh-install-observation.ts b/src/dsh-install-observation.ts index e31eb42..1e06d87 100644 --- a/src/dsh-install-observation.ts +++ b/src/dsh-install-observation.ts @@ -1,12 +1,21 @@ import { createHash } from 'node:crypto' import { spawn } from 'node:child_process' import { constants } from 'node:fs' -import { lstat, mkdir, mkdtemp, open, readdir, rm, writeFile } from 'node:fs/promises' +import { lstat, mkdir, mkdtemp, open, readdir, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { basename, join, relative, resolve, sep } from 'node:path' +import { basename, dirname, join, relative, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' +import { parsePnpmLockGraph } from './graph.js' +import { + discoverDshRuntimeHostNodeModulesDirectory, + discoverDshRuntimePackage, + discoverDshRuntimePackageDirectory, +} from './dsh-runtime.js' +import { parseInstalledNodeModulesGraph } from './installed-graph.js' import { parseNpmSpec } from './npm.js' +import type { DependencyKind, RootPeerContract } from './radar-types.js' import { satisfiesSemverRange } from './semver.js' -import { parseNpmTarball } from './tar.js' +import { parseNpmTarball, type TarEntry } from './tar.js' import { TOOL_VERSION } from './version.js' export const DSH_INSTALL_OBSERVATION_SCHEMA = 'upstream-radar.dsh-install-observation/v1alpha1' as const @@ -22,10 +31,21 @@ const MAX_TRACE_EVENTS = 512 const MAX_SNAPSHOT_ENTRIES = 25_000 const MAX_DIFF_PATHS = 512 const MAX_ALLOWED_BUILDS = 16 +const MAX_PROFILE_LOCKFILE_BYTES = 16 * 1024 * 1024 +const MAX_PROFILE_GRAPH_GAPS = 32 +const MAX_PLUGIN_PEERS = 64 +const MAX_STATIC_PEER_SCAN_BYTES = 8 * 1024 * 1024 +const MAX_RUNTIME_DISCOVERY_DIRECTORIES = 12_000 +const MAX_RUNTIME_DISCOVERY_DEPTH = 16 const PROFILE = 'headless' const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'] as const +const PROFILE_LOCKFILE_CANDIDATES = [ + ['pnpm-lock.yaml'], + ['node_modules', '.pnpm', 'lock.yaml'], +] as const +const SYNTHETIC_PROFILE_GRAPH_ROOT = { name: 'dsh-profile-headless', version: '0.0.0' } as const -export type DshInstallObservationResult = 'compatible' | 'runtime-incompatible' | 'install-failed' | 'load-failed' | 'unknown' +export type DshInstallObservationResult = 'compatible' | 'runtime-incompatible' | 'peer-contract-incompatible' | 'install-failed' | 'load-failed' | 'unknown' export type InstallObservationPhase = 'runtime' | 'artifact' | 'profile' | 'install' | 'load' export type InstallObservationIsolationProvider = 'github-actions-hosted-runner' | 'firecracker' | 'other' @@ -112,6 +132,93 @@ export interface InstallFilesystemDiff { snapshotErrors: number } +export interface DshInstallProfileLockfileEvidence { + sha256: string + bytes: number + graphDigest?: string + nodes?: number + edges?: number + unresolved?: number + /** A bounded, normalized sample of graph edges that could not be resolved. */ + unresolvedDependencies?: DshInstallProfileGraphGap[] +} + +export interface DshInstallProfileGraphGap { + from: string + name: string + spec: string + kind: DependencyKind +} + +/** A direct, required plugin peer that did not line up with the DSH runtime. */ +export interface DshInstallPeerContractIssue { + name: string + required: string + status: 'mismatched' | 'indeterminate' | 'missing' + /** Static evidence explains whether a literal runtime import was observed. */ + staticUsage: DshInstallPeerStaticUsage + resolvedVersion?: string +} + +/** + * What the packed artifact itself reveals about a declared peer. This is + * intentionally syntactic evidence, not a claim that an unobserved import can + * never happen at runtime. + */ +export type DshInstallPeerStaticUsage = + | 'runtime-import-observed' + | 'type-only-reference-observed' + | 'no-literal-reference-observed' + | 'scan-incomplete' + +/** One direct plugin peer requirement aligned to its exact runtime resolution. */ +export interface DshInstallPeerContractRelation { + name: string + required: string + status: 'satisfied' | 'mismatched' | 'indeterminate' | 'missing' + staticUsage: DshInstallPeerStaticUsage + resolvedVersion?: string +} + +/** + * Direct plugin-to-DSH host contracts evaluated from the final installed + * graph. This is distinct from whether the generic load probe happened to + * exercise every API path. + */ +export interface DshInstallPluginPeerContracts { + declared: number + satisfied: number + mismatched: number + indeterminate: number + missing: number + /** Full bounded relation set; this is the plugin-to-DSH compatibility IR boundary. */ + relations: DshInstallPeerContractRelation[] + issues?: DshInstallPeerContractIssue[] +} + +/** + * The dependency tree DSH can actually resolve after installing the plugin. + * Unlike the profile lockfile alone, this may include the shared DSH host + * dependency plane that satisfies plugin peer dependencies. + */ +export interface DshInstallRuntimeGraphEvidence { + digest: string + nodes: number + edges: number + /** Required runtime/peer gaps only. Platform-selected optional packages are reported separately. */ + unresolved: number + unresolvedDependencies?: DshInstallProfileGraphGap[] + /** Optional platform or feature packages absent from this exact runtime. */ + optionalUnavailable?: number + optionalUnavailableDependencies?: DshInstallProfileGraphGap[] + pluginPeerContracts: DshInstallPluginPeerContracts + hostRuntime?: { + source: 'dsh-profile-fallback' | 'dsh-process' + resolvedNodes: number + dshVersion?: string + } +} + export interface DshInstallObservationReport { schema: typeof DSH_INSTALL_OBSERVATION_SCHEMA tool: { name: 'upstream-radar'; version: string } @@ -119,6 +226,7 @@ export interface DshInstallObservationReport { scope: 'install-and-load-behavior' startedAt: string completedAt: string + caseId?: string dshVersion: string runtime: { platform: string @@ -156,6 +264,14 @@ export interface DshInstallObservationReport { install: InstallFilesystemDiff load: InstallFilesystemDiff } + resolution: { + /** The exact DSH profile lockfile produced by the isolated install, never its contents. */ + profileLockfile?: DshInstallProfileLockfileEvidence + /** The final profile plus shared-DSH-host graph observed after loading. */ + runtimeGraph?: DshInstallRuntimeGraphEvidence + /** Bounded collector diagnostic when the effective graph could not be read. */ + runtimeGraphError?: string + } result: DshInstallObservationResult reason: string boundary: { @@ -173,6 +289,7 @@ export interface DshInstallObservationReport { export interface DshInstallObservationOptions { packageSpec: string dshVersion: string + caseId?: string allowExecution: boolean isolationProvider: InstallObservationIsolationProvider allowedBuilds?: readonly string[] @@ -217,6 +334,81 @@ interface ParsedArtifact { bundlePatch: string nodeEngine?: string lifecycleScripts: string[] + requiredPeerDependencies: ProfilePeerRequirement[] +} + +function isNpmPackageName(value: string): boolean { + return /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/.test(value) +} + +function staticPeerUsage(entries: readonly TarEntry[], requirements: readonly { name: string, required: string }[]): DshInstallPeerStaticUsage[] { + const state = new Map(requirements.map(requirement => [requirement.name, 'no-literal-reference-observed' as DshInstallPeerStaticUsage])) + let scanned = 0 + let incomplete = false + for (const entry of entries) { + if (entry.type !== 'file' || entry.contents === undefined + || !/\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(entry.path)) continue + if (scanned + entry.contents.length > MAX_STATIC_PEER_SCAN_BYTES) { + incomplete = true + continue + } + scanned += entry.contents.length + const text = entry.contents.toString('utf8') + const declarationFile = /\.d\.(?:[cm]?ts)$/i.test(entry.path) + for (const requirement of requirements) { + if (state.get(requirement.name) === 'runtime-import-observed') continue + const escaped = requirement.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const literal = `['"]${escaped}(?:/[^'"]*)?['"]` + // Stay within one statement line. A wider expression can start at an + // unrelated runtime import and accidentally consume a later `import + // type`, which would turn declaration-only evidence into a false runtime + // claim. Missing a heavily formatted import is safer than that claim. + const typeOnly = new RegExp(`(?:^|[;\\n])[\\t ]*import[\\t ]+type\\b[^;\\n]{0,1024}?\\bfrom[\\t ]*${literal}`) + const runtime = new RegExp([ + `(?:^|[;\\n])[\\t ]*import[\\t ]+(?!type\\b)(?:[^;\\n]{0,1024}?[\\t ]+from[\\t ]+)?${literal}`, + `(?:^|[;\\n])[\\t ]*export[\\t ]+(?!type\\b)[^;\\n]{0,1024}?[\\t ]+from[\\t ]+${literal}`, + `\\b(?:require|import)\\s*\\(\\s*${literal}`, + ].join('|')) + const literalReference = new RegExp(literal) + if (!declarationFile && runtime.test(text)) { + state.set(requirement.name, 'runtime-import-observed') + } else if (declarationFile ? literalReference.test(text) : typeOnly.test(text)) { + state.set(requirement.name, 'type-only-reference-observed') + } + } + } + return requirements.map(requirement => { + const observed = state.get(requirement.name) ?? 'no-literal-reference-observed' + return observed === 'no-literal-reference-observed' && incomplete ? 'scan-incomplete' : observed + }) +} + +function requiredPeerDependencies(manifest: Record, entries: readonly TarEntry[]): ProfilePeerRequirement[] { + const peers = typeof manifest.peerDependencies === 'object' && manifest.peerDependencies !== null && !Array.isArray(manifest.peerDependencies) + ? manifest.peerDependencies as Record + : {} + const metadata = typeof manifest.peerDependenciesMeta === 'object' && manifest.peerDependenciesMeta !== null && !Array.isArray(manifest.peerDependenciesMeta) + ? manifest.peerDependenciesMeta as Record + : {} + const requirements: Array<{ name: string, required: string }> = [] + for (const [name, rawRange] of Object.entries(peers).sort(([left], [right]) => left.localeCompare(right))) { + if (!isNpmPackageName(name)) throw new Error(`packed artifact has an invalid peer dependency name: ${name}`) + if (typeof rawRange !== 'string' || rawRange.trim() === '' || rawRange.length > 512) { + throw new Error(`packed artifact has an invalid peer dependency range for ${name}`) + } + const peerMetadata = metadata[name] + const optional = typeof peerMetadata === 'object' && peerMetadata !== null && !Array.isArray(peerMetadata) + && (peerMetadata as Record).optional === true + if (!optional) requirements.push({ name, required: bounded(rawRange.trim(), 512) }) + } + if (requirements.length > MAX_PLUGIN_PEERS) { + throw new Error(`packed artifact declares more than ${MAX_PLUGIN_PEERS} required peer dependencies`) + } + const usages = staticPeerUsage(entries, requirements) + return requirements.map((requirement, index) => ({ + ...requirement, + staticUsage: usages[index] ?? 'scan-incomplete', + })) } function bounded(value: string, maximum = MAX_REPORT_DETAIL_BYTES): string { @@ -644,6 +836,7 @@ async function parsePackedArtifact( } const nodeEngine = rawNodeEngine === undefined || rawNodeEngine === '' ? undefined : bounded(rawNodeEngine, 512) const integrity = typeof item.integrity === 'string' ? bounded(item.integrity, 1_024) : undefined + const peerRequirements = requiredPeerDependencies(manifest, parsed.entries) return { path, filename, @@ -653,6 +846,7 @@ async function parsePackedArtifact( bundlePatch, ...(nodeEngine === undefined ? {} : { nodeEngine }), lifecycleScripts, + requiredPeerDependencies: peerRequirements, } } @@ -783,6 +977,455 @@ async function registeredBundle(dshHome: string, packageName: string): Promise { + const profileDirectory = join(dshHome, 'profiles', PROFILE) + try { + const profileMetadata = await lstat(profileDirectory) + if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink()) return undefined + } catch { + return undefined + } + + for (const segments of PROFILE_LOCKFILE_CANDIDATES) { + let current = profileDirectory + try { + for (let index = 0; index < segments.length; index += 1) { + const segment = segments[index] as string + current = join(current, segment) + const metadata = await lstat(current) + if (metadata.isSymbolicLink()) return undefined + if (index < segments.length - 1 && !metadata.isDirectory()) return undefined + } + return await readRegularFileNoFollow(current, MAX_PROFILE_LOCKFILE_BYTES) + } catch (error: unknown) { + if (isNotFound(error)) continue + return undefined + } + } + return undefined +} + +function profileGraphRoot(manifest: Record): { name: string, version: string } { + return typeof manifest.name === 'string' && manifest.name.length > 0 + && typeof manifest.version === 'string' && EXACT_VERSION.test(manifest.version) + ? { name: manifest.name, version: manifest.version } + : SYNTHETIC_PROFILE_GRAPH_ROOT +} + +function graphGaps(graph: { unresolved?: readonly DshInstallProfileGraphGap[] }): DshInstallProfileGraphGap[] | undefined { + if (graph.unresolved === undefined || graph.unresolved.length === 0) return undefined + return graph.unresolved.slice(0, MAX_PROFILE_GRAPH_GAPS).map(gap => ({ + from: bounded(gap.from, 512), + name: bounded(gap.name, 214), + spec: bounded(gap.spec, 512), + kind: gap.kind, + })) +} + +function pluginPeerContractEvidence(contracts: readonly ObservedPeerContract[] | undefined): DshInstallPluginPeerContracts { + const entries = [...(contracts ?? [])].sort((left, right) => left.name.localeCompare(right.name)) + const issues = entries + .filter((entry): entry is ObservedPeerContract & { status: DshInstallPeerContractIssue['status'] } => entry.status !== 'satisfied') + .sort((left, right) => left.name.localeCompare(right.name)) + return { + declared: entries.length, + satisfied: entries.filter(entry => entry.status === 'satisfied').length, + mismatched: entries.filter(entry => entry.status === 'mismatched').length, + indeterminate: entries.filter(entry => entry.status === 'indeterminate').length, + missing: entries.filter(entry => entry.status === 'missing').length, + relations: entries.map(entry => ({ + name: bounded(entry.name, 214), + required: bounded(entry.required, 512), + status: entry.status, + staticUsage: entry.staticUsage, + ...(entry.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(entry.resolvedVersion, 256) }), + })), + ...(issues.length === 0 ? {} : { + issues: issues.slice(0, MAX_PROFILE_GRAPH_GAPS).map(issue => ({ + name: bounded(issue.name, 214), + required: bounded(issue.required, 512), + status: issue.status, + staticUsage: issue.staticUsage, + ...(issue.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(issue.resolvedVersion, 256) }), + })), + }), + } +} + +function graphGapPartitions(graph: { unresolved?: readonly DshInstallProfileGraphGap[] }): { + required: DshInstallProfileGraphGap[] + optional: DshInstallProfileGraphGap[] +} { + const required: DshInstallProfileGraphGap[] = [] + const optional: DshInstallProfileGraphGap[] = [] + for (const gap of graph.unresolved ?? []) { + if (gap.kind === 'optional') optional.push(gap) + else required.push(gap) + } + return { required, optional } +} + +function isLexicallyInside(root: string, candidate: string): boolean { + const child = relative(resolve(root), resolve(candidate)) + return child === '' || (!child.startsWith(`..${sep}`) && child !== '..') +} + +interface ProfilePeerRequirement { + name: string + required: string + staticUsage: DshInstallPeerStaticUsage +} + +interface ObservedPeerContract extends RootPeerContract { + staticUsage: DshInstallPeerStaticUsage +} + +interface ProfilePeerResolutionRecord { + name: string + status: 'resolved' | 'missing' + url?: string +} + +const PROFILE_PEER_RESOLUTION_SCHEMA = 'upstream-radar.profile-peer-resolution/v1alpha1' + +function indeterminatePeerContracts(requirements: readonly ProfilePeerRequirement[]): ObservedPeerContract[] { + return requirements.map(requirement => ({ ...requirement, status: 'indeterminate' })) +} + +async function peerVersionFromResolvedModule( + url: string, + expectedName: string, + sandboxRoot: string, +): Promise { + let resolvedModule: string + try { + const parsed = new URL(url) + if (parsed.protocol !== 'file:') return undefined + resolvedModule = await realpath(fileURLToPath(parsed)) + } catch { + return undefined + } + let sandboxReal: string + try { + sandboxReal = await realpath(sandboxRoot) + } catch { + return undefined + } + if (!isLexicallyInside(sandboxReal, resolvedModule)) return undefined + let cursor = dirname(resolvedModule) + for (let depth = 0; depth < 32 && isLexicallyInside(sandboxReal, cursor); depth += 1) { + try { + const manifest = JSON.parse((await readRegularFileNoFollow(join(cursor, 'package.json'), 1 * 1024 * 1024)).toString('utf8')) as unknown + if (typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)) { + const item = manifest as Record + if (item.name === expectedName && typeof item.version === 'string' && EXACT_VERSION.test(item.version)) return item.version + } + } catch { + // Continue toward the package root; a module may live in a nested directory. + } + const parent = dirname(cursor) + if (parent === cursor) break + cursor = parent + } + return undefined +} + +async function readProfilePeerContracts( + path: string, + requirements: readonly ProfilePeerRequirement[], + sandboxRoot: string, +): Promise { + if (requirements.length === 0) return [] + let records: ProfilePeerResolutionRecord[] + try { + const raw = JSON.parse((await readRegularFileNoFollow(path, 64 * 1024)).toString('utf8')) as unknown + if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return indeterminatePeerContracts(requirements) + const item = raw as Record + if (item.schema !== PROFILE_PEER_RESOLUTION_SCHEMA || !Array.isArray(item.peers) || item.peers.length !== requirements.length) { + return indeterminatePeerContracts(requirements) + } + records = item.peers.map((value): ProfilePeerResolutionRecord => { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('invalid peer resolver record') + const peer = value as Record + if (typeof peer.name !== 'string' || (peer.status !== 'resolved' && peer.status !== 'missing')) { + throw new Error('invalid peer resolver record') + } + if (peer.status === 'resolved' && (typeof peer.url !== 'string' || peer.url.length === 0 || peer.url.length > 4_096)) { + throw new Error('resolved peer has no bounded URL') + } + return { + name: peer.name, + status: peer.status, + ...(typeof peer.url === 'string' ? { url: peer.url } : {}), + } + }) + } catch { + return indeterminatePeerContracts(requirements) + } + const byName = new Map(records.map(record => [record.name, record])) + if (byName.size !== requirements.length || requirements.some(requirement => !byName.has(requirement.name))) { + return indeterminatePeerContracts(requirements) + } + const contracts: ObservedPeerContract[] = [] + for (const requirement of requirements) { + const record = byName.get(requirement.name) + if (record === undefined || record.status === 'missing') { + contracts.push({ ...requirement, status: 'missing' }) + continue + } + const resolvedVersion = record.url === undefined + ? undefined + : await peerVersionFromResolvedModule(record.url, requirement.name, sandboxRoot) + if (resolvedVersion === undefined) { + contracts.push({ ...requirement, status: 'indeterminate' }) + continue + } + const evaluation = satisfiesSemverRange(resolvedVersion, requirement.required) + contracts.push({ + ...requirement, + status: evaluation === true ? 'satisfied' : evaluation === false ? 'mismatched' : 'indeterminate', + resolvedVersion, + }) + } + return contracts +} + +/** + * Run the plugin's real ESM entry from the profile resolution anchor, then + * boot DSH. The config dumper intentionally skips `!!js` and module imports; + * this tiny trusted wrapper proves that the installed plugin can resolve its + * own direct imports before the DSH headless app exits through `--help`. + */ +async function writeProfileLoadProbe( + dshHome: string, + pluginName: string, + dshVersion: string, + pnpmCommand: string, + peerRequirements: readonly ProfilePeerRequirement[], +): Promise<{ path: string, peerResolutionPath: string, profileDirectory: string }> { + const profileDirectory = join(dshHome, 'profiles', PROFILE) + const profileMetadata = await lstat(profileDirectory) + if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink()) { + throw new Error('the DSH profile directory is not a regular directory for the load probe') + } + const [homeReal, profileReal] = await Promise.all([realpath(dshHome), realpath(profileDirectory)]) + if (!isLexicallyInside(homeReal, profileReal)) { + throw new Error('the DSH profile directory escaped the controlled DSH home') + } + const probePath = join(profileDirectory, '.upstream-radar-load-probe.mjs') + const peerResolutionPath = join(profileDirectory, '.upstream-radar-peer-resolution.json') + const dshBootArgs = dshArgs(dshVersion, ['--profile', PROFILE, '--help']) + const contents = [ + "import { spawn } from 'node:child_process'", + "import { writeFile } from 'node:fs/promises'", + `const peerRequirements = ${JSON.stringify(peerRequirements)}`, + 'const peers = []', + 'for (const peer of peerRequirements) {', + ' try { peers.push({ name: peer.name, status: \'resolved\', url: import.meta.resolve(peer.name) }) }', + ' catch { peers.push({ name: peer.name, status: \'missing\' }) }', + '}', + `await writeFile(${JSON.stringify(peerResolutionPath)}, JSON.stringify({ schema: ${JSON.stringify(PROFILE_PEER_RESOLUTION_SCHEMA)}, peers }), { encoding: 'utf8', mode: 0o600, flag: 'wx' })`, + `await import(${JSON.stringify(pluginName)})`, + `const child = spawn(${JSON.stringify(pnpmCommand)}, ${JSON.stringify(dshBootArgs)}, { cwd: process.cwd(), env: process.env, stdio: 'inherit' })`, + "const exitCode = await new Promise(resolve => {", + " child.once('error', error => { console.error(error.message); resolve(1) })", + " child.once('close', code => resolve(code ?? 1))", + '})', + 'process.exitCode = exitCode', + '', + ].join('\n') + // Never overwrite a path a target package may have planted in the profile. + await writeFile(probePath, contents, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + return { path: probePath, peerResolutionPath, profileDirectory } +} + +interface ExactDshRuntime { + packageDirectory: string + nodeModulesDirectory: string + package: { ecosystem: 'npm', name: '@deepseek-ai/dsh', version: string } +} + +/** + * `pnpm dlx` owns the DSH host dependency plane, rather than a plugin-created + * symlink in the profile. Discover the exact cached DSH package by manifest, + * with bounded directory traversal and no symlink following. + */ +async function discoverExactDshRuntime(cacheHome: string, dshVersion: string): Promise { + const root = resolve(cacheHome, 'pnpm', 'dlx') + const rootReal = await realpath(root) + const queue: Array<{ path: string, depth: number }> = [{ path: root, depth: 0 }] + const candidates = new Set() + let visited = 0 + while (queue.length > 0) { + const current = queue.shift() + if (current === undefined) break + if (visited >= MAX_RUNTIME_DISCOVERY_DIRECTORIES) { + throw new Error(`DSH runtime discovery exceeds ${MAX_RUNTIME_DISCOVERY_DIRECTORIES} directories`) + } + visited += 1 + let entries + try { + entries = await readdir(current.path, { withFileTypes: true }) + } catch (error: unknown) { + if (isNotFound(error)) continue + throw error + } + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + const child = join(current.path, entry.name) + if (entry.isDirectory()) { + if (current.depth < MAX_RUNTIME_DISCOVERY_DEPTH) queue.push({ path: child, depth: current.depth + 1 }) + continue + } + if (!entry.isFile() || entry.name !== 'package.json') continue + let manifest: unknown + try { + manifest = JSON.parse((await readRegularFileNoFollow(child, 1 * 1024 * 1024)).toString('utf8')) as unknown + } catch { + continue + } + if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) continue + const item = manifest as Record + if (item.name === '@deepseek-ai/dsh' && item.version === dshVersion) candidates.add(child) + } + } + if (candidates.size === 0) throw new Error(`exact @deepseek-ai/dsh@${dshVersion} was not found in the controlled pnpm dlx cache`) + if (candidates.size > 1) throw new Error(`multiple exact @deepseek-ai/dsh@${dshVersion} packages were found in the controlled pnpm dlx cache`) + const manifestPath = [...candidates][0] as string + const packageDirectory = discoverDshRuntimePackageDirectory(manifestPath) + const packageCoordinate = discoverDshRuntimePackage(manifestPath) + const nodeModulesDirectory = discoverDshRuntimeHostNodeModulesDirectory(manifestPath) + if (packageDirectory === undefined || packageCoordinate === undefined || nodeModulesDirectory === undefined) { + throw new Error('the exact DSH package did not expose a usable dependency plane') + } + if (!isLexicallyInside(rootReal, packageDirectory) || !isLexicallyInside(rootReal, nodeModulesDirectory)) { + throw new Error('the exact DSH dependency plane escaped the controlled pnpm dlx cache') + } + if (packageCoordinate.name !== '@deepseek-ai/dsh' || packageCoordinate.version !== dshVersion) { + throw new Error('the discovered DSH package does not match the requested exact version') + } + return { + packageDirectory, + nodeModulesDirectory, + package: { ecosystem: 'npm', name: '@deepseek-ai/dsh', version: packageCoordinate.version }, + } +} + +async function profileResolutionEvidence(dshHome: string): Promise { + const profileDirectory = join(dshHome, 'profiles', PROFILE) + const lockfile = await readProfileLockfile(dshHome) + if (lockfile === undefined) return {} + const profileLockfile: DshInstallProfileLockfileEvidence = { + sha256: createHash('sha256').update(lockfile).digest('hex'), + bytes: lockfile.length, + } + try { + const manifestBuffer = await readRegularFileNoFollow(join(profileDirectory, 'package.json'), 4 * 1024 * 1024) + const manifest = JSON.parse(manifestBuffer.toString('utf8')) as Record + const graph = parsePnpmLockGraph(lockfile.toString('utf8'), profileGraphRoot(manifest)) + const gaps = graphGaps(graph) + return { + profileLockfile: { + ...profileLockfile, + ...(graph.digest === undefined ? {} : { graphDigest: graph.digest }), + nodes: graph.nodes.length, + edges: graph.edges.length, + unresolved: graph.unresolved?.length ?? 0, + ...(gaps === undefined ? {} : { unresolvedDependencies: gaps }), + }, + } + } catch { + return { profileLockfile } + } +} + +async function runtimeGraphEvidence( + dshHome: string, + rootPackage: { name: string, version: string }, + dshVersion: string, + cacheHome: string, + profileResolvedPeerContracts?: readonly ObservedPeerContract[], +): Promise> { + try { + const dshRuntime = await discoverExactDshRuntime(cacheHome, dshVersion) + const graph = await parseInstalledNodeModulesGraph( + join(dshHome, 'profiles', PROFILE), + rootPackage, + { + hostNodeModulesDirectory: dshRuntime.nodeModulesDirectory, + hostRuntimeSource: 'dsh-process', + hostRuntimePackage: dshRuntime.package, + hostRuntimePackageDirectory: dshRuntime.packageDirectory, + }, + ) + if (graph.digest === undefined) return {} + const gaps = graphGapPartitions(graph) + const contracts = profileResolvedPeerContracts ?? graph.rootPeerContracts?.map(contract => ({ + ...contract, + staticUsage: 'scan-incomplete' as const, + })) + const concretelyResolvedRootPeers = new Set((contracts ?? []) + .filter(contract => contract.status === 'satisfied' || contract.status === 'mismatched') + .map(contract => contract.name)) + const effectiveRequiredGaps = gaps.required.filter(gap => !( + gap.from === graph.rootNodeId + && gap.kind === 'peer' + && concretelyResolvedRootPeers.has(gap.name) + )) + const requiredGaps = graphGaps({ unresolved: effectiveRequiredGaps }) + const optionalGaps = graphGaps({ unresolved: gaps.optional }) + return { + runtimeGraph: { + digest: graph.digest, + nodes: graph.nodes.length, + edges: graph.edges.length, + unresolved: effectiveRequiredGaps.length, + ...(requiredGaps === undefined ? {} : { unresolvedDependencies: requiredGaps }), + ...(gaps.optional.length === 0 ? {} : { + optionalUnavailable: gaps.optional.length, + ...(optionalGaps === undefined ? {} : { optionalUnavailableDependencies: optionalGaps }), + }), + pluginPeerContracts: pluginPeerContractEvidence(contracts), + ...(graph.hostRuntime === undefined ? {} : { + hostRuntime: { + source: graph.hostRuntime.source, + resolvedNodes: graph.hostRuntime.resolvedNodes, + ...(graph.hostRuntime.package === undefined ? {} : { dshVersion: graph.hostRuntime.package.version }), + }, + }), + }, + } + } catch (error: unknown) { + return { + runtimeGraphError: bounded(error instanceof Error ? error.message : String(error), 512), + } + } +} + +async function resolutionEvidence( + dshHome: string, + rootPackage: { name: string, version: string }, + dshVersion: string, + cacheHome: string, + profileResolvedPeerContracts?: readonly ObservedPeerContract[], +): Promise { + const [profile, runtime] = await Promise.all([ + profileResolutionEvidence(dshHome), + runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts), + ]) + return { ...profile, ...runtime } +} + function finishReport(report: DshInstallObservationReport, result: DshInstallObservationResult, reason: string): DshInstallObservationReport { report.completedAt = new Date().toISOString() report.result = result @@ -790,9 +1433,60 @@ function finishReport(report: DshInstallObservationReport, result: DshInstallObs return report } +/** + * A load success is necessary but not enough for the compatibility claim. The + * final verdict also requires a complete required-edge graph and a direct + * plugin peer contract that the observer could actually evaluate. + */ +function finalCompatibilityConclusion( + resolution: DshInstallObservationReport['resolution'], +): { result: DshInstallObservationResult, reason: string } { + const graph = resolution.runtimeGraph + if (graph === undefined) { + return { + result: 'unknown', + reason: resolution.runtimeGraphError === undefined + ? 'the exact artifact installed and loaded, but the effective DSH runtime graph was not established' + : `the exact artifact installed and loaded, but the effective DSH runtime graph could not be established: ${resolution.runtimeGraphError}`, + } + } + const contracts = graph.pluginPeerContracts + const firstIssue = contracts.issues?.[0] + if (contracts.mismatched > 0 || contracts.missing > 0) { + const detail = firstIssue === undefined + ? `${contracts.mismatched + contracts.missing} required plugin peer contract(s) do not match the DSH runtime` + : firstIssue.status === 'missing' + ? `${firstIssue.name}@${firstIssue.required} was not resolved by the DSH runtime (${firstIssue.staticUsage})` + : `${firstIssue.name}@${firstIssue.resolvedVersion ?? 'unknown'} does not satisfy ${firstIssue.required} (${firstIssue.staticUsage})` + return { + result: 'peer-contract-incompatible', + reason: `the exact artifact installed and loaded, but ${detail}`, + } + } + if (graph.unresolved > 0) { + return { + result: 'unknown', + reason: `the exact artifact installed and loaded, but the effective DSH runtime graph has ${graph.unresolved} required unresolved edge(s)`, + } + } + if (contracts.indeterminate > 0) { + return { + result: 'unknown', + reason: `the exact artifact installed and loaded, but ${contracts.indeterminate} required plugin peer range(s) could not be evaluated safely`, + } + } + return { + result: 'compatible', + reason: 'the exact artifact installed, registered, loaded, and satisfied its direct peer contracts under the requested DSH version', + } +} + export async function observeDshPluginInstall(options: DshInstallObservationOptions): Promise { const spec = parseNpmSpec(options.packageSpec) if (!EXACT_VERSION.test(options.dshVersion)) throw new Error('DSH version must be an exact semantic version') + if (options.caseId !== undefined && !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.caseId)) { + throw new Error('DSH install observation caseId must be a short lowercase label') + } const allowedBuilds = normalizeAllowedBuilds(options.allowedBuilds) if (!options.allowExecution) throw new Error('DSH install observation requires explicit execution consent') if (!['github-actions-hosted-runner', 'firecracker', 'other'].includes(options.isolationProvider)) { @@ -816,6 +1510,7 @@ export async function observeDshPluginInstall(options: DshInstallObservationOpti scope: 'install-and-load-behavior', startedAt, completedAt: startedAt, + ...(options.caseId === undefined ? {} : { caseId: options.caseId }), dshVersion: options.dshVersion, runtime: { platform: process.platform, @@ -839,6 +1534,7 @@ export async function observeDshPluginInstall(options: DshInstallObservationOpti }, observations: { install: emptyTraceObservation(), load: emptyTraceObservation() }, filesystem: { install: emptyFilesystemDiff(), load: emptyFilesystemDiff() }, + resolution: {}, result: 'unknown', reason: 'observation did not complete', boundary: { @@ -981,6 +1677,7 @@ export async function observeDshPluginInstall(options: DshInstallObservationOpti const afterInstall = await snapshotSandbox(sandboxRoot, environment) report.observations.install = await readTrace(installTracePath, sandboxRoot) report.filesystem.install = diffSnapshots(beforeInstall, afterInstall) + report.resolution = await resolutionEvidence(environment.DSH_HOME as string, spec, options.dshVersion, environment.XDG_CACHE_HOME as string) report.stages.install = commandStage(installResult) if (installResult.timedOut || installResult.outputExceeded || installResult.launchError !== undefined) { return finishReport(report, 'unknown', 'the plugin install did not produce a bounded command result') @@ -1003,12 +1700,19 @@ export async function observeDshPluginInstall(options: DshInstallObservationOpti : { status: 'failed', detail: `the DSH profile did not register ${spec.name}` } if (!registered) return finishReport(report, 'install-failed', 'DSH accepted the install command but did not register the plugin bundle') + const loadProbe = await writeProfileLoadProbe( + environment.DSH_HOME as string, + spec.name, + options.dshVersion, + pnpmCommand, + artifact.requiredPeerDependencies, + ) const loadTracePath = join(traceDirectory, 'load.strace') const loadResult = await runSafely(runner, { phase: 'load', - command: pnpmCommand, - args: dshArgs(options.dshVersion, ['--profile', PROFILE, '--dump-config']), - cwd: artifactDirectory, + command: process.execPath, + args: [loadProbe.path], + cwd: loadProbe.profileDirectory, env: scriptsEnvironment, timeoutMs, sandboxRoot, @@ -1017,6 +1721,20 @@ export async function observeDshPluginInstall(options: DshInstallObservationOpti const afterLoad = await snapshotSandbox(sandboxRoot, environment) report.observations.load = await readTrace(loadTracePath, sandboxRoot) report.filesystem.load = diffSnapshots(afterInstall, afterLoad) + const profilePeerContracts = await readProfilePeerContracts( + loadProbe.peerResolutionPath, + artifact.requiredPeerDependencies, + sandboxRoot, + ) + // Loading may trigger one final DSH profile reconciliation. Preserve the + // final resolved graph rather than only the state immediately after add. + report.resolution = await resolutionEvidence( + environment.DSH_HOME as string, + spec, + options.dshVersion, + environment.XDG_CACHE_HOME as string, + profilePeerContracts, + ) report.stages.load = commandStage(loadResult) if (loadResult.timedOut || loadResult.outputExceeded || loadResult.launchError !== undefined) { return finishReport(report, 'unknown', 'the plugin load did not produce a bounded command result') @@ -1026,7 +1744,8 @@ export async function observeDshPluginInstall(options: DshInstallObservationOpti return finishReport(report, 'unknown', 'the load command ran without readable trace evidence') } if (loadResult.code !== 0) return finishReport(report, 'load-failed', 'the traced DSH profile load command failed') - return finishReport(report, 'compatible', 'the exact artifact installed, registered and loaded under the requested DSH version') + const conclusion = finalCompatibilityConclusion(report.resolution) + return finishReport(report, conclusion.result, conclusion.reason) } catch (error: unknown) { const detail = bounded(error instanceof Error ? error.message : String(error)) const currentStage = (['runtime', 'profile', 'install', 'registration', 'load'] as const) @@ -1045,6 +1764,7 @@ export function renderDshInstallObservation(report: DshInstallObservationReport) const lifecycle = report.artifact.lifecycleScripts.length === 0 ? 'none' : report.artifact.lifecycleScripts.join(', ') const lines = [ 'DSH isolated install observation', + ...(report.caseId === undefined ? [] : [`Case: ${report.caseId}`]), `Artifact: ${report.artifact.name}@${report.artifact.version}${report.artifact.sha256 === undefined ? '' : ` (sha256:${report.artifact.sha256.slice(0, 12)}…)`}`, `DSH: ${report.dshVersion}`, `Runtime: Node ${report.runtime.nodeVersion} (${report.runtime.platform}/${report.runtime.architecture}), pnpm ${report.runtime.packageManager.version ?? 'unknown'}`, @@ -1057,11 +1777,24 @@ export function renderDshInstallObservation(report: DshInstallObservationReport) `Install evidence: ${install.processes.length} process, ${install.network.length} network, ${install.fileWrites.length} file-write event(s); trace ${install.coverage.status}`, `Load evidence: ${load.processes.length} process, ${load.network.length} network, ${load.fileWrites.length} file-write event(s); trace ${load.coverage.status}`, `Final filesystem delta: install +${report.filesystem.install.totals.created} ~${report.filesystem.install.totals.modified} -${report.filesystem.install.totals.deleted}; load +${report.filesystem.load.totals.created} ~${report.filesystem.load.totals.modified} -${report.filesystem.load.totals.deleted}`, + `Resolved profile graph: ${report.resolution.profileLockfile?.graphDigest ?? 'not established'}${report.resolution.profileLockfile === undefined ? '' : ` (lock sha256:${report.resolution.profileLockfile.sha256.slice(0, 12)}…)`}`, + `Effective DSH runtime graph: ${report.resolution.runtimeGraph?.digest ?? 'not established'}${report.resolution.runtimeGraph === undefined ? '' : ` (${report.resolution.runtimeGraph.nodes} nodes, ${report.resolution.runtimeGraph.edges} edges, ${report.resolution.runtimeGraph.unresolved} required unresolved${report.resolution.runtimeGraph.optionalUnavailable === undefined ? '' : `, ${report.resolution.runtimeGraph.optionalUnavailable} optional unavailable`})`}`, + `Plugin peer contracts: ${report.resolution.runtimeGraph === undefined + ? 'not established' + : `${report.resolution.runtimeGraph.pluginPeerContracts.satisfied}/${report.resolution.runtimeGraph.pluginPeerContracts.declared} satisfied; ${report.resolution.runtimeGraph.pluginPeerContracts.mismatched} mismatched, ${report.resolution.runtimeGraph.pluginPeerContracts.missing} missing, ${report.resolution.runtimeGraph.pluginPeerContracts.indeterminate} indeterminate`}`, + `Effective graph collector: ${report.resolution.runtimeGraphError ?? 'captured'}`, '', ] for (const [name, stage] of Object.entries(report.stages)) { lines.push(` ${name}: ${stage.status}${stage.detail === undefined ? '' : ` (${stage.detail})`}`) } + const peerIssues = report.resolution.runtimeGraph?.pluginPeerContracts.issues ?? [] + if (peerIssues.length > 0) { + lines.push('', 'Direct peer-contract findings:') + for (const issue of peerIssues) { + lines.push(` ${issue.name}: ${issue.status}; requires ${issue.required}${issue.resolvedVersion === undefined ? '' : `, resolved ${issue.resolvedVersion}`}; static use ${issue.staticUsage}`) + } + } lines.push('', report.boundary.note) return `${lines.join('\n')}\n` } diff --git a/src/dsh-install-plan.ts b/src/dsh-install-plan.ts index eda9576..36d93ad 100644 --- a/src/dsh-install-plan.ts +++ b/src/dsh-install-plan.ts @@ -1,4 +1,14 @@ +import { + createDshCompatibilityContractFingerprint, + createDshCompatibilityStaticFingerprint, + dshCompatibilityCaseId, + emptyDshCompatibilityLedger, + parseDshCompatibilityLedger, + type DshCompatibilityExpectedCase, + type DshCompatibilityLedger, +} from './dsh-compatibility-ledger.js' import { parseNpmSpec } from './npm.js' +import { satisfiesSemverRange } from './semver.js' export const DSH_INSTALL_TARGETS_SCHEMA = 'upstream-radar.dsh-install-targets/v1alpha1' as const @@ -6,6 +16,8 @@ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/ const DSH_TARGET_ID = 'deepseek-harness' const DSH_PACKAGE = '@deepseek-ai/dsh' const MAX_TARGETS = 50 +const MAX_RUNTIME_PROFILES = 8 +const DEFAULT_REFRESH_AFTER_HOURS = 7 * 24 export interface DshInstallTarget { id: string @@ -13,10 +25,18 @@ export interface DshInstallTarget { reason: string observerTargetId?: string allowedBuilds?: string[] + runtimeProfiles?: string[] +} + +export interface DshInstallRuntimeProfile { + id: string + nodeMajor: number } export interface DshInstallTargets { schema: typeof DSH_INSTALL_TARGETS_SCHEMA + refreshAfterHours: number + runtimeProfiles: DshInstallRuntimeProfile[] plugins: DshInstallTarget[] } @@ -24,7 +44,7 @@ export interface DshInstallPlan { run: boolean dshVersion?: string matrix: { - include: Array<{ id: string; plugin: string; allowedBuilds: string }> + include: DshCompatibilityExpectedCase[] } triggers: string[] reason: string @@ -45,6 +65,34 @@ function boundedString(value: unknown, label: string, maximum: number): string { export function parseDshInstallTargets(input: unknown): DshInstallTargets { const root = record(input, 'DSH install targets') if (root.schema !== DSH_INSTALL_TARGETS_SCHEMA) throw new Error(`DSH install targets schema must be ${DSH_INSTALL_TARGETS_SCHEMA}`) + const refreshAfterHours = root.refreshAfterHours === undefined + ? DEFAULT_REFRESH_AFTER_HOURS + : Number(root.refreshAfterHours) + if (!Number.isSafeInteger(refreshAfterHours) || refreshAfterHours < 1 || refreshAfterHours > 90 * 24) { + throw new Error('DSH install target refreshAfterHours must be an integer between 1 and 2160') + } + const rawRuntimeProfiles = root.runtimeProfiles === undefined + ? [{ id: 'node22', nodeMajor: 22 }] + : root.runtimeProfiles + if (!Array.isArray(rawRuntimeProfiles) || rawRuntimeProfiles.length === 0 || rawRuntimeProfiles.length > MAX_RUNTIME_PROFILES) { + throw new Error(`DSH install targets runtimeProfiles must contain between 1 and ${MAX_RUNTIME_PROFILES} profiles`) + } + const runtimeProfileIds = new Set() + const nodeMajors = new Set() + const runtimeProfiles = rawRuntimeProfiles.map((value, index): DshInstallRuntimeProfile => { + const item = record(value, `runtimeProfiles[${index}]`) + const id = boundedString(item.id, `runtimeProfiles[${index}].id`, 48) + if (!/^[a-z0-9][a-z0-9._-]{0,47}$/.test(id)) throw new Error(`runtimeProfiles[${index}].id must be a short lowercase label`) + if (runtimeProfileIds.has(id)) throw new Error(`duplicate DSH install runtime profile id: ${id}`) + runtimeProfileIds.add(id) + if (!Number.isSafeInteger(item.nodeMajor) || (item.nodeMajor as number) < 16 || (item.nodeMajor as number) > 40) { + throw new Error(`runtimeProfiles[${index}].nodeMajor must be a supported Node.js major version`) + } + const nodeMajor = item.nodeMajor as number + if (nodeMajors.has(nodeMajor)) throw new Error(`duplicate DSH install runtime Node major: ${nodeMajor}`) + nodeMajors.add(nodeMajor) + return { id, nodeMajor } + }) if (!Array.isArray(root.plugins) || root.plugins.length === 0 || root.plugins.length > MAX_TARGETS) { throw new Error(`DSH install targets must contain between 1 and ${MAX_TARGETS} plugins`) } @@ -81,16 +129,31 @@ export function parseDshInstallTargets(input: unknown): DshInstallTargets { }) if (new Set(allowedBuilds).size !== allowedBuilds.length) throw new Error(`plugins[${index}].allowedBuilds must be unique`) allowedBuilds.sort() + const rawRuntimeProfileIds = item.runtimeProfiles + if (rawRuntimeProfileIds !== undefined && (!Array.isArray(rawRuntimeProfileIds) || rawRuntimeProfileIds.length === 0 || rawRuntimeProfileIds.length > MAX_RUNTIME_PROFILES)) { + throw new Error(`plugins[${index}].runtimeProfiles must be an array of between 1 and ${MAX_RUNTIME_PROFILES} runtime profile ids`) + } + const selectedRuntimeProfiles = rawRuntimeProfileIds === undefined + ? undefined + : rawRuntimeProfileIds.map((value, profileIndex) => { + const profileId = boundedString(value, `plugins[${index}].runtimeProfiles[${profileIndex}]`, 48) + if (!runtimeProfileIds.has(profileId)) throw new Error(`plugins[${index}].runtimeProfiles[${profileIndex}] is not a configured runtime profile`) + return profileId + }) + if (selectedRuntimeProfiles !== undefined && new Set(selectedRuntimeProfiles).size !== selectedRuntimeProfiles.length) { + throw new Error(`plugins[${index}].runtimeProfiles must be unique`) + } return { id, spec, reason, ...(observerTargetId === undefined ? {} : { observerTargetId }), ...(allowedBuilds.length === 0 ? {} : { allowedBuilds }), + ...(selectedRuntimeProfiles === undefined ? {} : { runtimeProfiles: selectedRuntimeProfiles }), } }) plugins.sort((left, right) => left.id.localeCompare(right.id)) - return { schema: DSH_INSTALL_TARGETS_SCHEMA, plugins } + return { schema: DSH_INSTALL_TARGETS_SCHEMA, refreshAfterHours, runtimeProfiles, plugins } } interface PackageCoordinate { @@ -136,69 +199,246 @@ function observedDshVersion(stateInput: unknown): string | undefined { return coordinate?.name === DSH_PACKAGE ? coordinate.version : undefined } -export function buildDshInstallPlan(corpusInput: unknown, stateInput: unknown, reportInput: unknown): DshInstallPlan { +function observedTarget(stateInput: unknown, targetId: string): Record | undefined { + if (typeof stateInput !== 'object' || stateInput === null || Array.isArray(stateInput)) return undefined + const targets = (stateInput as Record).targets + if (typeof targets !== 'object' || targets === null || Array.isArray(targets)) return undefined + const target = (targets as Record)[targetId] + if (typeof target !== 'object' || target === null || Array.isArray(target)) return undefined + return target as Record +} + +function pick(value: unknown, keys: readonly string[]): Record | undefined { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined + const record = value as Record + const result = Object.fromEntries(keys + .filter(key => record[key] !== undefined) + .map(key => [key, record[key]])) + return Object.keys(result).length === 0 ? undefined : result +} + +/** + * Retain only static facts that can invalidate behavior evidence. In + * particular, observedAt is intentionally absent: a steady scan must not make + * every dynamic cell stale merely because the static collector ran again. + */ +function staticTargetEvidence(stateInput: unknown, targetId: string): unknown { + const target = observedTarget(stateInput, targetId) + if (target === undefined) return undefined + const source = pick(target.source, ['repository', 'commit', 'packagePath', 'lockfile']) + const manifest = pick(target.manifest, ['name', 'version', 'engines', 'peerDependencies', 'dependencies', 'optionalDependencies', 'dsh']) + const packageCoordinate = pick(target.package, ['name', 'version', 'integrity', 'artifactDigest', 'tarball']) + const alignment = pick(target.alignment, ['status', 'checks']) + const graph = pick(target.graph, ['digest', 'rootNodeId', 'source', 'unresolved']) + return { + ...(source === undefined ? {} : { source }), + ...(manifest === undefined ? {} : { manifest }), + ...(packageCoordinate === undefined ? {} : { package: packageCoordinate }), + ...(graph === undefined ? {} : { graph }), + ...(alignment === undefined ? {} : { alignment }), + } +} + +function resolvedPluginCoordinate(target: DshInstallTarget, stateInput: unknown): string { + const expected = parseNpmSpec(target.spec) + const observed = target.observerTargetId === undefined + ? undefined + : observedPackage(stateInput, target.observerTargetId) + return observed?.name === expected.name ? `${observed.name}@${observed.version}` : target.spec +} + +function changeForTarget(changes: readonly { value: Record }[], targetId: string): Record | undefined { + return changes.find(({ value }) => value.targetId === targetId)?.value +} + +function changedPluginTargets( + corpus: DshInstallTargets, + changes: readonly { value: Record }[], +): Set { + const changed = new Set() + for (const target of corpus.plugins) { + if (target.observerTargetId === undefined) continue + const change = changeForTarget(changes, target.observerTargetId) + if (change === undefined) continue + const before = snapshotPackage(change.previous) + const after = snapshotPackage(change.current) + const expected = parseNpmSpec(target.spec) + if (coordinateChanged(before, after) && after?.name === expected.name) changed.add(target.id) + } + return changed +} + +function candidateProfiles( + corpus: DshInstallTargets, + target: DshInstallTarget, + plugin: string, + ledger: DshCompatibilityLedger, +): DshInstallRuntimeProfile[] { + const configured = new Map(corpus.runtimeProfiles.map(profile => [profile.id, profile])) + const selected = new Map() + for (const id of target.runtimeProfiles ?? [corpus.runtimeProfiles[0]?.id]) { + if (id === undefined) continue + const profile = configured.get(id) + if (profile !== undefined) selected.set(profile.id, profile) + } + + // A static Node engine check may stop a dynamic run before third-party code + // executes. Its result is useful input: try every configured, potentially + // matching runtime once, instead of calling a Node 22 failure "the plugin is + // incompatible" without checking the package's stated runtime. + if (target.runtimeProfiles === undefined) { + const runtimeMismatches = ledger.entries.filter(entry => ( + entry.targetId === target.id + && entry.plugin === plugin + && entry.result === 'runtime-incompatible' + && entry.artifact.nodeEngine !== undefined + )) + for (const entry of runtimeMismatches) { + const engine = entry.artifact.nodeEngine + if (engine === undefined) continue + for (const profile of corpus.runtimeProfiles) { + if (selected.has(profile.id)) continue + const potential = satisfiesSemverRange(`${profile.nodeMajor}.999.999`, engine) + if (potential !== false) selected.set(profile.id, profile) + } + } + } + return [...selected.values()] +} + +function isStale(entry: DshCompatibilityLedger['entries'][number], refreshAfterHours: number, now: Date): boolean { + const observedAt = Date.parse(entry.observedAt) + if (!Number.isFinite(observedAt)) return true + const effectiveHours = entry.result === 'unknown' ? Math.min(refreshAfterHours, 24) : refreshAfterHours + return now.getTime() - observedAt >= effectiveHours * 60 * 60 * 1_000 +} + +/** + * A successful install/load alone does not establish the dependency relation + * that Radar promises to monitor. Keep such a cell pending until its final + * DSH profile plus host-runtime graph is complete. Failed installs deliberately + * do not use this rule: there may be no final profile to read. + */ +function hasCompleteResolutionEvidence(entry: DshCompatibilityLedger['entries'][number]): boolean { + if (entry.result !== 'compatible') return true + const graph = entry.resolution?.runtimeGraph + const contracts = graph?.pluginPeerContracts + return graph?.digest !== undefined + && graph.unresolved === 0 + && contracts !== undefined + && contracts.mismatched === 0 + && contracts.missing === 0 + && contracts.indeterminate === 0 +} + +/** + * Reconcile the desired current compatibility matrix with durable evidence. + * Upstream diffs accelerate a retest, but do not decide whether a cell gets + * tested: missing, stale, or static/contract-invalidated cells are selected + * even when neither package published a new version. + */ +export function buildDshInstallPlan( + corpusInput: unknown, + stateInput: unknown, + reportInput: unknown, + ledgerInput: unknown = emptyDshCompatibilityLedger(), + now = new Date(), +): DshInstallPlan { const corpus = parseDshInstallTargets(corpusInput) + const ledger = parseDshCompatibilityLedger(ledgerInput) + if (!Number.isFinite(now.getTime())) throw new Error('DSH install plan requires a valid current time') const report = record(reportInput, 'observer report') if (!Array.isArray(report.changes)) throw new Error('observer report changes must be an array') const changes = report.changes .map((value, index) => ({ value: record(value, `changes[${index}]`), index })) .filter(({ value }) => value.meaningful === true && typeof value.targetId === 'string') - const dshChange = changes.find(({ value }) => value.targetId === DSH_TARGET_ID) - const dshBefore = dshChange === undefined ? undefined : snapshotPackage(dshChange.value.previous) - const dshAfter = dshChange === undefined ? undefined : snapshotPackage(dshChange.value.current) + const dshChange = changeForTarget(changes, DSH_TARGET_ID) + const dshBefore = dshChange === undefined ? undefined : snapshotPackage(dshChange.previous) + const dshAfter = dshChange === undefined ? undefined : snapshotPackage(dshChange.current) const dshPackageChanged = dshAfter?.name === DSH_PACKAGE && coordinateChanged(dshBefore, dshAfter) const dshVersion = dshPackageChanged ? dshAfter.version : observedDshVersion(stateInput) - const selected = new Map() + const pluginChanges = changedPluginTargets(corpus, changes) const triggers = new Set() + if (dshPackageChanged) triggers.add(DSH_TARGET_ID) + for (const target of corpus.plugins) { + if (pluginChanges.has(target.id) && target.observerTargetId !== undefined) triggers.add(target.observerTargetId) + } - if (dshPackageChanged) { - triggers.add(DSH_TARGET_ID) - for (const target of corpus.plugins) { - const expected = parseNpmSpec(target.spec) - const observed = target.observerTargetId === undefined - ? undefined - : observedPackage(stateInput, target.observerTargetId) - const plugin = observed?.name === expected.name - ? `${observed.name}@${observed.version}` - : target.spec - selected.set(target.id, { id: target.id, plugin, allowedBuilds: target.allowedBuilds?.join(',') ?? '' }) + if (dshVersion === undefined) { + return { + run: false, + matrix: { include: [] }, + triggers: [...triggers].sort(), + reason: 'no exact observed DSH release is available, so the compatibility matrix cannot be formed', } } + const dshStatic = staticTargetEvidence(stateInput, DSH_TARGET_ID) + const selected = new Map() + let desiredCells = 0 for (const target of corpus.plugins) { - if (target.observerTargetId === undefined) continue - const change = changes.find(({ value }) => value.targetId === target.observerTargetId) - if (change === undefined) continue - const before = snapshotPackage(change.value.previous) - const after = snapshotPackage(change.value.current) - if (!coordinateChanged(before, after) || after === undefined) continue - const expected = parseNpmSpec(target.spec) - if (after.name !== expected.name) continue - triggers.add(target.observerTargetId) - selected.set(target.id, { - id: target.id, - plugin: `${after.name}@${after.version}`, - allowedBuilds: target.allowedBuilds?.join(',') ?? '', + const plugin = resolvedPluginCoordinate(target, stateInput) + const staticFingerprint = createDshCompatibilityStaticFingerprint({ + plugin, + dshVersion, + pluginStatic: target.observerTargetId === undefined ? undefined : staticTargetEvidence(stateInput, target.observerTargetId), + dshStatic, }) + const allowedBuilds = target.allowedBuilds ?? [] + for (const runtimeProfile of candidateProfiles(corpus, target, plugin, ledger)) { + desiredCells += 1 + const id = dshCompatibilityCaseId(target.id, runtimeProfile.id) + const contractFingerprint = createDshCompatibilityContractFingerprint({ + plugin, + dshVersion, + nodeMajor: runtimeProfile.nodeMajor, + allowedBuilds, + }) + const previous = ledger.entries.find(entry => entry.caseId === id) + const reasons = new Set() + if (dshPackageChanged) reasons.add('dsh-coordinate-changed') + if (pluginChanges.has(target.id)) reasons.add('plugin-coordinate-changed') + if (previous === undefined) reasons.add('missing-evidence') + else { + if (previous.plugin !== plugin || previous.dshVersion !== dshVersion || previous.runtime.nodeMajor !== runtimeProfile.nodeMajor) { + reasons.add('exact-coordinate-changed') + } + if (previous.staticFingerprint !== staticFingerprint) reasons.add('static-evidence-changed') + if (previous.contractFingerprint !== contractFingerprint) reasons.add('execution-contract-changed') + if (isStale(previous, corpus.refreshAfterHours, now)) reasons.add('stale-evidence') + if (!hasCompleteResolutionEvidence(previous)) { + const graph = previous.resolution?.runtimeGraph + if (graph?.digest === undefined) reasons.add('runtime-graph-missing') + else if (graph.unresolved > 0) reasons.add('runtime-graph-incomplete') + else if (graph.pluginPeerContracts === undefined) reasons.add('peer-contract-not-evaluated') + else if (graph.pluginPeerContracts.indeterminate > 0) reasons.add('peer-contract-indeterminate') + else reasons.add('peer-contract-incomplete') + } + } + if (reasons.size === 0) continue + selected.set(id, { + id, + targetId: target.id, + plugin, + dshVersion, + nodeMajor: runtimeProfile.nodeMajor, + allowedBuilds: allowedBuilds.join(','), + staticFingerprint, + contractFingerprint, + reasons: [...reasons].sort(), + }) + } } const include = [...selected.values()].sort((left, right) => left.id.localeCompare(right.id)) if (include.length === 0) { return { run: false, - ...(dshVersion === undefined ? {} : { dshVersion }), - matrix: { include: [] }, - triggers: [], - reason: 'no maintained install target changed its exact published coordinate', - } - } - if (dshVersion === undefined) { - return { - run: false, + dshVersion, matrix: { include: [] }, triggers: [...triggers].sort(), - reason: 'a maintained plugin changed, but no exact observed DSH release is available', + reason: `all ${desiredCells} active DSH compatibility cells have fresh evidence`, } } return { @@ -206,8 +446,6 @@ export function buildDshInstallPlan(corpusInput: unknown, stateInput: unknown, r dshVersion, matrix: { include }, triggers: [...triggers].sort(), - reason: dshPackageChanged - ? `the official DSH package changed to ${DSH_PACKAGE}@${dshVersion}` - : 'a maintained plugin changed its exact published coordinate', + reason: `the compatibility ledger requires ${include.length} isolated recheck${include.length === 1 ? '' : 's'} across ${desiredCells} active cells`, } } diff --git a/src/dsh-plugin.ts b/src/dsh-plugin.ts index 0c4b6a8..df860c4 100644 --- a/src/dsh-plugin.ts +++ b/src/dsh-plugin.ts @@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' import { renderAgentAnalysisGroupPrompt, renderAgentAnalysisPrompt } from './dsh-analysis.js' import { - discoverDshRuntimeNodeModulesDirectory, + discoverDshRuntimeHostNodeModulesDirectory, discoverDshRuntimePackage, discoverDshRuntimePackageDirectory, } from './dsh-runtime.js' @@ -565,7 +565,11 @@ export function apply(ctx: DshRadarContext, config: Config = {}): void { const feishuSecret = process.env.UPSTREAM_RADAR_FEISHU_SECRET?.trim() || undefined const dshHostNodeModulesDirectory = config.profile === undefined || config.refreshProfile === false ? undefined - : discoverDshRuntimeNodeModulesDirectory() + // A package-local node_modules directory is enough for a flat npm + // installation, but pnpm's actual dependency links may resolve through + // the enclosing .pnpm virtual store. The host-plane variant deliberately + // includes that controlled outer directory. + : discoverDshRuntimeHostNodeModulesDirectory() const dshHostRuntimePackage = config.profile === undefined || config.refreshProfile === false ? undefined : discoverDshRuntimePackage() diff --git a/src/dsh-runtime.ts b/src/dsh-runtime.ts index c2e3051..787c84b 100644 --- a/src/dsh-runtime.ts +++ b/src/dsh-runtime.ts @@ -78,6 +78,29 @@ function runtimeNodeModulesDirectory(root: string): string | undefined { return undefined } +/** + * pnpm's logical host plane can point into an outer `.pnpm` virtual store. + * Return that enclosing `node_modules` root when present so a read-only graph + * collector can resolve both the links and their physical package targets. + */ +function runtimeHostNodeModulesDirectory(root: string): string | undefined { + let cursor = root + for (let depth = 0; depth < MAX_ANCESTORS; depth += 1) { + if (basename(cursor) === 'node_modules') { + try { + const resolved = realpathSync(cursor) + if (statSync(join(resolved, '.pnpm')).isDirectory()) return resolved + } catch { + // Keep walking to the ordinary runtime-plane fallback below. + } + } + const parent = dirname(cursor) + if (parent === cursor) break + cursor = parent + } + return undefined +} + /** * Find the DSH CLI's own dependency plane without importing or executing it. * The entrypoint is normally process.argv[1] inside a running DSH process. @@ -91,6 +114,20 @@ export function discoverDshRuntimeNodeModulesDirectory( return runtimeNodeModulesDirectory(root) } +/** + * Find the safe enclosing DSH host plane. For pnpm this includes the virtual + * store that logical package links resolve into; non-pnpm layouts use the + * ordinary runtime dependency plane. + */ +export function discoverDshRuntimeHostNodeModulesDirectory( + entrypoint = process.argv[1], +): string | undefined { + if (typeof entrypoint !== 'string' || entrypoint.trim() === '') return undefined + const root = packageRoot(entrypoint) + if (root === undefined) return undefined + return runtimeHostNodeModulesDirectory(root) ?? runtimeNodeModulesDirectory(root) +} + /** Read the exact DSH executable package without importing or executing DSH. */ export function discoverDshRuntimePackage( entrypoint = process.argv[1], diff --git a/src/index.ts b/src/index.ts index 732421a..e3b46f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ export { } from './dependency-index.js' export { parseInstalledNodeModulesGraph } from './installed-graph.js' export { + discoverDshRuntimeHostNodeModulesDirectory, discoverDshRuntimeNodeModulesDirectory, discoverDshRuntimePackage, discoverDshRuntimePackageDirectory, @@ -85,6 +86,12 @@ export { parseDshInstallTrace, renderDshInstallObservation, type DshInstallObservationOptions, + type DshInstallProfileLockfileEvidence, + type DshInstallPluginPeerContracts, + type DshInstallPeerContractIssue, + type DshInstallPeerContractRelation, + type DshInstallPeerStaticUsage, + type DshInstallRuntimeGraphEvidence, type DshInstallObservationReport, type DshInstallObservationResult, type InstallFilesystemDiff, @@ -99,11 +106,61 @@ export { type InstallTraceObservation, type InstallTraceProcess, } from './dsh-install-observation.js' +export { + DSH_COMPATIBILITY_LEDGER_SCHEMA, + createDshCompatibilityContractFingerprint, + createDshCompatibilityStaticFingerprint, + dshCompatibilityCaseId, + emptyDshCompatibilityLedger, + mergeDshCompatibilityLedger, + parseDshCompatibilityLedger, + renderDshCompatibilityLedgerMerge, + type DshCompatibilityExpectedCase, + type DshCompatibilityLedger, + type DshCompatibilityLedgerEntry, + type DshCompatibilityLedgerMerge, + type DshCompatibilityPeerContractIssue, + type DshCompatibilityPeerContractRelation, + type DshCompatibilityPluginPeerContracts, + type DshCompatibilityProfileLockfile, + type DshCompatibilityRuntimeGraph, + type DshCompatibilityTransition, + type DshCompatibilityTransitionStatus, +} from './dsh-compatibility-ledger.js' +export { + DSH_COMPATIBILITY_ISSUE_LABELS, + buildDshCompatibilityIssuePlan, + dshCompatibilityIssueCaseId, + dshCompatibilityIssueMarker, + renderDshCompatibilityIssue, + renderDshCompatibilityResolution, + type DshCompatibilityExistingIssue, + type DshCompatibilityIssueAction, + type DshCompatibilityIssuePlan, +} from './dsh-compatibility-issues.js' +export { + DSH_COMPATIBILITY_IR_SCHEMA, + DSH_COMPATIBILITY_REVERSE_INDEX_SCHEMA, + buildDshCompatibilityIR, + buildDshCompatibilityReverseIndex, + parseDshCompatibilityIR, + parseDshCompatibilityReverseIndex, + type DshCompatibilityIR, + type DshCompatibilityIrCell, + type DshCompatibilityIrPlugin, + type DshCompatibilityIrRelation, + type DshCompatibilityIrRuntime, + type DshCompatibilityPeerStatus, + type DshCompatibilityReverseDependency, + type DshCompatibilityReverseImpact, + type DshCompatibilityReverseIndex, +} from './dsh-compatibility-ir.js' export { DSH_INSTALL_TARGETS_SCHEMA, buildDshInstallPlan, parseDshInstallTargets, type DshInstallPlan, + type DshInstallRuntimeProfile, type DshInstallTarget, type DshInstallTargets, } from './dsh-install-plan.js' @@ -378,6 +435,8 @@ export { type PluginInstallation, type ProjectInventory, type ProjectReference, + type RootPeerContract, + type RootPeerContractStatus, type RadarEvent, type RadarIncidentMute, type RadarIncidentTriage, diff --git a/src/installed-graph.ts b/src/installed-graph.ts index d26c974..7ec4771 100644 --- a/src/installed-graph.ts +++ b/src/installed-graph.ts @@ -2,6 +2,7 @@ import { readFile, realpath } from 'node:fs/promises' import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' import { parsePackageManifestSnapshot } from './inventory.js' import { dependencyGraphDigest } from './graph.js' +import { satisfiesSemverRange } from './semver.js' import { DEPENDENCY_GRAPH_SCHEMA, type DependencyEdge, @@ -11,6 +12,7 @@ import { type DependencyNode, type PackageCoordinate, type PackageManifestSnapshot, + type RootPeerContract, } from './radar-types.js' const MAX_MANIFEST_BYTES = 8 * 1024 * 1024 @@ -90,10 +92,10 @@ function nodeId(rootDirectory: string, packageDirectory: string): string { return value.split(sep).join('/') } -function hostNodeId(hostNodeModulesDirectory: string, packageDirectory: string): string { - const value = relative(dirname(hostNodeModulesDirectory), resolve(packageDirectory)) +function hostNodeId(hostNodeModulesDirectoryReal: string, packageDirectoryReal: string): string { + const value = relative(dirname(hostNodeModulesDirectoryReal), resolve(packageDirectoryReal)) if (value === '' || value.startsWith(`..${sep}`) || value === '..' || isAbsolute(value)) { - throw new Error(`DSH host package path escapes the shared dependency plane: ${packageDirectory}`) + throw new Error(`DSH host package path escapes the shared dependency plane: ${packageDirectoryReal}`) } return `dsh-host/${value.split(sep).join('/')}` } @@ -128,9 +130,16 @@ async function readProfileManifest( async function readHostManifest( packageDirectory: string, hostNodeModulesDirectory: string, + hostNodeModulesDirectoryReal: string, ): Promise { const realDirectory = await realpath(packageDirectory) + if (!isLexicallyInside(hostNodeModulesDirectoryReal, realDirectory)) { + throw new Error(`DSH host package path escapes the shared dependency plane: ${packageDirectory}`) + } const manifestPath = await realpath(join(packageDirectory, 'package.json')) + if (!isLexicallyInside(hostNodeModulesDirectoryReal, manifestPath)) { + throw new Error(`DSH host package manifest escapes the shared dependency plane: ${packageDirectory}`) + } const contents = await readFile(manifestPath, 'utf8') if (Buffer.byteLength(contents) > MAX_MANIFEST_BYTES) { throw new Error(`DSH host package manifest exceeds the ${MAX_MANIFEST_BYTES} byte limit: ${packageDirectory}`) @@ -143,7 +152,7 @@ async function readHostManifest( } const manifest = parseInstalledManifestSnapshot(parsed) return { - id: hostNodeId(hostNodeModulesDirectory, packageDirectory), + id: hostNodeId(hostNodeModulesDirectoryReal, realDirectory), directory: realDirectory, source: 'dsh-host', manifest, @@ -197,23 +206,60 @@ async function findProfilePackage( } async function findHostPackage( + parentDirectory: string, dependencyName: string, hostNodeModulesDirectory: string, + hostNodeModulesDirectoryReal: string, ): Promise { if (!isPackageName(dependencyName)) return undefined - const dependencyDirectory = resolve(hostNodeModulesDirectory, ...dependencyName.split('/')) - if (!isLexicallyInside(hostNodeModulesDirectory, dependencyDirectory)) return undefined + let cursor: string try { - const target = await readHostManifest(dependencyDirectory, hostNodeModulesDirectory) - if (target.manifest.name !== dependencyName) { - throw new Error(`DSH host package manifest name does not match resolved dependency: ${dependencyName}`) - } - return target + cursor = await realpath(parentDirectory) } catch (error: unknown) { const code = (error as NodeJS.ErrnoException).code - if (code !== 'ENOENT' && code !== 'ENOTDIR') throw error - return undefined + if (code === 'ENOENT' || code === 'ENOTDIR') return undefined + throw error } + if (!isLexicallyInside(hostNodeModulesDirectoryReal, cursor)) { + try { + const adjacentNodeModules = await realpath(join(cursor, 'node_modules')) + if (adjacentNodeModules !== hostNodeModulesDirectoryReal) return undefined + cursor = hostNodeModulesDirectoryReal + } catch (error: unknown) { + const code = (error as NodeJS.ErrnoException).code + if (code === 'ENOENT' || code === 'ENOTDIR') return undefined + throw error + } + } + + /** + * pnpm resolves a package's dependencies from the package-local virtual + * `node_modules`, then walks outward. Looking only at the outer host plane + * misses the links beside `@deepseek-ai/dsh` itself, which is how `pnpm dlx` + * stores most of DSH's runtime dependencies. + */ + while (isLexicallyInside(hostNodeModulesDirectoryReal, cursor)) { + const dependencyDirectory = cursor === hostNodeModulesDirectoryReal + ? resolve(cursor, ...dependencyName.split('/')) + : resolve(cursor, 'node_modules', ...dependencyName.split('/')) + if (isLexicallyInside(hostNodeModulesDirectoryReal, dependencyDirectory)) { + try { + const target = await readHostManifest(dependencyDirectory, hostNodeModulesDirectory, hostNodeModulesDirectoryReal) + if (target.manifest.name !== dependencyName) { + throw new Error(`DSH host package manifest name does not match resolved dependency: ${dependencyName}`) + } + return target + } catch (error: unknown) { + const code = (error as NodeJS.ErrnoException).code + if (code !== 'ENOENT' && code !== 'ENOTDIR') throw error + } + } + if (cursor === hostNodeModulesDirectoryReal) break + const next = dirname(cursor) + if (next === cursor) break + cursor = next + } + return undefined } /** Read the package tree that DSH can actually resolve from its installed profile. */ @@ -257,12 +303,17 @@ export async function parseInstalledNodeModulesGraph( const packages = new Map([[root.id, root]]) const edges: DependencyEdge[] = [] const unresolved: NonNullable = [] + const rootPeerContracts: RootPeerContract[] = [] const queue = [root.id] + // A pnpm-installed DSH executable can keep its runtime dependencies behind + // package-local links in the outer virtual store. A plugin peer is allowed + // to be supplied by that verified DSH closure, but an ordinary plugin + // dependency is not: it must still resolve from the profile itself. + let runtimeRoot: InstalledPackage | undefined if (hostNodeModulesDirectoryReal !== undefined && options.hostRuntimePackage !== undefined) { const resolvedHostNodeModulesDirectory = hostNodeModulesDirectory if (resolvedHostNodeModulesDirectory === undefined) throw new Error('DSH host dependency plane is unexpectedly unavailable') const runtimePackageDirectory = options.hostRuntimePackageDirectory - let runtimeRoot: InstalledPackage | undefined if (runtimePackageDirectory !== undefined) { let runtimeDirectoryReal: string | undefined try { @@ -273,27 +324,33 @@ export async function parseInstalledNodeModulesGraph( } if (runtimeDirectoryReal !== undefined) { runtimeRoot = isLexicallyInside(hostNodeModulesDirectoryReal, runtimeDirectoryReal) - ? await readHostManifest(runtimePackageDirectory, resolvedHostNodeModulesDirectory) + ? await readHostManifest(runtimePackageDirectory, resolvedHostNodeModulesDirectory, hostNodeModulesDirectoryReal) : await readRuntimeManifest(runtimePackageDirectory) } } else { - runtimeRoot = await findHostPackage('@deepseek-ai/dsh', resolvedHostNodeModulesDirectory) + runtimeRoot = await findHostPackage( + resolvedHostNodeModulesDirectory, + '@deepseek-ai/dsh', + resolvedHostNodeModulesDirectory, + hostNodeModulesDirectoryReal, + ) } if (runtimeRoot !== undefined) { + const runtimeRootId = runtimeRoot.id if (runtimeRoot.manifest.name !== options.hostRuntimePackage.name) { throw new Error(`DSH runtime package manifest name does not match discovered coordinate: expected ${options.hostRuntimePackage.name}, found ${runtimeRoot.manifest.name}`) } if (runtimeRoot.manifest.version !== options.hostRuntimePackage.version) { throw new Error(`DSH runtime package does not match discovered coordinate: expected @deepseek-ai/dsh@${options.hostRuntimePackage.version}, found ${runtimeRoot.manifest.version}`) } - if (!packages.has(runtimeRoot.id)) { + if (!packages.has(runtimeRootId)) { if (packages.size >= MAX_NODES) throw new Error(`installed dependency graph exceeds the ${MAX_NODES} node limit`) - packages.set(runtimeRoot.id, runtimeRoot) - queue.push(runtimeRoot.id) + packages.set(runtimeRootId, runtimeRoot) + queue.push(runtimeRootId) } - if (!edges.some(edge => edge.from === root.id && edge.to === runtimeRoot.id && edge.kind === HOST_RUNTIME_EDGE_KIND)) { + if (!edges.some(edge => edge.from === root.id && edge.to === runtimeRootId && edge.kind === HOST_RUNTIME_EDGE_KIND)) { if (edges.length >= MAX_EDGES) throw new Error(`installed dependency graph exceeds the ${MAX_EDGES} edge limit`) - edges.push({ from: root.id, to: runtimeRoot.id, kind: HOST_RUNTIME_EDGE_KIND }) + edges.push({ from: root.id, to: runtimeRootId, kind: HOST_RUNTIME_EDGE_KIND }) } } else { unresolved.push({ @@ -311,16 +368,54 @@ export async function parseInstalledNodeModulesGraph( if (current === undefined) continue for (const dependency of dependencyEntries(current.manifest)) { const target = current.source === 'dsh-host' - ? (hostNodeModulesDirectory === undefined ? undefined : await findHostPackage(dependency.name, hostNodeModulesDirectory)) + ? (hostNodeModulesDirectory === undefined || hostNodeModulesDirectoryReal === undefined + ? undefined + : await findHostPackage( + current.directory, + dependency.name, + hostNodeModulesDirectory, + hostNodeModulesDirectoryReal, + )) : (await findProfilePackage(current.directory, dependency.name, profileRoot, profileRootReal) - ?? (hostNodeModulesDirectory === undefined ? undefined : await findHostPackage(dependency.name, hostNodeModulesDirectory))) + ?? (hostNodeModulesDirectory === undefined || hostNodeModulesDirectoryReal === undefined + ? undefined + : await findHostPackage( + hostNodeModulesDirectory, + dependency.name, + hostNodeModulesDirectory, + hostNodeModulesDirectoryReal, + )) + // DSH is a valid supplier for a plugin's declared required peer. + // Start at the exact runtime package so pnpm's package-local link is + // followed, while keeping all real paths inside the verified outer + // host plane. Do not apply this fallback to ordinary dependencies. + ?? (dependency.kind !== 'peer' || runtimeRoot === undefined || hostNodeModulesDirectory === undefined || hostNodeModulesDirectoryReal === undefined + ? undefined + : await findHostPackage( + runtimeRoot.directory, + dependency.name, + hostNodeModulesDirectory, + hostNodeModulesDirectoryReal, + ))) if (target === undefined) { unresolved.push({ from: current.id, ...dependency }) if (unresolved.length > MAX_EDGES) throw new Error(`installed dependency graph exceeds the ${MAX_EDGES} edge limit`) + if (current.id === root.id && dependency.kind === 'peer') { + rootPeerContracts.push({ name: dependency.name, required: dependency.spec, status: 'missing' }) + } continue } if (edges.length >= MAX_EDGES) throw new Error(`installed dependency graph exceeds the ${MAX_EDGES} edge limit`) edges.push({ from: current.id, to: target.id, kind: dependency.kind }) + if (current.id === root.id && dependency.kind === 'peer') { + const evaluation = satisfiesSemverRange(target.manifest.version, dependency.spec) + rootPeerContracts.push({ + name: dependency.name, + required: dependency.spec, + status: evaluation === true ? 'satisfied' : evaluation === false ? 'mismatched' : 'indeterminate', + resolvedVersion: target.manifest.version, + }) + } if (packages.has(target.id)) continue if (packages.size >= MAX_NODES) throw new Error(`installed dependency graph exceeds the ${MAX_NODES} node limit`) packages.set(target.id, target) @@ -351,6 +446,9 @@ export async function parseInstalledNodeModulesGraph( ...(options.hostRuntimePackage === undefined ? {} : { package: { ...options.hostRuntimePackage } }), }, }), + ...(rootPeerContracts.length === 0 ? {} : { + rootPeerContracts: rootPeerContracts.sort((left, right) => left.name.localeCompare(right.name)), + }), ...(reachableUnresolved.length === 0 ? {} : { unresolved: reachableUnresolved }), } } diff --git a/src/radar-types.ts b/src/radar-types.ts index 00dad02..4ae43ab 100644 --- a/src/radar-types.ts +++ b/src/radar-types.ts @@ -55,6 +55,21 @@ export interface DependencyEdge { kind: DependencyKind } +/** Whether the installed version actually honors one required peer declared by the graph root. */ +export type RootPeerContractStatus = 'satisfied' | 'mismatched' | 'indeterminate' | 'missing' + +/** + * A root-plugin peer requirement joined to the physical version that DSH made + * available at runtime. Optional peers are deliberately excluded: their + * absence is not a broken host contract. + */ +export interface RootPeerContract { + name: string + required: string + status: RootPeerContractStatus + resolvedVersion?: string +} + export interface DependencyGraph { schema: typeof DEPENDENCY_GRAPH_SCHEMA rootNodeId: string @@ -69,6 +84,8 @@ export interface DependencyGraph { /** The exact DSH executable package that owns this shared host plane. */ package?: PackageCoordinate } + /** Direct required peer contracts of the graph root, evaluated from the installed tree. */ + rootPeerContracts?: RootPeerContract[] digest?: string unresolved?: Array<{ from: string diff --git a/src/version.ts b/src/version.ts index ccd3610..a1594f8 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const TOOL_VERSION = '0.40.0' +export const TOOL_VERSION = '0.41.0' diff --git a/test/awesome-dsh-cohort.test.ts b/test/awesome-dsh-cohort.test.ts index 294e4f5..465f16f 100644 --- a/test/awesome-dsh-cohort.test.ts +++ b/test/awesome-dsh-cohort.test.ts @@ -66,5 +66,12 @@ describe('awesome-dsh-plugin monitored cohort', () => { assert.equal(observed?.observeNpm, false) assert.equal(installTargets.plugins.some(target => target.observerTargetId === plugin.id), false) } + + // The published OpenPencil artifact declares Node >=24.11. Running its + // maintained cell on Node 22 only proves the engine gate, not DSH behavior. + assert.deepEqual( + installTargets.plugins.find(target => target.id === 'openpencil')?.runtimeProfiles, + ['node24'], + ) }) }) diff --git a/test/dsh-compatibility-ir.test.ts b/test/dsh-compatibility-ir.test.ts new file mode 100644 index 0000000..5c010d3 --- /dev/null +++ b/test/dsh-compatibility-ir.test.ts @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + buildDshCompatibilityIR, + buildDshCompatibilityReverseIndex, + parseDshCompatibilityIR, + parseDshCompatibilityReverseIndex, +} from '../src/dsh-compatibility-ir.js' +import { + DSH_COMPATIBILITY_LEDGER_SCHEMA, + type DshCompatibilityLedger, +} from '../src/dsh-compatibility-ledger.js' + +function ledger(): DshCompatibilityLedger { + return { + schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, + entries: [{ + caseId: 'openpencil-node24', + targetId: 'openpencil', + plugin: '@zseven-w/dsh-openpencil@0.1.0-rc.1', + dshVersion: '0.1.0-rc.8', + runtime: { + nodeMajor: 24, + nodeVersion: '24.19.0', + platform: 'linux', + architecture: 'x64', + pnpmVersion: '11.7.0', + }, + staticFingerprint: `sha256:${'a'.repeat(64)}`, + contractFingerprint: `sha256:${'b'.repeat(64)}`, + observedAt: '2026-08-22T00:00:00.000Z', + result: 'peer-contract-incompatible', + reason: 'one direct host peer is absent and one resolved version is outside the declared range', + artifact: { + lifecycleScripts: [], + sha256: 'c'.repeat(64), + }, + resolution: { + runtimeGraph: { + digest: `sha256:${'d'.repeat(64)}`, + nodes: 447, + edges: 2020, + unresolved: 1, + optionalUnavailable: 59, + pluginPeerContracts: { + declared: 3, + satisfied: 1, + mismatched: 1, + indeterminate: 0, + missing: 1, + relations: [ + { + name: '@deepseek-ai/dsh-client-ui-slots', + required: '^0.1.0-rc.6', + status: 'missing', + staticUsage: 'type-only-reference-observed', + }, + { + name: '@deepseek-ai/dsh-tools', + required: '^0.1.0-rc.6', + status: 'satisfied', + staticUsage: 'runtime-import-observed', + resolvedVersion: '0.1.0-rc.8', + }, + { + name: 'react-dom', + required: '^18.2.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '19.2.8', + }, + ], + issues: [ + { + name: '@deepseek-ai/dsh-client-ui-slots', + required: '^0.1.0-rc.6', + status: 'missing', + staticUsage: 'type-only-reference-observed', + }, + { + name: 'react-dom', + required: '^18.2.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '19.2.8', + }, + ], + }, + hostRuntime: { source: 'dsh-profile-fallback', resolvedNodes: 38, dshVersion: '0.1.0-rc.8' }, + }, + }, + observer: { + schema: 'upstream-radar.dsh-install-observation/v1alpha1', + version: '0.41.0', + }, + }], + } +} + +describe('DSH compatibility IR', () => { + it('normalizes the measured plugin-to-host contracts and materializes reverse impacts', () => { + const ir = buildDshCompatibilityIR(ledger()) + + assert.equal(ir.cells.length, 1) + assert.equal(ir.relations.length, 3) + assert.equal(ir.cells[0]?.plugin.artifactSha256, 'c'.repeat(64)) + assert.equal(ir.cells[0]?.evidence.runtimeGraphNodes, 447) + assert.deepEqual(ir.relations.map(relation => ({ + name: relation.dependency.name, + status: relation.dependency.status, + staticUsage: relation.dependency.staticUsage, + resolvedVersion: relation.dependency.resolvedVersion, + })), [ + { name: '@deepseek-ai/dsh-client-ui-slots', status: 'missing', staticUsage: 'type-only-reference-observed', resolvedVersion: undefined }, + { name: '@deepseek-ai/dsh-tools', status: 'satisfied', staticUsage: 'runtime-import-observed', resolvedVersion: '0.1.0-rc.8' }, + { name: 'react-dom', status: 'mismatched', staticUsage: 'runtime-import-observed', resolvedVersion: '19.2.8' }, + ]) + assert.deepEqual(parseDshCompatibilityIR(ir), ir) + + const reverse = buildDshCompatibilityReverseIndex(ir) + assert.deepEqual(parseDshCompatibilityReverseIndex(reverse), reverse) + assert.deepEqual(reverse.dependencies.find(item => item.name === 'react-dom')?.impacts, [{ + relationId: ir.relations.find(item => item.dependency.name === 'react-dom')?.id, + cellId: ir.cells[0]?.id, + caseId: 'openpencil-node24', + plugin: '@zseven-w/dsh-openpencil@0.1.0-rc.1', + dshVersion: '0.1.0-rc.8', + nodeMajor: 24, + required: '^18.2.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '19.2.8', + }]) + }) + + it('refuses an IR that silently drops a declared peer relation', () => { + const ir = buildDshCompatibilityIR(ledger()) + const incomplete = { + ...ir, + relations: ir.relations.slice(1), + } + assert.throws(() => parseDshCompatibilityIR(incomplete), /relation count does not match declared peer contracts/) + }) +}) diff --git a/test/dsh-compatibility-issues.test.ts b/test/dsh-compatibility-issues.test.ts new file mode 100644 index 0000000..4b3337d --- /dev/null +++ b/test/dsh-compatibility-issues.test.ts @@ -0,0 +1,163 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + buildDshCompatibilityIssuePlan, + dshCompatibilityIssueMarker, + renderDshCompatibilityIssue, + type DshCompatibilityExistingIssue, +} from '../src/dsh-compatibility-issues.js' +import { + DSH_COMPATIBILITY_LEDGER_SCHEMA, + type DshCompatibilityLedger, + type DshCompatibilityLedgerEntry, +} from '../src/dsh-compatibility-ledger.js' + +function entry(result: DshCompatibilityLedgerEntry['result']): DshCompatibilityLedgerEntry { + return { + caseId: 'openpencil-node24', + targetId: 'openpencil', + plugin: '@zseven-w/dsh-openpencil@0.1.0-rc.1', + dshVersion: '0.1.1-rc.2', + runtime: { + nodeMajor: 24, + nodeVersion: '24.11.1', + platform: 'linux', + architecture: 'x64', + pnpmVersion: '11.7.0', + }, + staticFingerprint: `sha256:${'a'.repeat(64)}`, + contractFingerprint: `sha256:${'b'.repeat(64)}`, + observedAt: '2026-08-23T00:00:00.000Z', + result, + reason: result === 'compatible' + ? 'the exact artifact installed, registered and loaded with a complete host contract' + : 'react-dom requires ^18.2.0 but the DSH profile resolves 19.2.8', + artifact: { lifecycleScripts: [], sha256: 'c'.repeat(64), nodeEngine: '>=24.11.0' }, + resolution: { + profileLockfile: { + sha256: 'd'.repeat(64), + bytes: 1_024, + graphDigest: `sha256:${'e'.repeat(64)}`, + nodes: 400, + edges: 1_200, + unresolved: 0, + }, + runtimeGraph: { + digest: `sha256:${'f'.repeat(64)}`, + nodes: 420, + edges: 1_500, + unresolved: 0, + optionalUnavailable: 3, + pluginPeerContracts: result === 'peer-contract-incompatible' + ? { + declared: 1, + satisfied: 0, + mismatched: 1, + missing: 0, + indeterminate: 0, + relations: [{ + name: 'react-dom', + required: '^18.2.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '19.2.8', + }], + issues: [{ + name: 'react-dom', + required: '^18.2.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '19.2.8', + }], + } + : { + declared: 1, + satisfied: 1, + mismatched: 0, + missing: 0, + indeterminate: 0, + relations: [{ + name: 'react-dom', + required: '^19.0.0', + status: 'satisfied', + staticUsage: 'runtime-import-observed', + resolvedVersion: '19.2.8', + }], + }, + }, + }, + observer: { + schema: 'upstream-radar.dsh-install-observation/v1alpha1', + version: '0.41.0', + }, + } +} + +function ledger(value: DshCompatibilityLedgerEntry): DshCompatibilityLedger { + return { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries: [value] } +} + +function issue(value: DshCompatibilityLedgerEntry, state: 'open' | 'closed' = 'open'): DshCompatibilityExistingIssue { + const rendered = renderDshCompatibilityIssue(value, 'https://github.com/MicroMilo/upstream-radar/actions/runs/1') + return { number: 42, state, ...rendered } +} + +describe('DSH compatibility issue reconciliation', () => { + it('creates one managed issue for an actionable incompatibility', () => { + const plan = buildDshCompatibilityIssuePlan({ + ledger: ledger(entry('peer-contract-incompatible')), + existingIssues: [], + runUrl: 'https://github.com/MicroMilo/upstream-radar/actions/runs/1', + }) + assert.equal(plan.actions.length, 1) + assert.equal(plan.actions[0]?.kind, 'create') + assert.match(plan.actions[0]?.kind === 'create' ? plan.actions[0].body : '', /react-dom/) + assert.deepEqual(plan.openCaseIds, ['openpencil-node24']) + }) + + it('is quiet when the desired open issue already matches the ledger', () => { + const incompatible = entry('peer-contract-incompatible') + const plan = buildDshCompatibilityIssuePlan({ + ledger: ledger(incompatible), + existingIssues: [issue(incompatible)], + runUrl: 'https://github.com/MicroMilo/upstream-radar/actions/runs/1', + }) + assert.deepEqual(plan.actions, []) + }) + + it('reopens a managed incident when the same maintained cell regresses', () => { + const incompatible = entry('load-failed') + const plan = buildDshCompatibilityIssuePlan({ + ledger: ledger(incompatible), + existingIssues: [{ + number: 42, + state: 'closed', + title: 'resolved', + body: dshCompatibilityIssueMarker(incompatible.caseId), + }], + }) + assert.equal(plan.actions[0]?.kind, 'reopen') + }) + + it('comments and closes an open incident after an isolated compatible recheck', () => { + const compatible = entry('compatible') + const plan = buildDshCompatibilityIssuePlan({ + ledger: ledger(compatible), + existingIssues: [{ + number: 42, + state: 'open', + title: 'old incident', + body: dshCompatibilityIssueMarker(compatible.caseId), + }], + runUrl: 'https://github.com/MicroMilo/upstream-radar/actions/runs/2', + }) + assert.equal(plan.actions[0]?.kind, 'close') + assert.match(plan.actions[0]?.kind === 'close' ? plan.actions[0].comment : '', /verified resolved/) + }) + + it('does not blame a plugin when the observer result is unknown', () => { + const plan = buildDshCompatibilityIssuePlan({ ledger: ledger(entry('unknown')), existingIssues: [] }) + assert.deepEqual(plan.actions, []) + assert.deepEqual(plan.ignoredUnknownCaseIds, ['openpencil-node24']) + }) +}) diff --git a/test/dsh-compatibility-ledger.test.ts b/test/dsh-compatibility-ledger.test.ts new file mode 100644 index 0000000..8b1436d --- /dev/null +++ b/test/dsh-compatibility-ledger.test.ts @@ -0,0 +1,319 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + emptyDshCompatibilityLedger, + mergeDshCompatibilityLedger, + parseDshCompatibilityLedger, + renderDshCompatibilityLedgerMerge, + type DshCompatibilityExpectedCase, +} from '../src/dsh-compatibility-ledger.js' + +const expected: DshCompatibilityExpectedCase = { + id: 'openpencil-node24', + targetId: 'openpencil', + plugin: '@zseven-w/dsh-openpencil@0.1.0-rc.1', + dshVersion: '0.1.1-rc.1', + nodeMajor: 24, + allowedBuilds: '', + staticFingerprint: `sha256:${'a'.repeat(64)}`, + contractFingerprint: `sha256:${'b'.repeat(64)}`, + reasons: ['missing-evidence'], +} + +function report(overrides: Record = {}): unknown { + return { + schema: 'upstream-radar.dsh-install-observation/v1alpha1', + tool: { name: 'upstream-radar', version: '0.41.0' }, + probe: 'dsh-install', + scope: 'install-and-load-behavior', + caseId: expected.id, + completedAt: '2026-08-21T00:00:00.000Z', + dshVersion: expected.dshVersion, + runtime: { + platform: 'linux', + architecture: 'x64', + nodeVersion: '24.11.1', + packageManager: { name: 'pnpm', version: '11.7.0' }, + }, + artifact: { + spec: expected.plugin, + sha256: 'c'.repeat(64), + nodeEngine: '>=24.11.0', + lifecycleScripts: [], + }, + resolution: { + profileLockfile: { + sha256: 'd'.repeat(64), + bytes: 1234, + graphDigest: `sha256:${'e'.repeat(64)}`, + nodes: 12, + edges: 14, + unresolved: 0, + }, + }, + result: 'compatible', + reason: 'the exact artifact installed, registered and loaded under the requested DSH version', + boundary: { approvedDependencyBuilds: [] }, + ...overrides, + } +} + +describe('DSH compatibility ledger', () => { + it('accepts only a report that proves it belongs to the exact scheduled cell', () => { + const merged = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report()], + }) + assert.deepEqual(merged.acceptedCaseIds, ['openpencil-node24']) + assert.deepEqual(merged.missingCaseIds, []) + assert.deepEqual(merged.rejectedReports, []) + assert.equal(merged.ledger.entries[0]?.result, 'compatible') + assert.equal(merged.ledger.entries[0]?.resolution?.profileLockfile?.graphDigest, `sha256:${'e'.repeat(64)}`) + assert.equal(merged.transitions[0]?.status, 'compatible') + assert.equal(parseDshCompatibilityLedger(merged.ledger).entries.length, 1) + }) + + it('keeps a report with the wrong runtime out of the ledger and schedules it again', () => { + const wrongRuntime = report({ + runtime: { + platform: 'linux', + architecture: 'x64', + nodeVersion: '22.23.2', + packageManager: { name: 'pnpm', version: '11.7.0' }, + }, + }) + const merged = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [wrongRuntime], + }) + assert.deepEqual(merged.acceptedCaseIds, []) + assert.deepEqual(merged.missingCaseIds, ['openpencil-node24']) + assert.match(merged.rejectedReports[0] ?? '', /does not match scheduled Node 24/) + assert.equal(merged.ledger.entries.length, 0) + }) + + it('makes a newly observed incompatible result actionable, then records its resolution', () => { + const first = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report({ result: 'runtime-incompatible', reason: 'the plugin declares Node >=25' })], + }) + assert.equal(first.transitions[0]?.status, 'new-incompatibility') + + const resolved = mergeDshCompatibilityLedger({ + ledger: first.ledger, + expected: [expected], + reports: [report()], + }) + assert.equal(resolved.transitions[0]?.status, 'resolved-incompatibility') + assert.match(renderDshCompatibilityLedgerMerge(resolved), /resolved-incompatibility/) + }) + + it('does not hide a new incompatibility behind artifact drift', () => { + const compatible = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report()], + }) + const incompatible = mergeDshCompatibilityLedger({ + ledger: compatible.ledger, + expected: [expected], + reports: [report({ + artifact: { + spec: expected.plugin, + sha256: 'f'.repeat(64), + nodeEngine: '>=24.11.0', + lifecycleScripts: [], + }, + result: 'load-failed', + reason: 'the new artifact fails during DSH boot', + })], + }) + assert.equal(incompatible.transitions[0]?.status, 'new-incompatibility') + }) + + it('records a fixed replacement artifact as resolved instead of generic drift', () => { + const incompatible = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report({ result: 'install-failed', reason: 'the package cannot resolve its host dependency' })], + }) + const resolved = mergeDshCompatibilityLedger({ + ledger: incompatible.ledger, + expected: [expected], + reports: [report({ + artifact: { + spec: expected.plugin, + sha256: 'f'.repeat(64), + nodeEngine: '>=24.11.0', + lifecycleScripts: [], + }, + })], + }) + assert.equal(resolved.transitions[0]?.status, 'resolved-incompatibility') + }) + + it('surfaces a newly resolved dependency graph even when install/load still succeeds', () => { + const first = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report()], + }) + const drifted = mergeDshCompatibilityLedger({ + ledger: first.ledger, + expected: [expected], + reports: [report({ + resolution: { + profileLockfile: { + sha256: 'f'.repeat(64), + bytes: 1300, + graphDigest: `sha256:${'0'.repeat(64)}`, + nodes: 13, + edges: 16, + unresolved: 0, + }, + }, + })], + }) + assert.equal(drifted.transitions[0]?.status, 'resolution-drift') + assert.match(renderDshCompatibilityLedgerMerge(drifted), /resolution-drift/) + }) + + it('does not call a non-semantic pnpm lockfile rewrite resolution drift', () => { + const first = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report()], + }) + const rewritten = mergeDshCompatibilityLedger({ + ledger: first.ledger, + expected: [expected], + reports: [report({ + resolution: { + profileLockfile: { + sha256: 'f'.repeat(64), + bytes: 1300, + graphDigest: `sha256:${'e'.repeat(64)}`, + nodes: 12, + edges: 14, + unresolved: 0, + }, + }, + })], + }) + assert.equal(rewritten.transitions[0]?.status, 'compatible') + }) + + it('retains bounded unresolved profile edges so an incomplete graph is explainable', () => { + const merged = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report({ + resolution: { + profileLockfile: { + sha256: 'd'.repeat(64), + bytes: 1234, + graphDigest: `sha256:${'e'.repeat(64)}`, + nodes: 2, + edges: 1, + unresolved: 1, + unresolvedDependencies: [{ + from: 'pnpm:example@1.0.0', + name: 'host-only', + spec: '^2.0.0', + kind: 'peer', + }], + }, + runtimeGraph: { + digest: `sha256:${'f'.repeat(64)}`, + nodes: 2, + edges: 1, + unresolved: 1, + unresolvedDependencies: [{ + from: 'node_modules/example', + name: 'host-only', + spec: '^2.0.0', + kind: 'peer', + }], + hostRuntime: { + source: 'dsh-profile-fallback', + resolvedNodes: 1, + }, + }, + }, + })], + }) + assert.deepEqual(merged.ledger.entries[0]?.resolution?.profileLockfile?.unresolvedDependencies, [{ + from: 'pnpm:example@1.0.0', + name: 'host-only', + spec: '^2.0.0', + kind: 'peer', + }]) + assert.deepEqual(merged.ledger.entries[0]?.resolution?.runtimeGraph, { + digest: `sha256:${'f'.repeat(64)}`, + nodes: 2, + edges: 1, + unresolved: 1, + unresolvedDependencies: [{ + from: 'node_modules/example', + name: 'host-only', + spec: '^2.0.0', + kind: 'peer', + }], + hostRuntime: { source: 'dsh-profile-fallback', resolvedNodes: 1 }, + }) + }) + + it('retains a direct peer-contract violation as an incompatibility, not a green load result', () => { + const merged = mergeDshCompatibilityLedger({ + ledger: emptyDshCompatibilityLedger(), + expected: [expected], + reports: [report({ + result: 'peer-contract-incompatible', + reason: 'the exact artifact loaded, but host-runtime@2.1.0 does not satisfy ^3.0.0', + resolution: { + runtimeGraph: { + digest: `sha256:${'f'.repeat(64)}`, + nodes: 3, + edges: 2, + unresolved: 0, + optionalUnavailable: 7, + pluginPeerContracts: { + declared: 1, + satisfied: 0, + mismatched: 1, + indeterminate: 0, + missing: 0, + relations: [{ + name: 'host-runtime', + required: '^3.0.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '2.1.0', + }], + issues: [{ + name: 'host-runtime', + required: '^3.0.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '2.1.0', + }], + }, + }, + }, + })], + }) + assert.equal(merged.transitions[0]?.status, 'new-incompatibility') + assert.equal(merged.ledger.entries[0]?.result, 'peer-contract-incompatible') + assert.deepEqual(merged.ledger.entries[0]?.resolution?.runtimeGraph?.pluginPeerContracts?.issues, [{ + name: 'host-runtime', + required: '^3.0.0', + status: 'mismatched', + staticUsage: 'runtime-import-observed', + resolvedVersion: '2.1.0', + }]) + assert.equal(merged.ledger.entries[0]?.resolution?.runtimeGraph?.optionalUnavailable, 7) + }) +}) diff --git a/test/dsh-install-observation.test.ts b/test/dsh-install-observation.test.ts index 44ec047..88e4873 100644 --- a/test/dsh-install-observation.test.ts +++ b/test/dsh-install-observation.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert/strict' -import { mkdir, writeFile } from 'node:fs/promises' +import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' +import { pathToFileURL } from 'node:url' import { describe, it } from 'node:test' import { observeDshPluginInstall, @@ -120,6 +121,7 @@ describe('DSH install observation', () => { it('observes one exact artifact through DSH install and load without inheriting host secrets', async () => { const calls: InstallObservationCommand[] = [] + let hostRuntimeEntry: string | undefined const runner = async (command: InstallObservationCommand): Promise => { calls.push(command) assert.equal(command.env.GITHUB_TOKEN, undefined) @@ -134,10 +136,12 @@ describe('DSH install observation', () => { name: 'example-plugin', version: '1.0.0', engines: { node: '>=18.0.0' }, + peerDependencies: { 'host-runtime': '^2.0.0' }, scripts: { postinstall: 'node scripts/postinstall.js' }, dsh: { bundle: { patch: './cordis.patch.yml' } }, }) }, { path: 'package/cordis.patch.yml', contents: '[]\n' }, + { path: 'package/lib/index.js', contents: 'module.exports = require("host-runtime")\n' }, ])) return passed({ stdout: JSON.stringify([{ filename: 'example-plugin-1.0.0.tgz', integrity: 'sha512-demo' }]) }) } @@ -148,11 +152,65 @@ describe('DSH install observation', () => { const dshHome = command.env.DSH_HOME assert.equal(typeof dshHome, 'string') const profileDirectory = join(dshHome as string, 'profiles', 'headless') + await mkdir(join(profileDirectory, 'node_modules', '.pnpm'), { recursive: true }) + await mkdir(join(profileDirectory, 'node_modules', 'example-plugin'), { recursive: true }) await mkdir(join(profileDirectory, 'generated'), { recursive: true }) await writeFile(join(profileDirectory, 'generated', 'install.txt'), 'created during install\n') await writeFile(join(profileDirectory, 'package.json'), JSON.stringify({ dsh: { profile: { bundles: ['example-plugin'] } }, })) + await writeFile(join(profileDirectory, 'node_modules', 'example-plugin', 'package.json'), JSON.stringify({ + name: 'example-plugin', + version: '1.0.0', + peerDependencies: { 'host-runtime': '^2.0.0' }, + })) + const cacheHome = command.env.XDG_CACHE_HOME + assert.equal(typeof cacheHome, 'string') + const dshRuntimeNodeModules = join(cacheHome as string, 'pnpm', 'dlx', 'fixture', 'node_modules') + await mkdir(join(dshRuntimeNodeModules, '@deepseek-ai', 'dsh'), { recursive: true }) + await writeFile(join(dshRuntimeNodeModules, '@deepseek-ai', 'dsh', 'package.json'), JSON.stringify({ + name: '@deepseek-ai/dsh', + version: '0.1.0-rc.8', + })) + await mkdir(join(dshRuntimeNodeModules, 'host-runtime'), { recursive: true }) + await writeFile(join(dshRuntimeNodeModules, 'host-runtime', 'package.json'), JSON.stringify({ + name: 'host-runtime', + version: '2.1.0', + })) + hostRuntimeEntry = join(dshRuntimeNodeModules, 'host-runtime', 'index.js') + await writeFile(hostRuntimeEntry, 'export {}\n') + // DSH currently keeps the resolved profile graph in pnpm's virtual + // store rather than beside the profile manifest. + await writeFile(join(profileDirectory, 'node_modules', '.pnpm', 'lock.yaml'), ` +lockfileVersion: '9.0' + +importers: + .: + dependencies: + example-plugin: + specifier: 1.0.0 + version: 1.0.0 + +packages: + 'example-plugin@1.0.0': {} + +snapshots: + 'example-plugin@1.0.0': + dependencies: + missing-dependency: 1.0.0 +`) + } + + if (command.phase === 'load') { + assert.equal(command.command, process.execPath) + const probe = await readFile(command.args[0] as string, 'utf8') + assert.match(probe, /await import\("example-plugin"\)/) + assert.match(probe, /"--profile","headless","--help"/) + assert.notEqual(hostRuntimeEntry, undefined) + await writeFile(join(command.cwd, '.upstream-radar-peer-resolution.json'), JSON.stringify({ + schema: 'upstream-radar.profile-peer-resolution/v1alpha1', + peers: [{ name: 'host-runtime', status: 'resolved', url: pathToFileURL(hostRuntimeEntry as string).href }], + })) } if (command.tracePath !== undefined) { @@ -164,6 +222,7 @@ describe('DSH install observation', () => { const report = await observeDshPluginInstall({ packageSpec: 'example-plugin@1.0.0', dshVersion: '0.1.0-rc.8', + caseId: 'example-node22', allowExecution: true, isolationProvider: 'github-actions-hosted-runner', hostEnvironment: { @@ -175,18 +234,55 @@ describe('DSH install observation', () => { }) assert.equal(report.result, 'compatible') + assert.equal(report.caseId, 'example-node22') assert.equal(report.artifact.name, 'example-plugin') assert.equal(report.artifact.version, '1.0.0') assert.equal(report.artifact.nodeEngine, '>=18.0.0') assert.match(report.artifact.sha256 ?? '', /^[0-9a-f]{64}$/) assert.deepEqual(report.artifact.lifecycleScripts, ['postinstall']) assert.equal(report.runtime.packageManager.version, '11.7.0') + assert.match(report.resolution.profileLockfile?.sha256 ?? '', /^[a-f0-9]{64}$/) + assert.match(report.resolution.profileLockfile?.graphDigest ?? '', /^sha256:[a-f0-9]{64}$/) + assert.equal(report.resolution.profileLockfile?.nodes, 2) + assert.equal(report.resolution.profileLockfile?.edges, 1) + assert.equal(report.resolution.profileLockfile?.unresolved, 1) + assert.deepEqual(report.resolution.profileLockfile?.unresolvedDependencies, [{ + from: 'pnpm:example-plugin@1.0.0', + name: 'missing-dependency', + spec: '1.0.0', + kind: 'runtime', + }]) + assert.equal(report.resolution.runtimeGraphError, undefined) + assert.match(report.resolution.runtimeGraph?.digest ?? '', /^sha256:[a-f0-9]{64}$/) + assert.equal(report.resolution.runtimeGraph?.nodes, 3) + assert.equal(report.resolution.runtimeGraph?.edges, 2) + assert.equal(report.resolution.runtimeGraph?.unresolved, 0) + assert.deepEqual(report.resolution.runtimeGraph?.pluginPeerContracts, { + declared: 1, + satisfied: 1, + mismatched: 0, + indeterminate: 0, + missing: 0, + relations: [{ + name: 'host-runtime', + required: '^2.0.0', + status: 'satisfied', + staticUsage: 'runtime-import-observed', + resolvedVersion: '2.1.0', + }], + }) + assert.deepEqual(report.resolution.runtimeGraph?.hostRuntime, { + source: 'dsh-process', + resolvedNodes: 2, + dshVersion: '0.1.0-rc.8', + }) assert.deepEqual(report.boundary.approvedDependencyBuilds, []) assert.equal(report.stages.registration.status, 'passed') assert.equal(report.observations.install.processes.length, 1) assert.equal(report.observations.install.fileWrites.length >= 1, true) assert.equal(report.filesystem.install.created.some(path => path.endsWith('/generated/install.txt')), true) assert.equal(calls.map(call => call.phase).join(','), 'runtime,artifact,profile,install,load') + assert.match(calls.find(call => call.phase === 'load')?.args[0] ?? '', /\.upstream-radar-load-probe\.mjs$/) assert.match(renderDshInstallObservation(report), /COMPATIBLE/) assert.match(renderDshInstallObservation(report), /pnpm 11\.7\.0/) assert.match(renderDshInstallObservation(report), /Plugin Node requirement: >=18\.0\.0/) @@ -194,6 +290,94 @@ describe('DSH install observation', () => { assert.match(renderDshInstallObservation(report), /Lifecycle scripts declared: postinstall/) }) + it('does not call a successful load compatible when the DSH host violates a required plugin peer range', async () => { + let hostRuntimeEntry: string | undefined + const runner = async (command: InstallObservationCommand): Promise => { + if (command.phase === 'runtime') return passed({ stdout: '11.7.0\n' }) + if (command.phase === 'artifact') { + await writeFile(join(command.cwd, 'mismatch-plugin-1.0.0.tgz'), makeTarball([ + { path: 'package/package.json', contents: JSON.stringify({ + name: 'mismatch-plugin', + version: '1.0.0', + peerDependencies: { 'host-runtime': '^3.0.0' }, + dsh: { bundle: { patch: 'cordis.patch.yml' } }, + }) }, + { path: 'package/cordis.patch.yml', contents: '[]\n' }, + { path: 'package/src/types.ts', contents: "import { nothing } from 'unrelated-runtime'\nimport type { Service } from 'host-runtime'\nexport type Host = Service\n" }, + ])) + return passed({ stdout: JSON.stringify([{ filename: 'mismatch-plugin-1.0.0.tgz' }]) }) + } + if (command.phase === 'install') { + const dshHome = command.env.DSH_HOME as string + const profileDirectory = join(dshHome, 'profiles', 'headless') + await mkdir(join(profileDirectory, 'node_modules', 'mismatch-plugin'), { recursive: true }) + await writeFile(join(profileDirectory, 'package.json'), JSON.stringify({ + dsh: { profile: { bundles: ['mismatch-plugin'] } }, + })) + await writeFile(join(profileDirectory, 'node_modules', 'mismatch-plugin', 'package.json'), JSON.stringify({ + name: 'mismatch-plugin', + version: '1.0.0', + peerDependencies: { 'host-runtime': '^3.0.0' }, + })) + const dshRuntimeNodeModules = join(command.env.XDG_CACHE_HOME as string, 'pnpm', 'dlx', 'fixture', 'node_modules') + await mkdir(join(dshRuntimeNodeModules, '@deepseek-ai', 'dsh'), { recursive: true }) + await writeFile(join(dshRuntimeNodeModules, '@deepseek-ai', 'dsh', 'package.json'), JSON.stringify({ + name: '@deepseek-ai/dsh', + version: '0.1.0-rc.8', + })) + await mkdir(join(dshRuntimeNodeModules, 'host-runtime'), { recursive: true }) + await writeFile(join(dshRuntimeNodeModules, 'host-runtime', 'package.json'), JSON.stringify({ + name: 'host-runtime', + version: '2.1.0', + })) + hostRuntimeEntry = join(dshRuntimeNodeModules, 'host-runtime', 'index.js') + await writeFile(hostRuntimeEntry, 'export {}\n') + } + if (command.phase === 'load') { + assert.notEqual(hostRuntimeEntry, undefined) + await writeFile(join(command.cwd, '.upstream-radar-peer-resolution.json'), JSON.stringify({ + schema: 'upstream-radar.profile-peer-resolution/v1alpha1', + peers: [{ name: 'host-runtime', status: 'resolved', url: pathToFileURL(hostRuntimeEntry as string).href }], + })) + } + if (command.tracePath !== undefined) await writeFile(command.tracePath, TRACE.replaceAll('/sandbox', command.sandboxRoot)) + return passed() + } + + const report = await observeDshPluginInstall({ + packageSpec: 'mismatch-plugin@1.0.0', + dshVersion: '0.1.0-rc.8', + allowExecution: true, + isolationProvider: 'other', + runner, + }) + + assert.equal(report.stages.load.status, 'passed') + assert.equal(report.result, 'peer-contract-incompatible') + assert.match(report.reason, /host-runtime@2\.1\.0 does not satisfy \^3\.0\.0/) + assert.deepEqual(report.resolution.runtimeGraph?.pluginPeerContracts, { + declared: 1, + satisfied: 0, + mismatched: 1, + indeterminate: 0, + missing: 0, + relations: [{ + name: 'host-runtime', + required: '^3.0.0', + status: 'mismatched', + staticUsage: 'type-only-reference-observed', + resolvedVersion: '2.1.0', + }], + issues: [{ + name: 'host-runtime', + required: '^3.0.0', + status: 'mismatched', + staticUsage: 'type-only-reference-observed', + resolvedVersion: '2.1.0', + }], + }) + }) + it('keeps an observed install failure distinct from missing trace evidence', async () => { const artifact = makeTarball([ { path: 'package/package.json', contents: JSON.stringify({ diff --git a/test/dsh-install-plan.test.ts b/test/dsh-install-plan.test.ts index 252eee1..bdfafce 100644 --- a/test/dsh-install-plan.test.ts +++ b/test/dsh-install-plan.test.ts @@ -1,73 +1,247 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' +import { emptyDshCompatibilityLedger, type DshCompatibilityExpectedCase, type DshCompatibilityLedgerEntry } from '../src/dsh-compatibility-ledger.js' import { buildDshInstallPlan, parseDshInstallTargets } from '../src/dsh-install-plan.js' const corpus = { schema: 'upstream-radar.dsh-install-targets/v1alpha1', + refreshAfterHours: 168, + runtimeProfiles: [ + { id: 'node22', nodeMajor: 22 }, + { id: 'node24', nodeMajor: 24 }, + ], plugins: [ { id: 'feishu', spec: 'dsh-feishu-bot@0.16.0', observerTargetId: 'dsh-feishu-bot', allowedBuilds: ['protobufjs'], reason: 'messaging plugin' }, { id: 'browser', spec: 'dsh-browser@1.2.3', reason: 'browser plugin' }, ], } -function state(dshVersion = '0.1.0-rc.8', feishuVersion = '0.16.1'): unknown { +function state(dshVersion = '0.1.0-rc.8', feishuVersion = '0.16.1', sourceCommit = 'a'.repeat(40)): unknown { return { targets: { - 'deepseek-harness': { package: { name: '@deepseek-ai/dsh', version: dshVersion } }, - 'dsh-feishu-bot': { package: { name: 'dsh-feishu-bot', version: feishuVersion } }, + 'deepseek-harness': { + source: { repository: 'deepseek-ai/dsh', commit: 'd'.repeat(40), packagePath: 'package.json' }, + package: { name: '@deepseek-ai/dsh', version: dshVersion, integrity: 'sha512-dsh' }, + graph: { digest: 'sha256:dsh-graph' }, + }, + 'dsh-feishu-bot': { + source: { repository: 'example/dsh-feishu-bot', commit: sourceCommit, packagePath: 'package.json', lockfile: 'pnpm-lock.yaml' }, + package: { name: 'dsh-feishu-bot', version: feishuVersion, integrity: 'sha512-feishu' }, + graph: { digest: 'sha256:feishu-graph' }, + alignment: { status: 'aligned', checks: [{ code: 'source-published-identity', status: 'aligned' }] }, + }, }, } } -describe('DSH install observation plan', () => { - it('tests the whole maintained corpus when the official DSH coordinate changes', () => { - const plan = buildDshInstallPlan(corpus, state(), { +const now = new Date('2026-08-21T00:00:00.000Z') + +function entry(expected: DshCompatibilityExpectedCase, overrides: Partial = {}): DshCompatibilityLedgerEntry { + return { + caseId: expected.id, + targetId: expected.targetId, + plugin: expected.plugin, + dshVersion: expected.dshVersion, + runtime: { nodeMajor: expected.nodeMajor, nodeVersion: `${expected.nodeMajor}.23.2`, platform: 'linux', architecture: 'x64', pnpmVersion: '11.7.0' }, + staticFingerprint: expected.staticFingerprint, + contractFingerprint: expected.contractFingerprint, + observedAt: '2026-08-20T23:00:00.000Z', + result: 'compatible', + reason: 'the exact artifact installed, registered and loaded', + artifact: { lifecycleScripts: [] }, + resolution: { + profileLockfile: { + sha256: 'c'.repeat(64), + bytes: 128, + graphDigest: `sha256:${'d'.repeat(64)}`, + nodes: 2, + edges: 1, + unresolved: 0, + }, + runtimeGraph: { + digest: `sha256:${'e'.repeat(64)}`, + nodes: 12, + edges: 14, + unresolved: 0, + pluginPeerContracts: { + declared: 0, + satisfied: 0, + mismatched: 0, + indeterminate: 0, + missing: 0, + relations: [], + }, + }, + }, + observer: { schema: 'upstream-radar.dsh-install-observation/v1alpha1', version: '0.41.0' }, + ...overrides, + } +} + +function baseline() { + return buildDshInstallPlan(corpus, state(), { changes: [] }, emptyDshCompatibilityLedger(), now) +} + +describe('DSH compatibility reconciliation plan', () => { + it('backfills every default runtime cell even when no package coordinate changed', () => { + const plan = baseline() + assert.equal(plan.run, true) + assert.equal(plan.dshVersion, '0.1.0-rc.8') + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node22', 'feishu-node22']) + assert.deepEqual(plan.matrix.include.map(item => item.allowedBuilds), ['', 'protobufjs']) + assert.deepEqual(plan.matrix.include.map(item => item.reasons), [['missing-evidence'], ['missing-evidence']]) + assert.deepEqual(plan.triggers, []) + }) + + it('stays quiet only after all desired cells have fresh exact evidence', () => { + const first = baseline() + const ledger = { schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', entries: first.matrix.include.map(item => entry(item)) } + const plan = buildDshInstallPlan(corpus, state(), { changes: [] }, ledger, now) + assert.equal(plan.run, false) + assert.equal(plan.dshVersion, '0.1.0-rc.8') + assert.deepEqual(plan.matrix.include, []) + assert.match(plan.reason, /fresh evidence/) + }) + + it('does not let a green install/load satisfy the ledger without an effective runtime graph', () => { + const first = baseline() + const browser = first.matrix.include.find(item => item.id === 'browser-node22') as DshCompatibilityExpectedCase + const feishu = first.matrix.include.find(item => item.id === 'feishu-node22') as DshCompatibilityExpectedCase + const ledger = { + schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', + entries: [ + entry(browser, { resolution: { profileLockfile: { sha256: 'e'.repeat(64), bytes: 128 } } }), + entry(feishu), + ], + } + const plan = buildDshInstallPlan(corpus, state(), { changes: [] }, ledger, now) + assert.equal(plan.run, true) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node22']) + assert.deepEqual(plan.matrix.include[0]?.reasons, ['runtime-graph-missing']) + }) + + it('does not let an unresolved effective runtime graph satisfy the ledger', () => { + const first = baseline() + const browser = first.matrix.include.find(item => item.id === 'browser-node22') as DshCompatibilityExpectedCase + const feishu = first.matrix.include.find(item => item.id === 'feishu-node22') as DshCompatibilityExpectedCase + const ledger = { + schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', + entries: [ + entry(browser, { resolution: { runtimeGraph: { + digest: `sha256:${'f'.repeat(64)}`, nodes: 2, edges: 1, unresolved: 1, + } } }), + entry(feishu), + ], + } + const plan = buildDshInstallPlan(corpus, state(), { changes: [] }, ledger, now) + assert.equal(plan.run, true) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node22']) + assert.deepEqual(plan.matrix.include[0]?.reasons, ['runtime-graph-incomplete']) + }) + + it('rechecks a green runtime graph until direct plugin peer contracts were evaluated', () => { + const first = baseline() + const browser = first.matrix.include.find(item => item.id === 'browser-node22') as DshCompatibilityExpectedCase + const feishu = first.matrix.include.find(item => item.id === 'feishu-node22') as DshCompatibilityExpectedCase + const ledger = { + schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', + entries: [ + entry(browser, { resolution: { runtimeGraph: { + digest: `sha256:${'f'.repeat(64)}`, nodes: 2, edges: 1, unresolved: 0, + } } }), + entry(feishu), + ], + } + const plan = buildDshInstallPlan(corpus, state(), { changes: [] }, ledger, now) + assert.equal(plan.run, true) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node22']) + assert.deepEqual(plan.matrix.include[0]?.reasons, ['peer-contract-not-evaluated']) + }) + + it('retests the whole maintained default corpus when the official DSH coordinate changes', () => { + const existing = baseline() + const ledger = { schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', entries: existing.matrix.include.map(item => entry(item)) } + const plan = buildDshInstallPlan(corpus, state('0.1.0-rc.9'), { changes: [{ targetId: 'deepseek-harness', meaningful: true, previous: { package: { name: '@deepseek-ai/dsh', version: '0.1.0-rc.8' } }, current: { package: { name: '@deepseek-ai/dsh', version: '0.1.0-rc.9' } }, }], - }) + }, ledger, now) assert.equal(plan.run, true) assert.equal(plan.dshVersion, '0.1.0-rc.9') - assert.deepEqual(plan.matrix.include.map(item => item.plugin), ['dsh-browser@1.2.3', 'dsh-feishu-bot@0.16.1']) - assert.deepEqual(plan.matrix.include.map(item => item.allowedBuilds), ['', 'protobufjs']) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node22', 'feishu-node22']) + assert.equal(plan.matrix.include.every(item => item.reasons.includes('dsh-coordinate-changed')), true) assert.deepEqual(plan.triggers, ['deepseek-harness']) }) - it('tests only the changed mapped plugin against the last observed DSH release', () => { - const plan = buildDshInstallPlan(corpus, state(), { + it('retests only a mapped plugin when its exact published coordinate changes', () => { + const existing = baseline() + const ledger = { schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', entries: existing.matrix.include.map(item => entry(item)) } + const plan = buildDshInstallPlan(corpus, state('0.1.0-rc.8', '0.17.0'), { changes: [{ targetId: 'dsh-feishu-bot', meaningful: true, previous: { package: { name: 'dsh-feishu-bot', version: '0.16.0' } }, current: { package: { name: 'dsh-feishu-bot', version: '0.17.0' } }, }], - }) + }, ledger, now) assert.equal(plan.run, true) - assert.equal(plan.dshVersion, '0.1.0-rc.8') - assert.deepEqual(plan.matrix.include, [{ id: 'feishu', plugin: 'dsh-feishu-bot@0.17.0', allowedBuilds: 'protobufjs' }]) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['feishu-node22']) + assert.equal(plan.matrix.include[0]?.plugin, 'dsh-feishu-bot@0.17.0') + assert.equal(plan.matrix.include[0]?.reasons.includes('plugin-coordinate-changed'), true) assert.deepEqual(plan.triggers, ['dsh-feishu-bot']) }) - it('stays quiet when no meaningful DSH or maintained-plugin coordinate changed', () => { - const plan = buildDshInstallPlan(corpus, state(), { changes: [] }) - assert.equal(plan.run, false) - assert.equal(plan.dshVersion, '0.1.0-rc.8') - assert.deepEqual(plan.matrix.include, []) - assert.match(plan.reason, /no maintained install target changed/) + it('invalidates a fresh runtime result when static source/graph evidence drifts without an npm publication', () => { + const first = baseline() + const ledger = { schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', entries: first.matrix.include.map(item => entry(item)) } + const plan = buildDshInstallPlan(corpus, state('0.1.0-rc.8', '0.16.1', 'b'.repeat(40)), { changes: [] }, ledger, now) + assert.equal(plan.run, true) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['feishu-node22']) + assert.deepEqual(plan.matrix.include[0]?.reasons, ['static-evidence-changed']) + }) + + it('periodically rechecks unchanged cells instead of treating a historical pass as permanent', () => { + const first = baseline() + const staleEntry = entry(first.matrix.include[0] as DshCompatibilityExpectedCase, { observedAt: '2026-08-13T00:00:00.000Z' }) + const freshEntry = entry(first.matrix.include[1] as DshCompatibilityExpectedCase) + const ledger = { schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', entries: [staleEntry, freshEntry] } + const plan = buildDshInstallPlan(corpus, state(), { changes: [] }, ledger, now) + assert.equal(plan.run, true) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node22']) + assert.deepEqual(plan.matrix.include[0]?.reasons, ['stale-evidence']) + }) + + it('uses a static Node engine mismatch to schedule an alternate runtime profile once', () => { + const first = baseline() + const browser = first.matrix.include.find(item => item.id === 'browser-node22') as DshCompatibilityExpectedCase + const feishu = first.matrix.include.find(item => item.id === 'feishu-node22') as DshCompatibilityExpectedCase + const ledger = { + schema: 'upstream-radar.dsh-compatibility-ledger/v1alpha1', + entries: [ + entry(browser, { result: 'runtime-incompatible', artifact: { lifecycleScripts: [], nodeEngine: '>=24.11.0' } }), + entry(feishu), + ], + } + const plan = buildDshInstallPlan(corpus, state(), { changes: [] }, ledger, now) + assert.equal(plan.run, true) + assert.deepEqual(plan.matrix.include.map(item => item.id), ['browser-node24']) + assert.deepEqual(plan.matrix.include[0]?.reasons, ['missing-evidence']) + assert.equal(plan.matrix.include[0]?.nodeMajor, 24) }) - it('refuses ranges and duplicate ids in the maintained corpus', () => { + it('refuses ranges, duplicate ids, and unknown runtime profiles in the maintained corpus', () => { assert.throws(() => parseDshInstallTargets({ schema: 'upstream-radar.dsh-install-targets/v1alpha1', + runtimeProfiles: [{ id: 'node22', nodeMajor: 22 }], plugins: [ { id: 'duplicate', spec: 'one@^1.0.0', reason: 'bad range' }, - { id: 'duplicate', spec: 'two@1.0.0', reason: 'duplicate id' }, + { id: 'duplicate', spec: 'two@1.0.0', runtimeProfiles: ['node24'], reason: 'duplicate id' }, ], - }), /exact|duplicate/) + }), /exact|duplicate|configured/) }) }) diff --git a/test/dsh-runtime.test.ts b/test/dsh-runtime.test.ts index 3fb2c2b..0ec505b 100644 --- a/test/dsh-runtime.test.ts +++ b/test/dsh-runtime.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, it } from 'node:test' import { + discoverDshRuntimeHostNodeModulesDirectory, discoverDshRuntimeNodeModulesDirectory, discoverDshRuntimePackage, discoverDshRuntimePackageDirectory, @@ -25,6 +26,10 @@ describe('DSH runtime dependency discovery', () => { discoverDshRuntimeNodeModulesDirectory(join(dshRoot, 'lib', 'bin.js')), await realpath(join(root, 'node_modules')), ) + assert.equal( + discoverDshRuntimeHostNodeModulesDirectory(join(dshRoot, 'lib', 'bin.js')), + await realpath(join(root, 'node_modules')), + ) assert.deepEqual(discoverDshRuntimePackage(join(dshRoot, 'lib', 'bin.js')), { ecosystem: 'npm', name: '@deepseek-ai/dsh', @@ -41,6 +46,28 @@ describe('DSH runtime dependency discovery', () => { } }) + it('widens a pnpm DSH runtime plane to its enclosing virtual store', async () => { + const root = await mkdtemp(join(tmpdir(), 'upstream-radar-dsh-runtime-')) + const hostNodeModules = join(root, 'node_modules') + const dshRoot = join(hostNodeModules, '.pnpm', '@deepseek-ai+dsh@0.1.0-rc.8', 'node_modules', '@deepseek-ai', 'dsh') + try { + await mkdir(join(dshRoot, 'lib'), { recursive: true }) + await writeFile(join(dshRoot, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh', version: '0.1.0-rc.8' })) + await writeFile(join(dshRoot, 'lib', 'bin.js'), '') + + assert.equal( + discoverDshRuntimeNodeModulesDirectory(join(dshRoot, 'lib', 'bin.js')), + await realpath(join(dshRoot, '..', '..')), + ) + assert.equal( + discoverDshRuntimeHostNodeModulesDirectory(join(dshRoot, 'lib', 'bin.js')), + await realpath(hostNodeModules), + ) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + it('does not trust an arbitrary package or a missing entrypoint', async () => { const root = await mkdtemp(join(tmpdir(), 'upstream-radar-dsh-runtime-')) try { diff --git a/test/installed-graph.test.ts b/test/installed-graph.test.ts index 9bc6954..5509de5 100644 --- a/test/installed-graph.test.ts +++ b/test/installed-graph.test.ts @@ -1,5 +1,5 @@ import assert from 'node:assert/strict' -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { describe, it } from 'node:test' @@ -125,12 +125,45 @@ describe('installed DSH dependency graph', () => { to: 'dsh-host/node_modules/host-runtime', kind: 'peer', }]) + assert.deepEqual(graph.rootPeerContracts, [{ + name: 'host-runtime', + required: '^2.0.0', + status: 'satisfied', + resolvedVersion: '2.1.0', + }]) assert.equal(graph.unresolved, undefined) } finally { await rm(root, { recursive: true, force: true }) } }) + it('refuses a shared-host symlink that escapes the dependency plane', async () => { + const root = await mkdtemp(join(tmpdir(), 'upstream-radar-installed-graph-')) + const profile = join(root, 'profiles', 'web') + const hostNodeModules = join(root, 'profiles', 'node_modules') + const outside = join(root, 'outside-host-package') + try { + await writeManifest(join(profile, 'node_modules', 'plugin', 'package.json'), { + name: 'plugin', + version: '1.0.0', + peerDependencies: { 'host-runtime': '^2.0.0' }, + }) + await writeManifest(join(outside, 'package.json'), { + name: 'host-runtime', + version: '2.1.0', + }) + await mkdir(hostNodeModules, { recursive: true }) + await symlink(outside, join(hostNodeModules, 'host-runtime'), 'dir') + + await assert.rejects( + parseInstalledNodeModulesGraph(profile, { name: 'plugin', version: '1.0.0' }, { hostNodeModulesDirectory: hostNodeModules }), + /escapes the shared dependency plane/, + ) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + it('walks the DSH executable and its transitive dependencies across the host boundary', async () => { const root = await mkdtemp(join(tmpdir(), 'upstream-radar-installed-graph-')) const profile = join(root, 'profiles', 'web') @@ -179,4 +212,102 @@ describe('installed DSH dependency graph', () => { await rm(root, { recursive: true, force: true }) } }) + + it('follows pnpm virtual-store links beside the exact DSH runtime package', async () => { + const root = await mkdtemp(join(tmpdir(), 'upstream-radar-installed-graph-')) + const profile = join(root, 'profiles', 'web') + const hostNodeModules = join(root, 'dsh-cache', 'node_modules') + const runtimeRoot = join( + hostNodeModules, + '.pnpm', + '@deepseek-ai+dsh@0.1.0-rc.8', + 'node_modules', + '@deepseek-ai', + 'dsh', + ) + const cordisRoot = join( + hostNodeModules, + '.pnpm', + '@deepseek-ai+cordis@4.0.2', + 'node_modules', + '@deepseek-ai', + 'cordis', + ) + try { + await writeManifest(join(profile, 'node_modules', 'plugin', 'package.json'), { + name: 'plugin', + version: '1.0.0', + peerDependencies: { '@deepseek-ai/cordis': '^4.0.0' }, + }) + await writeManifest(join(runtimeRoot, 'package.json'), { + name: '@deepseek-ai/dsh', + version: '0.1.0-rc.8', + dependencies: { '@deepseek-ai/cordis': '^4.0.1' }, + }) + await writeManifest(join(cordisRoot, 'package.json'), { + name: '@deepseek-ai/cordis', + version: '4.0.2', + }) + await symlink(cordisRoot, join(dirname(runtimeRoot), 'cordis'), 'dir') + + const graph = await parseInstalledNodeModulesGraph(profile, { name: 'plugin', version: '1.0.0' }, { + hostNodeModulesDirectory: hostNodeModules, + hostRuntimeSource: 'dsh-process', + hostRuntimePackage: { ecosystem: 'npm', name: '@deepseek-ai/dsh', version: '0.1.0-rc.8' }, + hostRuntimePackageDirectory: runtimeRoot, + }) + + assert.equal(graph.unresolved, undefined) + assert.equal(graph.nodes.length, 3) + assert.deepEqual(graph.edges.map(edge => edge.kind), ['runtime', 'peer', 'host-runtime']) + assert.equal(graph.nodes.find(node => node.name === '@deepseek-ai/cordis')?.version, '4.0.2') + assert.deepEqual(graph.rootPeerContracts, [{ + name: '@deepseek-ai/cordis', + required: '^4.0.0', + status: 'satisfied', + resolvedVersion: '4.0.2', + }]) + assert.equal(graph.hostRuntime?.resolvedNodes, 2) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + + it('keeps missing, mismatched, and indeterminate root peer contracts distinct', async () => { + const root = await mkdtemp(join(tmpdir(), 'upstream-radar-installed-graph-')) + try { + await writeManifest(join(root, 'node_modules', 'plugin', 'package.json'), { + name: 'plugin', + version: '1.0.0', + peerDependencies: { + 'host-mismatch': '^3.0.0', + 'host-unknown': 'git+https://example.invalid/host.git', + 'host-missing': '^1.0.0', + }, + }) + await writeManifest(join(root, 'node_modules', 'host-mismatch', 'package.json'), { + name: 'host-mismatch', + version: '2.1.0', + }) + await writeManifest(join(root, 'node_modules', 'host-unknown', 'package.json'), { + name: 'host-unknown', + version: '1.0.0', + }) + + const graph = await parseInstalledNodeModulesGraph(root, { name: 'plugin', version: '1.0.0' }) + assert.deepEqual(graph.rootPeerContracts, [ + { name: 'host-mismatch', required: '^3.0.0', status: 'mismatched', resolvedVersion: '2.1.0' }, + { name: 'host-missing', required: '^1.0.0', status: 'missing' }, + { name: 'host-unknown', required: 'git+https://example.invalid/host.git', status: 'indeterminate', resolvedVersion: '1.0.0' }, + ]) + assert.deepEqual(graph.unresolved, [{ + from: 'node_modules/plugin', + name: 'host-missing', + kind: 'peer', + spec: '^1.0.0', + }]) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) }) diff --git a/test/release-preflight.test.ts b/test/release-preflight.test.ts index 72b249c..14f47b9 100644 --- a/test/release-preflight.test.ts +++ b/test/release-preflight.test.ts @@ -23,7 +23,7 @@ describe('release preflight', () => { checks: Array<{ status: string }> } assert.equal(report.schema, 'upstream-radar.release-preflight/v1alpha1') - assert.equal(report.version, '0.40.0') + assert.equal(report.version, '0.41.0') assert.equal(report.passed, true) assert.equal(report.publishedCheck, false) assert.ok(report.checks.length >= 4) diff --git a/test/scripts-syntax.test.ts b/test/scripts-syntax.test.ts new file mode 100644 index 0000000..c98ee5e --- /dev/null +++ b/test/scripts-syntax.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { readdir } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, it } from 'node:test' + +const repository = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const scriptsDirectory = resolve(repository, 'scripts') + +describe('executable script syntax', () => { + it('keeps every checked-in .mjs script parseable by the current Node runtime', async () => { + const entries = await readdir(scriptsDirectory, { withFileTypes: true }) + const scripts = entries + .filter(entry => entry.isFile() && entry.name.endsWith('.mjs')) + .map(entry => resolve(scriptsDirectory, entry.name)) + .sort() + assert.ok(scripts.length > 0) + + for (const script of scripts) { + const result = spawnSync(process.execPath, ['--check', script], { + cwd: repository, + encoding: 'utf8', + maxBuffer: 4 * 1024 * 1024, + }) + assert.equal(result.status, 0, `${script}: ${result.stderr || result.stdout}`) + } + }) +})