@autoview/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +407 -0
- package/lib/AutoViewAgent.d.ts +109 -0
- package/lib/AutoViewAgent.js +123 -0
- package/lib/AutoViewAgent.js.map +1 -0
- package/lib/agent/emitMcpServer.d.ts +15 -0
- package/lib/agent/emitMcpServer.js +157 -0
- package/lib/agent/emitMcpServer.js.map +1 -0
- package/lib/agent/emitReport.d.ts +14 -0
- package/lib/agent/emitReport.js +85 -0
- package/lib/agent/emitReport.js.map +1 -0
- package/lib/agent/toolSurface.d.ts +130 -0
- package/lib/agent/toolSurface.js +342 -0
- package/lib/agent/toolSurface.js.map +1 -0
- package/lib/agent/verifyAgentTasks.d.ts +87 -0
- package/lib/agent/verifyAgentTasks.js +126 -0
- package/lib/agent/verifyAgentTasks.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +295 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
- package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
- package/lib/constants/AutoViewFrontendTemplate.js +46 -0
- package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
- package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
- package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
- package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
- package/lib/context/IAutoViewAgentContext.d.ts +60 -0
- package/lib/context/IAutoViewAgentContext.js +3 -0
- package/lib/context/IAutoViewAgentContext.js.map +1 -0
- package/lib/fromSwagger.d.ts +53 -0
- package/lib/fromSwagger.js +513 -0
- package/lib/fromSwagger.js.map +1 -0
- package/lib/generateDeterministic.d.ts +26 -0
- package/lib/generateDeterministic.js +75 -0
- package/lib/generateDeterministic.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
- package/lib/orchestrate/orchestrateAutoView.js +491 -0
- package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
- package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
- package/lib/orchestrate/utils/HistoryMessage.js +25 -0
- package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
- package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
- package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
- package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
- package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
- package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
- package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
- package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
- package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
- package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
- package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
- package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
- package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
- package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
- package/lib/preview/renderPreview.d.ts +22 -0
- package/lib/preview/renderPreview.js +198 -0
- package/lib/preview/renderPreview.js.map +1 -0
- package/lib/typings/compiler.d.ts +39 -0
- package/lib/typings/compiler.js +3 -0
- package/lib/typings/compiler.js.map +1 -0
- package/lib/typings/events.d.ts +106 -0
- package/lib/typings/events.js +3 -0
- package/lib/typings/events.js.map +1 -0
- package/lib/typings/index.d.ts +10 -0
- package/lib/typings/index.js +27 -0
- package/lib/typings/index.js.map +1 -0
- package/lib/typings/misc.d.ts +78 -0
- package/lib/typings/misc.js +3 -0
- package/lib/typings/misc.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +8 -0
- package/lib/utils/ArrayUtil.js +30 -0
- package/lib/utils/ArrayUtil.js.map +1 -0
- package/lib/utils/StringUtil.d.ts +11 -0
- package/lib/utils/StringUtil.js +28 -0
- package/lib/utils/StringUtil.js.map +1 -0
- package/lib/utils/classifyEndpoints.d.ts +62 -0
- package/lib/utils/classifyEndpoints.js +216 -0
- package/lib/utils/classifyEndpoints.js.map +1 -0
- package/lib/utils/endpointFilter.d.ts +26 -0
- package/lib/utils/endpointFilter.js +0 -0
- package/lib/utils/endpointFilter.js.map +1 -0
- package/lib/utils/extractFields.d.ts +85 -0
- package/lib/utils/extractFields.js +231 -0
- package/lib/utils/extractFields.js.map +1 -0
- package/lib/utils/index.d.ts +13 -0
- package/lib/utils/index.js +30 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/normalizeForNestia.d.ts +34 -0
- package/lib/utils/normalizeForNestia.js +133 -0
- package/lib/utils/normalizeForNestia.js.map +1 -0
- package/lib/utils/resourcePlan.d.ts +39 -0
- package/lib/utils/resourcePlan.js +95 -0
- package/lib/utils/resourcePlan.js.map +1 -0
- package/lib/utils/sliceDocument.d.ts +17 -0
- package/lib/utils/sliceDocument.js +114 -0
- package/lib/utils/sliceDocument.js.map +1 -0
- package/lib/utils/toEndpoints.d.ts +90 -0
- package/lib/utils/toEndpoints.js +227 -0
- package/lib/utils/toEndpoints.js.map +1 -0
- package/lib/verify/runWorkflows.d.ts +25 -0
- package/lib/verify/runWorkflows.js +366 -0
- package/lib/verify/runWorkflows.js.map +1 -0
- package/lib/verify/workflows.d.ts +53 -0
- package/lib/verify/workflows.js +107 -0
- package/lib/verify/workflows.js.map +1 -0
- package/package.json +82 -0
- package/prompts/AUTOVIEW_RENDER.md +398 -0
- package/prompts/AUTOVIEW_REVIEW.md +60 -0
- package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
- package/src/AutoViewAgent.ts +222 -0
- package/src/agent/emitMcpServer.integration.test.ts +168 -0
- package/src/agent/emitMcpServer.test.ts +51 -0
- package/src/agent/emitMcpServer.ts +178 -0
- package/src/agent/emitReport.ts +117 -0
- package/src/agent/toolSurface.test.ts +243 -0
- package/src/agent/toolSurface.ts +501 -0
- package/src/agent/verifyAgentTasks.test.ts +106 -0
- package/src/agent/verifyAgentTasks.ts +171 -0
- package/src/cli/main.ts +363 -0
- package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
- package/src/constants/AutoViewFrontendTemplate.ts +42 -0
- package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
- package/src/context/IAutoViewAgentContext.ts +84 -0
- package/src/fromSwagger.test.ts +269 -0
- package/src/fromSwagger.ts +500 -0
- package/src/generateDeterministic.test.ts +39 -0
- package/src/generateDeterministic.ts +77 -0
- package/src/index.ts +30 -0
- package/src/orchestrate/orchestrateAutoView.ts +590 -0
- package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
- package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
- package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
- package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
- package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
- package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
- package/src/orchestrate/renderNavTs.test.ts +74 -0
- package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
- package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
- package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
- package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
- package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
- package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
- package/src/orchestrate/utils/HistoryMessage.ts +41 -0
- package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
- package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
- package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
- package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
- package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
- package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
- package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
- package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
- package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
- package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
- package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
- package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
- package/src/preview/renderPreview.ts +273 -0
- package/src/typings/compiler.ts +47 -0
- package/src/typings/events.ts +155 -0
- package/src/typings/index.ts +10 -0
- package/src/typings/misc.ts +93 -0
- package/src/utils/ArrayUtil.ts +16 -0
- package/src/utils/StringUtil.ts +29 -0
- package/src/utils/classifyEndpoints.test.ts +86 -0
- package/src/utils/classifyEndpoints.ts +291 -0
- package/src/utils/endpointFilter.test.ts +50 -0
- package/src/utils/endpointFilter.ts +0 -0
- package/src/utils/extractFields.test.ts +82 -0
- package/src/utils/extractFields.ts +306 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/normalizeForNestia.test.ts +93 -0
- package/src/utils/normalizeForNestia.ts +139 -0
- package/src/utils/resourcePlan.test.ts +104 -0
- package/src/utils/resourcePlan.ts +180 -0
- package/src/utils/sliceDocument.test.ts +85 -0
- package/src/utils/sliceDocument.ts +119 -0
- package/src/utils/toEndpoints.test.ts +251 -0
- package/src/utils/toEndpoints.ts +343 -0
- package/src/verify/runWorkflows.ts +403 -0
- package/src/verify/workflows.test.ts +117 -0
- package/src/verify/workflows.ts +154 -0
- package/template/CLAUDE.md +140 -0
- package/template/Dockerfile +31 -0
- package/template/PROMPT.md +80 -0
- package/template/SANDBOX.md +70 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/globals.css +97 -0
- package/template/app/layout.tsx +30 -0
- package/template/app/page.tsx +19 -0
- package/template/components/AppShell.tsx +114 -0
- package/template/components/auto/CatalogGrid.tsx +159 -0
- package/template/components/auto/ConfirmButton.tsx +67 -0
- package/template/components/auto/EmbeddedCollection.tsx +144 -0
- package/template/components/auto/ResourceDashboard.tsx +104 -0
- package/template/components/auto/ResourceDetail.tsx +93 -0
- package/template/components/auto/ResourceForm.tsx +235 -0
- package/template/components/auto/ResourceIcon.tsx +88 -0
- package/template/components/auto/ResourceLanding.tsx +155 -0
- package/template/components/auto/ResourceTable.tsx +223 -0
- package/template/components/auto/formatValue.tsx +186 -0
- package/template/components/auto/types.ts +42 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/button.tsx +57 -0
- package/template/components/ui/card.tsx +86 -0
- package/template/components/ui/dialog.tsx +119 -0
- package/template/components/ui/input.tsx +23 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/pagination.tsx +117 -0
- package/template/components/ui/select.tsx +92 -0
- package/template/components/ui/sheet.tsx +135 -0
- package/template/components/ui/skeleton.tsx +15 -0
- package/template/components/ui/table.tsx +120 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/lib/utils.ts +35 -0
- package/template/next.config.mjs +52 -0
- package/template/package.json +46 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/start-shopping-backend.sh +56 -0
- package/template/tailwind.config.ts +96 -0
- package/template/tsconfig.json +29 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
filterEndpoints,
|
|
5
|
+
IAutoViewEndpoint,
|
|
6
|
+
IEndpointFilter,
|
|
7
|
+
IPlannedScreen,
|
|
8
|
+
isCatalogCollection,
|
|
9
|
+
resourcePlan,
|
|
10
|
+
ScreenKind,
|
|
11
|
+
toEndpoints,
|
|
12
|
+
unsupportedOperations,
|
|
13
|
+
} from "../../utils";
|
|
14
|
+
import { IAutoViewProductPlan } from "../structures/IAutoViewProductPlan";
|
|
15
|
+
import { responseCollection } from "./renderResourcePage";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic Product Plan — the LLM ProductPlan's replacement.
|
|
19
|
+
*
|
|
20
|
+
* The old Phase 2 asked an LLM to invent an information architecture from a
|
|
21
|
+
* commerce-baked prompt: non-reproducible, domain-locked, no CRUD completeness.
|
|
22
|
+
* This builds the exact same {@link IAutoViewProductPlan} shape the Scaffold /
|
|
23
|
+
* Render phases consume, but structurally — from {@link resourcePlan}. Same
|
|
24
|
+
* swagger always yields the same screens; every resource that exposes a
|
|
25
|
+
* capability gets its screen; nothing is invented.
|
|
26
|
+
*
|
|
27
|
+
* Actors are not modeled yet (the READ layer dropped `authorizationActor` when
|
|
28
|
+
* it went swagger-native), so every screen is assigned the single `actor`
|
|
29
|
+
* passed in. Multi-actor navigation is a follow-up that restores per-endpoint
|
|
30
|
+
* actor tagging.
|
|
31
|
+
*/
|
|
32
|
+
const UI_PATTERN: Record<ScreenKind, IAutoViewProductPlan.UiPattern> = {
|
|
33
|
+
list: "table",
|
|
34
|
+
detail: "detail",
|
|
35
|
+
create: "form",
|
|
36
|
+
edit: "form",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A "top-level" screen earns a spot in the landing hub + primary nav: a list
|
|
41
|
+
* table, a catalog, or a singleton read (a bracket-less detail page like
|
|
42
|
+
* `/system`). Entity-detail / create / edit pages — which carry a `[param]` and
|
|
43
|
+
* are reached from a list — do not.
|
|
44
|
+
*/
|
|
45
|
+
function isTopLevel(screen: IAutoViewProductPlan.IScreen): boolean {
|
|
46
|
+
// A landing card is a TOP-LEVEL resource (chain depth 1). Depth ≥ 2 — a nested
|
|
47
|
+
// resource (`/sales/[saleId]/questions`) OR a param-less sub-view
|
|
48
|
+
// (`/sales/details`, `/channels/hierarchical`) — is reached from its parent,
|
|
49
|
+
// never the home hub. Using the structural depth, not a `[` heuristic, keeps
|
|
50
|
+
// bracket-less nested paths off the hub.
|
|
51
|
+
if (screen.depth !== undefined && screen.depth !== 1) return false;
|
|
52
|
+
if (screen.path.includes("[")) return false;
|
|
53
|
+
return (
|
|
54
|
+
screen.uiPattern === "table" ||
|
|
55
|
+
screen.uiPattern === "catalog" ||
|
|
56
|
+
screen.uiPattern === "detail"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Humanize a snake_case resource name for prose: `file_version_legal_holds`
|
|
61
|
+
* → `file version legal holds`. Keeps it lower-case for mid-sentence use. */
|
|
62
|
+
function humanizeResource(resource: string): string {
|
|
63
|
+
return resource.replace(/_/g, " ").trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function purposeOf(screen: IPlannedScreen): string {
|
|
67
|
+
const name = humanizeResource(screen.resource);
|
|
68
|
+
switch (screen.kind) {
|
|
69
|
+
case "list":
|
|
70
|
+
return `Browse and search ${name}.`;
|
|
71
|
+
case "detail":
|
|
72
|
+
return `View one ${name} and act on it.`;
|
|
73
|
+
case "create":
|
|
74
|
+
return `Create a new ${name}.`;
|
|
75
|
+
case "edit":
|
|
76
|
+
return `Edit an existing ${name}.`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function notesOf(screen: IPlannedScreen): string {
|
|
81
|
+
switch (screen.kind) {
|
|
82
|
+
case "list":
|
|
83
|
+
return screen.secondary.some((c) => c.role === "search")
|
|
84
|
+
? "Includes search/filter; rows link to the detail screen."
|
|
85
|
+
: "Rows link to the detail screen.";
|
|
86
|
+
case "detail":
|
|
87
|
+
return "Surfaces update / delete / action endpoints as buttons.";
|
|
88
|
+
case "create":
|
|
89
|
+
return "Render a form from the request-body schema; submit creates.";
|
|
90
|
+
case "edit":
|
|
91
|
+
return "Render a form from the request-body schema, prefilled from detail.";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function accessorsOf(screen: IPlannedScreen): string[] {
|
|
96
|
+
return [
|
|
97
|
+
screen.primary.accessor.join("."),
|
|
98
|
+
...screen.secondary.map((c) => c.endpoint.accessor.join(".")),
|
|
99
|
+
].filter((a, i, arr) => a.length > 0 && arr.indexOf(a) === i);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* How a would-be list screen's primary response reads structurally:
|
|
104
|
+
*
|
|
105
|
+
* - `browsable` — an array or a page wrapper (`{ pagination, data: T[] }`). The
|
|
106
|
+
* only shape that genuinely belongs in a table.
|
|
107
|
+
* - `void` — no response body (health checks, fire-and-forget). Nothing to show.
|
|
108
|
+
* - `singleton-read` — a single object returned by a GET (system status, the
|
|
109
|
+
* current actor, a singleton config). Real data, but it is one record, not a
|
|
110
|
+
* list — render it as a detail view, not a fake one-row table.
|
|
111
|
+
* - `action` — a single object returned by a mutating verb (auth login/refresh,
|
|
112
|
+
* activate). Not a view at all — omit it.
|
|
113
|
+
*
|
|
114
|
+
* Purely structural: response shape + HTTP method, no domain keywords. This is
|
|
115
|
+
* what turns "every PATCH/GET without an item param becomes a table" (which
|
|
116
|
+
* dumped auth/monitoring verbs into the nav) into an honest screen set.
|
|
117
|
+
*/
|
|
118
|
+
type ListResponseClass = "browsable" | "void" | "singleton-read" | "action";
|
|
119
|
+
|
|
120
|
+
function classifyListResponse(
|
|
121
|
+
op: IAutoViewEndpoint | undefined,
|
|
122
|
+
document: OpenApi.IDocument,
|
|
123
|
+
): ListResponseClass {
|
|
124
|
+
if (op === undefined || (op.responseBody === null && op.responseSchema === null))
|
|
125
|
+
return "void";
|
|
126
|
+
// Use the SAME collection detection the table renderer uses, so a response is
|
|
127
|
+
// classed browsable iff a real row collection (incl. resource-named / inline
|
|
128
|
+
// wrappers, DigitalOcean) can actually be rendered as a table.
|
|
129
|
+
if (responseCollection(op, document) !== null) return "browsable";
|
|
130
|
+
return op.method.toLowerCase() === "get" ? "singleton-read" : "action";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildDeterministicPlan(
|
|
134
|
+
document: OpenApi.IDocument,
|
|
135
|
+
actor: string,
|
|
136
|
+
filter: IEndpointFilter = {},
|
|
137
|
+
): IAutoViewProductPlan {
|
|
138
|
+
const endpoints = filterEndpoints(toEndpoints(document), filter);
|
|
139
|
+
const planned = resourcePlan(endpoints, document);
|
|
140
|
+
const byAccessor = new Map(endpoints.map((e) => [e.accessor.join("."), e]));
|
|
141
|
+
|
|
142
|
+
const noiseOmissions: IAutoViewProductPlan.IOmission[] = [];
|
|
143
|
+
const resourceScreens: IAutoViewProductPlan.IScreen[] = [];
|
|
144
|
+
for (const s of planned) {
|
|
145
|
+
const screen: IAutoViewProductPlan.IScreen = {
|
|
146
|
+
path: s.path,
|
|
147
|
+
title: s.title,
|
|
148
|
+
purpose: purposeOf(s),
|
|
149
|
+
actor,
|
|
150
|
+
endpoints: accessorsOf(s),
|
|
151
|
+
uiPattern: UI_PATTERN[s.kind],
|
|
152
|
+
notes: notesOf(s),
|
|
153
|
+
depth: s.depth,
|
|
154
|
+
};
|
|
155
|
+
// A table screen must have a browsable response, or it is noise. Downgrade
|
|
156
|
+
// singleton reads to detail views; omit voids and action verbs.
|
|
157
|
+
if (screen.uiPattern === "table") {
|
|
158
|
+
const op = byAccessor.get(s.primary.accessor.join("."));
|
|
159
|
+
const cls = classifyListResponse(op, document);
|
|
160
|
+
if (cls === "void" || cls === "action") {
|
|
161
|
+
noiseOmissions.push({
|
|
162
|
+
target: `${op?.method.toUpperCase() ?? "?"} ${op?.path ?? s.path}`,
|
|
163
|
+
reason:
|
|
164
|
+
cls === "void"
|
|
165
|
+
? "No response body to display."
|
|
166
|
+
: "Action endpoint (single object from a mutating verb) — not a browsable list.",
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (cls === "singleton-read") {
|
|
171
|
+
resourceScreens.push({
|
|
172
|
+
...screen,
|
|
173
|
+
uiPattern: "detail",
|
|
174
|
+
purpose: `View ${humanizeResource(s.resource)}.`,
|
|
175
|
+
notes: "Singleton read — one record, not a list.",
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Browsable: a collection whose rows carry an image + a title reads far
|
|
180
|
+
// better as a catalog of cards than a dense table. Purely structural —
|
|
181
|
+
// the element schema decides, no domain keywords.
|
|
182
|
+
const collection = op ? responseCollection(op, document) : null;
|
|
183
|
+
if (collection !== null && isCatalogCollection(collection.columns)) {
|
|
184
|
+
resourceScreens.push({
|
|
185
|
+
...screen,
|
|
186
|
+
uiPattern: "catalog",
|
|
187
|
+
notes: "Image + title rows — rendered as a card catalog.",
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
resourceScreens.push(screen);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Drop write-only resources. A browse app shows data, so a resource that has
|
|
196
|
+
// no list / detail / search screen at all — only a create/update form or an
|
|
197
|
+
// RPC verb (OAuth2 `token`/`revoke`, a `metadata_query/execute_read`) — is
|
|
198
|
+
// pure nav noise: a lone form seated next to real resources in the primary
|
|
199
|
+
// nav. Omit it (auditably) so a large swagger's sidebar shows browsable
|
|
200
|
+
// resources, not every endpoint.
|
|
201
|
+
const resourceKeyOf = (p: string): string =>
|
|
202
|
+
p.split("/").filter((s) => s.length > 0 && !s.startsWith("[")).at(0) ?? p;
|
|
203
|
+
const resourceHasRead = new Map<string, boolean>();
|
|
204
|
+
for (const s of resourceScreens) {
|
|
205
|
+
const key = resourceKeyOf(s.path);
|
|
206
|
+
const isRead = s.uiPattern === "table" || s.uiPattern === "detail";
|
|
207
|
+
resourceHasRead.set(key, (resourceHasRead.get(key) ?? false) || isRead);
|
|
208
|
+
}
|
|
209
|
+
const browsableScreens = resourceScreens
|
|
210
|
+
.filter((s) => resourceHasRead.get(resourceKeyOf(s.path)) === true)
|
|
211
|
+
// Alphabetical by path so a large swagger's sidebar is scannable (same
|
|
212
|
+
// resource's pages stay grouped: /x, /x/[id], /x/[id]/edit, /x/new).
|
|
213
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
214
|
+
for (const s of resourceScreens) {
|
|
215
|
+
if (resourceHasRead.get(resourceKeyOf(s.path)) === true) continue;
|
|
216
|
+
for (const accessor of s.endpoints) {
|
|
217
|
+
const op = byAccessor.get(accessor);
|
|
218
|
+
noiseOmissions.push({
|
|
219
|
+
target: op ? `${op.method.toUpperCase()} ${op.path}` : s.path,
|
|
220
|
+
reason:
|
|
221
|
+
"Write-only resource (no list/detail/search to browse) — omitted from the UI as nav noise.",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Root landing hub. Without an explicit `/` screen the dev server shows the
|
|
227
|
+
// template's "not customized yet" placeholder; this links to each resource's
|
|
228
|
+
// list and fetches nothing of its own.
|
|
229
|
+
const home: IAutoViewProductPlan.IScreen = {
|
|
230
|
+
path: "/",
|
|
231
|
+
title: "Home",
|
|
232
|
+
purpose: "Landing hub linking to each resource.",
|
|
233
|
+
actor,
|
|
234
|
+
endpoints: [],
|
|
235
|
+
uiPattern: "landing",
|
|
236
|
+
notes: "Links to each resource's list screen; no data fetch of its own.",
|
|
237
|
+
};
|
|
238
|
+
const screens = [home, ...browsableScreens];
|
|
239
|
+
|
|
240
|
+
// Single-actor navigation: home + every top-level screen leads the primary
|
|
241
|
+
// nav. "Top-level" = a list table or a singleton read (a bracket-less detail
|
|
242
|
+
// page like `/system`); entity-detail / create / edit pages (which carry a
|
|
243
|
+
// `[param]` or are reached from a list) go to secondary.
|
|
244
|
+
const topLevelPaths = browsableScreens
|
|
245
|
+
.filter((s) => isTopLevel(s))
|
|
246
|
+
.map((s) => s.path);
|
|
247
|
+
const secondary = browsableScreens
|
|
248
|
+
.filter((s) => !isTopLevel(s))
|
|
249
|
+
.map((s) => s.path);
|
|
250
|
+
const navigation: IAutoViewProductPlan.INavigation[] = [
|
|
251
|
+
{ actor, primary: ["/", ...topLevelPaths], secondary },
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
// Operations the READ layer could not turn into a screen — surfaced as
|
|
255
|
+
// honest omissions instead of vanishing. Noise screens (auth/monitoring verbs)
|
|
256
|
+
// removed above are reported here too, so "omitted, not broken" stays auditable.
|
|
257
|
+
const intentionalOmissions: IAutoViewProductPlan.IOmission[] = [
|
|
258
|
+
...noiseOmissions,
|
|
259
|
+
...unsupportedOperations(document).map((u) => ({
|
|
260
|
+
target: `${u.method} ${u.path}`,
|
|
261
|
+
reason: `Unsupported content type — ${u.reason}`,
|
|
262
|
+
})),
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
// Integrity backstop — "정확하게 읽는다" rule #1: nothing the READ layer parsed
|
|
266
|
+
// may silently vanish. Every operation must be referenced by some screen
|
|
267
|
+
// (primary or secondary) or appear in an omission. Anything left over (a
|
|
268
|
+
// second create on a resource that already had one, a deeply nested
|
|
269
|
+
// delete with no detail to host it, an auth verb that did not classify) is
|
|
270
|
+
// recorded here so coverage is total and auditable rather than lossy.
|
|
271
|
+
const referenced = new Set<string>();
|
|
272
|
+
for (const s of browsableScreens) for (const a of s.endpoints) referenced.add(a);
|
|
273
|
+
const omittedPaths = new Set(
|
|
274
|
+
intentionalOmissions.map((o) => o.target),
|
|
275
|
+
);
|
|
276
|
+
for (const e of endpoints) {
|
|
277
|
+
const accessor = e.accessor.join(".");
|
|
278
|
+
if (referenced.has(accessor)) continue;
|
|
279
|
+
const target = `${e.method.toUpperCase()} ${e.path}`;
|
|
280
|
+
if (omittedPaths.has(target)) continue;
|
|
281
|
+
intentionalOmissions.push({
|
|
282
|
+
target,
|
|
283
|
+
reason: `Not yet mapped to a screen (accessor ${accessor}) — a duplicate-role or deeply nested endpoint the deterministic plan does not surface yet.`,
|
|
284
|
+
});
|
|
285
|
+
omittedPaths.add(target);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { screens, navigation, intentionalOmissions };
|
|
289
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { buildDeterministicSdkMap } from "./buildDeterministicSdkMap";
|
|
5
|
+
|
|
6
|
+
function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
|
|
7
|
+
return OpenApiConverter.upgradeDocument({
|
|
8
|
+
openapi: "3.0.0",
|
|
9
|
+
info: { title: "t", version: "1.0.0" },
|
|
10
|
+
paths,
|
|
11
|
+
components: { schemas },
|
|
12
|
+
// biome-ignore lint: test fixture
|
|
13
|
+
} as never);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const listResp = (item: string) => ({
|
|
17
|
+
200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: `#/components/schemas/${item}` } } } } },
|
|
18
|
+
});
|
|
19
|
+
const objResp = (name: string) => ({
|
|
20
|
+
200: { description: "ok", content: { "application/json": { schema: { $ref: `#/components/schemas/${name}` } } } },
|
|
21
|
+
});
|
|
22
|
+
const param = (n: string) => ({ name: n, in: "path", required: true, schema: { type: "string" } });
|
|
23
|
+
|
|
24
|
+
describe("buildDeterministicSdkMap", () => {
|
|
25
|
+
it("derives resources from the swagger's CRUD shape (no LLM)", () => {
|
|
26
|
+
const d = doc(
|
|
27
|
+
{
|
|
28
|
+
"/sales": { get: { operationId: "index", responses: listResp("Sale") } },
|
|
29
|
+
"/sales/{saleId}": { get: { operationId: "at", parameters: [param("saleId")], responses: objResp("Sale") } },
|
|
30
|
+
"/orders": { get: { operationId: "index", responses: listResp("Order") } },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
34
|
+
Order: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
const map = buildDeterministicSdkMap(d);
|
|
38
|
+
expect(map.resources.map((r) => r.name).sort()).toEqual(["orders", "sales"]);
|
|
39
|
+
// every resource carries a non-empty namespace + purpose
|
|
40
|
+
for (const r of map.resources) {
|
|
41
|
+
expect(r.namespace.length).toBeGreaterThan(0);
|
|
42
|
+
expect(r.purpose.length).toBeGreaterThan(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("falls back to a single `user` actor when no role segment is present", () => {
|
|
47
|
+
const d = doc(
|
|
48
|
+
{ "/pet": { get: { operationId: "index", responses: listResp("Pet") } } },
|
|
49
|
+
{ Pet: { type: "object", properties: { id: { type: "string" } } } },
|
|
50
|
+
);
|
|
51
|
+
const map = buildDeterministicSdkMap(d);
|
|
52
|
+
expect(map.actors.map((a) => a.name)).toEqual(["user"]);
|
|
53
|
+
expect(map.actors[0]!.journeys.length).toBeGreaterThanOrEqual(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("derives multiple actors from role segments in the accessor", () => {
|
|
57
|
+
const d = doc(
|
|
58
|
+
{
|
|
59
|
+
"/shoppings/customers/sales": { get: { operationId: "index", responses: listResp("Sale") } },
|
|
60
|
+
"/shoppings/sellers/sales": { post: { operationId: "create", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Sale" } } } }, responses: objResp("Sale") } },
|
|
61
|
+
},
|
|
62
|
+
{ Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
63
|
+
);
|
|
64
|
+
const map = buildDeterministicSdkMap(d);
|
|
65
|
+
expect(map.actors.map((a) => a.name)).toContain("customer");
|
|
66
|
+
expect(map.actors.map((a) => a.name)).toContain("seller");
|
|
67
|
+
// every actor has at least one journey (the SDK-map invariant)
|
|
68
|
+
for (const a of map.actors) expect(a.journeys.length).toBeGreaterThanOrEqual(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("satisfies the downstream SDK-map invariants (≥1 resource, ≥1 actor, namespaces match accessors)", () => {
|
|
72
|
+
const d = doc(
|
|
73
|
+
{
|
|
74
|
+
"/sales/{saleId}": { get: { operationId: "at", parameters: [param("saleId")], responses: objResp("Sale") } },
|
|
75
|
+
},
|
|
76
|
+
{ Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
77
|
+
);
|
|
78
|
+
const map = buildDeterministicSdkMap(d);
|
|
79
|
+
expect(map.resources.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(map.actors.length).toBeGreaterThanOrEqual(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("is deterministic — same document, same map", () => {
|
|
84
|
+
const d = doc(
|
|
85
|
+
{ "/sales": { get: { operationId: "index", responses: listResp("Sale") } } },
|
|
86
|
+
{ Sale: { type: "object", properties: { id: { type: "string" } } } },
|
|
87
|
+
);
|
|
88
|
+
expect(buildDeterministicSdkMap(d)).toEqual(buildDeterministicSdkMap(d));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import { EndpointRole, classifyEndpoints } from "../../utils/classifyEndpoints";
|
|
4
|
+
import { toEndpoints } from "../../utils/toEndpoints";
|
|
5
|
+
import { IAutoViewSdkMap } from "../structures/IAutoViewSdkMap";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deterministic SDK domain map — the LLM-free replacement for the Phase-1 SDK
|
|
9
|
+
* Study, and the fix for "a large swagger overflows the model's context".
|
|
10
|
+
*
|
|
11
|
+
* The original SDK Study compiled the WHOLE SDK source and asked the model to
|
|
12
|
+
* infer resources / actors / journeys. On a 200-type swagger that prompt is
|
|
13
|
+
* 300k+ tokens — a hard context overflow. But the downstream phases barely use
|
|
14
|
+
* the map's prose: the Product Plan is already deterministic and reads only
|
|
15
|
+
* `actors[0].name`; Scaffold/Review embed the map as a wiki document; Render
|
|
16
|
+
* reads an actor's journeys only on the LLM rerender fallback. So the map can be
|
|
17
|
+
* derived structurally — same swagger, same map, zero tokens — without changing
|
|
18
|
+
* what reaches the user.
|
|
19
|
+
*
|
|
20
|
+
* - resources: the top-level CRUD groups from {@link classifyEndpoints}.
|
|
21
|
+
* - actors: role segments found in the accessors (`customers`, `sellers`,
|
|
22
|
+
* `admins`), normalized; falls back to a single `user`.
|
|
23
|
+
* - journeys: a short structural summary per actor (never empty — the SDK-map
|
|
24
|
+
* invariant requires ≥1 per actor).
|
|
25
|
+
*/
|
|
26
|
+
export function buildDeterministicSdkMap(
|
|
27
|
+
document: OpenApi.IDocument,
|
|
28
|
+
): IAutoViewSdkMap {
|
|
29
|
+
const endpoints = toEndpoints(document);
|
|
30
|
+
const groups = classifyEndpoints(endpoints, document);
|
|
31
|
+
|
|
32
|
+
const actors = deriveActors(endpoints);
|
|
33
|
+
const actorNames = actors.map((a) => a.name);
|
|
34
|
+
|
|
35
|
+
// Top-level resources only (depth 1). Nested groups (`sales/questions`) fold
|
|
36
|
+
// into their parent for the domain map; the Product Plan still renders their
|
|
37
|
+
// screens from the full classification.
|
|
38
|
+
const resources: IAutoViewSdkMap.IResource[] = groups
|
|
39
|
+
.filter((g) => g.depth === 1 && g.resource !== "root")
|
|
40
|
+
.map((g) => {
|
|
41
|
+
const sample = g.endpoints[0]?.endpoint;
|
|
42
|
+
const namespace =
|
|
43
|
+
sample !== undefined && sample.accessor.length > 1
|
|
44
|
+
? sample.accessor.slice(0, -1).join(".")
|
|
45
|
+
: g.resource;
|
|
46
|
+
return {
|
|
47
|
+
name: g.resource,
|
|
48
|
+
namespace,
|
|
49
|
+
purpose: purposeOf(g.resource, g.roles),
|
|
50
|
+
actorsInvolved: actorNames,
|
|
51
|
+
notes: notesOf(g.roles),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
resources,
|
|
57
|
+
actors,
|
|
58
|
+
notableConstraints: deriveConstraints(endpoints),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* -------------------------------------------------------------------------- */
|
|
63
|
+
/* actors */
|
|
64
|
+
/* -------------------------------------------------------------------------- */
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Accessor segments that name an actor role, mapped to their canonical
|
|
68
|
+
* singular display name. Real swaggers expose an actor as a namespace segment
|
|
69
|
+
* (`shoppings.customers.sales`, `shoppings.sellers.sales`); we read the role
|
|
70
|
+
* straight off the accessor instead of guessing from the domain.
|
|
71
|
+
*/
|
|
72
|
+
const ROLE_SEGMENTS: Record<string, string> = {
|
|
73
|
+
customer: "customer",
|
|
74
|
+
customers: "customer",
|
|
75
|
+
seller: "seller",
|
|
76
|
+
sellers: "seller",
|
|
77
|
+
admin: "administrator",
|
|
78
|
+
admins: "administrator",
|
|
79
|
+
administrator: "administrator",
|
|
80
|
+
administrators: "administrator",
|
|
81
|
+
member: "member",
|
|
82
|
+
members: "member",
|
|
83
|
+
buyer: "buyer",
|
|
84
|
+
buyers: "buyer",
|
|
85
|
+
manager: "manager",
|
|
86
|
+
managers: "manager",
|
|
87
|
+
guest: "guest",
|
|
88
|
+
guests: "guest",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function deriveActors(
|
|
92
|
+
endpoints: ReturnType<typeof toEndpoints>,
|
|
93
|
+
): IAutoViewSdkMap.IActor[] {
|
|
94
|
+
const names: string[] = [];
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
for (const e of endpoints) {
|
|
97
|
+
for (const seg of e.accessor) {
|
|
98
|
+
const canon = ROLE_SEGMENTS[seg.toLowerCase()];
|
|
99
|
+
if (canon !== undefined && !seen.has(canon)) {
|
|
100
|
+
seen.add(canon);
|
|
101
|
+
names.push(canon);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const finalNames = names.length > 0 ? names : ["user"];
|
|
106
|
+
// Lead with the end-user actor: the Product Plan assigns every screen to
|
|
107
|
+
// `actors[0]`, so a customer-facing label reads more naturally on the nav than
|
|
108
|
+
// an admin one. Purely cosmetic — the screen set is identical either way.
|
|
109
|
+
const ordered = [...finalNames].sort(
|
|
110
|
+
(a, b) => actorRank(a) - actorRank(b),
|
|
111
|
+
);
|
|
112
|
+
return ordered.map((name) => ({
|
|
113
|
+
name,
|
|
114
|
+
journeys: [`Sign in as ${name} → navigate the resources they can access.`],
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** End-users first, privileged roles last; unknowns keep their found order. */
|
|
119
|
+
const ACTOR_PRIORITY = [
|
|
120
|
+
"customer",
|
|
121
|
+
"buyer",
|
|
122
|
+
"member",
|
|
123
|
+
"user",
|
|
124
|
+
"guest",
|
|
125
|
+
"seller",
|
|
126
|
+
"manager",
|
|
127
|
+
"administrator",
|
|
128
|
+
];
|
|
129
|
+
function actorRank(name: string): number {
|
|
130
|
+
const i = ACTOR_PRIORITY.indexOf(name);
|
|
131
|
+
return i === -1 ? ACTOR_PRIORITY.length : i;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* -------------------------------------------------------------------------- */
|
|
135
|
+
/* resource prose (deterministic) */
|
|
136
|
+
/* -------------------------------------------------------------------------- */
|
|
137
|
+
|
|
138
|
+
function purposeOf(resource: string, roles: Set<EndpointRole>): string {
|
|
139
|
+
const verbs: string[] = [];
|
|
140
|
+
if (roles.has("list") || roles.has("search")) verbs.push("browse");
|
|
141
|
+
if (roles.has("detail")) verbs.push("view");
|
|
142
|
+
if (roles.has("create")) verbs.push("create");
|
|
143
|
+
if (roles.has("update")) verbs.push("update");
|
|
144
|
+
if (roles.has("delete")) verbs.push("remove");
|
|
145
|
+
if (roles.has("action")) verbs.push("act on");
|
|
146
|
+
const verb = verbs.length > 0 ? verbs.join(", ") : "work with";
|
|
147
|
+
return `Lets a user ${verb} ${resource}.`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function notesOf(roles: Set<EndpointRole>): string {
|
|
151
|
+
return roles.has("search")
|
|
152
|
+
? "Supports search/filter/pagination via query parameters."
|
|
153
|
+
: "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deriveConstraints(
|
|
157
|
+
endpoints: ReturnType<typeof toEndpoints>,
|
|
158
|
+
): string[] {
|
|
159
|
+
const constraints: string[] = [];
|
|
160
|
+
if (endpoints.some((e) => e.query !== null)) {
|
|
161
|
+
constraints.push(
|
|
162
|
+
"Some list endpoints accept query parameters for search/filter/pagination.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (endpoints.some((e) => e.method.toLowerCase() === "delete")) {
|
|
166
|
+
constraints.push("Delete endpoints are present — confirm before destructive actions.");
|
|
167
|
+
}
|
|
168
|
+
return constraints;
|
|
169
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-package.json cache directory holding an installed `node_modules`. Sits
|
|
8
|
+
* under `$XDG_CACHE_HOME` when set, else `~/.cache`, partitioned by
|
|
9
|
+
* `(cacheNamespace, hash)` so callers with different dependency sets — and
|
|
10
|
+
* different downstream consumers (typecheck vs runtime audit) — never collide
|
|
11
|
+
* on the same cache key.
|
|
12
|
+
*
|
|
13
|
+
* Returns the absolute path to the populated `node_modules` directory on
|
|
14
|
+
* success, or `null` when the cache could not be set up (no writable cache
|
|
15
|
+
* root, the install crashed, etc.). Callers fall back to a plain in-place
|
|
16
|
+
* install in that case so the behavior degrades gracefully.
|
|
17
|
+
*
|
|
18
|
+
* `extraDeps` lets a caller add packages on top of the bare frontend dependency
|
|
19
|
+
* set without renaming the cache namespace: the runtime-audit cache uses
|
|
20
|
+
* `["playwright@^1.50.0"]` so the cached `node_modules` already contains
|
|
21
|
+
* Playwright the next time the agent runs an audit, on top of the same
|
|
22
|
+
* React/Next/shadcn tree as the typecheck cache.
|
|
23
|
+
*/
|
|
24
|
+
export async function ensureCachedNodeModules(args: {
|
|
25
|
+
cacheNamespace: string;
|
|
26
|
+
hash: string;
|
|
27
|
+
packageJson: string;
|
|
28
|
+
extraDeps?: readonly string[];
|
|
29
|
+
}): Promise<string | null> {
|
|
30
|
+
if (args.hash === "no-package-json") return null;
|
|
31
|
+
const cacheRoot = path.join(
|
|
32
|
+
process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"),
|
|
33
|
+
`autoview-${args.cacheNamespace}`,
|
|
34
|
+
args.hash,
|
|
35
|
+
);
|
|
36
|
+
const nodeModules = path.join(cacheRoot, "node_modules");
|
|
37
|
+
try {
|
|
38
|
+
// `node_modules/.package-lock.json` is the canonical "install completed"
|
|
39
|
+
// sentinel npm writes at the end of `npm install`. Its presence means
|
|
40
|
+
// the previous run finished cleanly and the cache is reusable.
|
|
41
|
+
await fs.access(path.join(nodeModules, ".package-lock.json"));
|
|
42
|
+
return nodeModules;
|
|
43
|
+
} catch {
|
|
44
|
+
// Cache miss: populate it. Use a sibling temp dir + rename so a
|
|
45
|
+
// concurrent agent run cannot observe a half-written node_modules.
|
|
46
|
+
const installer = await fs.mkdtemp(
|
|
47
|
+
path.join(
|
|
48
|
+
os.tmpdir(),
|
|
49
|
+
`autoview-${args.cacheNamespace}-cache-${args.hash}-`,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
try {
|
|
53
|
+
await fs.writeFile(
|
|
54
|
+
path.join(installer, "package.json"),
|
|
55
|
+
args.packageJson,
|
|
56
|
+
"utf-8",
|
|
57
|
+
);
|
|
58
|
+
await runNpm(installer, [
|
|
59
|
+
"install",
|
|
60
|
+
"--silent",
|
|
61
|
+
"--no-audit",
|
|
62
|
+
"--no-fund",
|
|
63
|
+
]);
|
|
64
|
+
if (args.extraDeps !== undefined && args.extraDeps.length > 0) {
|
|
65
|
+
await runNpm(installer, [
|
|
66
|
+
"install",
|
|
67
|
+
"--silent",
|
|
68
|
+
"--no-audit",
|
|
69
|
+
"--no-fund",
|
|
70
|
+
"--no-save",
|
|
71
|
+
...args.extraDeps,
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
await fs.mkdir(cacheRoot, { recursive: true });
|
|
75
|
+
// Atomically move node_modules into place. If something is already
|
|
76
|
+
// there (concurrent run won the race), rm-then-rename overwrites it
|
|
77
|
+
// — both are valid installs of the same package.json.
|
|
78
|
+
await fs.rm(nodeModules, { recursive: true, force: true });
|
|
79
|
+
await fs.rename(path.join(installer, "node_modules"), nodeModules);
|
|
80
|
+
} catch {
|
|
81
|
+
await fs.rm(installer, { recursive: true, force: true }).catch(() => {});
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
await fs.rm(installer, { recursive: true, force: true }).catch(() => {});
|
|
85
|
+
try {
|
|
86
|
+
await fs.access(path.join(nodeModules, ".package-lock.json"));
|
|
87
|
+
return nodeModules;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a symlink from `<workingDir>/node_modules` → `<cachedNodeModules>`.
|
|
96
|
+
* Returns `false` when the symlink could not be created (Windows without admin,
|
|
97
|
+
* filesystem refuses links, etc.) so the caller falls back to a plain in-place
|
|
98
|
+
* install.
|
|
99
|
+
*/
|
|
100
|
+
export async function tryLinkNodeModules(
|
|
101
|
+
workingDir: string,
|
|
102
|
+
cachedNodeModules: string,
|
|
103
|
+
): Promise<boolean> {
|
|
104
|
+
const target = path.join(workingDir, "node_modules");
|
|
105
|
+
try {
|
|
106
|
+
await fs.symlink(cachedNodeModules, target, "dir");
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runNpm(cwd: string, args: string[]): Promise<void> {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const child = spawn("npm", args, { cwd, stdio: "pipe" });
|
|
116
|
+
let stderr = "";
|
|
117
|
+
child.stderr.on("data", (chunk) => {
|
|
118
|
+
stderr += chunk.toString("utf-8");
|
|
119
|
+
});
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
reject(
|
|
122
|
+
new Error(`Failed to spawn \`npm ${args.join(" ")}\`: ${err.message}`),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
child.on("close", (code) => {
|
|
126
|
+
if (code === 0) resolve();
|
|
127
|
+
else
|
|
128
|
+
reject(
|
|
129
|
+
new Error(
|
|
130
|
+
`\`npm ${args.slice(0, 3).join(" ")}...\` exited with code ${code}` +
|
|
131
|
+
(stderr.length > 0 ? `:\n${stderr.slice(-1500)}` : ""),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|