diff --git a/gitnexus-web/src/lib/constants.ts b/gitnexus-web/src/lib/constants.ts index f6804405b..8b7fee89d 100644 --- a/gitnexus-web/src/lib/constants.ts +++ b/gitnexus-web/src/lib/constants.ts @@ -38,6 +38,7 @@ export const NODE_COLORS: Record = { Template: '#a78bfa', // Violet light - like Type Route: '#f43f5e', // Rose - like Process Tool: '#a855f7', // Purple - like Project + StatusType: '#06b6d4', // Cyan - workflow state }; // Node sizes by type - clear visual hierarchy with dramatic size differences @@ -79,6 +80,7 @@ export const NODE_SIZES: Record = { Template: 3, // Like Type Route: 5, // Like Enum Tool: 5, // Like Enum + StatusType: 5, // Like Enum - workflow state }; // Community color palette for cluster-based coloring diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index f2605cb43..960453957 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -2975,6 +2975,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3423,6 +3424,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -4324,6 +4326,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5012,6 +5015,7 @@ "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "node-addon-api": "^8.0.0", "node-gyp-build": "^4.8.0" @@ -5358,6 +5362,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -5478,6 +5483,7 @@ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -5556,6 +5562,7 @@ "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", @@ -5745,6 +5752,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/gitnexus/test/unit/workflow-detector.test.ts b/gitnexus/test/unit/workflow-detector.test.ts index 5f02735a1..58c90f55d 100644 --- a/gitnexus/test/unit/workflow-detector.test.ts +++ b/gitnexus/test/unit/workflow-detector.test.ts @@ -1,41 +1,39 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; import { extractStatusTypes, extractStatusTransitions, - type DetectedStatusType, - type DetectedTransition, -} from "../../src/core/ingestion/workflow-detector.js"; +} from '../../src/core/ingestion/workflow-detector.js'; -describe("extractStatusTypes", () => { - it("extracts TypeScript union type status definitions", () => { +describe('extractStatusTypes', () => { + it('extracts TypeScript union type status definitions', () => { const content = ` export type GrantStatus = 'DRAFT' | 'ACTIVE' | 'AT_RISK' | 'ON_HOLD' | 'COMPLETED' | 'CLOSED'; export type ProjectStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; `; - const types = extractStatusTypes(content, "typescript", "status-rules.ts"); + const types = extractStatusTypes(content, 'typescript', 'status-rules.ts'); expect(types).toHaveLength(2); expect(types[0]).toMatchObject({ - name: "GrantStatus", - values: ["DRAFT", "ACTIVE", "AT_RISK", "ON_HOLD", "COMPLETED", "CLOSED"], - filePath: "status-rules.ts", + name: 'GrantStatus', + values: ['DRAFT', 'ACTIVE', 'AT_RISK', 'ON_HOLD', 'COMPLETED', 'CLOSED'], + filePath: 'status-rules.ts', }); expect(types[1]).toMatchObject({ - name: "ProjectStatus", - values: ["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "CANCELLED"], + name: 'ProjectStatus', + values: ['NOT_STARTED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'], }); }); - it("extracts lowercase approval status types", () => { + it('extracts lowercase approval status types', () => { const content = ` export type ApprovalInstanceStatus = 'pending' | 'approved' | 'rejected' | 'escalated' | 'cancelled'; `; - const types = extractStatusTypes(content, "typescript", "workflow.ts"); + const types = extractStatusTypes(content, 'typescript', 'workflow.ts'); expect(types).toHaveLength(1); - expect(types[0].values).toContain("pending"); - expect(types[0].values).toContain("approved"); + expect(types[0].values).toContain('pending'); + expect(types[0].values).toContain('approved'); }); - it("extracts enum-based status definitions", () => { + it('extracts enum-based status definitions', () => { const content = ` enum OrderStatus { PENDING = 'pending', @@ -44,61 +42,56 @@ enum OrderStatus { DELIVERED = 'delivered', } `; - const types = extractStatusTypes(content, "typescript", "order.ts"); + const types = extractStatusTypes(content, 'typescript', 'order.ts'); expect(types).toHaveLength(1); - expect(types[0].name).toBe("OrderStatus"); - expect(types[0].values).toEqual([ - "pending", - "processing", - "shipped", - "delivered", - ]); + expect(types[0].name).toBe('OrderStatus'); + expect(types[0].values).toEqual(['pending', 'processing', 'shipped', 'delivered']); }); - it("ignores non-status union types", () => { + it('ignores non-status union types', () => { const content = ` export type Color = 'red' | 'blue' | 'green'; export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; `; - const types = extractStatusTypes(content, "typescript", "utils.ts"); + const types = extractStatusTypes(content, 'typescript', 'utils.ts'); // HttpMethod might match due to keyword heuristics, but Color should not - expect(types.find((t) => t.name === "Color")).toBeUndefined(); + expect(types.find((t) => t.name === 'Color')).toBeUndefined(); }); - it("extracts Python Enum status types", () => { + it('extracts Python Enum status types', () => { const content = ` class OrderStatus(Enum): PENDING = 'pending' SHIPPED = 'shipped' DELIVERED = 'delivered' `; - const types = extractStatusTypes(content, "python", "models.py"); + const types = extractStatusTypes(content, 'python', 'models.py'); expect(types).toHaveLength(1); expect(types[0]).toMatchObject({ - name: "OrderStatus", - values: ["pending", "shipped", "delivered"], - kind: "enum", - filePath: "models.py", + name: 'OrderStatus', + values: ['pending', 'shipped', 'delivered'], + kind: 'enum', + filePath: 'models.py', }); }); - it("extracts Java enum status types (no string values)", () => { + it('extracts Java enum status types (no string values)', () => { const content = ` public enum OrderStatus { PENDING, ACTIVE, COMPLETED, CANCELLED } `; - const types = extractStatusTypes(content, "java", "Order.java"); + const types = extractStatusTypes(content, 'java', 'Order.java'); expect(types).toHaveLength(1); expect(types[0]).toMatchObject({ - name: "OrderStatus", - values: ["PENDING", "ACTIVE", "COMPLETED", "CANCELLED"], - kind: "enum", - filePath: "Order.java", + name: 'OrderStatus', + values: ['PENDING', 'ACTIVE', 'COMPLETED', 'CANCELLED'], + kind: 'enum', + filePath: 'Order.java', }); }); }); -describe("extractStatusTransitions", () => { - it("detects Prisma status update patterns", () => { +describe('extractStatusTransitions', () => { + it('detects Prisma status update patterns', () => { const content = ` async function approveGrant(grantId: string) { await prisma.grant.update({ @@ -107,21 +100,17 @@ async function approveGrant(grantId: string) { }); } `; - const transitions = extractStatusTransitions( - content, - "typescript", - "engine.ts", - ); + const transitions = extractStatusTransitions(content, 'typescript', 'engine.ts'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "ACTIVE", - entityType: "grant", - functionName: "approveGrant", - filePath: "engine.ts", + toStatus: 'ACTIVE', + entityType: 'grant', + functionName: 'approveGrant', + filePath: 'engine.ts', }); }); - it("detects conditional status transitions with fromStatus", () => { + it('detects conditional status transitions with fromStatus', () => { const content = ` async function submitGrant(grant: Grant) { if (grant.status === 'DRAFT') { @@ -132,19 +121,15 @@ async function submitGrant(grant: Grant) { } } `; - const transitions = extractStatusTransitions( - content, - "typescript", - "route.ts", - ); + const transitions = extractStatusTransitions(content, 'typescript', 'route.ts'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - fromStatus: "DRAFT", - toStatus: "ACTIVE", + fromStatus: 'DRAFT', + toStatus: 'ACTIVE', }); }); - it("detects transactional status updates", () => { + it('detects transactional status updates', () => { const content = ` await prisma.$transaction(async (tx) => { await tx.approvalStepInstance.update({ @@ -153,102 +138,82 @@ await prisma.$transaction(async (tx) => { }); }); `; - const transitions = extractStatusTransitions( - content, - "typescript", - "engine.ts", - ); + const transitions = extractStatusTransitions(content, 'typescript', 'engine.ts'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "approved", - entityType: "approvalStepInstance", + toStatus: 'approved', + entityType: 'approvalStepInstance', isTransactional: true, }); }); - it("detects direct assignment transitions", () => { + it('detects direct assignment transitions', () => { const content = ` function shipOrder(order) { order.status = 'shipped'; } `; - const transitions = extractStatusTransitions( - content, - "javascript", - "order.js", - ); + const transitions = extractStatusTransitions(content, 'javascript', 'order.js'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "shipped", - entityType: "order", - functionName: "shipOrder", + toStatus: 'shipped', + entityType: 'order', + functionName: 'shipOrder', }); }); - it("detects setter pattern transitions", () => { + it('detects setter pattern transitions', () => { const content = ` function approve(entity) { entity.setStatus('approved'); } `; - const transitions = extractStatusTransitions( - content, - "typescript", - "service.ts", - ); + const transitions = extractStatusTransitions(content, 'typescript', 'service.ts'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "approved", - entityType: "entity", + toStatus: 'approved', + entityType: 'entity', }); }); - it("detects Python self.status assignment", () => { + it('detects Python self.status assignment', () => { const content = ` def activate(self): self.status = 'active' `; - const transitions = extractStatusTransitions(content, "python", "model.py"); + const transitions = extractStatusTransitions(content, 'python', 'model.py'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "active", - entityType: "self", - filePath: "model.py", + toStatus: 'active', + entityType: 'self', + filePath: 'model.py', }); }); - it("detects generic .update() with status field", () => { + it('detects generic .update() with status field', () => { const content = ` function completeTask(task) { task.update({ status: 'completed', finishedAt: new Date() }); } `; - const transitions = extractStatusTransitions( - content, - "javascript", - "task.js", - ); + const transitions = extractStatusTransitions(content, 'javascript', 'task.js'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "completed", - entityType: "task", + toStatus: 'completed', + entityType: 'task', }); }); - it("detects set_status snake_case setter", () => { + it('detects set_status snake_case setter', () => { const content = ` def process(order): order.set_status('processing') `; - const transitions = extractStatusTransitions( - content, - "python", - "service.py", - ); + const transitions = extractStatusTransitions(content, 'python', 'service.py'); expect(transitions).toHaveLength(1); expect(transitions[0]).toMatchObject({ - toStatus: "processing", - entityType: "order", + toStatus: 'processing', + entityType: 'order', }); }); }); diff --git a/package-lock.json b/package-lock.json index 65c0870ed..edf595963 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,8 @@ "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "lint-staged": "^15.5.0", - "prettier": "^3.8.0", - "prettier-plugin-tailwindcss": "^0.7.0" + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.2" } }, "node_modules/@babel/code-frame": { @@ -49,6 +49,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -603,6 +604,7 @@ "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", @@ -632,6 +634,7 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -836,6 +839,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -988,6 +992,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1226,6 +1231,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2357,6 +2363,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -2696,6 +2703,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2884,6 +2892,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f2ac47785..bbc2f6b2f 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "lint-staged": "^15.5.0", - "prettier": "^3.8.0", - "prettier-plugin-tailwindcss": "^0.7.0" + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.2" }, "lint-staged": { "*.{ts,tsx}": [