mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-16 05:40:40 +08:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f80d860fc5 | ||
|
|
e0a2f00f95 | ||
|
|
4c56264b79 | ||
|
|
3fea2a96b2 | ||
|
|
4e490dfac3 | ||
|
|
7acf282050 | ||
|
|
9ce48081af | ||
|
|
7701149db3 | ||
|
|
9cb5ba8cec | ||
|
|
40db61856a | ||
|
|
1bce87f858 | ||
|
|
d9c703ab38 | ||
|
|
352fd3774a | ||
|
|
f8f8d4c5a5 | ||
|
|
8153fc5efc | ||
|
|
37597d88ce | ||
|
|
76d49a1d81 | ||
|
|
eb315c9d75 | ||
|
|
90c95c2a23 | ||
|
|
43820cae29 |
@@ -4,7 +4,7 @@ This directory contains the VS Code dev container configuration for OpenSpec dev
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Node.js 20 LTS** (>=20.19.0) - TypeScript/JavaScript runtime
|
||||
- **Node.js 22 LTS** - TypeScript/JavaScript runtime (OpenSpec itself supports Node 20.19.0 and newer)
|
||||
- **pnpm** - Fast, disk space efficient package manager
|
||||
- **Git + GitHub CLI** - Version control tools
|
||||
- **VS Code Extensions**:
|
||||
|
||||
@@ -55,19 +55,34 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Every OS runs the oldest supported Node (package.json engines).
|
||||
# Linux also runs the current LTS lines users are likely to have.
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
shell: bash
|
||||
label: linux-bash
|
||||
node: '20.19.0'
|
||||
vitest_workers: 4
|
||||
- os: macos-latest
|
||||
shell: bash
|
||||
label: macos-bash
|
||||
node: '20.19.0'
|
||||
vitest_workers: 4
|
||||
- os: windows-latest
|
||||
shell: pwsh
|
||||
label: windows-pwsh
|
||||
node: '20.19.0'
|
||||
vitest_workers: 2
|
||||
- os: ubuntu-latest
|
||||
shell: bash
|
||||
label: linux-bash-node22
|
||||
node: '22'
|
||||
vitest_workers: 4
|
||||
- os: ubuntu-latest
|
||||
shell: bash
|
||||
label: linux-bash-node24
|
||||
node: '24'
|
||||
vitest_workers: 4
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -86,7 +101,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '20.19.0'
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Print environment diagnostics
|
||||
@@ -105,7 +120,7 @@ jobs:
|
||||
run: pnpm test
|
||||
|
||||
- name: Upload test coverage
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node == '20.19.0'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-report-${{ github.event_name }}
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 1.14.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#2052](https://github.com/Fission-AI/OpenSpec/pull/2052) [`90c95c2`](https://github.com/Fission-AI/OpenSpec/commit/90c95c2a235e0d4088006a879c19cd1febff2a9e) Thanks [@drakeo338](https://github.com/drakeo338)! - ### Bug Fixes
|
||||
|
||||
- **Archive accepts `openspec/`, `openspec/changes` or `openspec/specs` as a symlink** — `openspec archive` no longer refuses a project-owned symlink that points to another directory, such as a shared docs repository. An `archive/` directory that resolves outside the changes directory is still refused.
|
||||
|
||||
- [#2061](https://github.com/Fission-AI/OpenSpec/pull/2061) [`7acf282`](https://github.com/Fission-AI/OpenSpec/commit/7acf282050c0b2a27bd18f30c48fbcc0986bfc45) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
|
||||
|
||||
- **Guard against deeply nested brace patterns** — An artifact `generates` or `apply.tracks` pattern that nests braces or parentheses more than 16 levels deep is now rejected with a clear error before glob matching, so a schema cannot overflow the stack through the unpatched `braces` advisory GHSA-vfj7-8cjw-p6xm.
|
||||
|
||||
- [#2086](https://github.com/Fission-AI/OpenSpec/pull/2086) [`9cb5ba8`](https://github.com/Fission-AI/OpenSpec/commit/9cb5ba8cec4bc47227dcac4974a74b4adf1819f6) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
|
||||
|
||||
- **Explore no longer offers to write default-schema files under a custom schema** — The explore workflow's "where to capture" table and example offers now say they describe the default spec-driven schema. With another schema, the agent captures each insight in an artifact that schema defines, names it in the offer, and never creates a file such as `design.md` that the schema does not have.
|
||||
|
||||
- [#2064](https://github.com/Fission-AI/OpenSpec/pull/2064) [`76d49a1`](https://github.com/Fission-AI/OpenSpec/commit/76d49a1d8176ad2bdb3696e29c4ffe222402f2da) Thanks [@ryandemelo](https://github.com/ryandemelo)! - Shell Tab completion is faster. `openspec __complete`, which the completion scripts run on every Tab press, now loads 48 modules instead of 344 when completing changes or specs, and no longer records usage, so a press never waits on the telemetry request and can't use up the first-run telemetry notice where nobody sees it. Completion output is unchanged.
|
||||
|
||||
- [#2083](https://github.com/Fission-AI/OpenSpec/pull/2083) [`3fea2a9`](https://github.com/Fission-AI/OpenSpec/commit/3fea2a96b2e04e01097f4617573734c47a57589b) Thanks [@clay-good](https://github.com/clay-good)! - The update workflow now states the glob new-file exception at both places that state the scope rule (step 2 and the guardrails), not only where the rule is restated. It said to edit only the concrete files in `existingOutputPaths`, while the file step 4 creates to fill a gap under a partly written glob artifact is by definition not in that list, so an agent applying the blanket rule refused the operation the flow exists to perform.
|
||||
|
||||
- [#2083](https://github.com/Fission-AI/OpenSpec/pull/2083) [`3fea2a9`](https://github.com/Fission-AI/OpenSpec/commit/3fea2a96b2e04e01097f4617573734c47a57589b) Thanks [@clay-good](https://github.com/clay-good)! - The verify workflow now reads a REMOVED requirement's baseline from the main spec before searching the codebase for it. A REMOVED delta block carries only the requirement name plus its Reason and Migration notes, so the search started from the heading alone - and because finding nothing is the passing result for a removal, an implementation written in different vocabulary read as a clean removal. When the baseline cannot be recovered from the main spec or that file's history - including a baseline that only restates its own title - Spec Coverage is now reported as not verified for that entry and the search is skipped, instead of passing.
|
||||
|
||||
- [#2054](https://github.com/Fission-AI/OpenSpec/pull/2054) [`1bce87f`](https://github.com/Fission-AI/OpenSpec/commit/1bce87f858c2591b2cb58108597ae6f0a9bb5d7c) Thanks [@runsonmypc](https://github.com/runsonmypc)! - `openspec validate` and `openspec archive` now warn when a delta spec holds text archive does not carry into the main spec: a `## ` section other than Purpose and the four delta sections, text above the first `## ` header, or prose before the first requirement in ADDED or MODIFIED. That text was dropped without notice before. Like other validation warnings, these fail `openspec validate --strict`. What archive carries is unchanged.
|
||||
|
||||
- [#2063](https://github.com/Fission-AI/OpenSpec/pull/2063) [`e0a2f00`](https://github.com/Fission-AI/OpenSpec/commit/e0a2f00f95731af7cf22a30c2c4a8b7ff910b509) Thanks [@gkinter](https://github.com/gkinter)! - Keys in a change's `.openspec.yaml` that start with a lowercase `x-` (for example `x-goal: G-12` or `x-jiraKey`) are now treated as your own extension metadata: OpenSpec no longer reports them as unknown, so `openspec validate --strict` passes on a change that carries them. Any other unrecognized key, including `X-Tracker` or a bare `x-`, is still reported.
|
||||
|
||||
## 1.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -33,11 +33,6 @@ Our philosophy:
|
||||
→ scalable from personal projects to enterprises
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
|
||||
>
|
||||
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
|
||||
|
||||
<p align="center">
|
||||
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
|
||||
</p>
|
||||
@@ -112,13 +107,13 @@ OpenSpec is built with OpenSpec — browse this repo's live [specs](openspec/spe
|
||||
|
||||
Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the hard part moves: a feature spans the API server, the web app, and a shared library; requirements are owned by one team and consumed by others; planning starts before any code exists.
|
||||
|
||||
**[Stores](docs/stores-beta/user-guide.md)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
|
||||
**[Stores](https://openspec.dev/docs/stores)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
|
||||
|
||||
- **Cross-repo features** — one change, one plan, even when the code lands in three repos.
|
||||
- **Shared requirements** — a platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No drifting wiki.
|
||||
- **Plan before code** — capture the plan in the store now; the code repos catch up later.
|
||||
|
||||
> Stores are in **beta**. Start with the [Stores User Guide](docs/stores-beta/user-guide.md).
|
||||
> Stores are in **beta**. Start with the [Stores guide](https://openspec.dev/docs/stores).
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -143,44 +138,33 @@ cd your-project
|
||||
openspec init
|
||||
```
|
||||
|
||||
> **Want your AI to do it?** Paste the [setup prompt](docs-lab/start/installation.md#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
|
||||
> **Want your AI to do it?** Paste the [setup prompt](https://openspec.dev/docs/installation#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
|
||||
|
||||
Now talk to your AI:
|
||||
|
||||
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before any code gets written. ([Explore guide](docs/explore.md))
|
||||
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before any code gets written. ([Quickstart](https://openspec.dev/docs/quickstart))
|
||||
- **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
|
||||
|
||||
Both are in the default profile. If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
||||
|
||||
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [How To Invoke](docs/supported-tools.md#how-to-invoke).
|
||||
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [Supported Tools](https://openspec.dev/docs/supported-tools).
|
||||
|
||||
> [!NOTE]
|
||||
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 30+ tools and growing.
|
||||
> Not sure if your tool is supported? [View the full list](https://openspec.dev/docs/supported-tools) – we support 30+ tools and growing.
|
||||
>
|
||||
> Also works with Homebrew, pnpm, yarn, bun, and Nix. [See installation options](docs-lab/start/installation.md).
|
||||
> Also works with Homebrew, pnpm, yarn, bun, and Nix. [See installation options](https://openspec.dev/docs/installation).
|
||||
|
||||
## Docs
|
||||
|
||||
**Start here:** the **[Documentation Home](docs/README.md)** maps everything. New to OpenSpec? Read [Getting Started](docs/getting-started.md), then [How Commands Work](docs/how-commands-work.md) (where you actually type `/opsx:propose`).
|
||||
|
||||
→ **[Getting Started](docs/getting-started.md)**: first steps<br>
|
||||
→ **[Explore First](docs/explore.md)**: think it through with `/opsx:explore` before you commit<br>
|
||||
→ **[How Commands Work](docs/how-commands-work.md)**: where slash commands run vs the CLI<br>
|
||||
→ **[Core Concepts at a Glance](docs/overview.md)**: the whole mental model, one page<br>
|
||||
→ **[Examples & Recipes](docs/examples.md)**: real changes, start to finish<br>
|
||||
→ **[Workflows](docs/workflows.md)**: combos and patterns<br>
|
||||
→ **[Existing Projects](docs/existing-projects.md)**: adopt OpenSpec on a brownfield codebase<br>
|
||||
→ **[Editing a Change](docs/editing-changes.md)**: update artifacts, go back, reconcile manual edits<br>
|
||||
→ **[Commands](docs/commands.md)**: slash commands & skills<br>
|
||||
→ **[CLI](docs/cli.md)**: terminal reference<br>
|
||||
→ **[Stores](docs/stores-beta/user-guide.md)**: plan in a separate repo, shared across your team (beta)<br>
|
||||
→ **[Supported Tools](docs/supported-tools.md)**: tool integrations & install paths<br>
|
||||
→ **[Concepts](docs/concepts.md)**: how it all fits<br>
|
||||
→ **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
|
||||
→ **[Customization](docs/customization.md)**: make it yours<br>
|
||||
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec<br>
|
||||
→ **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
|
||||
Full documentation lives at **[openspec.dev/docs](https://openspec.dev/docs)**.
|
||||
|
||||
→ **[Quickstart](https://openspec.dev/docs/quickstart)**: your first change, start to finish<br>
|
||||
→ **[Set up your project](https://openspec.dev/docs/setup)**: what `openspec init` installs<br>
|
||||
→ **[CLI](https://openspec.dev/docs/cli)**: terminal reference<br>
|
||||
→ **[Supported Tools](https://openspec.dev/docs/supported-tools)**: tool integrations & install paths<br>
|
||||
→ **[Customize](https://openspec.dev/docs/customize)**: profiles, project config, and schemas<br>
|
||||
→ **[Stores](https://openspec.dev/docs/stores)**: plan in a separate repo, shared across your team (beta)<br>
|
||||
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec
|
||||
|
||||
## Community schemas
|
||||
|
||||
|
||||
@@ -42,6 +42,24 @@ Declares the change intentionally makes no spec deltas: a pure refactor, tooling
|
||||
|
||||
Authorizes archive to retire a capability. When this change's REMOVED deltas take away the last requirement a capability has, archive deletes that capability's main spec instead of stopping. The flag exists because the deletion is only recoverable from git, so it stays the author's call.
|
||||
|
||||
### Extension keys
|
||||
|
||||
A top-level key that starts with a lowercase `x-` is yours, for data another tool or your team keeps beside the change, such as a tracker id or an owner:
|
||||
|
||||
```yaml
|
||||
schema: spec-driven
|
||||
x-tracker: SB-5497
|
||||
x-jiraKey: ABC-1
|
||||
x-owner:
|
||||
team: payments
|
||||
```
|
||||
|
||||
- **Name**: the lowercase `x-` prefix followed by at least one character, with no whitespace anywhere. Any case or spelling works after the prefix, so `x-jiraKey` and `x-trackerID` qualify. `X-Tracker`, a bare `x-`, and `x- tracker` do not, and are reported as unknown keys.
|
||||
- **Value**: any YAML value. OpenSpec does not read or validate it, and it changes no behavior.
|
||||
- **Scope**: top-level only. An `x-` key inside `initiative` is still rejected, like any other key there.
|
||||
- **Validation**: extension keys are never reported as unknown, so `openspec validate --strict` passes on a change that carries them.
|
||||
- **Archive**: the file moves with the change unchanged, extension keys included.
|
||||
|
||||
## Example
|
||||
|
||||
A filled-in .openspec.yaml:
|
||||
@@ -61,5 +79,5 @@ The file is validated whenever a command writes or reads it. A write that fails
|
||||
|
||||
Unlike [config.yaml](config-yaml.md), bad values are never dropped with a warning. A metadata error stops the command.
|
||||
|
||||
- **Unknown top-level keys**: OpenSpec ignores them. `status`, `instructions`, `validate`, and `archive` report that they have no effect. JSON output carries the warning in its structured result.
|
||||
- **Strict validation**: `openspec validate --strict` treats an unknown-key warning as a failure.
|
||||
- **Unknown top-level keys**: OpenSpec ignores them. `status`, `instructions`, `validate`, and `archive` report that they have no effect. JSON output carries the warning in its structured result. [Extension keys](#extension-keys) are not unknown and are never reported.
|
||||
- **Strict validation**: `openspec validate --strict` treats an unknown-key warning as a failure. A change whose only extra keys are extension keys passes.
|
||||
|
||||
@@ -84,7 +84,7 @@ OpenSpec recognizes these glob forms in `generates`:
|
||||
|
||||
**Literal filenames**: a leading `!` alone does not make a glob. Use `generates: '!review.md'` to name that file. Plain parentheses such as `(proposal|design).md` and single-element braces such as `review-{api}.md` also remain literal.
|
||||
|
||||
OpenSpec rejects absolute paths and paths containing a `..` segment.
|
||||
OpenSpec rejects absolute paths, paths containing a `..` segment, and globs that nest braces or parentheses more than 16 levels deep, such as `{a,{b,{c,...}}}`. Real patterns need one or two levels.
|
||||
|
||||
#### Completion
|
||||
|
||||
@@ -140,7 +140,7 @@ The path starts from the change folder. For a change named `add-auth`, `tracks:
|
||||
openspec/changes/add-auth/tasks.md
|
||||
```
|
||||
|
||||
A glob such as `tracks: "**/tasks.md"` reads every matching file, such as `backend/tasks.md` and `frontend/tasks.md`. OpenSpec combines their tasks and progress. Use the same value for an artifact's `generates` field so status and list track the same files.
|
||||
A glob such as `tracks: "**/tasks.md"` reads every matching file, such as `backend/tasks.md` and `frontend/tasks.md`. OpenSpec combines their tasks and progress. Use the same value for an artifact's `generates` field so status and list track the same files. The 16-level brace limit from `generates` applies here too.
|
||||
|
||||
Apply stays blocked if no file matches or the matched files contain no checkbox with task text. OpenSpec counts these checkbox forms:
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# OpenSpec Documentation
|
||||
|
||||
> [!WARNING]
|
||||
> These pages are legacy and some are out of date. The current docs are at **[openspec.dev/docs](https://openspec.dev/docs)** (source: [`docs-lab/`](../docs-lab/)).
|
||||
|
||||
Welcome. This is the home for everything OpenSpec.
|
||||
|
||||
OpenSpec helps you and your AI coding assistant **agree on what to build before any code is written.** You describe the change, the AI drafts a short spec and a task list, you both look at the same plan, and then the work happens. No more discovering halfway through that the AI built the wrong thing.
|
||||
|
||||
+17
@@ -678,6 +678,23 @@ Then archive the change normally:
|
||||
openspec archive retire-legacy --yes
|
||||
```
|
||||
|
||||
**Keep your own metadata:** a top-level key starting with a lowercase `x-` is
|
||||
yours. OpenSpec never reads, writes or reports it, so `openspec validate
|
||||
--strict` stays green on a change that carries tool-specific data:
|
||||
|
||||
```yaml
|
||||
# openspec/changes/add-dark-mode/.openspec.yaml
|
||||
schema: spec-driven
|
||||
x-tracker: SB-5497
|
||||
x-jiraKey: ABC-1
|
||||
```
|
||||
|
||||
The name after the prefix is yours to choose, in any case or spelling, as long
|
||||
as it is not empty and has no whitespace in it. Two limits: the prefix itself is
|
||||
lowercase, so `X-Tracker` is still reported, and extensions are top-level only —
|
||||
a key inside `initiative:` is rejected whatever it is called. Any other
|
||||
unrecognized top-level key is still reported as having no effect.
|
||||
|
||||
When the change removes the capability's last requirement, OpenSpec deletes its
|
||||
live `spec.md`. Other capability deltas in the same change still update their
|
||||
main specs. Without the marker, archive stops before changing any files and
|
||||
|
||||
+9
-1
@@ -7,9 +7,16 @@ Listed projects are maintained independently. Inclusion does not imply official
|
||||
## Projects and resources
|
||||
|
||||
- **[OpenSpec Workbench](https://github.com/VeryComplexAndLongName/OpenSpec-UI)**: Running and supervising agents on OpenSpec changes.
|
||||
- **[MySpec](https://myspec.dev)**: Cloud beta (account required) that conducts guided spec interviews and exports four-file bundles or OpenSpec-compatible changes. The service sends submitted content to third-party AI providers and is not designed for sensitive data.
|
||||
- **[openspec-guard](https://github.com/guillaume-flambard/spec-guard)**: CLI and GitHub Action that reports which OpenSpec scenarios are covered by a Vitest or Jest test, without running the tests.
|
||||
|
||||
## Hosted services
|
||||
|
||||
Hosted products that add an OpenSpec capability the CLI does not provide. Each entry states its account, payment, and data-handling terms.
|
||||
|
||||
- **[MySpec](https://myspec.dev)**: Cloud beta (account required; free tier, no credit card) that conducts guided spec interviews and exports four-file bundles or OpenSpec-compatible changes. Its MCP server (`@myspec/mcp-server`) and [Claude Code plugins](https://github.com/myspecs/claude-plugins) let coding agents read those specs, write OpenSpec changes, and implement them task by task. The service sends submitted content to third-party AI providers and is not designed for sensitive data.
|
||||
- **[Entalpa](https://entalpa.com/en/openspec)**: Imports a public GitHub repository's `openspec/specs/` (up to 300 requirements) into a browsable project, adds stakeholders, stories and open questions labeled as inferred, and exports the result as an `openspec/specs/` tree; it never writes to the repository. Importing and reading need no account; saving or editing needs one, and AI features use paid credits. Imported content is sent to third-party AI providers.
|
||||
- **[OpenSpec Review](https://skillgild.dev/skills/openspec-review) and [OpenSpec Compact](https://skillgild.dev/skills/openspec-compact)**: Hosted skills from SkillGild for Claude Code, Codex, Cursor, Gemini CLI or any MCP client. Review publishes a change as a link where anyone holding it can approve, request changes or comment per requirement without an account, shows the main spec as `openspec archive` would write it, and flags requirements that other open changes also touch. Compact finds lexically similar requirements and repeated scenarios across main specs and writes the cleanup as an OpenSpec change. Requires the SkillGild CLI and a free account (25 runs a month per skill). Both send spec content to SkillGild's servers, which run closed-source code and make no AI model call; Review also sends the proposal, design and tasks, and keeps the change for 30 days.
|
||||
|
||||
## Add your project
|
||||
|
||||
Open a pull request adding one line to this file with your project's name, a direct link, and a short description of how it relates to OpenSpec.
|
||||
@@ -17,5 +24,6 @@ Open a pull request adding one line to this file with your project's name, a dir
|
||||
- Keep entries focused on something built with OpenSpec or supporting its use, rather than general product advertising.
|
||||
- Describe what people can use. Avoid promotional claims, referral links, and tracking links.
|
||||
- Disclose paid features or required accounts in the entry, if any.
|
||||
- Hosted or paid services are listed under Hosted services only when they add an OpenSpec capability the CLI does not already provide and have a free way to try them. A service that repackages OpenSpec's own skills or commands is not listed.
|
||||
|
||||
Corrections and updates to existing entries are welcome too.
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ openspec/changes/add-dark-mode/
|
||||
├── proposal.md # Why and what
|
||||
├── design.md # How (technical approach)
|
||||
├── tasks.md # Implementation checklist
|
||||
├── .openspec.yaml # Change metadata (optional): schema, created, skip_specs, retire_capabilities
|
||||
├── .openspec.yaml # Change metadata (optional): schema, created, skip_specs, retire_capabilities, plus your own x-* keys
|
||||
└── specs/ # Delta specs
|
||||
└── ui/
|
||||
└── spec.md # What's changing in ui/spec.md
|
||||
|
||||
@@ -423,6 +423,7 @@ Community schemas are not vendored into OpenSpec core — they live in their own
|
||||
| `nanopm` | @nmrtn | [nmrtn/nanopm](https://github.com/nmrtn/nanopm/tree/main/openspec-schema) | PM-first workflow. Runs [nanopm](https://github.com/nmrtn/nanopm)'s planning pipeline (audit → strategy → roadmap → PRD) upstream of implementation. Bridges product planning to OpenSpec's spec-driven engineering workflow. Artifacts read from `.nanopm/` if present — proposal sources the audit, design sources the strategy, and tasks source the PRD breakdown. |
|
||||
| `e2e-runbooks` | @Lukk17 | [Lukk17/openspec-schemas](https://github.com/Lukk17/openspec-schemas/tree/master/openspec/schemas/e2e-runbooks) | Capability-level end-to-end test runbooks. Each capability gets an immutable spec, an immutable tasks-template, and one timestamped run record per execution. Assertions are observable behaviour only (HTTP status, response body, persisted state — never log substrings); each run records start/end UTC, duration, and best-estimate LLM token consumption. |
|
||||
| `anvil` | @jikkujoyce | [jikkujoyce/openspec-schemas](https://github.com/jikkujoyce/openspec-schemas/tree/main/schemas/anvil) | Spec-driven workflow with TDD discipline and an adversarial review step. Flow: `proposal` → `specs` → `design` → `review` → `test-plan` → `tasks` → `apply` → `verify`. `review` is written by a fresh-context, read-only reviewer (a second model when one is available) and emits a `VERDICT:` line telling the agent to gate `test-plan`, `tasks`, and `apply`; OpenSpec only checks that artifacts exist, so enforce the gate with your own CI or hook. `test-plan` maps every spec scenario to a named test and doubles as a red/green ledger that `verify` audits. |
|
||||
| `ml-research` | @tsenst | [idealo/openspec-schemas](https://github.com/idealo/openspec-schemas/tree/main/openspec/schemas/ml-research) | Machine-learning experiment workflow. Flow: `proposal` → `design` → `evaluation` → `tasks` → `apply` → `specs`. `evaluation` fixes metrics, success criteria, and the falsification condition before the run; `apply` writes `results.md`; `specs` records only evidence-backed findings (refuted hypotheses included) as `Finding:` requirements with linked `Evidence:` blocks. |
|
||||
|
||||
> Want to contribute a community schema? Open an issue with a link to your repository, or submit a PR adding a row to this table.
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
inherit (finalAttrs) pname version src;
|
||||
pnpm = final.pnpm_10;
|
||||
fetcherVersion = 3;
|
||||
hash = "sha256-NIMHf7t+FnBgsswDtB1k4E/zX1ks1r3708ggE2ps9E8=";
|
||||
hash = "sha256-hzhk/JEqix4m/k564GbcVuNtoi21E9TkJprPFnPV+Zk=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = with final; [
|
||||
|
||||
@@ -119,6 +119,14 @@ The system SHALL preserve literal filenames with a bare leading `!`, plain paren
|
||||
- **AND** "content/linked" is a symbolic link to a directory outside the change directory
|
||||
- **THEN** output resolution rejects traversal through that link even when no matching files exist
|
||||
|
||||
### Requirement: Glob Nesting Limit
|
||||
The system SHALL reject a glob `generates` or `apply.tracks` value that nests braces or parentheses more than 16 levels deep, counting them as the brace parser does, with an error naming the limit, before any glob matching runs.
|
||||
|
||||
#### Scenario: Brace nesting beyond the limit
|
||||
- **WHEN** an artifact generates a pattern whose braces nest 17 levels deep
|
||||
- **THEN** resolving its outputs fails with an error naming the 16-level limit
|
||||
- **AND** no glob matching runs for that pattern
|
||||
|
||||
### Requirement: Ready Artifact Query
|
||||
The system SHALL identify which artifacts are ready to be created based on dependency completion.
|
||||
|
||||
|
||||
@@ -224,12 +224,12 @@ The spec update confirmation SHALL provide clear visibility into changes before
|
||||
EXISTING specs to be updated:
|
||||
- cli-init (from changes/update-init-command/specs/cli-init/spec.md)
|
||||
|
||||
Update 2 specs and archive 'add-archive-command'? [y/N]:
|
||||
Proceed with spec updates? (Y/n)
|
||||
```
|
||||
#### Scenario: Handling confirmation response
|
||||
|
||||
- **WHEN** waiting for user confirmation
|
||||
- **THEN** default to "No" for safety (require explicit "y" or "yes")
|
||||
- **THEN** default to "Yes", so pressing Enter proceeds (declining requires an explicit "n" or "no")
|
||||
- **AND** skip confirmation when `--yes` or `-y` flag is provided
|
||||
|
||||
#### Scenario: User declines confirmation
|
||||
|
||||
@@ -103,7 +103,11 @@ The agent SHALL verify that implementation matches the specifications.
|
||||
|
||||
#### Scenario: Removed requirement
|
||||
- **WHEN** a requirement sits under `## REMOVED Requirements` in a delta spec
|
||||
- **THEN** the agent treats the absence of its implementation as the expected result
|
||||
- **THEN** the agent reads the baseline requirement (its body and scenarios in the main spec, matched by normalized name) before searching, rather than searching from the requirement name alone
|
||||
- **AND** recovers the baseline from the main spec file's history when that file no longer holds the requirement, for example because an earlier sync already applied the removal
|
||||
- **AND** treats a baseline that adds nothing to its own title as unusable, the same as a missing one
|
||||
- **AND** marks spec coverage as not verified for the entry, without searching, when no usable baseline can be found or read
|
||||
- **AND** treats the absence of an implementation of that baseline behavior as the expected result
|
||||
- **AND** does not report it as missing or suggest implementing it
|
||||
- **AND** reports it as CRITICAL only if the removed behavior is still present in the codebase
|
||||
- **AND** skips scenario coverage for it
|
||||
|
||||
@@ -92,14 +92,14 @@ The `openspec schemas` command SHALL display the source of each schema.
|
||||
|
||||
### Requirement: Use config schema as default for new changes
|
||||
|
||||
The system SHALL use the schema field from `openspec/config.yaml` as the default when creating new changes without explicit `--schema` flag and no planning-home default applies.
|
||||
The system SHALL use the schema field from `openspec/config.yaml` as the default when creating new changes without an explicit `--schema` flag, and SHALL fall back to the planning-home default when the config supplies no schema.
|
||||
|
||||
#### Scenario: Create change without --schema flag and config exists
|
||||
- **WHEN** user runs `openspec new change foo`, no planning-home default applies, and config contains `schema: "tdd"`
|
||||
- **THEN** system creates change with schema "tdd"
|
||||
- **WHEN** user runs `openspec new change foo` and config contains `schema: "tdd"`
|
||||
- **THEN** system creates change with schema "tdd", even when a planning-home default applies
|
||||
|
||||
#### Scenario: Create change without --schema flag and no config
|
||||
- **WHEN** user runs `openspec new change foo`, no planning-home default applies, and no config file exists
|
||||
- **WHEN** user runs `openspec new change foo`, no config file exists, and no planning-home default applies
|
||||
- **THEN** system creates change with default schema "spec-driven"
|
||||
|
||||
#### Scenario: Create change with explicit --schema flag
|
||||
@@ -108,7 +108,7 @@ The system SHALL use the schema field from `openspec/config.yaml` as the default
|
||||
|
||||
### Requirement: Resolve schema with updated precedence order
|
||||
|
||||
The system SHALL resolve the schema for a change using the following precedence order: CLI flag, change metadata, planning-home default, project config, hardcoded default.
|
||||
The system SHALL resolve the schema for a change using the following precedence order: CLI flag, change metadata, project config, planning-home default, hardcoded default.
|
||||
|
||||
#### Scenario: CLI flag is provided
|
||||
- **WHEN** user runs command with `--schema custom`
|
||||
@@ -119,11 +119,11 @@ The system SHALL resolve the schema for a change using the following precedence
|
||||
- **THEN** system uses "bound" from change metadata
|
||||
|
||||
#### Scenario: Only project config specifies schema
|
||||
- **WHEN** no CLI flag, change metadata, or planning-home default exists, but config has `schema: tdd`
|
||||
- **THEN** system uses "tdd" from project config
|
||||
- **WHEN** no CLI flag or change metadata exists, but config has `schema: tdd`
|
||||
- **THEN** system uses "tdd" from project config, even when a planning-home default applies
|
||||
|
||||
#### Scenario: No schema specified anywhere
|
||||
- **WHEN** no CLI flag, change metadata, planning-home default, or project config
|
||||
- **WHEN** no CLI flag, change metadata, project config, or planning-home default
|
||||
- **THEN** system uses hardcoded default "spec-driven"
|
||||
|
||||
### Requirement: Support project-local schema names in config
|
||||
|
||||
@@ -62,7 +62,7 @@ The system SHALL display a one-line telemetry disclosure notice on the first com
|
||||
#### Scenario: First command execution
|
||||
- **WHEN** a user runs their first openspec command
|
||||
- **AND** telemetry is enabled
|
||||
- **THEN** the system displays: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0"
|
||||
- **THEN** the system displays: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0 or openspec config set telemetry.enabled false"
|
||||
|
||||
#### Scenario: Subsequent command execution
|
||||
- **WHEN** a user has already seen the notice (noticeSeen: true in config)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.14.1",
|
||||
"version": "1.14.2",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
Generated
+228
-252
@@ -11,6 +11,7 @@ overrides:
|
||||
js-yaml@>=4.0.0 <4.3.1: '>=4.3.1 <5'
|
||||
nanoid@<3.3.17: '>=3.3.17 <4'
|
||||
fflate@<0.8.3: '>=0.8.3 <0.9'
|
||||
shell-quote@>=1.8.4 <1.11.0: '>=1.11.0 <2'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -18,7 +19,7 @@ importers:
|
||||
dependencies:
|
||||
'@inquirer/core':
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.3(@types/node@20.19.43)
|
||||
version: 12.0.4(@types/node@20.19.43)
|
||||
'@inquirer/prompts':
|
||||
specifier: ^8.5.2
|
||||
version: 8.5.2(@types/node@20.19.43)
|
||||
@@ -61,7 +62,7 @@ importers:
|
||||
version: 4.1.11(vitest@4.1.11)
|
||||
eslint:
|
||||
specifier: ^10.5.0
|
||||
version: 10.11.0
|
||||
version: 10.12.0
|
||||
smol-toml:
|
||||
specifier: ^1.7.1
|
||||
version: 1.9.0
|
||||
@@ -70,7 +71,7 @@ importers:
|
||||
version: 6.0.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.65.0
|
||||
version: 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
version: 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
vitest:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@20.19.43)(@vitest/ui@4.1.11)(vite@7.3.6(@types/node@20.19.43)(yaml@2.9.1))
|
||||
@@ -366,12 +367,8 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@inquirer/ansi@2.0.7':
|
||||
resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
|
||||
'@inquirer/ansi@2.0.8':
|
||||
resolution: {integrity: sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==}
|
||||
'@inquirer/ansi@2.0.9':
|
||||
resolution: {integrity: sha512-0Ywt3S8tqM6TWUcsRlBFQ1x+X3/o8kktX0ZiKAnByYb9GFDknnOhse1PVtfiD2BXPI8ar+BWx9PV9R0lQIcygw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
|
||||
'@inquirer/checkbox@5.2.1':
|
||||
@@ -401,8 +398,8 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@12.0.3':
|
||||
resolution: {integrity: sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==}
|
||||
'@inquirer/core@12.0.4':
|
||||
resolution: {integrity: sha512-M5fB67y7jPObI45vhD82RskWwCSm0scOvenV/I6K8tIi2gfr9DXqQqJ+iw+Wi6SgapikdOiJibYzz1gW47fsDg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
@@ -437,10 +434,6 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@2.0.7':
|
||||
resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
|
||||
'@inquirer/figures@2.0.9':
|
||||
resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
@@ -508,17 +501,8 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@4.0.7':
|
||||
resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@4.1.1':
|
||||
resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==}
|
||||
'@inquirer/type@4.2.0':
|
||||
resolution: {integrity: sha512-2rnHX8RPo4glJYqnFc/v3nQeIhDcjyRRyaT7MGcfL/8xv/8K03SXYoA2tX06WVSKywWOE/ncbG3dtuzLjR5TCQ==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
@@ -576,141 +560,141 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.5':
|
||||
resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==}
|
||||
'@rollup/rollup-android-arm-eabi@4.64.3':
|
||||
resolution: {integrity: sha512-a31GFbL70GlGUGhn8goVjRLtdDG1urLCJFWLZ7JcxdHK9pPvCohLFtYLgJDdNbtrNZSrJl1u9bK05ISsSC2EpA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==}
|
||||
'@rollup/rollup-android-arm64@4.64.3':
|
||||
resolution: {integrity: sha512-IRv757RLiPGlNxAX5JXU0eiUlUPvKiwFRbkqLd+gywptLK4uSEg1qkl6en6r0cXMazSnWUWOBABv+QfuLV3n5Q==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==}
|
||||
'@rollup/rollup-darwin-arm64@4.64.3':
|
||||
resolution: {integrity: sha512-UkzI35Fnz6+MJNATdnjnZ3TpdG/rXab8h7oDOD9R0zDVIFmuuYrvgUj8V8mh+A//+LR4TlEQo4ZW4RhyRtWv3A==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.5':
|
||||
resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==}
|
||||
'@rollup/rollup-darwin-x64@4.64.3':
|
||||
resolution: {integrity: sha512-C3AyMtAnKQ/jSkZ0chIoWbPXRbFz8JnPLhl1ULecEhcaOGkT19xt7KmO8BulQxt9S/XideHRXvsVQUbGj4JCOg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==}
|
||||
'@rollup/rollup-freebsd-arm64@4.64.3':
|
||||
resolution: {integrity: sha512-M8yAdBMyHh14JLeXDNGvEkljzuHdcoLBQ399O4Fp4nvX51lueuEY/5U2G9Ci0D6/bQlBp/3JLiBQH55STwwbwg==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.5':
|
||||
resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==}
|
||||
'@rollup/rollup-freebsd-x64@4.64.3':
|
||||
resolution: {integrity: sha512-WQjuOKGh2FN98ZK6i0uM6M6YqnPEds85p3oRkrxiEeTQo/p7RpuU8c9MwQ88LfgwVAzohumlzbQQSikz4Zw9rw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.5':
|
||||
resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==}
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.64.3':
|
||||
resolution: {integrity: sha512-5tDLZG5s9WyVKoEHgwos6FpKrgpotl61EYeHSLSVkQzMzTsaClwEr8mok9rm3y734zjiGInG4dfw/56ebWVvEQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.5':
|
||||
resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==}
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.64.3':
|
||||
resolution: {integrity: sha512-Mh5KbLeplM2HX6lQI18jsxSsqvPW8d+Hyk8HtY6Zl93woMitRSCZ92rBEzq6v8bfZGlpyMgj4P/sKW4KCjhH3A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==}
|
||||
'@rollup/rollup-linux-arm64-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-QZrrwHnznTv6y4iiHtCqDN+ZsF4qNFQObMOLgPrbxhh9O5o4rgoUe5OnkqG/ceCObMrAjhQt+eQ6HSDzBFgVMw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==}
|
||||
'@rollup/rollup-linux-arm64-musl@4.64.3':
|
||||
resolution: {integrity: sha512-4vLehpXSwlkltnvk5ETB+Hz+d3xayZS3NamZ5Vq4bSo+IUoIALovEJF1YgWPMjHwN7R5o/R4BpP9h3q8EK1xAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==}
|
||||
'@rollup/rollup-linux-loong64-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-SEN7TTTOdXJKZmsWKOnQeVfgy3Dje/08ISXWwFhTggQ3SxjuIaChvmOB5ZDZqJNlBjaXrV6nAHraqEAvdLGqcw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==}
|
||||
'@rollup/rollup-linux-loong64-musl@4.64.3':
|
||||
resolution: {integrity: sha512-1LDsQMRMCEdGF9Dih5qyh6fF1LK3NnA+pAtE2+9AgZwUdXyd19TOBVC9crSB63iZBHR4vOG7h8gN/LcUH3J7xw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==}
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-v3D2tAfRriqGOi+5Ac5CiYbBo2NXtLVxJlludTvUZRj502lVMFeVIuvvWbz4qCrotn3e3Uvpvh7FcMpG2QxxXQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==}
|
||||
'@rollup/rollup-linux-ppc64-musl@4.64.3':
|
||||
resolution: {integrity: sha512-db5q6dP189V2+JkMpsXBl8ym6bC0x8ydZ47fUJZ9YdM6T2OF5uqNzX9HaAZHRP/lqjA9bJeNwi0qw1/2/37dUA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==}
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-nArAVDmrL5ix9UoipcKEsYJw4kMUQCIPYRWuYF2POOC7jd9rmN95d/48ewsYFC/OdtwEx6VYYWYxrp8FbMOU2Q==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==}
|
||||
'@rollup/rollup-linux-riscv64-musl@4.64.3':
|
||||
resolution: {integrity: sha512-2dc7LElu7EVSvpQT8epNnBLa02wlqilNYqHN0FA/RaKqCr9U9ZFYQ0Eh4sfgSzLneLDuJsJRjcezR/l2UyYGXA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==}
|
||||
'@rollup/rollup-linux-s390x-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-9PYq2IEzSwXcvQLERcT9LKXRzw8J05GFTbJHA2anJR2WmlRjA1esgXVYhanp+bmjKsDVoZeU2L7Z2pRClLcZTw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==}
|
||||
'@rollup/rollup-linux-x64-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-vMh+mt0cw1BHldd8cqaU6A5s1W4EUKl6+8MRsv86mj954L4xyruKD7qPXMUI2icImadAZARK10h0onaGTLmCVg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==}
|
||||
'@rollup/rollup-linux-x64-musl@4.64.3':
|
||||
resolution: {integrity: sha512-PpiKU+sWbqDGc0U0su8oXiyeyqm0UQjmaaPKHktX+DaLe6YKJwW2YVD0f2AXHdVvblZbRqpwRVClWP2F5HCqrQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.5':
|
||||
resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==}
|
||||
'@rollup/rollup-openbsd-x64@4.64.3':
|
||||
resolution: {integrity: sha512-/3enY/AmaIt8IS0fWjX4ztYVPBqkj49cQTy/hCryhBFaEzDfJwwEolq9sxvyWAM2qmsmVuPdeRg5JXSrmeqvJg==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==}
|
||||
'@rollup/rollup-openharmony-arm64@4.64.3':
|
||||
resolution: {integrity: sha512-zsEP/QDuet52hhwvi5T1VPg0/Tkms0m3zkVv7CPsznvhdO4pL4oJTMz26EJsiCe18OeLKPgpTu3XVO3C1eq3ew==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.5':
|
||||
resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==}
|
||||
'@rollup/rollup-win32-arm64-msvc@4.64.3':
|
||||
resolution: {integrity: sha512-4ESknW4Db+kqvazVOfsReYJaHxcIsOqcgwjKV2J1tG5Bn4RVf5ocTQKSGuq2HWBJcOLuyim8IWX8DuH6nKmELA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.5':
|
||||
resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==}
|
||||
'@rollup/rollup-win32-ia32-msvc@4.64.3':
|
||||
resolution: {integrity: sha512-3B8VCQyIyv7FQxYYhb3HpR3bIpronR20txjMSffSoz/sm/DDs3/49CFpEueOsvq9qW9sWoI/6pi6mtciD9NXFg==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==}
|
||||
'@rollup/rollup-win32-x64-gnu@4.64.3':
|
||||
resolution: {integrity: sha512-YOoDkKh2oQxOxRB5D9AR8sj3qpMShpmyTkjojiBjMGigz3caV7UvCnXWWUQC0TBmrZ5zuHe2pWl8S+cMfvXzRg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.5':
|
||||
resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==}
|
||||
'@rollup/rollup-win32-x64-msvc@4.64.3':
|
||||
resolution: {integrity: sha512-qTv4bG+eKJKifzxNJevMUh8L6CjJrz9/Kn1IbP2F5bo0tBzNIR7HMg4vni6XmDzcKUl00lHHY+J5ZgjbGiujOQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -735,63 +719,63 @@ packages:
|
||||
'@types/node@20.19.43':
|
||||
resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.70.1':
|
||||
resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
|
||||
'@typescript-eslint/eslint-plugin@8.71.0':
|
||||
resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.70.1
|
||||
'@typescript-eslint/parser': ^8.71.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/parser@8.70.1':
|
||||
resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==}
|
||||
'@typescript-eslint/parser@8.71.0':
|
||||
resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.70.1':
|
||||
resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==}
|
||||
'@typescript-eslint/project-service@8.71.0':
|
||||
resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@8.70.1':
|
||||
resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==}
|
||||
'@typescript-eslint/scope-manager@8.71.0':
|
||||
resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.70.1':
|
||||
resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==}
|
||||
'@typescript-eslint/tsconfig-utils@8.71.0':
|
||||
resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.70.1':
|
||||
resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==}
|
||||
'@typescript-eslint/type-utils@8.71.0':
|
||||
resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/types@8.70.1':
|
||||
resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==}
|
||||
'@typescript-eslint/types@8.71.0':
|
||||
resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.70.1':
|
||||
resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==}
|
||||
'@typescript-eslint/typescript-estree@8.71.0':
|
||||
resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/utils@8.70.1':
|
||||
resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==}
|
||||
'@typescript-eslint/utils@8.71.0':
|
||||
resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.70.1':
|
||||
resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==}
|
||||
'@typescript-eslint/visitor-keys@8.71.0':
|
||||
resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vitest/expect@4.1.11':
|
||||
@@ -941,8 +925,8 @@ packages:
|
||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
eslint@10.11.0:
|
||||
resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==}
|
||||
eslint@10.12.0:
|
||||
resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1165,8 +1149,8 @@ packages:
|
||||
resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
nanoid@3.3.19:
|
||||
resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
|
||||
nanoid@3.3.20:
|
||||
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
@@ -1226,8 +1210,8 @@ packages:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postcss@8.5.28:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
postcss@8.5.29:
|
||||
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
@@ -1253,8 +1237,8 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rollup@4.63.5:
|
||||
resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==}
|
||||
rollup@4.64.3:
|
||||
resolution: {integrity: sha512-iIMWbptLYsZdGTJMH8ngApXdsCatE6bCMv2eqGIfGE1++kwh7TCWBdWKK0FKC0yx++196vGK5VdagzRXSLMa8A==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1277,8 +1261,8 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shell-quote@1.10.0:
|
||||
resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==}
|
||||
shell-quote@1.12.0:
|
||||
resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
@@ -1299,8 +1283,8 @@ packages:
|
||||
resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
source-map-js@1.2.2:
|
||||
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stackback@0.0.2:
|
||||
@@ -1358,8 +1342,8 @@ packages:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
typescript-eslint@8.70.1:
|
||||
resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==}
|
||||
typescript-eslint@8.71.0:
|
||||
resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
@@ -1704,9 +1688,9 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.10.1(eslint@10.11.0)':
|
||||
'@eslint-community/eslint-utils@4.10.1(eslint@10.12.0)':
|
||||
dependencies:
|
||||
eslint: 10.11.0
|
||||
eslint: 10.12.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
@@ -1750,31 +1734,29 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@inquirer/ansi@2.0.7': {}
|
||||
|
||||
'@inquirer/ansi@2.0.8': {}
|
||||
'@inquirer/ansi@2.0.9': {}
|
||||
|
||||
'@inquirer/checkbox@5.2.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.7
|
||||
'@inquirer/ansi': 2.0.9
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/figures': 2.0.7
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/figures': 2.0.9
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/confirm@6.1.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/core@11.2.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.7
|
||||
'@inquirer/figures': 2.0.7
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/ansi': 2.0.9
|
||||
'@inquirer/figures': 2.0.9
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
cli-width: 4.1.0
|
||||
fast-wrap-ansi: 0.2.2
|
||||
mute-stream: 3.0.0
|
||||
@@ -1782,11 +1764,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/core@12.0.3(@types/node@20.19.43)':
|
||||
'@inquirer/core@12.0.4(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.8
|
||||
'@inquirer/ansi': 2.0.9
|
||||
'@inquirer/figures': 2.0.9
|
||||
'@inquirer/type': 4.1.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
cli-width: 4.1.0
|
||||
fast-wrap-ansi: 0.2.2
|
||||
mute-stream: 3.0.0
|
||||
@@ -1798,14 +1780,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/external-editor': 3.0.3(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/expand@5.1.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
@@ -1816,29 +1798,27 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/figures@2.0.7': {}
|
||||
|
||||
'@inquirer/figures@2.0.9': {}
|
||||
|
||||
'@inquirer/input@5.1.2(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/number@4.1.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/password@5.1.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.7
|
||||
'@inquirer/ansi': 2.0.9
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
@@ -1860,32 +1840,28 @@ snapshots:
|
||||
'@inquirer/rawlist@5.3.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/search@4.2.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/figures': 2.0.7
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/figures': 2.0.9
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/select@5.2.1(@types/node@20.19.43)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.7
|
||||
'@inquirer/ansi': 2.0.9
|
||||
'@inquirer/core': 11.2.1(@types/node@20.19.43)
|
||||
'@inquirer/figures': 2.0.7
|
||||
'@inquirer/type': 4.0.7(@types/node@20.19.43)
|
||||
'@inquirer/figures': 2.0.9
|
||||
'@inquirer/type': 4.2.0(@types/node@20.19.43)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/type@4.0.7(@types/node@20.19.43)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
'@inquirer/type@4.1.1(@types/node@20.19.43)':
|
||||
'@inquirer/type@4.2.0(@types/node@20.19.43)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
@@ -1933,79 +1909,79 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.63.5':
|
||||
'@rollup/rollup-android-arm-eabi@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.5':
|
||||
'@rollup/rollup-android-arm64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.5':
|
||||
'@rollup/rollup-darwin-arm64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.5':
|
||||
'@rollup/rollup-darwin-x64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.5':
|
||||
'@rollup/rollup-freebsd-arm64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.5':
|
||||
'@rollup/rollup-freebsd-x64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.5':
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.5':
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.5':
|
||||
'@rollup/rollup-linux-arm64-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.5':
|
||||
'@rollup/rollup-linux-arm64-musl@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.5':
|
||||
'@rollup/rollup-linux-loong64-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.5':
|
||||
'@rollup/rollup-linux-loong64-musl@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.5':
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.5':
|
||||
'@rollup/rollup-linux-ppc64-musl@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.5':
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.5':
|
||||
'@rollup/rollup-linux-riscv64-musl@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.5':
|
||||
'@rollup/rollup-linux-s390x-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.5':
|
||||
'@rollup/rollup-linux-x64-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.5':
|
||||
'@rollup/rollup-linux-x64-musl@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.5':
|
||||
'@rollup/rollup-openbsd-x64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.5':
|
||||
'@rollup/rollup-openharmony-arm64@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.5':
|
||||
'@rollup/rollup-win32-arm64-msvc@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.5':
|
||||
'@rollup/rollup-win32-ia32-msvc@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.5':
|
||||
'@rollup/rollup-win32-x64-gnu@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.5':
|
||||
'@rollup/rollup-win32-x64-msvc@4.64.3':
|
||||
optional: true
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
@@ -2026,15 +2002,15 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/scope-manager': 8.70.1
|
||||
'@typescript-eslint/type-utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
eslint: 10.11.0
|
||||
'@typescript-eslint/parser': 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/scope-manager': 8.71.0
|
||||
'@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/visitor-keys': 8.71.0
|
||||
eslint: 10.12.0
|
||||
ignore: 7.0.10
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||
@@ -2042,56 +2018,56 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.70.1
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
'@typescript-eslint/scope-manager': 8.71.0
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
'@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3)
|
||||
'@typescript-eslint/visitor-keys': 8.71.0
|
||||
debug: 4.4.3
|
||||
eslint: 10.11.0
|
||||
eslint: 10.12.0
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.70.1(typescript@6.0.3)':
|
||||
'@typescript-eslint/project-service@8.71.0(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
debug: 4.4.3
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.70.1':
|
||||
'@typescript-eslint/scope-manager@8.71.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
'@typescript-eslint/visitor-keys': 8.71.0
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)':
|
||||
'@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)':
|
||||
dependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/type-utils@8.71.0(eslint@10.12.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
'@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3)
|
||||
'@typescript-eslint/utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
debug: 4.4.3
|
||||
eslint: 10.11.0
|
||||
eslint: 10.12.0
|
||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.70.1': {}
|
||||
'@typescript-eslint/types@8.71.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.70.1(typescript@6.0.3)':
|
||||
'@typescript-eslint/typescript-estree@8.71.0(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
'@typescript-eslint/project-service': 8.71.0(typescript@6.0.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
'@typescript-eslint/visitor-keys': 8.71.0
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.6
|
||||
semver: 7.8.5
|
||||
@@ -2101,20 +2077,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/utils@8.71.0(eslint@10.12.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0)
|
||||
'@typescript-eslint/scope-manager': 8.70.1
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
|
||||
eslint: 10.11.0
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0)
|
||||
'@typescript-eslint/scope-manager': 8.71.0
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
'@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3)
|
||||
eslint: 10.12.0
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.70.1':
|
||||
'@typescript-eslint/visitor-keys@8.71.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/types': 8.71.0
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@vitest/expect@4.1.11':
|
||||
@@ -2282,9 +2258,9 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@10.11.0:
|
||||
eslint@10.12.0:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0)
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0)
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.23.5
|
||||
'@eslint/config-helpers': 0.7.0
|
||||
@@ -2464,7 +2440,7 @@ snapshots:
|
||||
launch-editor@2.14.1:
|
||||
dependencies:
|
||||
picocolors: 1.1.1
|
||||
shell-quote: 1.10.0
|
||||
shell-quote: 1.12.0
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
@@ -2503,7 +2479,7 @@ snapshots:
|
||||
|
||||
mute-stream@3.0.0: {}
|
||||
|
||||
nanoid@3.3.19: {}
|
||||
nanoid@3.3.20: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
@@ -2557,11 +2533,11 @@ snapshots:
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
postcss@8.5.28:
|
||||
postcss@8.5.29:
|
||||
dependencies:
|
||||
nanoid: 3.3.19
|
||||
nanoid: 3.3.20
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
source-map-js: 1.2.2
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
@@ -2580,36 +2556,36 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rollup@4.63.5:
|
||||
rollup@4.64.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
optionalDependencies:
|
||||
'@napi-rs/lzma-linux-x64-gnu': 1.5.1
|
||||
'@rollup/rollup-android-arm-eabi': 4.63.5
|
||||
'@rollup/rollup-android-arm64': 4.63.5
|
||||
'@rollup/rollup-darwin-arm64': 4.63.5
|
||||
'@rollup/rollup-darwin-x64': 4.63.5
|
||||
'@rollup/rollup-freebsd-arm64': 4.63.5
|
||||
'@rollup/rollup-freebsd-x64': 4.63.5
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.63.5
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.63.5
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.63.5
|
||||
'@rollup/rollup-linux-arm64-musl': 4.63.5
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.63.5
|
||||
'@rollup/rollup-linux-loong64-musl': 4.63.5
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.63.5
|
||||
'@rollup/rollup-linux-ppc64-musl': 4.63.5
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.63.5
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.63.5
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.63.5
|
||||
'@rollup/rollup-linux-x64-gnu': 4.63.5
|
||||
'@rollup/rollup-linux-x64-musl': 4.63.5
|
||||
'@rollup/rollup-openbsd-x64': 4.63.5
|
||||
'@rollup/rollup-openharmony-arm64': 4.63.5
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.63.5
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.63.5
|
||||
'@rollup/rollup-win32-x64-gnu': 4.63.5
|
||||
'@rollup/rollup-win32-x64-msvc': 4.63.5
|
||||
'@rollup/rollup-android-arm-eabi': 4.64.3
|
||||
'@rollup/rollup-android-arm64': 4.64.3
|
||||
'@rollup/rollup-darwin-arm64': 4.64.3
|
||||
'@rollup/rollup-darwin-x64': 4.64.3
|
||||
'@rollup/rollup-freebsd-arm64': 4.64.3
|
||||
'@rollup/rollup-freebsd-x64': 4.64.3
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.64.3
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.64.3
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.64.3
|
||||
'@rollup/rollup-linux-arm64-musl': 4.64.3
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.64.3
|
||||
'@rollup/rollup-linux-loong64-musl': 4.64.3
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.64.3
|
||||
'@rollup/rollup-linux-ppc64-musl': 4.64.3
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.64.3
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.64.3
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.64.3
|
||||
'@rollup/rollup-linux-x64-gnu': 4.64.3
|
||||
'@rollup/rollup-linux-x64-musl': 4.64.3
|
||||
'@rollup/rollup-openbsd-x64': 4.64.3
|
||||
'@rollup/rollup-openharmony-arm64': 4.64.3
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.64.3
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.64.3
|
||||
'@rollup/rollup-win32-x64-gnu': 4.64.3
|
||||
'@rollup/rollup-win32-x64-msvc': 4.64.3
|
||||
fsevents: 2.3.3
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -2626,7 +2602,7 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shell-quote@1.10.0: {}
|
||||
shell-quote@1.12.0: {}
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
@@ -2642,7 +2618,7 @@ snapshots:
|
||||
|
||||
smol-toml@1.9.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
source-map-js@1.2.2: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
@@ -2686,13 +2662,13 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
typescript-eslint@8.70.1(eslint@10.11.0)(typescript@6.0.3):
|
||||
typescript-eslint@8.71.0(eslint@10.12.0)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
|
||||
eslint: 10.11.0
|
||||
'@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/parser': 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3)
|
||||
'@typescript-eslint/utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||
eslint: 10.12.0
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -2710,8 +2686,8 @@ snapshots:
|
||||
esbuild: 0.28.2
|
||||
fdir: 6.5.0(picomatch@4.0.7)
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.28
|
||||
rollup: 4.63.5
|
||||
postcss: 8.5.29
|
||||
rollup: 4.64.3
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
+10
-5
@@ -26,15 +26,20 @@ overrides:
|
||||
# Dev-only (transitive via @vitest/ui); never in the published CLI. Remove once
|
||||
# transitive fflate is >=0.8.3 (check: pnpm why fflate).
|
||||
fflate@<0.8.3: '>=0.8.3 <0.9'
|
||||
# GHSA-pqg4-j6r4-53mv — shell-quote `quote()` command injection via a line
|
||||
# terminator after a `{ comment }` token. Dev-only (transitive via
|
||||
# @changesets/cli > launch-editor); never in the published CLI. Remove once
|
||||
# transitive shell-quote is >=1.11.0 (check: pnpm why shell-quote).
|
||||
shell-quote@>=1.8.4 <1.11.0: '>=1.11.0 <2'
|
||||
|
||||
auditConfig:
|
||||
ignoreGhsas:
|
||||
# GHSA-vfj7-8cjw-p6xm — braces <=3.0.3 stack exhaustion on deeply nested
|
||||
# patterns. Runtime (fast-glob > micromatch > braces), and no patched braces
|
||||
# exists, so no override can fix it. Accepted residual risk: schema artifact
|
||||
# `generates` and `apply.tracks` patterns reach fast-glob through
|
||||
# resolveArtifactOutputs without a brace-depth guard, and the 10,000-character
|
||||
# input limit does not prevent stack exhaustion. A crafted project-local or
|
||||
# user schema can still terminate the openspec process. Remove this exception
|
||||
# exists, so no override can fix it. Schema artifact `generates` and
|
||||
# `apply.tracks` patterns reach fast-glob only through resolveArtifactOutputs,
|
||||
# which rejects brace or parenthesis nesting deeper than 16 levels before globbing. Accepted
|
||||
# residual risk: braces itself stays unpatched, so any new caller that passes
|
||||
# it unchecked patterns is exposed. Remove this exception
|
||||
# once braces ships a patched release (check: pnpm why braces; pnpm audit --prod).
|
||||
- GHSA-vfj7-8cjw-p6xm
|
||||
|
||||
@@ -178,6 +178,8 @@ If the user mentions a change or you detect one is relevant:
|
||||
|
||||
`<capability-path>` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
||||
|
||||
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from `artifactPaths` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|-------------------------------------|
|
||||
| New requirement discovered | `specs/<capability-path>/spec.md` |
|
||||
@@ -187,7 +189,7 @@ If the user mentions a change or you detect one is relevant:
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
Example offers (default schema):
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
@@ -316,7 +318,7 @@ You: That changes everything.
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? Run `/openspec-propose` and this becomes a change."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Result in artifact updates**: "Updated the design with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ This workflow revises artifacts that already exist; `/openspec-continue-change`
|
||||
|
||||
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
||||
|
||||
The files to edit are `artifactPaths.<id>.existingOutputPaths` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. `specs/**/*.md`). Do NOT write to `resolvedOutputPath`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
The files to edit are `artifactPaths.<id>.existingOutputPaths` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. `specs/**/*.md`), plus any confirmed concrete path allowed under a partly populated glob artifact (step 4). Do NOT write to `resolvedOutputPath`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
|
||||
3. **Understand the request**
|
||||
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
|
||||
@@ -102,7 +102,7 @@ After each invocation, show:
|
||||
**Guardrails**
|
||||
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, stop and point to `/openspec-apply-change`.
|
||||
- Use the artifact ids and paths reported by `openspec status`; never branch on hardcoded artifact names.
|
||||
- Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`.
|
||||
- Edit only the concrete files in `existingOutputPaths`, plus any confirmed concrete path allowed under a partly populated glob artifact (step 4); never write to a glob `resolvedOutputPath`.
|
||||
- Do not advance the build frontier: if an artifact has empty `existingOutputPaths` and status `ready` or `blocked`, that is `/openspec-continue-change`'s job. Leave `skipped` artifacts untouched. The only new-file scope is a confirmed concrete path under a glob artifact whose `existingOutputPaths` is non-empty.
|
||||
- Confirm every edit with the user before writing.
|
||||
- If the request changes the change's *intent* rather than refining it, recommend starting fresh with `/openspec-new-change` (the "Update vs. Start Fresh" heuristic).
|
||||
|
||||
@@ -101,8 +101,11 @@ In both branches, never create the root as a side effect: do not run `openspec i
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
- For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
|
||||
- Search codebase for the removed behavior. Matches in `openspec/` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
||||
- Finding no implementation is the expected result. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
||||
- A REMOVED block carries only the requirement name and any `**Reason**`/`**Migration**` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at `<planningHome.root>/openspec/specs/<capability-path>/spec.md`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
|
||||
- If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: `git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md`, or `git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: `git log` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
|
||||
- A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
|
||||
- Search codebase for that behavior. Matches in `openspec/` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
||||
- Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
||||
- If the behavior is still present:
|
||||
- Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
|
||||
- Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
|
||||
@@ -118,7 +121,7 @@ In both branches, never create the root as a side effect: do not run `openspec i
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
||||
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
|
||||
@@ -172,7 +175,7 @@ In both branches, never create the root as a side effect: do not run `openspec i
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
In each Status cell, report the results of checks that ran and `Not verified (<reason>)` for every skipped check. If all checks in a dimension were skipped, start the cell with `Not verified`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads `Not applicable (no ADDED or MODIFIED requirements)`.
|
||||
In each Status cell, report the results of checks that ran and `Not verified (<reason>)` for every skipped check. If all checks in a dimension were skipped, start the cell with `Not verified`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads `Not applicable (no ADDED or MODIFIED requirements)`.
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
|
||||
@@ -123,6 +123,14 @@ export function isCompletionRun(commandPath: string): boolean {
|
||||
return commandPath.split(':')[0] === 'completion' || commandPath === '__complete';
|
||||
}
|
||||
|
||||
/**
|
||||
* True for the hidden `__complete` resolver alone. It runs on every Tab press,
|
||||
* so the hooks skip telemetry and the completion tip for it entirely.
|
||||
*/
|
||||
export function isTabCompletionRun(commandPath: string): boolean {
|
||||
return commandPath === '__complete';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the first-run completions tip must be deferred rather than shown.
|
||||
*
|
||||
@@ -196,6 +204,13 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
||||
process.env.NO_COLOR = '1';
|
||||
}
|
||||
|
||||
// `__complete` runs on every Tab press, with stderr sent to /dev/null by the
|
||||
// generated scripts. Tracking it would hold each press open for the request,
|
||||
// and the first-run notice would be marked seen without anyone reading it.
|
||||
if (isTabCompletionRun(getCommandPath(actionCommand))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show first-run telemetry notice (if not seen). It's written to stderr, so it
|
||||
// never pollutes stdout — but --json runs still defer it (see isJsonRun) so the
|
||||
// very first invocation stays free of any incidental output on either stream.
|
||||
@@ -210,6 +225,12 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
||||
|
||||
// Shutdown telemetry after command completes
|
||||
program.hook('postAction', async (_thisCommand, actionCommand) => {
|
||||
// Nothing to flush (preAction tracked nothing), and the tip is always
|
||||
// deferred for completion runs, so skip loading either module.
|
||||
if (isTabCompletionRun(getCommandPath(actionCommand))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the first-run shell-completions tip (on stderr, so piped stdout stays
|
||||
// clean). postAction, not preAction: the tip trails the command's own output
|
||||
// instead of pushing an error message or `init`'s setup summary down the
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import ora from 'ora';
|
||||
import { CompletionFactory } from '../core/completions/factory.js';
|
||||
import { COMMAND_REGISTRY } from '../core/completions/command-registry.js';
|
||||
import { detectShell, SupportedShell } from '../utils/shell-detection.js';
|
||||
@@ -125,6 +124,9 @@ export class CompletionCommand {
|
||||
const generator = CompletionFactory.createGenerator(shell);
|
||||
const installer = CompletionFactory.createInstaller(shell);
|
||||
|
||||
// Loaded here, not at the top: `__complete` shares this module and runs on
|
||||
// every Tab press, so it must not pay for the spinner.
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora(`Installing ${shell} completion script...`).start();
|
||||
|
||||
try {
|
||||
@@ -234,6 +236,7 @@ export class CompletionCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora(`Uninstalling ${shell} completion script...`).start();
|
||||
|
||||
try {
|
||||
|
||||
+19
-7
@@ -1358,13 +1358,22 @@ export class ArchiveCommand {
|
||||
const archiveDir = root.archiveDir;
|
||||
const mainSpecsDir = root.specsDir;
|
||||
|
||||
for (const [allowedDirectory, managedDir] of [
|
||||
[root.path, changesDir],
|
||||
[changesDir, archiveDir],
|
||||
[root.path, mainSpecsDir],
|
||||
const openspecDir = path.dirname(changesDir);
|
||||
|
||||
// openspec/, changes/ and specs/ may be project-owned symlinks to another
|
||||
// directory; archive/ may not leave the changes directory it lives in.
|
||||
for (const [allowedDirectory, managedDir, allowLinkedLeaf] of [
|
||||
[root.path, openspecDir, true],
|
||||
[openspecDir, changesDir, true],
|
||||
[changesDir, archiveDir, false],
|
||||
[openspecDir, mainSpecsDir, true],
|
||||
] as const) {
|
||||
try {
|
||||
FileSystemUtils.assertPathWithin(allowedDirectory, managedDir);
|
||||
if (allowLinkedLeaf) {
|
||||
FileSystemUtils.assertPathWithinAllowingLinkedLeaf(allowedDirectory, managedDir);
|
||||
} else {
|
||||
FileSystemUtils.assertPathWithin(allowedDirectory, managedDir);
|
||||
}
|
||||
} catch {
|
||||
throw new ArchiveBlockedError(
|
||||
'archive_path_outside_root',
|
||||
@@ -2190,6 +2199,9 @@ export class ArchiveCommand {
|
||||
retirementAuthorizationFingerprint!
|
||||
);
|
||||
}
|
||||
// update.source is canonical (symlinks resolved), so measure it from
|
||||
// the canonical change directory; changes/ may itself be a symlink.
|
||||
const canonicalChangeDir = FileSystemUtils.canonicalizeExistingPath(changeDir);
|
||||
const verifyArchivedDeltas = async (
|
||||
stagedSource?: string
|
||||
): Promise<void> => {
|
||||
@@ -2213,7 +2225,7 @@ export class ArchiveCommand {
|
||||
for (const proposed of prepared) {
|
||||
const archivedSource = path.join(
|
||||
archivePath,
|
||||
path.relative(changeDir, proposed.update.source)
|
||||
path.relative(canonicalChangeDir, proposed.update.source)
|
||||
);
|
||||
if (
|
||||
(await fingerprintPortableContent(archivedSource)) !==
|
||||
@@ -2226,7 +2238,7 @@ export class ArchiveCommand {
|
||||
if (stagedSource) {
|
||||
const stagedDelta = path.join(
|
||||
stagedSource,
|
||||
path.relative(changeDir, proposed.update.source)
|
||||
path.relative(canonicalChangeDir, proposed.update.source)
|
||||
);
|
||||
if (
|
||||
(await fingerprintPortableContent(stagedDelta)) !==
|
||||
|
||||
@@ -5,6 +5,39 @@ import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
|
||||
const EXTGLOB_RE = /[!*+?@]\([^(]*\)/u;
|
||||
const BRACE_EXPANSION_SEPARATORS_RE = /,|\.\./u;
|
||||
// braces <=3.0.3 (fast-glob > micromatch) has no depth guard (GHSA-vfj7-8cjw-p6xm),
|
||||
// so reject nesting no real artifact pattern needs before it reaches the parser.
|
||||
const MAX_BRACE_NESTING = 16;
|
||||
|
||||
function assertBraceNesting(pattern: string): void {
|
||||
// Mirror the braces parser: `[...]` and quoted runs are literal, `(` nests like
|
||||
// `{`, and a closer only closes the innermost group of its own kind.
|
||||
const open: string[] = [];
|
||||
for (let index = 0; index < pattern.length; index += 1) {
|
||||
const char = pattern[index];
|
||||
if (char === '[') {
|
||||
for (let brackets = 1; brackets > 0 && index + 1 < pattern.length; ) {
|
||||
index += 1;
|
||||
if (pattern[index] === '[') brackets += 1;
|
||||
else if (pattern[index] === ']') brackets -= 1;
|
||||
}
|
||||
} else if (char === '"' || char === "'" || char === '`') {
|
||||
const end = pattern.indexOf(char, index + 1);
|
||||
index = end === -1 ? pattern.length : end;
|
||||
} else if (char === '{' || char === '(') {
|
||||
open.push(char);
|
||||
if (open.length > MAX_BRACE_NESTING) {
|
||||
// A pattern this deep can run to thousands of characters; quote only its start.
|
||||
const shown = pattern.length > 80 ? `${pattern.slice(0, 80)}...` : pattern;
|
||||
throw new Error(
|
||||
`Artifact output pattern nests braces more than ${MAX_BRACE_NESTING} levels deep: ${shown}`
|
||||
);
|
||||
}
|
||||
} else if ((char === '}' && open.at(-1) === '{') || (char === ')' && open.at(-1) === '(')) {
|
||||
open.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasBraceExpansion(pattern: string): boolean {
|
||||
const openings: number[] = [];
|
||||
@@ -115,9 +148,11 @@ function assertGlobDirectoryTraversal(
|
||||
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
||||
*/
|
||||
export function resolveArtifactOutputs(changeDir: string, generates: string): string[] {
|
||||
const isGlob = isGlobPattern(generates);
|
||||
if (isGlob) assertBraceNesting(FileSystemUtils.toPosixPath(generates));
|
||||
const outputPath = resolveArtifactOutputPath(changeDir, generates);
|
||||
|
||||
if (!isGlobPattern(generates)) {
|
||||
if (!isGlob) {
|
||||
try {
|
||||
return fs.statSync(outputPath).isFile()
|
||||
? [FileSystemUtils.canonicalizeExistingPath(outputPath)]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getActiveChangeIds, getSpecIds } from '../../utils/item-discovery.js';
|
||||
import { listSchemas } from '../artifact-graph/index.js';
|
||||
|
||||
/**
|
||||
* Cache entry for completion data
|
||||
@@ -96,7 +95,10 @@ export class CompletionProvider {
|
||||
return this.schemaCache.data;
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
// Fetch fresh data. The resolver (and the yaml and zod it parses schemas
|
||||
// with) loads only here: completing changes or specs never needs it, and
|
||||
// the artifact-graph barrel would also pull in fast-glob.
|
||||
const { listSchemas } = await import('../artifact-graph/resolver.js');
|
||||
const schemaNames = listSchemas(this.projectRoot);
|
||||
|
||||
// Update cache
|
||||
|
||||
@@ -169,6 +169,16 @@ export interface OrphanedRequirement {
|
||||
line: number; // 1-based line number in the delta file
|
||||
}
|
||||
|
||||
/**
|
||||
* Authored text archive does not carry into the main spec: a non-delta `## `
|
||||
* section, text above the first `## `, or prose before the first requirement
|
||||
* in ADDED/MODIFIED. `section` is null for text above the first `## `.
|
||||
*/
|
||||
export interface UnappliedContent {
|
||||
section: string | null;
|
||||
line: number; // 1-based: the section header, or the first line of leading prose
|
||||
}
|
||||
|
||||
export interface DeltaPlan {
|
||||
added: RequirementBlock[];
|
||||
modified: RequirementBlock[];
|
||||
@@ -182,6 +192,7 @@ export interface DeltaPlan {
|
||||
unpairedRenames: UnpairedRename[];
|
||||
/** Canonical requirement blocks written outside every delta section. */
|
||||
orphanedRequirements: OrphanedRequirement[];
|
||||
unappliedContent: UnappliedContent[];
|
||||
skippedHeaders: SkippedHeader[]; // non-canonical ### headers the reader skipped
|
||||
sectionPresence: {
|
||||
added: boolean;
|
||||
@@ -256,6 +267,7 @@ export function parseDeltaSpec(content: string): DeltaPlan {
|
||||
renamed: renamedPairs,
|
||||
unpairedRenames,
|
||||
orphanedRequirements: findOrphanedRequirements(lines, fenceMask),
|
||||
unappliedContent: findUnappliedContent(lines, fenceMask),
|
||||
skippedHeaders,
|
||||
sectionPresence: {
|
||||
added: addedLookup.found,
|
||||
@@ -315,6 +327,57 @@ function findOrphanedRequirements(
|
||||
return orphans;
|
||||
}
|
||||
|
||||
export function describeUnappliedContent(content: UnappliedContent, sentenceStart = false): string {
|
||||
const text = content.section === null
|
||||
? 'text above the first "## " section'
|
||||
: DELTA_SECTION_TITLES.has(content.section.toLowerCase())
|
||||
? `text before the first requirement in "## ${content.section}"`
|
||||
: `section "## ${content.section}"`;
|
||||
return sentenceStart ? text.charAt(0).toUpperCase() + text.slice(1) : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where text the merge never reads begins. HTML comments, `# ` titles, Purpose
|
||||
* (warned about separately), REMOVED/RENAMED bodies, and stray `###` headers
|
||||
* before the first requirement (INFO since #498) are not reported.
|
||||
*/
|
||||
function findUnappliedContent(lines: string[], fenceMask: boolean[]): UnappliedContent[] {
|
||||
// Blank out comments outside fences, keeping line count; an unterminated one
|
||||
// runs to EOF. Comments only decide whether a line holds text: sections come
|
||||
// from the same split the merge reads, where a `## ` line inside a comment is
|
||||
// still a boundary. Fenced lines keep their text, so a `<!--` in a code sample
|
||||
// never hides what follows the fence.
|
||||
const comments = lines
|
||||
.map((line, i) => (fenceMask[i] ? '' : line))
|
||||
.join('\n')
|
||||
.replace(/<!--[\s\S]*?(?:--!?>|$)/g, (m) => m.replace(/[^\n]/g, ' '))
|
||||
.split('\n');
|
||||
const masked = lines.map((line, i) => (fenceMask[i] ? line : comments[i]));
|
||||
const sections = splitTopLevelSections(lines, fenceMask);
|
||||
const firstText = (from: number, to: number, skip?: RegExp) => {
|
||||
for (let i = from; i < to; i++) if (masked[i].trim() && !skip?.test(masked[i])) return i;
|
||||
return -1;
|
||||
};
|
||||
const found: UnappliedContent[] = [];
|
||||
const preamble = firstText(0, sections.length ? sections[0].body.bodyStartLine - 2 : lines.length, /^#\s/);
|
||||
if (preamble !== -1) found.push({ section: null, line: preamble + 1 });
|
||||
for (const { title, body } of sections) {
|
||||
const folded = title.toLowerCase();
|
||||
const start = body.bodyStartLine - 1;
|
||||
if (folded === 'added requirements' || folded === 'modified requirements') {
|
||||
const block = body.lines.findIndex((l, i) => !body.fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(l));
|
||||
const first = firstText(start, start + (block === -1 ? body.lines.length : block), /^###\s/);
|
||||
if (first !== -1) found.push({ section: title, line: first + 1 });
|
||||
} else if (!DELTA_SECTION_TITLES.has(folded)) {
|
||||
// The Purpose carry-over reads past commented-out headers, so only a
|
||||
// `## Purpose` outside a comment is the one archive handles.
|
||||
if (folded === 'purpose' && masked[start - 1].trim()) continue;
|
||||
if (firstText(start, start + body.lines.length) !== -1) found.push({ section: title, line: start });
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** One `## ` section of a delta file, in the order it was written. */
|
||||
interface DeltaSection {
|
||||
title: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
extractRequirementsSection,
|
||||
diffScenarioNames,
|
||||
describeScenarioBalance,
|
||||
describeUnappliedContent,
|
||||
foldRequirementName,
|
||||
parseDeltaSpec,
|
||||
normalizeRequirementName,
|
||||
@@ -251,6 +252,11 @@ export async function buildUpdatedSpec(
|
||||
);
|
||||
}
|
||||
|
||||
// Archive carries requirement blocks only; name any other text it leaves behind.
|
||||
for (const lost of plan.unappliedContent) {
|
||||
warn(`${specName} - ${describeUnappliedContent(lost)} (line ${lost.line}) is not carried into the main spec.`);
|
||||
}
|
||||
|
||||
// Pre-validate duplicates within sections
|
||||
const addedNames = new Set<string>();
|
||||
for (const add of plan.added) {
|
||||
|
||||
@@ -217,6 +217,8 @@ If the user mentions a change or you detect one is relevant:
|
||||
|
||||
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
||||
|
||||
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from \`artifactPaths\` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|-------------------------------------|
|
||||
| New requirement discovered | \`specs/<capability-path>/spec.md\` |
|
||||
@@ -226,7 +228,7 @@ If the user mentions a change or you detect one is relevant:
|
||||
| New work identified | \`tasks.md\` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
Example offers (default schema):
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
@@ -355,7 +357,7 @@ You: That changes everything.
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "${DISCOVERY_END_HANDOFF}"
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Result in artifact updates**: "Updated the design with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
@@ -551,6 +553,8 @@ If the user mentions a change or you detect one is relevant:
|
||||
|
||||
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
||||
|
||||
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from \`artifactPaths\` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|-------------------------------------|
|
||||
| New requirement discovered | \`specs/<capability-path>/spec.md\` |
|
||||
@@ -560,7 +564,7 @@ If the user mentions a change or you detect one is relevant:
|
||||
| New work identified | \`tasks.md\` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
Example offers (default schema):
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
@@ -585,7 +589,7 @@ If the user mentions a change or you detect one is relevant:
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "${DISCOVERY_END_HANDOFF}"
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Result in artifact updates**: "Updated the design with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ ${CONTINUE_SCOPE_NOTE}
|
||||
|
||||
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
||||
|
||||
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`), plus any confirmed concrete path allowed under a partly populated glob artifact (step 4). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
|
||||
3. **Understand the request**
|
||||
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
|
||||
@@ -161,7 +161,7 @@ After each invocation, show:
|
||||
**Guardrails**
|
||||
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, ${APPLY_GUARDRAIL}.
|
||||
- Use the artifact ids and paths reported by \`openspec status\`; never branch on hardcoded artifact names.
|
||||
- Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`.
|
||||
- Edit only the concrete files in \`existingOutputPaths\`, plus any confirmed concrete path allowed under a partly populated glob artifact (step 4); never write to a glob \`resolvedOutputPath\`.
|
||||
- Do not advance the build frontier: if an artifact has empty \`existingOutputPaths\` and status \`ready\` or \`blocked\`, ${CONTINUE_FRONTIER}. Leave \`skipped\` artifacts untouched. The only new-file scope is a confirmed concrete path under a glob artifact whose \`existingOutputPaths\` is non-empty.
|
||||
- Confirm every edit with the user before writing.
|
||||
- If the request changes the change's *intent* rather than refining it, ${INTENT_CHANGE_GUARDRAIL}.`,
|
||||
@@ -217,7 +217,7 @@ ${CONTINUE_SCOPE_NOTE}
|
||||
|
||||
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
||||
|
||||
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`), plus any confirmed concrete path allowed under a partly populated glob artifact (step 4). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
||||
|
||||
3. **Understand the request**
|
||||
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
|
||||
@@ -261,7 +261,7 @@ After each invocation, show:
|
||||
**Guardrails**
|
||||
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, ${APPLY_GUARDRAIL}.
|
||||
- Use the artifact ids and paths reported by \`openspec status\`; never branch on hardcoded artifact names.
|
||||
- Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`.
|
||||
- Edit only the concrete files in \`existingOutputPaths\`, plus any confirmed concrete path allowed under a partly populated glob artifact (step 4); never write to a glob \`resolvedOutputPath\`.
|
||||
- Do not advance the build frontier: if an artifact has empty \`existingOutputPaths\` and status \`ready\` or \`blocked\`, ${CONTINUE_FRONTIER}. Leave \`skipped\` artifacts untouched. The only new-file scope is a confirmed concrete path under a glob artifact whose \`existingOutputPaths\` is non-empty.
|
||||
- Confirm every edit with the user before writing.
|
||||
- If the request changes the change's *intent* rather than refining it, ${INTENT_CHANGE_GUARDRAIL}.`
|
||||
|
||||
@@ -95,8 +95,11 @@ ${PROJECT_ROOT_GUARD}
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
- For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
|
||||
- Search codebase for the removed behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
||||
- Finding no implementation is the expected result. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
||||
- A REMOVED block carries only the requirement name and any \`**Reason**\`/\`**Migration**\` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
|
||||
- If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: \`git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md\`, or \`git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md\` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: \`git log\` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
|
||||
- A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
|
||||
- Search codebase for that behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
||||
- Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
||||
- If the behavior is still present:
|
||||
- Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
|
||||
- Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
|
||||
@@ -112,7 +115,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
||||
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
|
||||
@@ -166,7 +169,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
| Coherence | Followed/Issues |
|
||||
\`\`\`
|
||||
|
||||
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
||||
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
@@ -305,8 +308,11 @@ ${PROJECT_ROOT_GUARD}
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
- For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
|
||||
- Search codebase for the removed behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
||||
- Finding no implementation is the expected result. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
||||
- A REMOVED block carries only the requirement name and any \`**Reason**\`/\`**Migration**\` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
|
||||
- If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: \`git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md\`, or \`git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md\` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: \`git log\` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
|
||||
- A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
|
||||
- Search codebase for that behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
|
||||
- Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
|
||||
- If the behavior is still present:
|
||||
- Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
|
||||
- Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
|
||||
@@ -322,7 +328,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
||||
If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
|
||||
@@ -376,7 +382,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
| Coherence | Followed/Issues |
|
||||
\`\`\`
|
||||
|
||||
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
||||
In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
extractRequirementsSection,
|
||||
diffScenarioNames,
|
||||
describeScenarioBalance,
|
||||
describeUnappliedContent,
|
||||
type RequirementBlock,
|
||||
} from '../parsers/requirement-blocks.js';
|
||||
import {
|
||||
@@ -257,6 +258,17 @@ export class Validator {
|
||||
});
|
||||
}
|
||||
|
||||
// Archive carries requirement blocks only, by design. Warn so text
|
||||
// written elsewhere is moved rather than lost without notice.
|
||||
for (const lost of plan.unappliedContent) {
|
||||
issues.push({
|
||||
level: 'WARNING',
|
||||
path: entryPath,
|
||||
line: lost.line,
|
||||
message: `${describeUnappliedContent(lost, true)} is not carried into the main spec by archive. Move it into a requirement, or into proposal.md or design.md.`,
|
||||
});
|
||||
}
|
||||
|
||||
const sectionNames: string[] = [];
|
||||
if (plan.sectionPresence.added) sectionNames.push('## ADDED Requirements');
|
||||
if (plan.sectionPresence.modified) sectionNames.push('## MODIFIED Requirements');
|
||||
|
||||
@@ -14,10 +14,26 @@ export const METADATA_FILENAME = '.openspec.yaml';
|
||||
|
||||
export { CHANGE_METADATA_KNOWN_KEYS };
|
||||
|
||||
/**
|
||||
* Extension keys: a top-level key starting with a lowercase `x-` belongs to a
|
||||
* tool or team that reads `.openspec.yaml` alongside OpenSpec (a tracker id, an
|
||||
* owner). OpenSpec ignores it and does not report it as unknown. As with
|
||||
* OpenAPI's `x-` extensions, the name after the prefix may use any case or
|
||||
* spelling, but must be non-empty and free of whitespace, so `x-`, `x- foo`
|
||||
* and `X-Foo` are still reported. Extensions are top-level only: a key inside
|
||||
* `initiative:` is rejected by that object's strict schema.
|
||||
*/
|
||||
export const EXTENSION_KEY_PATTERN = /^x-\S+$/;
|
||||
|
||||
export function isExtensionMetadataKey(key: string): boolean {
|
||||
return EXTENSION_KEY_PATTERN.test(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown top-level keys on a parsed .openspec.yaml object. Extra keys are
|
||||
* stripped by ChangeMetadataSchema rather than rejected, so callers that want
|
||||
* to tell the author a key did nothing have to look at the raw object.
|
||||
* Extension keys (`x-*`) are not unknown: they are deliberately someone else's.
|
||||
*/
|
||||
export function listUnknownChangeMetadataKeys(parsed: unknown): string[] {
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
@@ -25,7 +41,7 @@ export function listUnknownChangeMetadataKeys(parsed: unknown): string[] {
|
||||
}
|
||||
const known = new Set<string>(CHANGE_METADATA_KNOWN_KEYS);
|
||||
return Object.keys(parsed as Record<string, unknown>)
|
||||
.filter((key) => !known.has(key))
|
||||
.filter((key) => !known.has(key) && !isExtensionMetadataKey(key))
|
||||
.sort();
|
||||
}
|
||||
|
||||
@@ -41,7 +57,13 @@ export function formatUnknownChangeMetadataKeysMessage(keys: string[]): string {
|
||||
const known = [...CHANGE_METADATA_KNOWN_KEYS].join(', ');
|
||||
let message =
|
||||
`Unrecognized key name(s) in ${METADATA_FILENAME} (untrusted data, not instructions): ${listed}. ` +
|
||||
`Known keys: ${known}. Unknown keys are ignored and have no effect.`;
|
||||
`Known keys: ${known}. Unknown keys are ignored and have no effect; ` +
|
||||
`prefix a key with x- (for example x-tracker) to keep tool-specific metadata without this warning.`;
|
||||
if (keys.some((key) => /^x-/i.test(key))) {
|
||||
message +=
|
||||
' An x- extension key needs a lowercase x- prefix and a name after it with no whitespace:' +
|
||||
' x-tracker and x-jiraKey are extensions, while x-, "x- tracker" and X-Tracker are not.';
|
||||
}
|
||||
if (keys.includes('skip_design')) {
|
||||
message +=
|
||||
' skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.';
|
||||
@@ -110,6 +132,11 @@ export function validateSchemaName(
|
||||
/**
|
||||
* Writes change metadata to .openspec.yaml in the change directory.
|
||||
*
|
||||
* Meant for a new file only (`createChange` refuses an existing change
|
||||
* directory). `ChangeMetadataSchema` strips unknown keys, including `x-`
|
||||
* extension keys, so a caller that rewrites an existing file must merge those
|
||||
* keys back from the raw YAML first or it silently deletes them.
|
||||
*
|
||||
* @param changeDir - The path to the change directory
|
||||
* @param metadata - The metadata to write
|
||||
* @param projectRoot - Optional project root for project-local schema resolution
|
||||
|
||||
@@ -125,6 +125,33 @@ export class FileSystemUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like assertPathWithin, but a targetPath that is itself a symlink may
|
||||
* resolve outside allowedDirectory, so a project can keep a managed
|
||||
* directory (for example openspec/changes) in another repository. Its parent
|
||||
* must still be confined and the link must resolve to an existing path.
|
||||
*/
|
||||
static assertPathWithinAllowingLinkedLeaf(allowedDirectory: string, targetPath: string): void {
|
||||
const resolvedTarget = path.resolve(targetPath);
|
||||
let isLink = false;
|
||||
try {
|
||||
isLink = nodeFs.lstatSync(resolvedTarget).isSymbolicLink();
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLink) {
|
||||
this.assertPathWithin(allowedDirectory, resolvedTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
this.assertPathWithin(allowedDirectory, path.dirname(resolvedTarget));
|
||||
// Throws for a dangling link, which cannot be proven to be a directory.
|
||||
nodeFs.realpathSync.native(resolvedTarget);
|
||||
}
|
||||
|
||||
static resolveProjectArtifactPath(projectPath: string, artifactPath: string): string {
|
||||
if (path.isAbsolute(artifactPath)) {
|
||||
throw new Error(`Refusing to manage an artifact outside the project: ${artifactPath}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
||||
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
@@ -118,6 +118,101 @@ describe('CLI startup loads only what the command needs', () => {
|
||||
}
|
||||
);
|
||||
|
||||
// Shell completion scripts run `__complete` on every Tab press. It needs no
|
||||
// package beyond commander, and never telemetry: tracking would hold each
|
||||
// press open for the request.
|
||||
it.each(['changes', 'specs'])(
|
||||
'`openspec __complete %s` loads only commander and no telemetry',
|
||||
async (type) => {
|
||||
await ensureCliBuilt();
|
||||
const { own, packages, status } = loadedModules(['__complete', type]);
|
||||
|
||||
expect(status).toBe(0);
|
||||
expect([...packages]).toEqual(['commander']);
|
||||
expect(implementationsLoaded(own)).toEqual(['completion']);
|
||||
expect(own).not.toContain('telemetry/index.js');
|
||||
}
|
||||
);
|
||||
|
||||
it('`openspec __complete schemas` loads the schema reader but not fast-glob or ora', async () => {
|
||||
await ensureCliBuilt();
|
||||
const { own, packages, status } = loadedModules(['__complete', 'schemas']);
|
||||
|
||||
expect(status).toBe(0);
|
||||
expect([...packages]).not.toContain('fast-glob');
|
||||
expect([...packages]).not.toContain('ora');
|
||||
expect(own).not.toContain('telemetry/index.js');
|
||||
// The name promises a schema reader, so pin it: the resolver directly, not
|
||||
// the artifact-graph barrel, which re-exports it but also pulls outputs.js
|
||||
// and with it fast-glob.
|
||||
expect(own).toContain('core/artifact-graph/resolver.js');
|
||||
expect(own).not.toContain('core/artifact-graph/index.js');
|
||||
});
|
||||
|
||||
// Negative control for the two cases above. `isTabCompletionRun` matches the
|
||||
// command path exactly, unlike the adjacent `isCompletionRun`, which
|
||||
// prefix-matches `completion:*`. Widening one into the other would silently
|
||||
// stop tracking every user-facing command in that group, and every assertion
|
||||
// above would still pass.
|
||||
it.each([['list --json'], ['completion generate zsh']])(
|
||||
'`openspec %s` still reaches the telemetry hooks',
|
||||
async (invocation) => {
|
||||
await ensureCliBuilt();
|
||||
const { own } = loadedModules(invocation.split(' '));
|
||||
|
||||
expect(own).toContain('telemetry/index.js');
|
||||
expect(own).toContain('core/completion-tip.js');
|
||||
}
|
||||
);
|
||||
|
||||
// The module-count assertions above pin that telemetry is not LOADED. They do
|
||||
// not pin the user-visible consequence, which is the reason the skip exists:
|
||||
// the first-run disclosure goes to stderr, every generated completion script
|
||||
// sends `__complete` stderr to /dev/null, and `maybeShowTelemetryNotice` marks
|
||||
// `noticeSeen` before `trackCommand` re-reads it. So before this change a
|
||||
// user's first-ever interaction being a Tab press burned the disclosure where
|
||||
// nobody could read it and began collecting on that same press.
|
||||
//
|
||||
// Telemetry is deliberately left ENABLED here: the point is that an enabled
|
||||
// run still records nothing. HOME must be isolated as well as XDG_CONFIG_HOME,
|
||||
// or `migrateLegacyTelemetryConfig` imports the developer's real config from
|
||||
// ~/.config/openspec/config.json and the test passes vacuously.
|
||||
it('`openspec __complete` leaves the first-run telemetry notice unconsumed', async () => {
|
||||
await ensureCliBuilt();
|
||||
const home = mkdtempSync(path.join(os.tmpdir(), 'openspec-notice-'));
|
||||
workDirs.push(home);
|
||||
const configHome = path.join(home, 'config');
|
||||
|
||||
// These have to be DELETED, not blanked. Both opt-outs read as "set to
|
||||
// anything that is not an explicit on-value", so `OPENSPEC_TELEMETRY: ''`
|
||||
// and `DO_NOT_TRACK: ''` would switch telemetry OFF and the assertions
|
||||
// below would hold on any build, passing vacuously.
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: configHome,
|
||||
XDG_DATA_HOME: path.join(home, 'data'),
|
||||
};
|
||||
delete env.OPENSPEC_TELEMETRY;
|
||||
delete env.DO_NOT_TRACK;
|
||||
delete env.CI;
|
||||
|
||||
const result = spawnSync(process.execPath, [binPath, '__complete', 'changes'], {
|
||||
cwd: home,
|
||||
env,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30_000,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
// No disclosure written to a stream the completion scripts discard.
|
||||
expect(result.stderr).toBe('');
|
||||
// And no config file, so neither noticeSeen nor an anonymousId was minted.
|
||||
expect(existsSync(path.join(configHome, 'openspec', 'config.json'))).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['config list --json', 'config'],
|
||||
['config path', 'config'],
|
||||
|
||||
@@ -153,6 +153,31 @@ describe('ArchiveCommand', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('archives x- extension keys without a warning and keeps them byte-for-byte', async () => {
|
||||
const changeName = 'extension-metadata';
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', changeName);
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
await fs.writeFile(path.join(changeDir, 'tasks.md'), '- [x] Task 1\n');
|
||||
const metadata =
|
||||
'schema: spec-driven\nx-goal: G-12\nx-tracker:\n system: linear\n id: SB-5497\n';
|
||||
await fs.writeFile(path.join(changeDir, '.openspec.yaml'), metadata);
|
||||
|
||||
await archiveCommand.execute(changeName, { yes: true, noValidate: true, json: true });
|
||||
|
||||
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls
|
||||
.flat()
|
||||
.map(String);
|
||||
const jsonLine = logCalls.find((entry) => entry.trimStart().startsWith('{'));
|
||||
expect(jsonLine).toBeDefined();
|
||||
expect(JSON.parse(jsonLine!).archive.warnings).toBeUndefined();
|
||||
|
||||
const archiveDir = path.join(tempDir, 'openspec', 'changes', 'archive');
|
||||
const [archived] = await fs.readdir(archiveDir);
|
||||
expect(
|
||||
await fs.readFile(path.join(archiveDir, archived, '.openspec.yaml'), 'utf-8')
|
||||
).toBe(metadata);
|
||||
});
|
||||
|
||||
describe('a namespace folder holding nested changes (#1846)', () => {
|
||||
async function seedNamespaceFolder(): Promise<string> {
|
||||
const nested = path.join(tempDir, 'openspec', 'changes', 'mobile', 'refresh-token');
|
||||
@@ -619,6 +644,180 @@ describe('ArchiveCommand', () => {
|
||||
await expect(fs.readdir(outsideDir)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('archives when openspec/changes is a project-owned symlink to another directory', async () => {
|
||||
const changeName = 'linked-change';
|
||||
const docsRepo = realpathSync.native(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-docs-repo-'))
|
||||
);
|
||||
onTestFinished(() => fs.rm(docsRepo, { recursive: true, force: true }));
|
||||
const sharedChanges = path.join(docsRepo, 'changes');
|
||||
const changesLink = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.rm(changesLink, { recursive: true, force: true });
|
||||
await fs.mkdir(path.join(sharedChanges, changeName), { recursive: true });
|
||||
await fs.writeFile(path.join(sharedChanges, changeName, 'tasks.md'), '- [x] Task 1\n');
|
||||
await fs.symlink(sharedChanges, changesLink, process.platform === 'win32' ? 'junction' : 'dir');
|
||||
|
||||
await archiveCommand.execute(changeName, {
|
||||
yes: true,
|
||||
noValidate: true,
|
||||
skipSpecs: true,
|
||||
});
|
||||
|
||||
await expect(fs.access(path.join(sharedChanges, changeName))).rejects.toThrow();
|
||||
const archived = await fs.readdir(path.join(sharedChanges, 'archive'));
|
||||
expect(archived.some((name) => name.endsWith(`-${changeName}`))).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ linkChanges: true, linkSpecs: false },
|
||||
{ linkChanges: false, linkSpecs: true },
|
||||
{ linkChanges: true, linkSpecs: true },
|
||||
])(
|
||||
'applies a spec delta and archives with symlinked changes=$linkChanges specs=$linkSpecs',
|
||||
async ({ linkChanges, linkSpecs }) => {
|
||||
const changeName = 'linked-delta';
|
||||
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
||||
const docsRepo = realpathSync.native(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-docs-repo-'))
|
||||
);
|
||||
onTestFinished(() => fs.rm(docsRepo, { recursive: true, force: true }));
|
||||
const openspecDir = path.join(tempDir, 'openspec');
|
||||
const changesDir = linkChanges ? path.join(docsRepo, 'changes') : path.join(openspecDir, 'changes');
|
||||
const specsDir = linkSpecs ? path.join(docsRepo, 'specs') : path.join(openspecDir, 'specs');
|
||||
if (linkChanges) {
|
||||
await fs.rm(path.join(openspecDir, 'changes'), { recursive: true, force: true });
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
await fs.symlink(changesDir, path.join(openspecDir, 'changes'), linkType);
|
||||
}
|
||||
if (linkSpecs) {
|
||||
await fs.rm(path.join(openspecDir, 'specs'), { recursive: true, force: true });
|
||||
await fs.mkdir(specsDir, { recursive: true });
|
||||
await fs.symlink(specsDir, path.join(openspecDir, 'specs'), linkType);
|
||||
}
|
||||
const deltaDir = path.join(changesDir, changeName, 'specs', 'linked-capability');
|
||||
await fs.mkdir(deltaDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(deltaDir, 'spec.md'),
|
||||
`# Linked Capability - Changes
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: The system SHALL archive through linked directories
|
||||
|
||||
#### Scenario: Linked directories
|
||||
- **WHEN** the change is archived
|
||||
- **THEN** the delta is applied and the change is archived
|
||||
`
|
||||
);
|
||||
|
||||
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
|
||||
|
||||
const mainSpec = await fs.readFile(
|
||||
path.join(specsDir, 'linked-capability', 'spec.md'),
|
||||
'utf-8'
|
||||
);
|
||||
expect(mainSpec).toContain('### Requirement: The system SHALL archive through linked directories');
|
||||
await expect(fs.access(path.join(changesDir, changeName))).rejects.toThrow();
|
||||
const archived = await fs.readdir(path.join(changesDir, 'archive'));
|
||||
const archivedName = archived.find((name) => name.endsWith(`-${changeName}`));
|
||||
expect(archivedName).toBeDefined();
|
||||
await expect(
|
||||
fs.access(path.join(changesDir, 'archive', archivedName!, 'specs', 'linked-capability', 'spec.md'))
|
||||
).resolves.not.toThrow();
|
||||
}
|
||||
);
|
||||
|
||||
it('archives when the whole openspec directory is a project-owned symlink', async () => {
|
||||
const changeName = 'linked-openspec';
|
||||
const docsRepo = realpathSync.native(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-docs-repo-'))
|
||||
);
|
||||
onTestFinished(() => fs.rm(docsRepo, { recursive: true, force: true }));
|
||||
const sharedOpenspec = path.join(docsRepo, 'openspec');
|
||||
await fs.rm(path.join(tempDir, 'openspec'), { recursive: true, force: true });
|
||||
await fs.mkdir(path.join(sharedOpenspec, 'changes', changeName), { recursive: true });
|
||||
await fs.mkdir(path.join(sharedOpenspec, 'specs'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sharedOpenspec, 'changes', changeName, 'tasks.md'),
|
||||
'- [x] Task 1\n'
|
||||
);
|
||||
await fs.symlink(
|
||||
sharedOpenspec,
|
||||
path.join(tempDir, 'openspec'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir'
|
||||
);
|
||||
|
||||
await archiveCommand.execute(changeName, { yes: true, noValidate: true, skipSpecs: true });
|
||||
|
||||
const archived = await fs.readdir(path.join(sharedOpenspec, 'changes', 'archive'));
|
||||
expect(archived.some((name) => name.endsWith(`-${changeName}`))).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an archive directory that escapes a symlinked changes directory', async () => {
|
||||
const changeName = 'linked-escape';
|
||||
const docsRepo = realpathSync.native(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-docs-repo-'))
|
||||
);
|
||||
onTestFinished(() => fs.rm(docsRepo, { recursive: true, force: true }));
|
||||
const sharedChanges = path.join(docsRepo, 'changes');
|
||||
const outsideDir = path.join(docsRepo, 'outside');
|
||||
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
||||
await fs.rm(path.join(tempDir, 'openspec', 'changes'), { recursive: true, force: true });
|
||||
await fs.mkdir(path.join(sharedChanges, changeName), { recursive: true });
|
||||
await fs.mkdir(outsideDir);
|
||||
await fs.writeFile(path.join(sharedChanges, changeName, 'tasks.md'), '- [x] Task 1\n');
|
||||
await fs.symlink(sharedChanges, path.join(tempDir, 'openspec', 'changes'), linkType);
|
||||
await fs.symlink(outsideDir, path.join(sharedChanges, 'archive'), linkType);
|
||||
|
||||
await expect(
|
||||
archiveCommand.execute(changeName, { yes: true, noValidate: true, skipSpecs: true })
|
||||
).rejects.toThrow(/outside the OpenSpec root/u);
|
||||
await expect(fs.access(path.join(sharedChanges, changeName))).resolves.not.toThrow();
|
||||
await expect(fs.readdir(outsideDir)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('verifies deltas through a symlinked changes directory on the cross-device fallback', async () => {
|
||||
const changeName = 'linked-exdev';
|
||||
const docsRepo = realpathSync.native(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-docs-repo-'))
|
||||
);
|
||||
onTestFinished(() => fs.rm(docsRepo, { recursive: true, force: true }));
|
||||
const sharedChanges = path.join(docsRepo, 'changes');
|
||||
const deltaDir = path.join(sharedChanges, changeName, 'specs', 'linked-capability');
|
||||
await fs.rm(path.join(tempDir, 'openspec', 'changes'), { recursive: true, force: true });
|
||||
await fs.mkdir(deltaDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(deltaDir, 'spec.md'),
|
||||
`## ADDED Requirements
|
||||
|
||||
### Requirement: The system SHALL survive EXDEV
|
||||
The system SHALL archive.
|
||||
|
||||
#### Scenario: Cross device
|
||||
- **WHEN** rename fails with EXDEV
|
||||
- **THEN** the copy fallback archives the change
|
||||
`
|
||||
);
|
||||
await fs.symlink(
|
||||
sharedChanges,
|
||||
path.join(tempDir, 'openspec', 'changes'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir'
|
||||
);
|
||||
|
||||
const rename = vi.spyOn(fs, 'rename').mockRejectedValueOnce(
|
||||
Object.assign(new Error('cross-device move'), { code: 'EXDEV' })
|
||||
);
|
||||
try {
|
||||
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
|
||||
} finally {
|
||||
rename.mockRestore();
|
||||
}
|
||||
|
||||
await expect(fs.access(path.join(sharedChanges, changeName))).rejects.toThrow();
|
||||
const archived = await fs.readdir(path.join(sharedChanges, 'archive'));
|
||||
expect(archived.some((name) => name.endsWith(`-${changeName}`))).toBe(true);
|
||||
});
|
||||
|
||||
it('archives normally when the project root is reached through a symlink alias', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
|
||||
@@ -272,6 +272,46 @@ describe('artifact-graph/outputs', () => {
|
||||
expect(artifactOutputExists(tempDir, 'file-{1..3}.md')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects brace nesting deeper than any artifact pattern needs', () => {
|
||||
const fileA = path.join(tempDir, 'a.md');
|
||||
fs.writeFileSync(fileA, 'content');
|
||||
const nested = (depth: number) => `${'{a,'.repeat(depth)}b${'}'.repeat(depth)}.md`;
|
||||
const globSpy = vi.spyOn(fg, 'generateTasks');
|
||||
|
||||
expect(resolveArtifactOutputs(tempDir, nested(16))).toEqual([canonical(fileA)]);
|
||||
globSpy.mockClear();
|
||||
expect(() => resolveArtifactOutputs(tempDir, nested(17))).toThrow(
|
||||
'Artifact output pattern nests braces more than 16 levels deep'
|
||||
);
|
||||
// A pattern deep enough to crash braces is also too long for the filesystem;
|
||||
// the guard must still be the error the user sees.
|
||||
expect(() => resolveArtifactOutputs(tempDir, nested(4000))).toThrow(
|
||||
'Artifact output pattern nests braces more than 16 levels deep'
|
||||
);
|
||||
// The error quotes only the start of a pattern that long.
|
||||
expect(() => resolveArtifactOutputs(tempDir, nested(4000))).toThrow(
|
||||
`16 levels deep: ${nested(4000).slice(0, 80)}...`
|
||||
);
|
||||
expect(() => resolveArtifactOutputs(tempDir, nested(4000))).not.toThrow(nested(4000));
|
||||
expect(globSpy).not.toHaveBeenCalled();
|
||||
globSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a bracket', (depth: number) => `*${'{a,[}]'.repeat(depth)}${'}'.repeat(depth)}`],
|
||||
['a quote', (depth: number) => `*${'{a,"}"'.repeat(depth)}${'}'.repeat(depth)}`],
|
||||
['an open parenthesis', (depth: number) => `*${'{a,(}'.repeat(depth)}${')}'.repeat(depth)}`],
|
||||
['parentheses alone', (depth: number) => `*{a,b}${'('.repeat(depth)}${')'.repeat(depth)}`],
|
||||
])('counts nesting the braces parser sees past %s', (_label, nested) => {
|
||||
const globSpy = vi.spyOn(fg, 'generateTasks');
|
||||
|
||||
expect(() => resolveArtifactOutputs(tempDir, nested(17))).toThrow(
|
||||
'Artifact output pattern nests braces more than 16 levels deep'
|
||||
);
|
||||
expect(globSpy).not.toHaveBeenCalled();
|
||||
globSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each(['@(proposal|design).md', '+(proposal|design).md'])('supports extglob %s', (pattern) => {
|
||||
const proposalPath = path.join(tempDir, 'proposal.md');
|
||||
fs.writeFileSync(proposalPath, 'content');
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Command, Option } from 'commander';
|
||||
|
||||
import { isJsonRun, isCompletionRun, shouldDeferCompletionTip } from '../../src/cli/index.js';
|
||||
import {
|
||||
isJsonRun,
|
||||
isCompletionRun,
|
||||
isTabCompletionRun,
|
||||
shouldDeferCompletionTip,
|
||||
} from '../../src/cli/index.js';
|
||||
|
||||
/**
|
||||
* Reproduce the three ways `--json` reaches a command in the real CLI, so a
|
||||
@@ -106,6 +111,23 @@ describe('isCompletionRun', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('isTabCompletionRun', () => {
|
||||
/**
|
||||
* Only the hidden resolver skips telemetry. The user-facing `completion`
|
||||
* commands are run by a person and are tracked like any other command.
|
||||
*/
|
||||
it('matches the hidden resolver', () => {
|
||||
expect(isTabCompletionRun('__complete')).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['completion', 'completion:install', 'completion:generate', 'list'])(
|
||||
'does not match "%s"',
|
||||
(commandPath) => {
|
||||
expect(isTabCompletionRun(commandPath)).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('shouldDeferCompletionTip', () => {
|
||||
/**
|
||||
* The tip must survive every run that cannot display it. Deferring (rather
|
||||
|
||||
@@ -421,5 +421,35 @@ describe('ZshGenerator', () => {
|
||||
expect(script).toContain('_openspec_view() {');
|
||||
expect(script).toContain('_arguments');
|
||||
});
|
||||
|
||||
// The bash, fish and powershell generator tests each pin this redirect;
|
||||
// zsh was the one that did not, and it has the most `__complete` call
|
||||
// sites and is the default shell on macOS. The redirect is what keeps a
|
||||
// Tab press silent, so anything `__complete` ever writes to stderr - a
|
||||
// deprecation notice, a Node experimental warning - would otherwise print
|
||||
// into the middle of the user's prompt with no test objecting.
|
||||
it('sends every __complete call stderr to /dev/null', () => {
|
||||
const commands: CommandDefinition[] = [
|
||||
{ name: 'archive', description: 'Archive a change', flags: [] },
|
||||
{ name: 'show', description: 'Show an item', flags: [] },
|
||||
{ name: 'new', description: 'Create a change', flags: [] },
|
||||
];
|
||||
|
||||
const script = generator.generate(commands);
|
||||
|
||||
expect(script).toContain('openspec __complete changes 2>/dev/null');
|
||||
expect(script).toContain('openspec __complete specs 2>/dev/null');
|
||||
expect(script).toContain('openspec __complete schemas 2>/dev/null');
|
||||
// No invocation may survive a template edit that drops a redirect. Only
|
||||
// real call sites count: the script also explains itself in comments
|
||||
// that name the command without running it.
|
||||
const invocations = script
|
||||
.split('\n')
|
||||
.filter((line) => line.includes('openspec __complete') && !line.trim().startsWith('#'));
|
||||
expect(invocations.length).toBeGreaterThan(0);
|
||||
for (const line of invocations) {
|
||||
expect(line, `unredirected __complete call: ${line.trim()}`).toContain('2>/dev/null');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { parseDeltaSpec } from '../../../src/core/parsers/requirement-blocks.js';
|
||||
import { buildUpdatedSpec, findSpecUpdates } from '../../../src/core/specs-apply.js';
|
||||
import { Validator } from '../../../src/core/validation/validator.js';
|
||||
|
||||
// Archive carries requirement blocks only. Text written anywhere else in a delta
|
||||
// used to vanish with no warning while `validate --strict` passed.
|
||||
const DELTA = [
|
||||
'# billing delta', // 1 - a title is not reported
|
||||
'Context above the first section.', // 2
|
||||
'## ADDED Requirements', // 3
|
||||
'Intro prose.', // 4
|
||||
'### Requirement: Invoices', // 5
|
||||
'The system SHALL generate invoices.', // 6
|
||||
'', // 7
|
||||
'#### Scenario: Period closes', // 8
|
||||
'- **WHEN** a', // 9
|
||||
'- **THEN** b', // 10
|
||||
'Trailing note, carried with the block.', // 11
|
||||
'## Acceptance Criteria', // 12
|
||||
'- Invoices reconcile.', // 13
|
||||
].join('\n');
|
||||
|
||||
describe('parseDeltaSpec unappliedContent', () => {
|
||||
it('reports text above the first section, leading prose, and non-delta sections', () => {
|
||||
expect(parseDeltaSpec(DELTA).unappliedContent).toEqual([
|
||||
{ section: null, line: 2 },
|
||||
{ section: 'ADDED Requirements', line: 4 },
|
||||
{ section: 'Acceptance Criteria', line: 12 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not exempt a commented-out Purpose, and honours the --!> terminator', () => {
|
||||
const plan = parseDeltaSpec(
|
||||
['<!--', '## Purpose', '--!>', 'Real prose.', '## ADDED Requirements', '### Requirement: A', 'a', '## Notes', 'n'].join(
|
||||
'\n'
|
||||
)
|
||||
);
|
||||
expect(plan.unappliedContent).toEqual([
|
||||
{ section: 'Purpose', line: 2 },
|
||||
{ section: 'Notes', line: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
// The merge splits sections on every `## ` line outside a fence, comment or not.
|
||||
// Reading boundaries from comment-masked lines disagreed with it both ways.
|
||||
it('reads section boundaries exactly as the merge does', () => {
|
||||
// `<!--` in inline code and a later `-->` are not a comment to the merge;
|
||||
// masking across them hid `## ADDED Requirements` and blamed a carried requirement.
|
||||
const inlineOpener = parseDeltaSpec(
|
||||
['# Delta: strip `<!--` openers', '## ADDED Requirements', '### Requirement: A', 'Flow: ingest --> render', 'The system SHALL a.'].join('\n')
|
||||
);
|
||||
expect(inlineOpener.added.map((b) => b.name)).toEqual(['A']);
|
||||
expect(inlineOpener.unappliedContent).toEqual([]);
|
||||
|
||||
// A header inside a comment is still a section to the merge, so the
|
||||
// requirement after the comment is carried, not text above the first section.
|
||||
const commentedHeader = parseDeltaSpec(
|
||||
['<!-- add requirements below', '## ADDED Requirements', '-->', '### Requirement: A', 'The system SHALL a.'].join('\n')
|
||||
);
|
||||
expect(commentedHeader.added.map((b) => b.name)).toEqual(['A']);
|
||||
expect(commentedHeader.unappliedContent).toEqual([]);
|
||||
|
||||
// ...and text after a commented-out `## Notes` is dropped by the merge.
|
||||
const commentedNotes = parseDeltaSpec(
|
||||
['## ADDED Requirements', '### Requirement: A', 'a', '<!--', '## Notes', '-->', 'Dropped.'].join('\n')
|
||||
);
|
||||
expect(commentedNotes.added[0].raw).not.toContain('Dropped.');
|
||||
expect(commentedNotes.unappliedContent).toEqual([{ section: 'Notes', line: 5 }]);
|
||||
});
|
||||
|
||||
it('does not let a `<!--` inside a code fence hide later sections', () => {
|
||||
const plan = parseDeltaSpec(
|
||||
['## ADDED Requirements', '### Requirement: A', 'a', '```html', '<!-- unterminated sample', '```', '## Notes', 'Dropped.'].join('\n')
|
||||
);
|
||||
expect(plan.unappliedContent).toEqual([{ section: 'Notes', line: 7 }]);
|
||||
});
|
||||
|
||||
it('ignores Purpose, REMOVED/RENAMED bodies, HTML comments, fences, and stray ### headers', () => {
|
||||
const plan = parseDeltaSpec(
|
||||
[
|
||||
'<!--',
|
||||
'Multi-line instructions.',
|
||||
'-->',
|
||||
'## Purpose',
|
||||
'A new capability.',
|
||||
'## ADDED Requirements',
|
||||
'<!-- add requirements below -->',
|
||||
'### Documentation Requirements',
|
||||
'### Requirement: A',
|
||||
'a',
|
||||
'```markdown',
|
||||
'## Notes',
|
||||
'```',
|
||||
'## REMOVED Requirements',
|
||||
'### Requirement: B',
|
||||
'**Reason**: obsolete',
|
||||
'## RENAMED Requirements',
|
||||
'- FROM: `### Requirement: C`',
|
||||
'- TO: `### Requirement: D`',
|
||||
].join('\n')
|
||||
);
|
||||
expect(plan.unappliedContent).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('archive and validate report unapplied content', () => {
|
||||
let tempDir: string;
|
||||
let changeDir: string;
|
||||
let specsRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-unapplied-'));
|
||||
specsRoot = path.join(tempDir, 'openspec', 'specs');
|
||||
changeDir = path.join(tempDir, 'openspec', 'changes', 'c');
|
||||
await fs.mkdir(path.join(changeDir, 'specs', 'billing'), { recursive: true });
|
||||
await fs.mkdir(specsRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(changeDir, 'specs', 'billing', 'spec.md'), DELTA);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('archive warns and still carries only the requirement', async () => {
|
||||
const [update] = await findSpecUpdates(changeDir, specsRoot);
|
||||
const built = await buildUpdatedSpec(update, 'c', { silent: true });
|
||||
|
||||
expect(built.warnings).toContain(
|
||||
'billing - section "## Acceptance Criteria" (line 12) is not carried into the main spec.'
|
||||
);
|
||||
expect(built.rebuilt).toContain('### Requirement: Invoices');
|
||||
expect(built.rebuilt).not.toContain('Acceptance Criteria');
|
||||
});
|
||||
|
||||
it('validate warns, which fails --strict', async () => {
|
||||
const report = await new Validator(true).validateChangeDeltaSpecs(changeDir);
|
||||
const lost = report.issues.filter((issue) => /not carried into the main spec/.test(issue.message));
|
||||
|
||||
expect(lost.map((issue) => [issue.level, issue.line])).toEqual([
|
||||
['WARNING', 2],
|
||||
['WARNING', 4],
|
||||
['WARNING', 12],
|
||||
]);
|
||||
expect(report.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -76,14 +76,14 @@ function specDrivenTitles(): Record<string, string> {
|
||||
}
|
||||
|
||||
const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
|
||||
getExploreSkillTemplate: 'c1fddb294758004936add586f5826694cb06175cff935b75fd3a8d92332332e6',
|
||||
getExploreSkillTemplate: 'efeb143c50472bf0db0f031f7fc9e32f3c2662b569101f6473b2cbbb99425367',
|
||||
getNewChangeSkillTemplate: '0e5035b7b42198afc430206a1dbc9579096650ef0813d85e837d5a6cd0b98a85',
|
||||
getContinueChangeSkillTemplate: '32ec5965a2e90c033c9285f57a73cd6f39a31b58029d7546dba3cb6ba4c091f7',
|
||||
getApplyChangeSkillTemplate: 'c4f02c29137e19b34bf1dc59146941b8c920214aa2cf3ce80ae7f1b982e976fc',
|
||||
getFfChangeSkillTemplate: 'd091600476a815ba99f69b446bcd46af5bf73d1c2810215a0c6196937d019cf6',
|
||||
getSyncSpecsSkillTemplate: 'bc80fe9b07eaa289e5eb8a3ce65eb7df722a16d864e37283c678220712e4f230',
|
||||
getOnboardSkillTemplate: '84258a06c0ca88de708a23dd74e9a17efe11eff63a071b3864c781dcd5a0a4b7',
|
||||
getOpsxExploreCommandTemplate: '5d11f8ecb4c457140a3e874a8bf7aa72674e922e698c208832b1f34d3c617719',
|
||||
getOpsxExploreCommandTemplate: '477c1a97cf272778953d1ad032113e3f20afaefa332a5c0882e536d919c176e2',
|
||||
getOpsxNewCommandTemplate: '6d504fef1e0d4ced7c423f4cc9d9d2cee11b1a6224edf685e06a3f0757e0ebff',
|
||||
getOpsxContinueCommandTemplate: '659596cef4859162707d8acbd411afb863110f34a7b90ad037bae5eb84dd858d',
|
||||
getOpsxApplyCommandTemplate: '9b733946aa7216b45d084bb5affd7071086db6f91282c3666731a17543825ff1',
|
||||
@@ -91,20 +91,20 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
|
||||
getArchiveChangeSkillTemplate: '04a029782fc4137971fad6f54cfda3685d7e0b835b06565f9a509b4878093099',
|
||||
getBulkArchiveChangeSkillTemplate: '44dbd3c7a347e5f8339b2141393f2ac36017527cce251483fe70f2059c1e286e',
|
||||
getOpsxSyncCommandTemplate: '60550b7bb9829421656d6324a9e4c951bc912f48f88882d1a07ce7f78397a5e7',
|
||||
getVerifyChangeSkillTemplate: '9522cdf3fd638baee6098d480445c615b908b2f35d776b2a1eebeb6e2b208480',
|
||||
getVerifyChangeSkillTemplate: '5c188c8e94fa0d4717cc3475813017d9e0bea9c53ad1df7303d18533cc26ee7f',
|
||||
getOpsxArchiveCommandTemplate: '5d153490bf1ca24207f49856826720f793c8884a6ec15059104107b0e34ee345',
|
||||
getOpsxOnboardCommandTemplate: '0cf66e164c0e14c916c6d1ebb5d80ded07d7fb8e55d4eb34eba43e8ca9c28558',
|
||||
getOpsxBulkArchiveCommandTemplate: 'cb1d55d6ce53686bfe94be5e081c7a4d06a8e4d10b63019132df5bb3db7144cb',
|
||||
getOpsxVerifyCommandTemplate: '62ba027d789186416620f963341129bf8f7acf956c8f5b806f78945e1109d7ab',
|
||||
getOpsxVerifyCommandTemplate: '9f3b4551a3cce3f1d1aa6c14fc595424ba22cd049326412460b30bd32e4cb0fc',
|
||||
getOpsxProposeSkillTemplate: '1aa2f2eb9c8cbc4dcab9d777bf8832b92ca04f9ef91d0494f1224a566aefdfe8',
|
||||
getOpsxProposeCommandTemplate: '3b7090ce5e79e879ab9b5bdaf4ff2b52e3c02211f71188838772d36ac337f96c',
|
||||
getFeedbackSkillTemplate: 'dabeb5e825b9349abc8156c3e7b8608f27987912a6d9bf47ef29addde6138133',
|
||||
getUpdateChangeSkillTemplate: '8380139769cf9b247cb64089c07628e73923405fca4704052d52a67f35526fe8',
|
||||
getOpsxUpdateCommandTemplate: 'ec6c8b7f3f366d65a216c4ba423bc41b8dbd18974a432186ad44e60c1891340b',
|
||||
getUpdateChangeSkillTemplate: '14c209b8a18d1b100d372b0ef96707b70a7c64117a16309ee463f66d39180b3e',
|
||||
getOpsxUpdateCommandTemplate: '33528a80c9436c9dad74084a37d2fce5e78e34abfb71185fa21495a4f2a8b4d3',
|
||||
};
|
||||
|
||||
const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
|
||||
'openspec-explore': '7d80caf9cd25a2565ba190b1297f1631c7f2c2db5e614597b4284abc0118ea70',
|
||||
'openspec-explore': '92055ecb0f60011b76369defb441883a985fb798f4cb90397f956f73383deda2',
|
||||
'openspec-new-change': '27e09d43785953827efc9a98bb9d6cf06db48fe6abe7e1c049409fe5b5061323',
|
||||
'openspec-continue-change': '4edf6d8b220f09146029e7671f140b7ef94a666d8e29fe7ad78ce40bc7ec9017',
|
||||
'openspec-apply-change': '991a1d4687f1c8c7147d8c3b68cf14e9c515c6c53bb026356dac3b80e909b827',
|
||||
@@ -112,10 +112,10 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
|
||||
'openspec-sync-specs': '3909936a236a21a9a6d5bf495f90b396b3b68fc9220d7b2c1894668653beb2e4',
|
||||
'openspec-archive-change': '5f0d131a885dcdcd9ba2172ea9a42bc6748125e24b8c4eecb7c86f1a4aea83af',
|
||||
'openspec-bulk-archive-change': 'd2a258055ab2f0d8086c4348d37212ebc95a5adef2d5f524db959fb93490d5c8',
|
||||
'openspec-verify-change': 'ed7c3f0287ef246fc1c19665fd64c275259712950bfe95fb94971ddcc8fb64ac',
|
||||
'openspec-verify-change': '9422e0afeec1dbd27c425258e67d52d73686c07765a54edeba62043c9640c8b5',
|
||||
'openspec-onboard': '6993eff867d97d485e080078f9dfb80e968e242f3b17a924eeb077715fd548fa',
|
||||
'openspec-propose': '66e3395adf9f2d93a09e8ef1d20e4efb010e5e8d4811f2d42a9316e4d1ca5a8b',
|
||||
'openspec-update-change': '5f4ea19aa732b33d87a2120ec393ee34578e70678d97e8c3bb10f988c00cb4d3',
|
||||
'openspec-update-change': 'a6fc9d6972bfb024b91f0a01e14d8a7666ae641bcad83cfae937e41dcccc545a',
|
||||
};
|
||||
|
||||
// Intentionally excludes getFeedbackSkillTemplate: this list only models templates
|
||||
@@ -1253,6 +1253,20 @@ describe('skill templates split parity', () => {
|
||||
expect(text, variant).not.toContain('Loose prose left under `## Requirements` does NOT block');
|
||||
}
|
||||
});
|
||||
|
||||
// The explore capture table names spec-driven files; without this caveat an
|
||||
// agent on a custom schema offers to write artifacts it never defines (#2084).
|
||||
it('scopes the explore capture table to the default schema', () => {
|
||||
const variants = [
|
||||
['explore skill', getExploreSkillTemplate().instructions],
|
||||
['explore command', getOpsxExploreCommandTemplate().content],
|
||||
] as const;
|
||||
for (const [variant, text] of variants) {
|
||||
expect(text, variant).toContain(
|
||||
"The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from `artifactPaths` fits it"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('apply skill/command shared instruction core', () => {
|
||||
|
||||
@@ -55,7 +55,7 @@ const SANCTIONED_OUTSIDE_STEP_FIVE = [
|
||||
STEP_FOUR_DRAFT_RULE,
|
||||
'that is the starting edit.',
|
||||
'Do NOT write to `resolvedOutputPath`',
|
||||
'- Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`.',
|
||||
'- Edit only the concrete files in `existingOutputPaths`, plus any confirmed concrete path allowed under a partly populated glob artifact (step 4); never write to a glob `resolvedOutputPath`.',
|
||||
'Confirm every edit with the user before writing.',
|
||||
'`/opsx:apply`',
|
||||
'(tasks checked off / already applied)',
|
||||
@@ -292,6 +292,49 @@ describe('update-change templates', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// #2068: the first guardrail read "Edit only the concrete files in
|
||||
// existingOutputPaths", and a file step 4 creates is by definition not in
|
||||
// that list. The exception sat two bullets below, so an agent applying the
|
||||
// blanket rule refused the very operation the glob-gap flow exists to
|
||||
// perform. Every place that states the scope rule has to carry the
|
||||
// exception - a slice of the Guardrails section alone would miss step 2,
|
||||
// which states the same rule two steps before the exception appears.
|
||||
it('carries the glob new-file exception wherever the scope rule is stated (#2068)', () => {
|
||||
for (const [label, body] of bodies) {
|
||||
const scopeLines = body
|
||||
.split('\n')
|
||||
.filter((line) =>
|
||||
/- Edit only the concrete files in `existingOutputPaths`|The files to edit are `artifactPaths/.test(line)
|
||||
);
|
||||
// Both sites: step 2's prose and the Guardrails bullet.
|
||||
expect(scopeLines.length, `${label}: expected both scope-rule sites`).toBe(2);
|
||||
|
||||
for (const line of scopeLines) {
|
||||
expect(line, `${label}: unqualified scope rule: ${line}`).toContain(
|
||||
'any confirmed concrete path allowed under a partly populated glob artifact (step 4)'
|
||||
);
|
||||
}
|
||||
|
||||
// A reword must not be able to restore the unqualified prohibition.
|
||||
expect(body, label).not.toMatch(/existingOutputPaths`;\s*never write to a glob/);
|
||||
expect(body, label).not.toMatch(/existingOutputPaths` - the concrete files that exist on disk[^.]*\.\s*Do NOT write/);
|
||||
|
||||
// The prohibition itself and the frontier rule must survive the fix.
|
||||
const guardrails = body.slice(body.indexOf('**Guardrails**'));
|
||||
expect(guardrails, label).toContain('never write to a glob `resolvedOutputPath`');
|
||||
expect(guardrails, label).toContain(
|
||||
'The only new-file scope is a confirmed concrete path under a glob artifact whose `existingOutputPaths` is non-empty.'
|
||||
);
|
||||
|
||||
// "(step 4)" is a cross-reference; pin that step 4 is still the step
|
||||
// that owns the confirmation and the path checks, so renumbering or
|
||||
// moving the flow cannot leave a dangling pointer.
|
||||
const stepFour = section(body, '4. **Read and reconcile**', '5. **Confirm and apply', `${label} step 4`);
|
||||
expect(stepFour, label).toContain('whose `existingOutputPaths` is non-empty');
|
||||
expect(stepFour, label).toContain('create it only after the user confirms');
|
||||
}
|
||||
});
|
||||
|
||||
it('writes to existingOutputPaths, never to a glob resolvedOutputPath (3.4)', () => {
|
||||
for (const [label, body] of bodies) {
|
||||
expect(body, label).toContain('artifactPaths.<id>.existingOutputPaths');
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('verify checks each requirement by its delta operation', () => {
|
||||
const coverage = section(body, '**Spec Coverage**', '6. **Verify Correctness**', label);
|
||||
const removed = section(coverage, '- For each REMOVED requirement', '- For each RENAMED entry', label);
|
||||
|
||||
expect(removed, label).toContain('Finding no implementation is the expected result.');
|
||||
expect(removed, label).toContain('Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for.');
|
||||
expect(removed, label).toContain('Never report a REMOVED requirement as "Requirement not found"');
|
||||
expect(removed, label).toContain('Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"');
|
||||
expect(removed, label).not.toContain('Recommendation: "Implement');
|
||||
@@ -89,7 +89,7 @@ describe('verify checks each requirement by its delta operation', () => {
|
||||
|
||||
expect(renamed, label).toContain('If the baseline requirement cannot be found or read, mark **Spec Coverage** as not verified for that entry');
|
||||
expect(renamed, label).toContain('Never count an unchecked rename as passing.');
|
||||
expect(body, label).toContain('(each RENAMED entry is checked there against its baseline behavior)');
|
||||
expect(body, label).toContain('each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior');
|
||||
});
|
||||
|
||||
it.each(bodies)('%s: maps implementation and scenarios only for ADDED or MODIFIED requirements', (label, body) => {
|
||||
@@ -122,6 +122,97 @@ describe('verify checks each requirement by its delta operation', () => {
|
||||
expect(removed, label).toContain('Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.');
|
||||
});
|
||||
|
||||
// #2067: the REMOVED branch said only "search codebase for the removed
|
||||
// behavior". A REMOVED block carries just the requirement name plus Reason
|
||||
// and Migration notes, so the search started from the heading alone - and
|
||||
// because an absent match is the passing result here, an implementation
|
||||
// written in different vocabulary read as a clean removal. The RENAMED
|
||||
// branch beside it already loaded the baseline from the main spec first.
|
||||
it.each(bodies)('%s: reads the removed requirement baseline before searching', (label, body) => {
|
||||
const removed = section(body, '- For each REMOVED requirement', '- For each RENAMED entry', label);
|
||||
|
||||
expect(removed, label).toContain('A REMOVED block carries only the requirement name and any `**Reason**`/`**Migration**` notes');
|
||||
expect(removed, label).toContain('`<planningHome.root>/openspec/specs/<capability-path>/spec.md`');
|
||||
expect(removed, label).toContain('Its body and scenarios are the evidence for the behavior that must be gone');
|
||||
expect(removed, label).toContain('never search from the requirement name alone');
|
||||
// A literal title lookup misses a differently-worded baseline and gets
|
||||
// misread as "the sync already removed it".
|
||||
expect(removed, label).toContain('Match it by normalized name');
|
||||
});
|
||||
|
||||
// The baseline read, the history fallback and the search have to stay in
|
||||
// that order. Each index is checked present first: an unguarded indexOf
|
||||
// returns -1, and -1 is less than everything, so a reworded sentence would
|
||||
// satisfy a bare ordering comparison and silently retire this guard.
|
||||
it.each(bodies)('%s: orders the baseline read, then the fallback, then the search', (label, body) => {
|
||||
const removed = section(body, '- For each REMOVED requirement', '- For each RENAMED entry', label);
|
||||
|
||||
const baselineAt = removed.indexOf('Before searching, read the baseline requirement');
|
||||
const fallbackAt = removed.indexOf("recover the baseline from the same file's history");
|
||||
const bailOutAt = removed.indexOf('mark **Spec Coverage** as not verified for that entry');
|
||||
const searchAt = removed.indexOf('Search codebase for that behavior.');
|
||||
|
||||
expect(baselineAt, `${label}: baseline-read sentence missing`).toBeGreaterThanOrEqual(0);
|
||||
expect(fallbackAt, `${label}: history fallback missing`).toBeGreaterThanOrEqual(0);
|
||||
expect(bailOutAt, `${label}: not-verified bail-out missing`).toBeGreaterThanOrEqual(0);
|
||||
expect(searchAt, `${label}: search step missing`).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// The main spec is the primary source; history is only its fallback.
|
||||
expect(baselineAt, `${label}: fallback must follow the primary read`).toBeLessThan(fallbackAt);
|
||||
// Bailing out must be reachable before the search, not two bullets after
|
||||
// the rule that says finding nothing is expected.
|
||||
expect(bailOutAt, `${label}: bail-out must precede the search`).toBeLessThan(searchAt);
|
||||
});
|
||||
|
||||
// The git example is the one piece of the fallback an agent runs verbatim.
|
||||
// `git log` treats an unmatched pathspec as success and prints nothing, so a
|
||||
// path missing the `openspec/specs/` prefix reads as "no baseline in
|
||||
// history" and sends every post-sync removal to the not-verified branch.
|
||||
it.each(bodies)('%s: gives a runnable, store-aware history command', (label, body) => {
|
||||
const removed = section(body, '- For each REMOVED requirement', '- For each RENAMED entry', label);
|
||||
|
||||
expect(removed, label).toContain('git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md');
|
||||
// `rev:path` resolves from the repository root, not from `-C`; the `./`
|
||||
// keeps it relative to a planning root that is a subdirectory (monorepo).
|
||||
expect(removed, label).toContain('git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md');
|
||||
expect(removed, label).not.toMatch(/show <commit>\^:openspec\//);
|
||||
// An unquoted planning root splits on spaces and runs git in the wrong place.
|
||||
expect(removed, label).not.toMatch(/git -C <planningHome\.root>/);
|
||||
expect(removed, label).toContain('Empty output means the pathspec matched nothing');
|
||||
// A bare `<capability-path>/spec.md` pathspec matches nothing: the
|
||||
// placeholder is defined relative to `specs/`.
|
||||
expect(removed, label).not.toMatch(/git[^`\n]*log[^`\n]*--\s+<capability-path>/);
|
||||
// History must be read from the planning home, which may be a store repo
|
||||
// rather than the current checkout (#1702).
|
||||
expect(removed, label).not.toMatch(/\bgit log -p -- openspec/);
|
||||
});
|
||||
|
||||
it.each(bodies)('%s: never counts an unsearched removal as passing', (label, body) => {
|
||||
const removed = section(body, '- For each REMOVED requirement', '- For each RENAMED entry', label);
|
||||
|
||||
expect(removed, label).toContain('mark **Spec Coverage** as not verified for that entry with the reason and stop there');
|
||||
expect(removed, label).toContain('do not search, and never count an unsearched removal as passing');
|
||||
// A baseline that only restates its title gives no more vocabulary than
|
||||
// the heading did, which is the #2067 failure mode in miniature.
|
||||
expect(removed, label).toContain('A baseline that adds nothing to its own title');
|
||||
expect(removed, label).toContain('is unusable evidence, the same as a missing one');
|
||||
// Reworded re-introductions must fail too, not just the exact old
|
||||
// sentence: the bug was searching from the name, however it is phrased.
|
||||
expect(removed, label).not.toMatch(/Search\b[^.\n]*\bfor the removed behavior/i);
|
||||
expect(removed, label).not.toMatch(/Finding no implementation is the expected result[.;]/);
|
||||
});
|
||||
|
||||
// Step 6 asserts that Spec Coverage carries the evidence for a removal-only
|
||||
// change. That claim has to leave room for the entry the branch above marks
|
||||
// not verified, or it reads as licence to drop the mark.
|
||||
it.each(bodies)('%s: does not claim a baseline check that may not have happened', (label, body) => {
|
||||
expect(body, label).toContain('each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior');
|
||||
expect(body, label).toContain('an entry already marked not verified there stays not verified');
|
||||
// The scorecard must model the not-verified wording, not only the
|
||||
// confirming kind.
|
||||
expect(body, label).toContain('"1 removal not verified (baseline unavailable)"');
|
||||
});
|
||||
|
||||
it.each(bodies)('%s: counts removals separately from covered requirements', (label, body) => {
|
||||
expect(body, label).toContain('Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately');
|
||||
expect(body, label).toContain('the Correctness cell reads `Not applicable (no ADDED or MODIFIED requirements)`');
|
||||
|
||||
@@ -82,6 +82,34 @@ describe('unrecognized keys in .openspec.yaml', () => {
|
||||
expect(report.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('passes --strict when the only extra keys are x- extension keys, whatever their values', async () => {
|
||||
await writeMetadata(
|
||||
'schema: spec-driven\nskip_specs: true\nx-goal: G-12\nx-jiraKey: ABC-1\n' +
|
||||
'x-owner:\n team: payments\nx-list: [1, 2]\nx-empty:\n'
|
||||
);
|
||||
|
||||
const report = await new Validator(true).validateChangeDeltaSpecs(testDir);
|
||||
|
||||
expect(report.issues.filter((issue) => issue.path === METADATA_FILENAME)).toHaveLength(0);
|
||||
expect(report.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('still fails --strict on near-miss extension keys beside a valid one', async () => {
|
||||
await writeMetadata(
|
||||
'schema: spec-driven\nskip_specs: true\nx-goal: G-12\nX-Tracker: 1\n"x-": 1\n"x- tracker": 1\n'
|
||||
);
|
||||
|
||||
const report = await new Validator(true).validateChangeDeltaSpecs(testDir);
|
||||
|
||||
const warning = report.issues.find(
|
||||
(issue) => issue.level === 'WARNING' && issue.path === METADATA_FILENAME
|
||||
);
|
||||
expect(report.valid).toBe(false);
|
||||
expect(warning?.message).toContain('X-Tracker');
|
||||
expect(warning?.message).toContain('x- tracker');
|
||||
expect(warning?.message).not.toContain('x-goal');
|
||||
});
|
||||
|
||||
it('still accepts skip_specs when an unknown key sits beside it', async () => {
|
||||
await writeMetadata('schema: spec-driven\nskip_specs: true\nskip_design: true\n');
|
||||
|
||||
@@ -118,4 +146,17 @@ describe('status/instructions report unrecognized change metadata keys', () => {
|
||||
expect(context.metadata?.skip_specs).toBe(true);
|
||||
expect(context.warnings).toEqual([expect.stringContaining('skip_design')]);
|
||||
});
|
||||
|
||||
it('returns no warning when the only extra keys are x- extension keys', async () => {
|
||||
await fs.writeFile(
|
||||
path.join(changeDir, METADATA_FILENAME),
|
||||
'schema: spec-driven\nskip_specs: true\nx-goal: G-12\nx-owner:\n team: payments\n',
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const context = loadChangeContext(tempDir, 'probe');
|
||||
|
||||
expect(context.metadata?.skip_specs).toBe(true);
|
||||
expect(context.warnings).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,8 +22,8 @@ import { fileURLToPath } from 'node:url';
|
||||
// unless you ask" is the wording this very test recommends.
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
// The pages that pitch explore to a user. Both trees ship: `docs/` is what
|
||||
// README links today, `docs-lab/` is what the documentation site publishes
|
||||
// The pages that pitch explore to a user. Both trees ship: `docs/` is the
|
||||
// legacy tree still readable on GitHub, `docs-lab/` is what the documentation site publishes
|
||||
// (`website/content/docs` is generated from it and is not committed).
|
||||
const PAGES = [
|
||||
'README.md',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ChangeMetadataError,
|
||||
readRetireCapabilitiesMarker,
|
||||
listUnknownChangeMetadataKeys,
|
||||
isExtensionMetadataKey,
|
||||
formatUnknownChangeMetadataKeysMessage,
|
||||
} from '../../src/utils/change-metadata.js';
|
||||
import { ChangeMetadataSchema } from '../../src/core/change-metadata/index.js';
|
||||
@@ -175,6 +176,23 @@ describe('listUnknownChangeMetadataKeys', () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not report x- extension keys, and still reports everything else', () => {
|
||||
expect(
|
||||
listUnknownChangeMetadataKeys({
|
||||
schema: 'spec-driven',
|
||||
'x-goal': 'G-12',
|
||||
'x-tracker': { system: 'linear', id: 'SB-5497' },
|
||||
'x-': 'not an extension key',
|
||||
'X-Upper': 1,
|
||||
bogus_key: 1,
|
||||
})
|
||||
).toEqual(['X-Upper', 'bogus_key', 'x-']);
|
||||
});
|
||||
|
||||
it('tells the author how to keep tool metadata without the warning', () => {
|
||||
expect(formatUnknownChangeMetadataKeysMessage(['bogus_key'])).toContain('x-');
|
||||
});
|
||||
|
||||
it('explains that skip_design is not skip_specs', () => {
|
||||
const message = formatUnknownChangeMetadataKeysMessage(['skip_design']);
|
||||
expect(message).toContain('skip_design');
|
||||
@@ -488,3 +506,93 @@ describe('boolean marker reasons', () => {
|
||||
expect(marker.invalidReason).not.toMatch(/[\u0000-\u001f\u007f]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExtensionMetadataKey', () => {
|
||||
it('accepts x- followed by a name', () => {
|
||||
expect(isExtensionMetadataKey('x-goal')).toBe(true);
|
||||
expect(isExtensionMetadataKey('x-kf_part-2')).toBe(true);
|
||||
expect(isExtensionMetadataKey('x-')).toBe(false);
|
||||
expect(isExtensionMetadataKey('X-goal')).toBe(false);
|
||||
expect(isExtensionMetadataKey('goal')).toBe(false);
|
||||
});
|
||||
|
||||
// The name after the prefix is the owner's business, in any case or spelling.
|
||||
it('accepts the spellings a tracker id is actually written in', () => {
|
||||
expect(isExtensionMetadataKey('x-jiraKey')).toBe(true);
|
||||
expect(isExtensionMetadataKey('x-trackerID')).toBe(true);
|
||||
expect(isExtensionMetadataKey('x-Owner')).toBe(true);
|
||||
expect(isExtensionMetadataKey('x-foo.bar')).toBe(true);
|
||||
expect(isExtensionMetadataKey('x--foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a prefix that names nothing, or a name carrying whitespace', () => {
|
||||
expect(isExtensionMetadataKey('x-')).toBe(false);
|
||||
expect(isExtensionMetadataKey('x- ')).toBe(false);
|
||||
expect(isExtensionMetadataKey('x- tracker')).toBe(false);
|
||||
expect(isExtensionMetadataKey(' x-foo')).toBe(false);
|
||||
expect(isExtensionMetadataKey('x-foo ')).toBe(false);
|
||||
expect(isExtensionMetadataKey('x-a\nb')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a near-miss prefix, including the wrong case', () => {
|
||||
expect(isExtensionMetadataKey('x_foo')).toBe(false);
|
||||
expect(isExtensionMetadataKey('xfoo')).toBe(false);
|
||||
expect(isExtensionMetadataKey('x')).toBe(false);
|
||||
expect(isExtensionMetadataKey('-x-foo')).toBe(false);
|
||||
expect(isExtensionMetadataKey('X-Foo')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extension keys alongside the unknown-key warning', () => {
|
||||
it('keeps reporting a misspelled real key beside a valid extension', () => {
|
||||
expect(
|
||||
listUnknownChangeMetadataKeys({
|
||||
schema: 'spec-driven',
|
||||
'x-goal': 'G-12',
|
||||
skip_spec: true,
|
||||
taskss: true,
|
||||
})
|
||||
).toEqual(['skip_spec', 'taskss']);
|
||||
});
|
||||
|
||||
// An x- key inside a recognized object is not a top-level extension; the
|
||||
// object's own strict schema rejects it, so it must not be silently accepted.
|
||||
it('does not treat an x- key inside a recognized object as an extension', () => {
|
||||
expect(
|
||||
listUnknownChangeMetadataKeys({
|
||||
schema: 'spec-driven',
|
||||
initiative: { store: 'a', id: 'b', 'x-extra': 1 },
|
||||
})
|
||||
).toEqual([]);
|
||||
expect(
|
||||
ChangeMetadataSchema.safeParse({
|
||||
schema: 'spec-driven',
|
||||
initiative: { store: 'a', id: 'b', 'x-extra': 1 },
|
||||
}).success
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// The message must not tell someone who already wrote x-jiraKey to "prefix a
|
||||
// key with x-". It now names the two conditions the prefix does not cover.
|
||||
it('explains the near-miss to a user who did use the prefix', () => {
|
||||
const message = formatUnknownChangeMetadataKeysMessage(['X-Tracker', 'x-']);
|
||||
expect(message).toContain('lowercase x- prefix');
|
||||
expect(message).toContain('no whitespace');
|
||||
expect(message).toContain('x-jiraKey are extensions');
|
||||
});
|
||||
|
||||
it('leaves the message alone when no key used the prefix', () => {
|
||||
expect(formatUnknownChangeMetadataKeysMessage(['taskss'])).not.toContain(
|
||||
'lowercase x- prefix'
|
||||
);
|
||||
});
|
||||
|
||||
// ChangeMetadataSchema strips unknown keys, so writeChangeMetadata is only
|
||||
// safe on a new file. If a future caller rewrites an existing one without
|
||||
// merging the raw YAML back, every x- key in it is silently deleted.
|
||||
it('strips extension keys, so the writer must never rewrite an existing file', () => {
|
||||
expect(
|
||||
ChangeMetadataSchema.safeParse({ schema: 'spec-driven', 'x-goal': 'G-12' }).data
|
||||
).toEqual({ schema: 'spec-driven' });
|
||||
});
|
||||
});
|
||||
|
||||
+11
-11
@@ -13,22 +13,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"beautiful-mermaid": "^1.1.3",
|
||||
"fumadocs-core": "^16.15.17",
|
||||
"fumadocs-mdx": "^15.3.1",
|
||||
"fumadocs-ui": "^16.15.17",
|
||||
"lucide-react": "^1.34.0",
|
||||
"fumadocs-core": "^16.16.1",
|
||||
"fumadocs-mdx": "^15.4.6",
|
||||
"fumadocs-ui": "^16.16.1",
|
||||
"lucide-react": "^1.52.0",
|
||||
"next": "16.3.8",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"zod": "^4.4.3"
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"zod": "^4.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.1",
|
||||
"@types/mdx": "^2.0.14",
|
||||
"@types/node": "^26.3.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"postcss": "^8.5.28",
|
||||
"@types/node": "^26.6.4",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"postcss": "^8.5.29",
|
||||
"serve": "^14.2.6",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"typescript": "^6.0.3"
|
||||
|
||||
Generated
+530
-535
File diff suppressed because it is too large
Load Diff
@@ -22,3 +22,7 @@ overrides:
|
||||
# only (transitive via postcss); this is a statically exported site with no server
|
||||
# runtime. Remove once transitive nanoid is >=3.3.17 (check: pnpm why nanoid).
|
||||
nanoid@<3.3.17: '>=3.3.17 <4'
|
||||
# GHSA-vc2v-76pw-4v95 — compression memory-leak DoS on premature response close.
|
||||
# Dev-only (serve > compression); never in the static site. Remove once
|
||||
# transitive compression is >=1.8.2 (check: pnpm why compression).
|
||||
compression@<1.8.2: '>=1.8.2 <2'
|
||||
|
||||
Reference in New Issue
Block a user