Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 2 additions & 63 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ export * from "./git.js";
export * from "./repository.js";
export * from "./validate.js";
export * from "./mutations.js";
export * from "./report.js";
2 changes: 1 addition & 1 deletion packages/core/src/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 0 additions & 19 deletions packages/core/src/report.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/vscode/README.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 9 additions & 15 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,17 @@
"engines": {
"vscode": "^1.107.0"
},
"categories": ["Other", "Visualization"],
"categories": ["Other"],
"activationEvents": ["workspaceContains:.reqly/AGENTS.md"],
"scripts": {
"build": "node ./scripts/build.mjs",
"check": "tsc -p tsconfig.json --noEmit",
"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"
},
Expand All @@ -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" },
Expand All @@ -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" }
]
}
Expand Down
5 changes: 1 addition & 4 deletions packages/vscode/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
36 changes: 19 additions & 17 deletions packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
createItem,
createVerification,
deleteItem,
graphData,
initRepository,
parseArtifactLink,
setArtifact,
Expand All @@ -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] };
Expand Down Expand Up @@ -204,6 +203,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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)); });
Expand All @@ -213,7 +213,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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<string, vscode.Uri>();
Expand All @@ -238,6 +237,23 @@ async function createFromUi(state: ExtensionState): Promise<void> {
if (itemId) await openItem(state, itemId);
}

async function createSubRequirementFromUi(state: ExtensionState, value: ItemNode | string): Promise<void> {
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<void> {
const itemId = await promptCreateItem(state, "verification"); await state.refresh();
if (itemId) await openItem(state, itemId);
Expand Down Expand Up @@ -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 `<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} data:; style-src 'nonce-${token}'; script-src 'nonce-${token}' ${webview.cspSource};"><style nonce="${token}">html,body,#root{width:100%;height:100%;margin:0;background:var(--vscode-editor-background);color:var(--vscode-editor-foreground);font-family:var(--vscode-font-family)}</style></head><body><div id="root"></div><script nonce="${token}">window.__REQLY_DATA__=JSON.parse(atob("${encoded}"));</script><script nonce="${token}" src="${script}"></script></body></html>`;
}

async function openGraph(context: vscode.ExtensionContext, state: ExtensionState): Promise<void> {
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 {}
2 changes: 0 additions & 2 deletions packages/vscode/src/webviews/globals.d.ts

This file was deleted.

27 changes: 0 additions & 27 deletions packages/vscode/src/webviews/graph.tsx

This file was deleted.

3 changes: 1 addition & 2 deletions packages/vscode/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion scripts/verify-conventional-title.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading