diff --git a/AGENTS.md b/AGENTS.md index 7df0e21..4b5c761 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,10 +4,10 @@ Reqly is an npm-workspace TypeScript project for a Git-native requirements and traceability VS Code extension. -- `packages/core` is the extension's private implementation library for formats, schemas, Git behavior, validation, reports, and mutations. -- `packages/vscode` contains the VS Code extension and requirement graph webview. +- `packages/core` is the extension's private implementation library for formats, schemas, Git behavior, validation, and mutations. +- `packages/vscode` contains the VS Code extension and requirement explorer. -Keep Markdown and YAML files as the source of truth. Caches, build output, webviews, and reports must remain disposable projections. +Keep Markdown and YAML files as the source of truth. Caches and build output must remain disposable projections. ## Working Rules diff --git a/package-lock.json b/package-lock.json index c00eb15..53c6a73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1880,11 +1880,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/cytoscape": { - "version": "3.21.9", - "dev": true, - "license": "MIT" - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "dev": true, @@ -1909,23 +1904,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.17", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, "node_modules/@types/sarif": { "version": "2.1.7", "dev": true, @@ -3046,18 +3024,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/csstype": { - "version": "3.2.3", - "dev": true, - "license": "MIT" - }, - "node_modules/cytoscape": { - "version": "3.34.0", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.4.3", "dev": true, @@ -7566,24 +7532,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/react": { - "version": "19.2.7", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, "node_modules/read": { "version": "1.0.7", "dev": true, @@ -7810,10 +7758,6 @@ "node": ">=11.0.0" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "license": "MIT" - }, "node_modules/secretlint": { "version": "10.2.2", "dev": true, @@ -9036,6 +8980,7 @@ "version": "7.3.6", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", @@ -9475,15 +9420,9 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@reqly/core": "0.1.0", - "cytoscape": "^3.33.0", - "react": "^19.1.0", - "react-dom": "^19.1.0" + "@reqly/core": "0.1.0" }, "devDependencies": { - "@types/cytoscape": "^3.21.0", - "@types/react": "^19.1.0", - "@types/react-dom": "^19.1.0", "@types/vscode": "1.107.0", "esbuild": "^0.25.0" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ccb8fc..e42a38b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,4 +7,3 @@ export * from "./git.js"; export * from "./repository.js"; export * from "./validate.js"; export * from "./mutations.js"; -export * from "./report.js"; diff --git a/packages/core/src/mutations.ts b/packages/core/src/mutations.ts index 64658b3..2a7489c 100644 --- a/packages/core/src/mutations.ts +++ b/packages/core/src/mutations.ts @@ -34,7 +34,7 @@ function generatedAgentBlock(config: ReqlyConfig = defaultConfig): string { ## Reqly - Use Reqly's VS Code actions for interactive mutations; follow the format rules below when editing \`index.md\` directly. -- Query only the item and graph depth needed for the current work. +- Query only the item and relation depth needed for the current work. - Keep IDs stable. Never reuse or invent an existing ID. - Requirements need a \`## Requirement\` section. - Verifications need \`## Procedure\`, \`## Expected Result\`, and \`## Evidence\` sections and use only \`pass\` or \`fail\` status. diff --git a/packages/core/src/report.ts b/packages/core/src/report.ts deleted file mode 100644 index 0cfacc4..0000000 --- a/packages/core/src/report.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ReqlyRepository } from "./repository.js"; - -export function graphData(repository: ReqlyRepository): { nodes: unknown[]; edges: unknown[] } { - return { - nodes: [...repository.records.values()].map((record) => ({ - id: record.data.id, - label: record.data.title, - type: record.type, - status: record.status, - ...(record.type === "requirement" ? { verified: repository.verificationState(record.data.id) } : {}), - })), - edges: [...repository.records.values()].flatMap((record) => (record.data.relations ?? []).map((relation) => ({ - source: record.data.id, - target: relation.target, - type: relation.type, - fingerprint: relation.fingerprint, - }))), - }; -} diff --git a/packages/vscode/README.md b/packages/vscode/README.md index abc22b3..213b3d3 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -1,3 +1,3 @@ # Reqly for VS Code -Reqly is delivered entirely as a VS Code extension and keeps product requirements, verification procedures, traceability, and linked product documents close to the code. Open a repository containing `.reqly/AGENTS.md` to use the parent-first requirements explorer, linked verifications, computed verification icons, impact queue, graph, diagnostics, and Markdown editor assistance. Relation management can select an existing compatible item or create and link a new one. Removing a local artifact deletes its referenced file after confirmation. Deleting an item removes its folder and all relations to it after a separate destructive confirmation. Configuration defaults are bundled with the extension. +Reqly is delivered entirely as a VS Code extension and keeps product requirements, verification procedures, traceability, and linked product documents close to the code. Open a repository containing `.reqly/AGENTS.md` to use the parent-first requirements explorer, linked verifications, computed verification icons, impact queue, diagnostics, and Markdown editor assistance. Use a requirement's context menu to create and link a new sub-requirement in one action. Relation management can select an existing compatible item or create and link a new one. Removing a local artifact deletes its referenced file after confirmation. Deleting an item removes its folder and all relations to it after a separate destructive confirmation. Configuration defaults are bundled with the extension. diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 228fce1..d31a8ad 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -24,7 +24,7 @@ "engines": { "vscode": "^1.107.0" }, - "categories": ["Other", "Visualization"], + "categories": ["Other"], "activationEvents": ["workspaceContains:.reqly/AGENTS.md"], "scripts": { "build": "node ./scripts/build.mjs", @@ -32,15 +32,9 @@ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"" }, "dependencies": { - "@reqly/core": "0.1.0", - "cytoscape": "^3.33.0", - "react": "^19.1.0", - "react-dom": "^19.1.0" + "@reqly/core": "0.1.0" }, "devDependencies": { - "@types/cytoscape": "^3.21.0", - "@types/react": "^19.1.0", - "@types/react-dom": "^19.1.0", "@types/vscode": "1.107.0", "esbuild": "^0.25.0" }, @@ -62,9 +56,9 @@ { "command": "reqly.init", "title": "Reqly: Initialize Project" }, { "command": "reqly.refresh", "title": "Reqly: Refresh" }, { "command": "reqly.newRequirement", "title": "Reqly: New Requirement" }, + { "command": "reqly.newSubRequirement", "title": "Reqly: New Sub-requirement" }, { "command": "reqly.newVerification", "title": "Reqly: New Verification" }, { "command": "reqly.validate", "title": "Reqly: Validate Project" }, - { "command": "reqly.openGraph", "title": "Reqly: Open Requirement Graph" }, { "command": "reqly.openItem", "title": "Reqly: Open Item" }, { "command": "reqly.previewItem", "title": "Reqly: Preview Markdown" }, { "command": "reqly.setStatus", "title": "Reqly: Change Status" }, @@ -77,15 +71,15 @@ "view/title": [ { "command": "reqly.newRequirement", "when": "view == reqly.requirements", "group": "navigation" }, { "command": "reqly.newVerification", "when": "view == reqly.requirements", "group": "navigation" }, - { "command": "reqly.openGraph", "when": "view == reqly.requirements", "group": "navigation" }, { "command": "reqly.refresh", "when": "view =~ /reqly\\./", "group": "navigation" } ], "view/item/context": [ - { "command": "reqly.previewItem", "when": "viewItem == reqlyItem", "group": "inline" }, - { "command": "reqly.setStatus", "when": "viewItem == reqlyItem", "group": "reqly" }, - { "command": "reqly.manageRelations", "when": "viewItem == reqlyItem", "group": "reqly" }, - { "command": "reqly.manageArtifacts", "when": "viewItem == reqlyItem", "group": "reqly" }, - { "command": "reqly.deleteItem", "when": "viewItem == reqlyItem", "group": "reqly" }, + { "command": "reqly.newSubRequirement", "when": "viewItem == reqlyRequirement", "group": "reqly@1" }, + { "command": "reqly.previewItem", "when": "viewItem =~ /reqly(Item|Requirement)/", "group": "inline" }, + { "command": "reqly.setStatus", "when": "viewItem =~ /reqly(Item|Requirement)/", "group": "reqly" }, + { "command": "reqly.manageRelations", "when": "viewItem =~ /reqly(Item|Requirement)/", "group": "reqly" }, + { "command": "reqly.manageArtifacts", "when": "viewItem =~ /reqly(Item|Requirement)/", "group": "reqly" }, + { "command": "reqly.deleteItem", "when": "viewItem =~ /reqly(Item|Requirement)/", "group": "reqly" }, { "command": "reqly.acknowledgeImpact", "when": "viewItem == reqlyImpactAcknowledgeable", "group": "reqly" } ] } diff --git a/packages/vscode/scripts/build.mjs b/packages/vscode/scripts/build.mjs index 77b4072..8c16488 100644 --- a/packages/vscode/scripts/build.mjs +++ b/packages/vscode/scripts/build.mjs @@ -3,7 +3,4 @@ import { rm } from "node:fs/promises"; await rm("dist", { recursive: true, force: true }); -await Promise.all([ - build({ entryPoints: ["src/extension.ts"], outfile: "dist/extension.cjs", bundle: true, platform: "node", format: "cjs", target: "node22", external: ["vscode"], sourcemap: true }), - build({ entryPoints: ["src/webviews/graph.tsx"], outfile: "dist/graph.js", bundle: true, platform: "browser", format: "iife", target: "es2022", minify: true }), -]); +await build({ entryPoints: ["src/extension.ts"], outfile: "dist/extension.cjs", bundle: true, platform: "node", format: "cjs", target: "node22", external: ["vscode"], sourcemap: true }); diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index abe882a..2ca5a6a 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -7,7 +7,6 @@ import { createItem, createVerification, deleteItem, - graphData, initRepository, parseArtifactLink, setArtifact, @@ -29,7 +28,7 @@ class ItemNode extends vscode.TreeItem { const verificationLabel = record.type === "requirement" ? verified === true ? "verified" : verified === false ? "failed" : "verification incomplete" : ""; this.description = `${record.status}${verificationLabel ? ` · ${verificationLabel}` : ""}${health.length ? ` · ${health.join(", ")}` : ""}`; this.tooltip = `${record.data.id}\n${record.data.title}\n${this.description}`; - this.contextValue = "reqlyItem"; + this.contextValue = record.type === "requirement" ? "reqlyRequirement" : "reqlyItem"; const result = record.type === "verification" ? record.status === "pass" ? true : record.status === "fail" ? false : undefined : verified; this.iconPath = result === true ? new vscode.ThemeIcon("check", new vscode.ThemeColor("testing.iconPassed")) : result === false ? new vscode.ThemeIcon("close", new vscode.ThemeColor("testing.iconFailed")) : new vscode.ThemeIcon("symbol-interface"); this.command = { command: "reqly.openItem", title: "Open Item", arguments: [this] }; @@ -204,6 +203,7 @@ export async function activate(context: vscode.ExtensionContext): Promise register("reqly.refresh", () => state.refresh()); register("reqly.init", async () => { const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!root) return; await initRepository(root); await state.refresh(); }); register("reqly.newRequirement", async () => createFromUi(state)); + register("reqly.newSubRequirement", async (node: ItemNode | string) => createSubRequirementFromUi(state, node)); register("reqly.newVerification", async () => createVerificationFromUi(state)); register("reqly.openItem", (node: ItemNode | ImpactNode | string) => openItem(state, node)); register("reqly.previewItem", async (node: ItemNode | ImpactNode | string) => { const record = resolveRecord(state, node); if (record) await vscode.commands.executeCommand("markdown.showPreviewToSide", vscode.Uri.file(record.filePath)); }); @@ -213,7 +213,6 @@ export async function activate(context: vscode.ExtensionContext): Promise register("reqly.manageArtifacts", async (node: ItemNode | ImpactNode | string) => manageArtifactsFromUi(state, node)); register("reqly.deleteItem", async (node: ItemNode | ImpactNode | string) => deleteItemFromUi(state, node)); register("reqly.acknowledgeImpact", async (node: ImpactNode) => acknowledgeFromUi(state, node)); - register("reqly.openGraph", () => openGraph(context, state)); const watcher = vscode.workspace.createFileSystemWatcher("**/{.reqly,requirements}/**/*"); let timer: NodeJS.Timeout | undefined; const changed = new Map(); @@ -238,6 +237,23 @@ async function createFromUi(state: ExtensionState): Promise { if (itemId) await openItem(state, itemId); } +async function createSubRequirementFromUi(state: ExtensionState, value: ItemNode | string): Promise { + if (!state.repository) return; + const parent = resolveRecord(state, value); + if (!parent || parent.type !== "requirement" || !await ensureSaved(parent)) return; + const title = await vscode.window.showInputBox({ title: `New sub-requirement · ${parent.data.id}`, prompt: `Title for a child of ${parent.data.id}`, validateInput: (input) => input.trim() ? undefined : "A title is required." }); + if (!title) return; + const created = await createItem(state.repository, { title: title.trim() }); + const childId = created.itemId; + if (!childId) return; + await state.refresh(); + const child = state.repository.records.get(childId); + if (!child) return; + await setRelation(state.repository, child.data.id, "add", { type: "required-by", target: parent.data.id }, child.version); + await state.refresh(); + await openItem(state, childId); +} + async function createVerificationFromUi(state: ExtensionState): Promise { const itemId = await promptCreateItem(state, "verification"); await state.refresh(); if (itemId) await openItem(state, itemId); @@ -387,18 +403,4 @@ async function acknowledgeFromUi(state: ExtensionState, node: ImpactNode): Promi await acknowledgeImpact(state.repository, node.entry.record.data.id, node.entry.relatedId, node.entry.record.version); await state.refresh(); } -function nonce(): string { return [...crypto.getRandomValues(new Uint8Array(16))].map((value) => value.toString(16).padStart(2, "0")).join(""); } - -function webviewHtml(webview: vscode.Webview, extensionUri: vscode.Uri, entry: "graph", data: unknown): string { - const script = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "dist", `${entry}.js`)); const token = nonce(); - const encoded = Buffer.from(JSON.stringify(data)).toString("base64"); - return `
`; -} - -async function openGraph(context: vscode.ExtensionContext, state: ExtensionState): Promise { - if (!state.repository) return; const panel = vscode.window.createWebviewPanel("reqly.graph", "Reqly Graph", vscode.ViewColumn.Active, { enableScripts: true, localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "dist")] }); - panel.webview.html = webviewHtml(panel.webview, context.extensionUri, "graph", graphData(state.repository)); - panel.webview.onDidReceiveMessage((message) => { if (message.type === "open") void openItem(state, message.id); }); -} - export function deactivate(): void {} diff --git a/packages/vscode/src/webviews/globals.d.ts b/packages/vscode/src/webviews/globals.d.ts deleted file mode 100644 index 7db27f2..0000000 --- a/packages/vscode/src/webviews/globals.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare function acquireVsCodeApi(): { postMessage(message: unknown): void; getState(): unknown; setState(state: unknown): void }; -interface Window { __REQLY_DATA__: any; } diff --git a/packages/vscode/src/webviews/graph.tsx b/packages/vscode/src/webviews/graph.tsx deleted file mode 100644 index 20b5f7a..0000000 --- a/packages/vscode/src/webviews/graph.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { createRoot } from "react-dom/client"; -import cytoscape from "cytoscape"; - -function Graph(): React.JSX.Element { - const host = useRef(null); - useEffect(() => { - const data = window.__REQLY_DATA__; - const graph = cytoscape({ - container: host.current, - elements: [ - ...data.nodes.map((node: any) => ({ data: { id: node.id, label: `${node.id}\n${node.label}`, status: node.status, type: node.type } })), - ...data.edges.map((edge: any, index: number) => ({ data: { id: `edge-${index}`, source: edge.source, target: edge.target, label: edge.type } })), - ], - style: [ - { selector: "node", style: { label: "data(label)", "text-wrap": "wrap", color: "var(--vscode-editor-foreground)", "background-color": "var(--vscode-charts-blue)", "font-size": 11, width: 42, height: 42 } }, - { selector: "edge", style: { label: "data(label)", width: 1.5, "line-color": "var(--vscode-editorWidget-border)", "target-arrow-color": "var(--vscode-editorWidget-border)", "target-arrow-shape": "triangle", "curve-style": "bezier", "font-size": 9, color: "var(--vscode-descriptionForeground)" } }, - ], - layout: { name: "breadthfirst", directed: true, padding: 30, spacingFactor: 1.4 }, - }); - graph.on("tap", "node", (event) => acquireVsCodeApi().postMessage({ type: "open", id: event.target.id() })); - return () => graph.destroy(); - }, []); - return
; -} - -createRoot(document.getElementById("root")!).render(); diff --git a/packages/vscode/tsconfig.json b/packages/vscode/tsconfig.json index af1e6c9..31eeb47 100644 --- a/packages/vscode/tsconfig.json +++ b/packages/vscode/tsconfig.json @@ -2,9 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, - "jsx": "react-jsx", "rootDir": "src", "types": ["node", "vscode"] }, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.ts"] } diff --git a/scripts/verify-conventional-title.mjs b/scripts/verify-conventional-title.mjs index bea5102..c12b34d 100644 --- a/scripts/verify-conventional-title.mjs +++ b/scripts/verify-conventional-title.mjs @@ -1,6 +1,6 @@ const title = process.env.PR_TITLE?.trim() ?? ""; const conventional = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._/-]+\))?!?: .+$/; if (!conventional.test(title)) { - process.stderr.write(`Pull request title must use Conventional Commits syntax, for example "feat: add graph filtering". Received: ${title || "(empty)"}\n`); + process.stderr.write(`Pull request title must use Conventional Commits syntax, for example "feat: add relation filtering". Received: ${title || "(empty)"}\n`); process.exit(1); }